Udostępnij za pośrednictwem


WCF Extensibility – Serialization Surrogates

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.

After the callbacks, the next extensibility point on the list for serialization are the surrogates. This will be a short post, since the surrogate idea is simple (once you understand it) and there is some good documentation on MSDN about this issue, so I don’t think I need to repeat it here.

Surrogates have been around for a while, even before WCF, and their idea is simple: replacing one type A which is part of an object graph to be serialized with another type B (the “surrogate”). The main reasons why we’d want to do that are either because the type A isn’t serializable at all, or because it doesn’t have a serialization format which we want, so we use a surrogate to change it. The first case is straightforward – sometimes you have a type from a 3rd party or a legacy library which cannot be modified to accommodate serialization, but it’s part of the object graph which you want to exchange between client and server. One possible solution is to replicate the graph in Data Transfer Objects (DTO) which only contain the data which needs to be serialized. Sometimes, however, this may not be the best approach (too many types, high cost to convert between DTOs and objects with business logic, etc.), so a surrogate can be an way out. The second case (wanting to change the serialization format) doesn’t happen very often, but there are some scenarios where users want to change the way a type is serialized.

Before WCF was released, the way to implement surrogates was by using the ISurrogateSelector and ISerializationSurrogate interfaces.. Most of the previous “serializers” (including the BinaryFormatter and the SoapFormatter) supported it via the IFormatter interface. When the formatters were serializing an object graph, they’d search in their “surrogate chain” to see if any surrogate was present to deal with each type in the graph; if this was the case, then it would use the surrogate to serialize / deserialize the objects instead of the object itself.

With WCF, and the notion of a data contract, those surrogates could still be used without problems during runtime, but their contract didn’t specify how the data would be serialized (it’s just some code being executed after all), so the whole notion of a shared contract cannot really be enforced using the traditional surrogacy. Enter the new interface, IDataContractSurrogate, which, besides the ability to replace one type with another for serialization during runtime, also contains additional methods which are invoked during type export and import (when the contract is being exposed in the metadata to be consumed by tools such as svcutil.exe or Add Service Reference, or when those tools are being used to create a service proxy). This new surrogate interface is supported by both the DataContractSerializer and the DataContractJsonSerializer (although as far as I know for the latter the import / export methods aren’t called) – the NetDataContractSerializer implements the IFormatter interface, so it supports the “old-style” surrogacy, and the XmlSerializer doesn’t have support for surrogates.

Interface definition

  1. public interface IDataContractSurrogate
  2. {
  3.     object GetCustomDataToExport(MemberInfo memberInfo, Type dataContractType);
  4.     object GetCustomDataToExport(Type clrType, Type dataContractType);
  5.     Type GetDataContractType(Type type);
  6.     object GetDeserializedObject(object obj, Type targetType);
  7.     void GetKnownCustomDataTypes(Collection<Type> customDataTypes);
  8.     object GetObjectToSerialize(object obj, Type targetType);
  9.     Type GetReferencedTypeOnImport(string typeName, string typeNamespace, object customData);
  10.     CodeTypeDeclaration ProcessImportedType(CodeTypeDeclaration typeDeclaration, CodeCompileUnit compileUnit);
  11. }

As I mentioned, the IDataContractSurrogate interface is quite complex, and most implementations don’t use all of its methods. The main ones (the ones which are implemented in the majority of uses) are the ones called in the runtime (serialization and deserialization): GetDataContractType (given a type in the original data contract, return a possible surrogate type which can be used in place of it), GetObjectToSerialize (during serialization, can be used to replace one object with another) and GetDeserializedObject (during deserialization, does the opposite of GetObjectToDeserialize). The MSDN article about data contract surrogates has a more thorough description of all the methods, so I’ll just leave the link here instead of repeating what is already there.

How to use a data contract surrogate

