다음을 통해 공유


WCF Extensibility – Custom Serialization in Silverlight

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 a detour from the normal flow of the series, since I was working on a scenario last week which I think will add value to the series as a whole. On the post about contract behaviors, I showed an example of how to hook up a custom serializer to the WCF pipeline. That sample used the DataContractSerializerOperationBehavior extensibility in which you could inherit from that class, override the CreateSerializer methods and replace that behavior on the operation with your own custom class. That’s fairly simple and has been done extensively in WCF. However, this doesn’t work in Silverlight. This post isn’t about one specific extensibility point, but about a scenario which can be covered by the extensibility points already covered in this series, in a different platform.

The WCF portions of Silverlight is a subset (*) of the full framework. Due to many restrictions in the Silverlight platform, such as the sandbox, support for multiple OS, and primarily size (the runtime download time could not be too large, otherwise people might give up installing it), many features in the full WCF didn’t make it into SL. Other features actually went in, but in order to reduce the (large) number of extensibility points, they were made internal. The class DataContractSerializerOperationBehavior is one of those which didn’t make the cut, and for most of the cases it hasn’t been a problem – SL has been released for about 3 years now and only recently I’ve seen a couple of requests for being able to replace the serializer in SL.

(*) It’s not really a proper subset, as there are some features in SL which aren’t available in the full framework, such as the classes in the System.Json namespace; they may eventually make it to the framework, but at this time they’re only available in the WCF Codeplex site.

Real World Scenario: expanding custom serialization to Silverlight

The first thing to be able to replace the DataContractSerializerOperationBehavior in SL is to actually find out what it does at the client side. I’ve been using Reflector (while it’s free) and it shows that the ApplyClientBehavior method does two things: determines whether the operation input / output should be serialized (if the operation uses untyped messages, then no serialization is necessary, and it then creates the client formatter which will be used to serialize / deserialize the request / reply. So like in the desktop case, our new operation behavior will do something similar. Also, like in the DataContractSerializerOperationBehavior, I’ve made the method CreateSerializer virtual, so that it can be extended to easily replace the serializer.

  1. public class SLSerializerOperationBehavior : IOperationBehavior
  2. {
  3.     OperationDescription operationDescription;
  4.     public SLSerializerOperationBehavior(OperationDescription operationDescription)
  5.     {
  6.         this.operationDescription = operationDescription;
  7.     }
  8.  
  9.     public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters)
  10.     {
  11.     }
  12.  
  13.     public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
  14.     {
  15.         clientOperation.Formatter = new MyClientFormatter(operationDescription, this);
  16.         clientOperation.SerializeRequest = !IsUntypedMessage(operationDescription.Messages[0]);
  17.         clientOperation.DeserializeReply = operationDescription.Messages.Count > 1 && !IsUntypedMessage(operationDescription.Messages[1]);
  18.     }
  19.  
  20.     public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
  21.     {
  22.     }
  23.  
  24.     public void Validate(OperationDescription operationDescription)
  25.     {
  26.     }
  27.  
  28.     public virtual XmlObjectSerializer CreateSerializer(Type type, string name, string ns, IList<Type> knownTypes)
  29.     {
  30.         XmlObjectSerializer result = new DataContractSerializer(type, name, ns, knownTypes);
  31.         return new MyNewSerializer(type, result);
  32.     }
  33.  
  34.     private bool IsUntypedMessage(MessageDescription md)
  35.     {
  36.         return (md.Body.ReturnValue != null && md.Body.Parts.Count == 0 && md.Body.ReturnValue.Type == typeof(Message)) ||
  37.              (md.Body.ReturnValue == null && md.Body.Parts.Count == 1 && md.Body.Parts[0].Type == typeof(Message));
  38.     }
  39. }

And before I go further, 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 (please let me know if you find a bug or something missing). Also, for simplicity sake it doesn’t have a lot of error handling which a production-level code would, and doesn’t support all contract types (operations with ref / out parameters or duplex contracts, for example, are not supported; headers / properties from the operation context are also not added to the message; I’ve also not tested for unwrapped message contracts – it may work though).

Now for the client formatter. To be able to call the CreateSerializer method in the behavior, the formatter will hold a reference to its “parent”, as well as the operation description (which will be used to serialize the request and deserialize the response). The known types is stored once to be passed to the serializers for the parameter / return values.

  1. class MyClientFormatter : IClientMessageFormatter
  2. {
  3.     OperationDescription operationDescription;
  4.     SLSerializerOperationBehavior serializerOperationBehavior;
  5.     List<Type> knownTypes;
  6.  
  7.     public MyClientFormatter(OperationDescription operationDescription, SLSerializerOperationBehavior serializerOperationBehavior)
  8.     {
  9.         this.operationDescription = operationDescription;
  10.         this.serializerOperationBehavior = serializerOperationBehavior;
  11.         this.knownTypes = new List<Type>();
  12.         foreach (Type type in operationDescription.KnownTypes)
  13.         {
  14.             this.knownTypes.Add(type);
  15.         }
  16.     }
  17. }

