Share via


WCF Extensibility – Extensibility in Windows Phone (and Silverlight 3)

This post is part of a series about WCF extensibility points. For a list of all previous posts and planned future ones, go to the index page.

This post takes again a detour from the normal flow of the series as I’ve seen lots of requests for this scenario – the ability to use extensibility points which were added in Silverlight 4 in a Silverlight 3 (and Windows Phone 7) application. In SL4, all the client behavior classes (IEndpointBehavior, IContractBehavior, IOperationBehavior) along with the runtime interfaces used to interact with the WCF runtime (IClientMessageInspector, IClientMessageFormatter, IParameterInspector, IClientOperationSelector) were added to the platform. For Silverlight applications it just meant that the developer who needed that extensibility needed to move to SL4 (and since most of the Silverlight installations are auto-upgraded to the latest released version, the client base which could consume those new applications would be fairly large). Windows Phone developers, however, are stuck with the SL3 API, and many scenarios (such as custom serialization which I mentioned in a previous post) are quite hard to accomplish.

The primary extensibility which exists in SL3 (and SL2) is to write a custom channel. With a custom channel you can actually do pretty much anything in WCF – you have total control over the Message objects which are being sent / received from / by the client. However, writing a custom channel is really hard – besides working with directly with messages, you also need to implement all of the channel boilerplate, worry about channel shapes – custom channels is one of the less used features in WCF for a good reason, which is why most of the “nice” extensibility points were added at the first place.

A well-written custom channel, however, is a very powerful tool, and as Yavor Georgiev and Matthew McDermott wrote in their blogs, it can be used to implement a simple message inspector (in their posts they used it to work around a known issue in SL3 / SL4 / WP7). The idea is interesting, so I decided to expand on their sample and add the other extensibility points (except IClientOperationSelector because, as I’ve mentioned before, I don’t think it’s worth the hassle). And to validate that the new extensibility points could be used in a real scenario, I decided to try the same custom serialization example I used for Silverlight 4.

Message inspectors

Yavor / Matthew sample had an implementation of a custom channel that exposed a message inspector. Here I modified it a little to enable two more features: correlation state (where a call to AfterReceiveReply can receive an object which is returned by BeforeSendRequest), and the ability to have multiple inspectors (in their example they only had one). The idea is that you add a binding element right before the transport element in a custom binding, and as the Message object passes through the channel stack, right before it reaches the transport, the inspectors are invoked to inspect / modify the message. The only difference between the implementation and the “real” one is that the IClientChannel parameter for BeforeSendRequest is not set.

And before I dive into code, the usual disclaimer: this is a sample, not production-ready code. I tested it for a few contracts and it worked, but I cannot guarantee that it will work for all scenarios – actually, I can guarantee that it won’t work for many scenarios, see missing features list below. For simplicity sake it does not do a lot of error handling which should be present in a production code.