When creating the WCF serializers (both the DataContractSerializer and the DataContractJsonSerializer) you can use one of their constructor overloads which take an IDataContractSurrogate parameter, so this is straightforward. The example below shows one non-serializable type which is part of an object graph (NonSerializablePerson isn’t serializable because it’s not decorated with any attributes, and does not have a parameter-less constructor). The surrogate is used to replace it with another type, and when it’s deserialized, the surrogate does the opposite, recreating the original type from the replacement one.

  1. public class NonSerializablePerson
  2. {
  3.     public string Name { get; private set; }
  4.     public int Age { get; private set; }
  5.  
  6.     public NonSerializablePerson(string name, int age)
  7.     {
  8.         this.Name = name;
  9.         this.Age = age;
  10.     }
  11.  
  12.     public override string ToString()
  13.     {
  14.         return string.Format("Person[Name={0},Age={1}]", this.Name, this.Age);
  15.     }
  16. }
  17.  
  18. public class Family
  19. {
  20.     public NonSerializablePerson[] Members;
  21.  
  22.     public override string ToString()
  23.     {
  24.         StringBuilder sb = new StringBuilder();
  25.         sb.AppendLine("Family members:");
  26.         foreach (var member in this.Members)
  27.         {
  28.             sb.AppendLine("  " + member);
  29.         }
  30.  
  31.         return sb.ToString();
  32.     }
  33. }
  34.  
  35. [DataContract]
  36. public class PersonReplacement
  37. {
  38.     [DataMember(Name = "PersonName")]
  39.     public string Name { get; set; }
  40.     [DataMember(Name = "PersonAge")]
  41.     public int Age { get; set; }
  42. }
  43.  
  44. public class MyPersonSurrogate : IDataContractSurrogate
  45. {
  46.     public Type GetDataContractType(Type type)
  47.     {
  48.         if (type == typeof(NonSerializablePerson))
  49.         {
  50.             return typeof(PersonReplacement);
  51.         }
  52.         else
  53.         {
  54.             return type;
  55.         }
  56.     }
  57.  
  58.     public object GetDeserializedObject(object obj, Type targetType)
  59.     {
  60.         if (obj is PersonReplacement)
  61.         {
  62.             PersonReplacement person = (PersonReplacement)obj;
  63.             return new NonSerializablePerson(person.Name, person.Age);
  64.         }
  65.  
  66.         return obj;
  67.     }
  68.  
  69.     public object GetObjectToSerialize(object obj, Type targetType)
  70.     {
  71.         if (obj is NonSerializablePerson)
  72.         {
  73.             NonSerializablePerson nsp = (NonSerializablePerson)obj;
  74.             PersonReplacement serializablePerson = new PersonReplacement
  75.             {
  76.                 Name = nsp.Name,
  77.                 Age = nsp.Age,
  78.             };
  79.  
  80.             return serializablePerson;
  81.         }
  82.  
  83.         return obj;
  84.     }
  85.  
  86.     public void GetKnownCustomDataTypes(Collection<Type> customDataTypes)
  87.     {
  88.         throw new NotSupportedException("unused");
  89.     }
  90.  
  91.     public object GetCustomDataToExport(Type clrType, Type dataContractType)
  92.     {
  93.         throw new NotSupportedException("unused");
  94.     }
  95.  
  96.     public object GetCustomDataToExport(MemberInfo memberInfo, Type dataContractType)
  97.     {
  98.         throw new NotSupportedException("unused");
  99.     }
  100.  
  101.     public Type GetReferencedTypeOnImport(string typeName, string typeNamespace, object customData)
  102.     {
  103.         throw new NotSupportedException("unused");
  104.     }
  105.  
  106.     public CodeTypeDeclaration ProcessImportedType(CodeTypeDeclaration typeDeclaration, CodeCompileUnit compileUnit)
  107.     {
  108.         throw new NotSupportedException("unused");
  109.     }
  110. }
  111.  
  112. class SurrogatesTest
  113. {
  114.     public static void Test()
  115.     {
  116.         DataContractSerializer dcs = new DataContractSerializer(typeof(Family), null, int.MaxValue, false, false, new MyPersonSurrogate());
  117.         MemoryStream ms = new MemoryStream();
  118.         Family myFamily = new Family
  119.         {
  120.             Members = new NonSerializablePerson[]
  121.             {
  122.                 new NonSerializablePerson("John", 34),
  123.                 new NonSerializablePerson("Jane", 32),
  124.                 new NonSerializablePerson("Bob", 5),
  125.             }
  126.         };
  127.         dcs.WriteObject(ms, myFamily);
  128.         Console.WriteLine("Serialized: {0}", Encoding.UTF8.GetString(ms.ToArray()));
  129.         ms.Position = 0;
  130.         object newFamily = dcs.ReadObject(ms);
  131.         Console.WriteLine("Deserialized:");
  132.         Console.WriteLine(newFamily);
  133.     }
  134. }

