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.
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.
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.
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 ).
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.
More serialization extensibility, with the data contract resolver and the extension data object.