The implementation of the custom channel starts with a class derived from ChannelBase and IRequestChannel. The original sample only supported this channel shape (IRequestChannel), but that’s ok for most scenarios since this is the most common shape (used in request / reply calls, which is the default for HTTP transport, which is the only transport supported in WP7). Since the call is asynchronous, I’ve used the same technique described at another post for chaining the calls and passing some additional information (the correlation state) to the other side.

  1. class ClientMessageInspectorChannel : ChannelBase, IRequestChannel
  2. {
  3.     private IRequestChannel innerChannel;
  4.     private List<IClientMessageInspector> messageInspectors;
  5.  
  6.     public ClientMessageInspectorChannel(ChannelManagerBase channelManager, IRequestChannel innerChannel, List<IClientMessageInspector> messageInspectors)
  7.         : base(channelManager)
  8.     {
  9.         this.innerChannel = innerChannel;
  10.         this.messageInspectors = messageInspectors;
  11.     }
  12.  
  13.     public IAsyncResult BeginRequest(Message message, TimeSpan timeout, AsyncCallback callback, object state)
  14.     {
  15.         object[] correlationStates = new object[this.messageInspectors.Count];
  16.         for (int i = 0; i < this.messageInspectors.Count; i++)
  17.         {
  18.             correlationStates[i] = this.messageInspectors[i].BeforeSendRequest(ref message, null);
  19.         }
  20.  
  21.         CorrelationStateAsyncResult.State newState = new CorrelationStateAsyncResult.State
  22.         {
  23.             OriginalAsyncCallback = callback,
  24.             ExtensibilityCorrelationState = correlationStates,
  25.             OriginalAsyncState = state,
  26.         };
  27.         IAsyncResult originalResult = this.innerChannel.BeginRequest(message, timeout, this.BeginRequestCallback, newState);
  28.         return new CorrelationStateAsyncResult(originalResult, state, correlationStates);
  29.     }
  30.  
  31.     void BeginRequestCallback(IAsyncResult asyncResult)
  32.     {
  33.         CorrelationStateAsyncResult.State newState = asyncResult.AsyncState as CorrelationStateAsyncResult.State;
  34.         IAsyncResult newAsyncResult = new CorrelationStateAsyncResult(asyncResult, newState.OriginalAsyncState, newState.ExtensibilityCorrelationState);
  35.         newState.OriginalAsyncCallback(newAsyncResult);
  36.     }
  37.  
  38.     public Message EndRequest(IAsyncResult result)
  39.     {
  40.         CorrelationStateAsyncResult correlationAsyncResult = (CorrelationStateAsyncResult)result;
  41.         object[] correlationStates = (object[])correlationAsyncResult.CorrelationState;
  42.         Message reply = this.innerChannel.EndRequest(correlationAsyncResult.OriginalAsyncResult);
  43.         for (int i = 0; i < this.messageInspectors.Count; i++)
  44.         {
  45.             this.messageInspectors[i].AfterReceiveReply(ref reply, correlationStates[i]);
  46.         }
  47.  
  48.         return reply;
  49.     }
  50. }

The next step is to create a channel factory which knows how to create that inspector channel. The implementation is trivial, with many methods simply delegating to the inner factory (they’re omitted here for brevity).

  1. class ClientMessageInspectorChannelFactory : ChannelFactoryBase<IRequestChannel>
  2. {
  3.     private IChannelFactory<IRequestChannel> innerFactory;
  4.     private List<IClientMessageInspector> messageInspectors;
  5.  
  6.     public ClientMessageInspectorChannelFactory(IChannelFactory<IRequestChannel> innerFactory, List<IClientMessageInspector> messageInspectors)
  7.     {
  8.         this.innerFactory = innerFactory;
  9.         this.messageInspectors = messageInspectors;
  10.     }
  11.  
  12.     protected override IRequestChannel OnCreateChannel(EndpointAddress address, Uri via)
  13.     {
  14.         IRequestChannel innerChannel = this.innerFactory.CreateChannel(address, via);
  15.         ClientMessageInspectorChannel clientChannel = new ClientMessageInspectorChannel(this, innerChannel, this.messageInspectors);
  16.         return clientChannel;
  17.     }
  18. }

Finally we need a public binding element which can be added to the binding. This implementation does some minimal validation, and in case where there are no inspectors registered in this binding element it simply skips completely the inspector channel and just return what would have if this binding didn’t exist.

  1. public class ClientMessageInspectorBindingElement : BindingElement
  2. {
  3.     List<IClientMessageInspector> messageInspectors = new List<IClientMessageInspector>();
  4.  
  5.     public ClientMessageInspectorBindingElement()
  6.     {
  7.     }
  8.  
  9.     public List<IClientMessageInspector> MessageInspectors
  10.     {
  11.         get
  12.         {
  13.             return this.messageInspectors;
  14.         }
  15.     }
  16.  
  17.     public override bool CanBuildChannelFactory<TChannel>(BindingContext context)
  18.     {
  19.         return typeof(TChannel) == typeof(IRequestChannel) &&
  20.             context.CanBuildInnerChannelFactory<TChannel>();
  21.     }
  22.  
  23.     public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context)
  24.     {
  25.         if (context == null)
  26.         {
  27.             throw new ArgumentNullException("context");
  28.         }
  29.  
  30.         if (typeof(TChannel) != typeof(IRequestChannel))
  31.         {
  32.             throw new InvalidOperationException("Invalid channel shape");
  33.         }
  34.  
  35.         if (this.MessageInspectors.Count == 0)
  36.         {
  37.             return base.BuildChannelFactory<TChannel>(context);
  38.         }
  39.         else
  40.         {
  41.             ClientMessageInspectorChannelFactory factory = new ClientMessageInspectorChannelFactory(
  42.                 context.BuildInnerChannelFactory<IRequestChannel>(),
  43.                 this.MessageInspectors);
  44.  
  45.             return (IChannelFactory<TChannel>)factory;
  46.         }
  47.     }
  48.  
  49.     public override BindingElement Clone()
  50.     {
  51.         return new ClientMessageInspectorBindingElement { messageInspectors = new List<IClientMessageInspector>(this.messageInspectors) };
  52.     }
  53. }