What about surrogates using within the context of WCF services? One possible way is to replace the serializer used in WCF, which involves replacing the DataContractSerializerOperationBehavior. That surely works, but using surrogates is something simple enough that WCF added a property to that behavior itself, so it doesn’t need to be replaced with a custom class. In the example below, we use the same Family type in a service, but we set the DataContractSurrogate property to our surrogate. Notice that we need to do it on both the server and the client, otherwise when the server (or the proxy) is being opened an exception would be thrown.

  1. [ServiceContract]
  2. public interface IFamilyManager
  3. {
  4.     [OperationContract]
  5.     void AddMember(string name, int age);
  6.     [OperationContract]
  7.     Family GetFamily();
  8. }
  9.  
  10. public class FamilyService : IFamilyManager
  11. {
  12.     static List<NonSerializablePerson> members = new List<NonSerializablePerson>();
  13.  
  14.     public void AddMember(string name, int age)
  15.     {
  16.         members.Add(new NonSerializablePerson(name, age));
  17.     }
  18.  
  19.     public Family GetFamily()
  20.     {
  21.         return new Family { Members = members.ToArray() };
  22.     }
  23. }
  24.  
  25. class SurrogatesTest
  26. {
  27.     static void DefineSurrogate(ServiceEndpoint endpoint)
  28.     {
  29.         foreach (var operation in endpoint.Contract.Operations)
  30.         {
  31.             var dcsob = operation.Behaviors.Find<DataContractSerializerOperationBehavior>();
  32.             dcsob.DataContractSurrogate = new MyPersonSurrogate();
  33.         }
  34.     }
  35.  
  36.     public static void Test()
  37.     {
  38.         string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
  39.         var host = new ServiceHost(typeof(FamilyService), new Uri(baseAddress));
  40.         var endpoint = host.AddServiceEndpoint(typeof(IFamilyManager), new BasicHttpBinding(), "");
  41.         host.Open();
  42.  
  43.         DefineSurrogate(endpoint);
  44.  
  45.         Console.WriteLine("Host opened");
  46.  
  47.         var factory = new ChannelFactory<IFamilyManager>(new BasicHttpBinding(), new EndpointAddress(baseAddress));
  48.         DefineSurrogate(factory.Endpoint);
  49.         var proxy = factory.CreateChannel();
  50.  
  51.         proxy.AddMember("John", 34);
  52.         proxy.AddMember("Jane", 32);
  53.  
  54.         Console.WriteLine(proxy.GetFamily());
  55.  
  56.         ((IClientChannel)proxy).Close();
  57.         factory.Close();
  58.  
  59.         Console.Write("Press ENTER to close the host");
  60.         Console.ReadLine();
  61.         host.Close();
  62.     }
  63. }

One more comment: for endpoints which take / return JSON data (i.e., the ones which use the DataContractJsonSerializer), we also set the DataContractSurrogate property on the DataContractSerializerOperationBehavior – there’s no DataContractJsonSerializerOperationBehavior (the type name is already too large Smile).

Final thoughts about data contract surrogates

In the example above we showed how to use a surrogate to serialize a type which isn’t serializable itself, but it doesn’t always have to be this way. It’s perfectly fine to use a surrogate to replace a serializable type, it works just as well. The main exception for this are the types considered as primitives by the serializers . Those types cannot be surrogated (this is a common request). The only workaround which I know is the one I showed in the previous post, using callbacks to convert between the primitive type and the expected output.

Coming up

More serialization extensibility, with the data contract resolver and the extension data object.

[Back to the index]

Comments

  • Anonymous
    October 15, 2015
    Great article. But what if NonSerializeablePerson is a object array? I have this problem with my application. I tried using KnownTypes as per the documentation but this doesn't work either. Can you not use Surrogates and KnownTypes together?