Share via


ChannelFactory Contract and Generated Types

Last Monday we had an introduction to the contract associated with a ChannelFactory. Today's article is a tangential continuation of that discussion. Let's take a look at the typed version of the contract for the same echo service that we had before.

 [ServiceContract(Namespace="")]
public interface IService
{
   [OperationContract(Action="Echo")]
   string Echo(string text);
}

public class Service : IService
{
   public string Echo(string text)
   {
      return text;
   }
}

I've set the namespace and action to specific values here just to make the service call simpler. Now, here's a service that uses a metadata exchange connection to get the service metadata for the IService contract. Using the metadata, we can create the same IRequestChannel interface to the service that we had last time. What is different here of course is that the format of the message has to change because the service really is using a typed contract.

 string uri = "localhost:8000/service";
string mex = "localhost:8000/mex";
Binding binding = new CustomBinding(new HttpTransportBindingElement());

ServiceHost service = new ServiceHost(typeof(Service));
service.Description.Behaviors.Add(new ServiceMetadataBehavior());
service.AddServiceEndpoint(typeof(IService), binding, uri);
service.AddServiceEndpoint(typeof(IMetadataExchange), binding, mex);
service.Open();

MetadataExchangeClient client = new MetadataExchangeClient(new EndpointAddress(mex));
MetadataSet metadata = client.GetMetadata();
WsdlImporter importer = new WsdlImporter(metadata);
ServiceEndpointCollection endpoints = importer.ImportAllEndpoints();
ChannelFactory<IRequestChannel> factory = new ChannelFactory<IRequestChannel>(endpoints[0]);
IRequestChannel channel = factory.CreateChannel();
XmlReader reader = XmlReader.Create(new StringReader("<Echo><text>a message</text></Echo>")); 
Message message = Message.CreateMessage(binding.MessageVersion, "Echo", reader);
Console.WriteLine(channel.Request(message));

message.Close();
factory.Close();
service.Close();

The attempt to invoke the service fails with a different InvalidOperationException this time: "Instance of MessagePartDescription Name='text' Namespace='' cannot be used in this context: required 'Type' property was not set."

Why is there a missing Type property for the contract definition? The lack of types is because the dynamic metadata generation call does not create the runtime contract types needed to describe the typed message parameters of the service operation.

You might be able to spot two ways that this problem can be fixed. On the client end, the problem is that we don't have runtime types in the generated contract description. The only reason someone looked at the generated contract description is because we're implicitly using the contract to bootstrap the creation of the ChannelFactory. We don't actually need the contract because everything is hidden behind IRequestChannel. The ChannelFactory constructor has an alternative overload to just take the address and binding but not the contract. If we use that overload, then we can completely sidestep the problem and get back the expected message.

 <s:Envelope xmlns:s="www.w3.org/2003/05/soap-envelope" xmlns:a="www.w3.org/2005/08/addressing">
  <s:Header>
    <a:Action s:mustUnderstand="1">urn:IService/EchoResponse</a:Action>
    <a:RelatesTo>urn:uuid:8976d9b5-92c5-4491-aa2b-a1d293552f11</a:RelatesTo>
  </s:Header>
  <s:Body>
    <EchoResponse>
      <EchoResult>a message</EchoResult>
    </EchoResponse>
  </s:Body>
</s:Envelope>

On the server end, the problem is that we need to describe typed message parameters for a contract. If all of the clients are going to be accessing the service through an untyped interface, then it may make more sense to use an untyped contract for the service operations. An untyped contract would use the same message structure that we had last week and entirely avoid the issue of runtime contract types.

Next time: Adding HTTP Headers