And that’s it for the inspector.

Message formatter and parameter inspectors

The message inspector was somehow easy to be implemented (or as easy as writing a custom channel can be) because the parameter to the inspector methods are of the same type that the channels deal with: Message. Formatters and parameter inspectors, however, deal with the CLR objects, and at the point where the message arrives at the channels, those objects have already been consumed (for outgoing messages) or have yet to be decoded (for incoming messages) by some internal WCF code. The solution for this issue was to undo what WCF had already done (i.e., convert back from a message into the operation parameters, or from the incoming message to the operation results before handing them back to WCF. Another complication is that the formatter and the inspectors are bound not to the endpoint but to individual operations, and the channel applies to the whole endpoint, so we need a way to filter the messages (and the extensibility interfaces) on a per operation basis.

Solving the problem of the formatter / inspectors binding to the operations requires passing the endpoint contract to the channel – this way it can map each incoming message to the proper extensions. In this sample I implemented the formatter and the parameter inspectors as mappings between an OperationDescription object and the corresponding extension. Also, since there’s no DataContractSerializerOperationBehavior (in SL3 or SL4), I added a new class, DataContractSerializerEndpointBehavior which can be used to replace the serializer on a per type basis (currently it has to be applied to the whole endpoint, but it can be changed to be bound to the operation as well).

The mapping between the operations and the extensions, done at the channel creation, iterates through the operations of the endpoint contract passed to the class, and maps the Action property of the outgoing message to the appropriate extensions.

  1. class ClientMessageFormatterChannel : ChannelBase, IRequestChannel
  2. {
  3.     private IRequestChannel innerChannel;
  4.     private ContractDescription serviceContract;
  5.     private Func<OperationDescription, IClientMessageFormatter> messageFormatterForOperation;
  6.     private Func<OperationDescription, List<IParameterInspector>> parameterInspectorsForOperation;
  7.     private DataContractSerializerEndpointBehavior dataContractSerializerEndpointBehavior;
  8.     private readonly DataContractSerializerEndpointBehavior defaultSerializerEndpointBehavior = new DataContractSerializerEndpointBehavior();
  9.     private Dictionary<string, NewClientOperation> clientOperations;
  10.  
  11.     public ClientMessageFormatterChannel(
  12.         ChannelManagerBase channelManager,
  13.         IRequestChannel innerChannel,
  14.         ContractDescription serviceContract,
  15.         Func<OperationDescription, IClientMessageFormatter> messageFormatterForOperation,
  16.         Func<OperationDescription, List<IParameterInspector>> parameterInspectorsForOperation,
  17.         DataContractSerializerEndpointBehavior dataContractSerializerEndpointBehavior)
  18.         : base(channelManager)
  19.     {
  20.         this.innerChannel = innerChannel;
  21.         this.serviceContract = serviceContract;
  22.         this.messageFormatterForOperation = messageFormatterForOperation;
  23.         this.parameterInspectorsForOperation = parameterInspectorsForOperation;
  24.         this.dataContractSerializerEndpointBehavior = dataContractSerializerEndpointBehavior;
  25.  
  26.         this.InitializeClientOperations();
  27.     }
  28.  
  29.     private void InitializeClientOperations()
  30.     {
  31.         this.clientOperations = new Dictionary<string, NewClientOperation>();
  32.         foreach (OperationDescription operation in this.serviceContract.Operations)
  33.         {
  34.             string action = operation.Messages[0].Action;
  35.             NewClientOperation clientOperation = new NewClientOperation();
  36.             clientOperation.OperationDescription = operation;
  37.             this.clientOperations.Add(action, clientOperation);
  38.             if (this.messageFormatterForOperation != null)
  39.             {
  40.                 clientOperation.MessageFormatter = this.messageFormatterForOperation(operation);
  41.             }
  42.  
  43.             if (this.parameterInspectorsForOperation != null)
  44.             {
  45.                 List<IParameterInspector> inspectors = this.parameterInspectorsForOperation(operation);
  46.                 if (inspectors != null && inspectors.Count > 0)
  47.                 {
  48.                     clientOperation.ParameterInspectors = inspectors.ToArray();
  49.                 }
  50.             }
  51.  
  52.             if (clientOperation.ParameterInspectors != null && clientOperation.MessageFormatter == null)
  53.             {
  54.                 // No formatter, but some inspectors; will need to unwrap the message
  55.                 // anyway, so adding a formatter to do the task.
  56.                 clientOperation.MessageFormatter = new SimpleClientMessageFormatter(operation);
  57.             }
  58.         }
  59.     }
  60. }

When an outgoing message arrives at the channel, if there are any formatters or parameter inspectors for its operation, then the code does a “reverse formatting”, unwrapping the operation parameters from the (WCF-packaged) message. It then passes the parameters to any registered inspectors, and then calls the formatter to wrap them back into a Message object. On incoming messages the code is similar – first the message is unwrapped with the given formatter, then passed to any parameter inspectors, and finally it’s wrapped again to be handed over to the rest of the WCF stack.

  1. public IAsyncResult BeginRequest(Message message, TimeSpan timeout, AsyncCallback callback, object state)
  2. {
  3.     NewClientOperation operation = this.clientOperations[message.Headers.Action];
  4.     object[] parameterInspectorCorrelationStates = null;
  5.  
  6.     if (operation.MessageFormatter != null)
  7.     {
  8.         SimpleReverseClientMessageFormatter dispatchFormatter = new SimpleReverseClientMessageFormatter(operation.OperationDescription, this.defaultSerializerEndpointBehavior);
  9.         object[] parameters = new object[operation.OperationDescription.Messages[0].Body.Parts.Count];
  10.         dispatchFormatter.DeserializeRequest(message, parameters);
  11.         if (operation.ParameterInspectors != null)
  12.         {
  13.             parameterInspectorCorrelationStates = new object[operation.ParameterInspectors.Length];
  14.             for (int i = 0; i < operation.ParameterInspectors.Length; i++)
  15.             {
  16.                 parameterInspectorCorrelationStates[i] = operation.ParameterInspectors[i].BeforeCall(operation.OperationDescription.Name, parameters);
  17.             }
  18.         }
  19.  
  20.         message = operation.MessageFormatter.SerializeRequest(message.Version, parameters);
  21.     }
  22.  
  23.     CorrelationStateAsyncResult.State newState = new CorrelationStateAsyncResult.State
  24.     {
  25.         OriginalAsyncCallback = callback,
  26.         ExtensibilityCorrelationState = new FormatterCorrelationState
  27.         {
  28.             ClientOperation = operation,
  29.             ParameterInspectorCorrelationStates = parameterInspectorCorrelationStates,
  30.         },
  31.         OriginalAsyncState = state,
  32.     };
  33.  
  34.     IAsyncResult originalResult = this.innerChannel.BeginRequest(message, this.BeginRequestCallback, newState);
  35.     return new CorrelationStateAsyncResult(originalResult, state, operation);
  36. }
  37.  
  38. void BeginRequestCallback(IAsyncResult asyncResult)
  39. {
  40.     CorrelationStateAsyncResult.State newState = asyncResult.AsyncState as CorrelationStateAsyncResult.State;
  41.     IAsyncResult newAsyncResult = new CorrelationStateAsyncResult(asyncResult, newState.OriginalAsyncState, newState.ExtensibilityCorrelationState);
  42.     newState.OriginalAsyncCallback(newAsyncResult);
  43. }
  44.  
  45. public Message EndRequest(IAsyncResult result)
  46. {
  47.     CorrelationStateAsyncResult correlationAsyncResult = (CorrelationStateAsyncResult)result;
  48.     FormatterCorrelationState correlationState = (FormatterCorrelationState)correlationAsyncResult.CorrelationState;
  49.     OperationDescription operation = correlationState.ClientOperation.OperationDescription;
  50.     IClientMessageFormatter messageFormatter = correlationState.ClientOperation.MessageFormatter;
  51.     Message reply = this.innerChannel.EndRequest(correlationAsyncResult.OriginalAsyncResult);
  52.     if (messageFormatter != null)
  53.     {
  54.         object[] parameters = new object[operation.Messages[1].Body.Parts.Count];
  55.         object returnValue = messageFormatter.DeserializeReply(reply, parameters);
  56.         IParameterInspector[] parameterInspectors = correlationState.ClientOperation.ParameterInspectors;
  57.         if (parameterInspectors != null)
  58.         {
  59.             for (int i = 0; i < parameterInspectors.Length; i++)
  60.             {
  61.                 parameterInspectors[i].AfterCall(
  62.                     operation.Name,
  63.                     parameters,
  64.                     returnValue,
  65.                     correlationState.ParameterInspectorCorrelationStates[i]);
  66.             }
  67.         }
  68.  
  69.         SimpleReverseClientMessageFormatter dispatchFormatter = new SimpleReverseClientMessageFormatter(operation, this.defaultSerializerEndpointBehavior);
  70.         reply = dispatchFormatter.SerializeReply(reply.Version, parameters, returnValue);
  71.     }
  72.  
  73.     return reply;
  74. }

One thing to notice is that this channel must be the first one in the stack – to receive the message right after WCF formatted it, so the operation description still matches the message body – if some other channel had modified the message, the code would not work. This is enforced at the binding element responsible for creating the factory which creates this channel.

  1. public class ClientMessageFormatterBindingElement : BindingElement
  2. {
  3.     public Func<OperationDescription, IClientMessageFormatter> MessageFormatterForOperation { get; set; }
  4.     public Func<OperationDescription, List<IParameterInspector>> ParameterInspectorsForOperation { get; set; }
  5.     public DataContractSerializerEndpointBehavior DataContractSerializerEndpointBehavior { get; set; }
  6.  
  7.     private ContractDescription serviceContract;
  8.  
  9.     public ClientMessageFormatterBindingElement(ContractDescription serviceContract)
  10.     {
  11.         this.serviceContract = serviceContract;
  12.     }
  13.  
  14.     public override bool CanBuildChannelFactory<TChannel>(BindingContext context)
  15.     {
  16.         return typeof(TChannel) == typeof(IRequestChannel) &&
  17.             context.CanBuildInnerChannelFactory<TChannel>();
  18.     }
  19.  
  20.     public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context)
  21.     {
  22.         if (context == null)
  23.         {
  24.             throw new ArgumentNullException("context");
  25.         }
  26.  
  27.         if (typeof(TChannel) != typeof(IRequestChannel))
  28.         {
  29.             throw new InvalidOperationException("Invalid channel shape");
  30.         }
  31.  
  32.         if (context.Binding.Elements[0] != this)
  33.         {
  34.             throw new InvalidOperationException("This binding must be the first one in the stack");
  35.         }
  36.  
  37.         if (this.MessageFormatterForOperation == null && this.ParameterInspectorsForOperation == null)
  38.         {
  39.             return base.BuildChannelFactory<TChannel>(context);
  40.         }
  41.         else
  42.         {
  43.             ClientMessageFormatterChannelFactory factory = new ClientMessageFormatterChannelFactory(
  44.                 context.BuildInnerChannelFactory<IRequestChannel>(),
  45.                 this.serviceContract,
  46.                 this.MessageFormatterForOperation,
  47.                 this.ParameterInspectorsForOperation,
  48.                 this.DataContractSerializerEndpointBehavior ?? new DataContractSerializerEndpointBehavior());
  49.  
  50.             return (IChannelFactory<TChannel>)factory;
  51.         }
  52.     }
  53. }

