Tuesday, October 2, 2012

Data contract with a constructor that accepts parameters.

Recently, I had a nasty problem with WCF service. After several months of faultless functioning, it unexpectedly stopped working. Client-side code was throwing a CommunicationException that was saying very little:

There was an error reading from the pipe: Unrecognized error 109 (0x6d).

Changing the binding to BasicHttpBinding resulted in following exception.

An error occurred while receiving the HTTP response 
to http://localhost:xxxx/ShipmentService. This could be due to the service endpoint binding not using the HTTP protocol. This could also be due to an HTTP request context being aborted by the server (possibly due to the service shutting down). See server logs for more details. Inner exception: Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host.

The service was rather simple. It contained only a few methods exposed locally through the net.pipe. Both the hosting and consuming application were sharing the same contract assembly that eliminated possibility of any contract mismatch.

[ServiceContract]
public interface IShipment
{
  [OperationContract]
  Container GetFirstContainer();
}


public class Container
{
  public List<Box> Boxes { get; set; }
}

public class Box
{
  public string Owner { get; set; }
  public decimal Value { get; set; }

  public Box(string shipper, decimal value)
  {
    Owner = shipper;
    Value = value;
  }
}

Note that there are no contract attributes.
The service implementation:

public class ShipmentService : IShipment
{
  public Container GetFirstContainer()
  {
    return new Container
              {
                Boxes = new List<Box>
                          {
                            new Box("Cantoso", 399m),
                            new Box("Fabrikam", 100m)
                          }
              };
  }
}

Hosting code:

var host = new ServiceHost(typeof(ShipmentService));
host.AddServiceEndpoint(
                        typeof(IShipment), 
                        new NetNamedPipeBinding(NetNamedPipeSecurityMode.None),
                        new UriBuilder(@"net.pipe://localhost/ShippingService").Uri
                       );

host.Open();

Client code:

var shipping =
  new ChannelFactory<IShipment>
                (
                  new NetNamedPipeBinding(NetNamedPipeSecurityMode.None),
                  new EndpointAddress(
                    new UriBuilder(@"net.pipe://localhost/ShippingService").Uri)
                ).CreateChannel();
        
var container = shipping.GetFirstContainer(); /* Exception is thrown here! */


As I said before the types that are returned from remote method (Container, Box) have no attributes defining data contracts. WCF actually does not require tagging the types being sent with DataContract / DataMemeber. These attributes specify how the serialization/deserialization formatting is performed. Without them, WCF is still able to serialize the objects. It probably uses the same serialization rules that apply to XML serialization, sometimes called POCO serialization.

The problem with this code is that the 'Box' class has no public constructor but only the one with parameters. Parameterless constructor is exactly what the deserializing mechanism for POCO tries to invoke to re-create the object. Because such constructor is not available then the client-side code fails.

Two quick fixes to this.
1. Tag the type with DataContract attribute and properties with DataMemeber attributes. Specifying the DataContract attribute causes, surprisingly, that no constructor is needed to complete deserialization.
2. Provide all classes with parameterless constructor when there is already a constructor with parameters.

I try to apply DataContract always when a type is being exchanged through the service. This certainly helps to maintain abstract contract between client and server, for example, when an overzealous programmer removes empty public constructor because it has no references in code :)

8 comments:

  1. returns always empty container, maybe [CollectionDataContract] will be better..

    ReplyDelete
  2. I have used similiar service and have several problem's with NetPipeBinding on Windows Server, anyway - all already resolved. Have tested your example and recieve null collections instances - it was becouse my data contract hadn't [DataContract(Namespace="urn:my-test-namespace")] attribute :)
    - nice blog and articles -
    regards

    ReplyDelete
    Replies
    1. Thanks.
      I didn't have any issues with the default namespace (which is probably http://tempuri.org/), maybe because I also don't expose any metadata endpoints - just share the contract contained within a separate assembly.

      Delete
  3. Contract class List should have a public constructor with no arguments.

    ReplyDelete