In order to serialize the request, I’ve created a new BodyWriter class which first writes the operation wrapper, then it writes each parameter, wrapped by its appropriate name, using the serializer created by the SLSerializerOperationBehavior.

  1. public Message SerializeRequest(MessageVersion messageVersion, object[] parameters)
  2. {
  3.     Message result = Message.CreateMessage(
  4.         messageVersion,
  5.         this.operationDescription.Messages[0].Action,
  6.         new MyOperationBodyWriter(this.operationDescription, this.serializerOperationBehavior, this.knownTypes, parameters));
  7.     return result;
  8. }
  9.  
  10. class MyOperationBodyWriter : BodyWriter
  11. {
  12.     OperationDescription operationDescription;
  13.     SLSerializerOperationBehavior serializerOperationBehavior;
  14.     object[] operationParameters;
  15.     IList<Type> knownTypes;
  16.  
  17.     public MyOperationBodyWriter(OperationDescription operationDescription, SLSerializerOperationBehavior serializerOperationBehavior, IList<Type> knownTypes, object[] operationParameters)
  18.         : base(true)
  19.     {
  20.         this.operationDescription = operationDescription;
  21.         this.serializerOperationBehavior = serializerOperationBehavior;
  22.         this.knownTypes = knownTypes;
  23.         this.operationParameters = operationParameters;
  24.     }
  25.  
  26.     protected override void OnWriteBodyContents(XmlDictionaryWriter writer)
  27.     {
  28.         MessageDescription outgoingMessage = this.operationDescription.Messages[0];
  29.         if (outgoingMessage.Body.WrapperName != null)
  30.         {
  31.             writer.WriteStartElement(outgoingMessage.Body.WrapperName, outgoingMessage.Body.WrapperNamespace);
  32.         }
  33.  
  34.         foreach (var bodyPart in outgoingMessage.Body.Parts)
  35.         {
  36.             XmlObjectSerializer serializer = this.serializerOperationBehavior.CreateSerializer(bodyPart.Type, bodyPart.Name, bodyPart.Namespace, this.knownTypes);
  37.             serializer.WriteObject(writer, this.operationParameters[bodyPart.Index]);
  38.         }
  39.  
  40.         if (outgoingMessage.Body.WrapperName != null)
  41.         {
  42.             writer.WriteEndElement();
  43.         }
  44.     }
  45. }

Deserializing is a little simpler, and can be done in a single method. The body reader will first skip the wrapper name (<operationResponse>), then it will use the serializer from SLSerializerOperationBehavior to deserialize the return value. If this code were to support out / ref parameters, we’d need to then iterate through the body parts for the response message and deserialize those values into the parameters argument of DeserializeResponse.

  1. public object DeserializeReply(Message message, object[] parameters)
  2. {
  3.     MessageDescription incomingMessage = this.operationDescription.Messages[1];
  4.     MessagePartDescription returnPart = incomingMessage.Body.ReturnValue;
  5.     XmlDictionaryReader bodyReader = message.GetReaderAtBodyContents();
  6.     if (incomingMessage.Body.WrapperName != null)
  7.     {
  8.         bool isEmptyElement = bodyReader.IsEmptyElement;
  9.         bodyReader.ReadStartElement(incomingMessage.Body.WrapperName, incomingMessage.Body.WrapperNamespace);
  10.         if (isEmptyElement) return null;
  11.     }
  12.  
  13.     XmlObjectSerializer returnValueSerializer = this.serializerOperationBehavior.CreateSerializer(
  14.         returnPart.Type, returnPart.Name, returnPart.Namespace, this.knownTypes);
  15.     object result = returnValueSerializer.ReadObject(bodyReader, false);
  16.  
  17.     if (incomingMessage.Body.WrapperName != null)
  18.     {
  19.         bodyReader.ReadEndElement();
  20.     }
  21.  
  22.     return result;
  23. }