I’ll skip the channel factory and the other binding element methods, as they’re similar to the ones for the message inspector.

Missing features

Those are the features which I know don’t work right now:

  • Duplex contracts don’t work (the only channel shape supported is IRequestChannel)
  • IClientChannel parameter to IClientMessageInspector.BeforeSendRequest (always set to null)
  • Messages passing through the simple message formatter do not have the ReplyTo or MessageId SOAP headers

Those I did not test, and I’d be surprised if they worked:

  • Operations with message contracts
  • Operations with untyped messages (parameter and / or return value of type System.ServiceModel.Channels.Message)
  • One way operations (the contract may have a different shape)

Please let me know if there is any missing feature which is not listed above and I can update this list.

Real World Scenario: expanding custom serialization to Windows Phone

And now I’ll revisit the custom serialization for Silverlight which I posted before. The server code is exactly the same, and I’ll show the main differences in the client code:

  1. We can’t use attributes to add the “behaviors”, so that is removed
  2. The endpoint binding now needs to be converted to a custom binding (it used to be simply BasicHttpBinding), and I needed to add the formatter binding element to it
  3. The operation behavior used to change the serializer was replaced by a class derived from the new DataContractSerializerEndpointBehavior class, and plugged directly in the binding element.

The code for calling the service is the same as before.

  1. CustomBinding binding = new CustomBinding(new BasicHttpBinding());
  2. ChannelFactory<IOrderProcessing> factory = new ChannelFactory<IOrderProcessing>(binding, this.GetOrderProcessingServiceAddress());
  3.  
  4. ClientMessageFormatterBindingElement formatterElement = new ClientMessageFormatterBindingElement(factory.Endpoint.Contract);
  5. binding.Elements.Insert(0, formatterElement);
  6. var serializerBehavior = new CustomSerializerAwareDataContractSerializerEndpointBehavior();
  7. formatterElement.DataContractSerializerEndpointBehavior = serializerBehavior;
  8. formatterElement.MessageFormatterForOperation = x =>
  9.     new SimpleClientMessageFormatter(x) {
  10.         DataContractSerializerEndpointBehavior = serializerBehavior
  11.     };
  12.  
  13. IOrderProcessing proxy = factory.CreateChannel();
  14. Order order = new Order
  15. {
  16.     Id = 1,
  17.     Date = DateTime.Now,
  18.     Items = new Product[]
  19.     {
  20.         new Product { Name = "Bread", Unit = "un", UnitPrice = 1 },
  21.         new Product { Name = "Milk", Unit = "qt", UnitPrice = 3 },
  22.     }
  23. };
  24.  
  25. proxy.BeginProcessOrder(order, delegate(IAsyncResult asyncResult)
  26. {
  27.     this.AddToDebug("In ProcessOrder callback");
  28.     try
  29.     {
  30.         proxy.EndProcessOrder(asyncResult);
  31.         this.AddToDebug("Called EndProcessOrder");
  32.     }
  33.     catch (Exception ex)
  34.     {
  35.         this.AddToDebug("Exception: {0}", ex);
  36.     }
  37.  
  38.     proxy.BeginGetOrder(1, delegate(IAsyncResult asyncResult2)
  39.     {
  40.         this.AddToDebug("In GetOrder callback");
  41.         try
  42.         {
  43.             var newOrder = proxy.EndGetOrder(asyncResult2);
  44.             this.AddToDebug("Called EndGetOrder, order = {0}", newOrder);
  45.         }
  46.         catch (Exception ex)
  47.         {
  48.             this.AddToDebug("Exception: {0}", ex);
  49.         }
  50.     }, null);
  51.  
  52.     this.AddToDebug("Called BeginGetOrder");
  53. }, null);
  54.  
  55. this.AddToDebug("Called BeginProcessOrder");

And that’s it. In the [Code in this post] link below you can find the full solution with not only this scenario, but also some other tests for the extensibility interfaces.

[Code in this post]

[Back to the index]