Now to put it all together: just like on the original sample, I’ll use a contract behavior to replace the DataContractSerializerOperationBehavior. However, since that class isn’t public in Silverlight, I needed to add a name check (a small hack). I’m also using some compiler directive to share the same file between the desktop and the Silverlight projects.

  1.     public class MyNewSerializerContractBehaviorAttribute : Attribute, IContractBehavior
  2.     {
  3.         public void AddBindingParameters(ContractDescription contractDescription, ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
  4.         {
  5.         }
  6.  
  7.         public void ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, ClientRuntime clientRuntime)
  8.         {
  9.             this.ReplaceSerializerOperationBehavior(contractDescription);
  10.         }
  11.  
  12.         public void ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime)
  13.         {
  14. #if !SILVERLIGHT
  15.             this.ReplaceSerializerOperationBehavior(contractDescription);
  16. #endif
  17.         }
  18.  
  19.         public void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint)
  20.         {
  21.             foreach (OperationDescription operation in contractDescription.Operations)
  22.             {
  23.                 foreach (MessageDescription message in operation.Messages)
  24.                 {
  25.                     this.ValidateMessagePartDescription(message.Body.ReturnValue);
  26.                     foreach (MessagePartDescription part in message.Body.Parts)
  27.                     {
  28.                         this.ValidateMessagePartDescription(part);
  29.                     }
  30.  
  31. #if !SILVERLIGHT
  32.                     foreach (MessageHeaderDescription header in message.Headers)
  33.                     {
  34.                         this.ValidateCustomSerializableType(header.Type);
  35.                     }
  36. #endif
  37.                 }
  38.             }
  39.         }
  40.  
  41.         private void ValidateMessagePartDescription(MessagePartDescription part)
  42.         {
  43.             if (part != null)
  44.             {
  45.                 this.ValidateCustomSerializableType(part.Type);
  46.             }
  47.         }
  48.  
  49.         private void ValidateCustomSerializableType(Type type)
  50.         {
  51.             if (typeof(ICustomSerializable).IsAssignableFrom(type))
  52.             {
  53.                 if (!type.IsPublic)
  54.                 {
  55.                     throw new InvalidOperationException("Custom serialization is supported in public types only");
  56.                 }
  57.  
  58.                 ConstructorInfo defaultConstructor = type.GetConstructor(new Type[0]);
  59.                 if (defaultConstructor == null)
  60.                 {
  61.                     throw new InvalidOperationException("Custom serializable types must have a public, parameterless constructor");
  62.                 }
  63.             }
  64.         }
  65.  
  66.         private void ReplaceSerializerOperationBehavior(ContractDescription contract)
  67.         {
  68.             foreach (OperationDescription od in contract.Operations)
  69.             {
  70.                 for (int i = 0; i < od.Behaviors.Count; i++)
  71.                 {
  72. #if !SILVERLIGHT
  73.                     DataContractSerializerOperationBehavior dcsob = od.Behaviors[i] as DataContractSerializerOperationBehavior;
  74.                     if (dcsob != null)
  75.                     {
  76.                         od.Behaviors[i] = new MyNewSerializerOperationBehavior(od);
  77.                     }
  78. #else
  79.                     if (od.Behaviors[i].GetType().Name == "DataContractSerializerOperationBehavior")
  80.                     {
  81.                         od.Behaviors[i] = new NewSLSerializerOperationBehavior(od);
  82.                     }
  83. #endif
  84.                 }
  85.             }
  86.         }
  87.  
  88. #if !SILVERLIGHT
  89.         class MyNewSerializerOperationBehavior : DataContractSerializerOperationBehavior
  90.         {
  91.             public MyNewSerializerOperationBehavior(OperationDescription operation) : base(operation) { }
  92.             public override XmlObjectSerializer CreateSerializer(Type type, string name, string ns, IList<Type> knownTypes)
  93.             {
  94.                 return new MyNewSerializer(type, base.CreateSerializer(type, name, ns, knownTypes));
  95.             }
  96.  
  97.             public override XmlObjectSerializer CreateSerializer(Type type, XmlDictionaryString name, XmlDictionaryString ns, IList<Type> knownTypes)
  98.             {
  99.                 return new MyNewSerializer(type, base.CreateSerializer(type, name, ns, knownTypes));
  100.             }
  101.         }
  102. #else
  103.         class NewSLSerializerOperationBehavior : SLSerializerOperationBehavior
  104.         {
  105.             public NewSLSerializerOperationBehavior(OperationDescription operation)
  106.                 : base(operation)
  107.             {
  108.             }
  109.  
  110.             public override XmlObjectSerializer CreateSerializer(Type type, string name, string ns, IList<Type> knownTypes)
  111.             {
  112.                 return new MyNewSerializer(type, base.CreateSerializer(type, name, ns, knownTypes));
  113.             }
  114.         }
  115. #endif

And that’s it. In the [Code in this post] link I have the same Order / Product / MyNewSerializer objects from the previous sample.

Coming up

Back to the normal routine, with instance providers.

[Code in this post]

[Back to the index]

Comments

  • Anonymous
    June 02, 2011
    Do you have the code for this aricle as a download - please post the link.  Thanks  Nick
  • Anonymous
    June 02, 2011
    It's in the bottom, "code in this post". It will link you to the code samples gallery, where you can download it in full.