Introduction
This article will show how one can modify the serialization method of selected types if you are using DataContractSerializer
. The downloadable source code contains a complete example of the surrogate implementation and usage. The example surrogate serializes the selected classes with BinaryFormatter
. For example if you want to transfer a bigger object through WCF and you don't want to change the serializer you can use this BinarySurrogate
to shrink the size of the xml. This is just an example implementation, it is really easy to change to use a different serialization if the BinaryFormatter not the one you would like to use. ( eg. protobuf, custom serializer).
Background
It is important to know the basics of the data contract surrogation. Please read MSDN first.
The example contains two class library projects, one wcf service application, one console client. You should able to build and run these projects and configure WCF message logging if you want to check the result of the surrogation.
Using the code
The solution contains four projects:
- WcfSurrogateTutorial.Surrogate: this is a class library project. Only contains the classes and interfaces to do the surrogation without any concrete knowledge of the business logic.
- WcfSurrogateTutorial.Common: this is a class library which contains the "business logic" objects and other common classes. For example the type provider.
- WcfSurrogateTutorial.TestServer: This is a WCF Service Library project and contains the example service contracts which returns "business logic" objects.
- WcfSurrogateTutorial.TestClient: This is a console application which contains a service references to the TestServer.
With the data contract surrogate you can change the type of a class during the serialization process. We will serialize the object by hand and store the serialized string in a generic class. First we create this class which will substitute all the selected types. This is the BinaryStringContainer
, it has only one property, the BinaryString
. Every substituted class will be serialized with binary formatter and the result string will be stored in this property. Then WCF will serialize this class with DataContractSerializer
and don't have to serialize the complete object hiararchy with DataContractSerializer
.
[DataContract]
public class BinaryStringContainer
{
private string binaryString = null;
[DataMember]
public string BinaryString
{
get
{
return binaryString;
}
set
{
if (value == null)
{
throw new ArgumentNullException("BinaryString");
}
binaryString = value;
}
}
}
The first step of writing your own surrogate you should implement the IDataContractSurrogate
interface. This interface contains three important methods for us:
- GetDataContractType: gets the current type and returns the serialized type. In our case it returns the
BinaryStringContainer
type for all the types we want to serialize with BinaryFormatter
. For other types it returns what it gets. - GetObjectToSerialize: gets the object and convert to the serialized type. If the serialized type is
BinaryStringContainer
it serializes the object with BinaryFormatter
and returns a new instance with the serialized string. - GetDeserializedObject: gets the serialized object and convert back. If the serialized object is
BinaryStringContainer
it uses the BinaryFormatter
to deserialize the object and returns the original object.
We don't need to implement the other methods of the interface. We throw a not supported exception if one of them will be called anyhow. Our BinarySurrogate
class contains this implementation.
public Type GetDataContractType(Type type)
{
if (surrogatedTypes.Contains(type))
{
return typeof(BinaryStringContainer);
}
else
{
return type;
}
}
public object GetObjectToSerialize(object obj, Type targetType)
{
if (targetType == typeof(BinaryStringContainer))
{
BinaryStringContainer bs = surrogateSerializer.ConvertToSurrogate(obj);
return bs;
}
else
{
return obj;
}
}
public object GetDeserializedObject(object obj, Type targetType)
{
BinaryStringContainer bs = obj as BinaryStringContainer;
if (bs != null)
{
object ret = surrogateSerializer.ConvertFromSurrogate(bs);
return ret;
}
else
{
return obj;
}
}
To make this implementation more flexible this BinarySurrogate
uses two dependencies, configured through constructor. The IBinarySerializedTypeProvider
contains only one methods which returns the types we want to surrogate. The ISurrogateConverter
interface is the abstraction of the serialization. Its two methods are used to convert an object to BinaryStringContainer
and back. We implemented a BinarySurrogateConverter
class which uses the BinaryFormatter
to serialize an object to string. This means all the surrogated class now should be marked as Serializable
.
public class BinarySurrogateConverter : ISurrogateConverter
{
public BinaryStringContainer ConvertToSurrogate(object obj)
{
BinaryStringContainer ret = new BinaryStringContainer();
if (obj == null)
{
return ret;
}
using (MemoryStream ms = new MemoryStream())
{
IFormatter formatter = new BinaryFormatter();
formatter.Serialize(ms, obj);
var ba = ms.ToArray();
ret.BinaryString = Convert.ToBase64String(ba);
}
return ret;
}
public object ConvertFromSurrogate(BinaryStringContainer bs)
{
if (bs.BinaryString == null)
{
return null;
}
object ret;
byte[] ba = Convert.FromBase64String(bs.BinaryString);
using (MemoryStream ms = new MemoryStream(ba))
{
IFormatter formatter = new BinaryFormatter();
ret = formatter.Deserialize(ms);
}
return ret;
}
The type provider interface doesn't have implementation in the Surrogate project because it should know concrete classes. So we create a type converter provider class in the Common project. As you can see it only returns the SurrogatedData
type, we only want to surrogate this type and leave the others unmodified.
public class TestSurrogateTypeConverter : IBinarySerializedTypeProvider
{
private static readonly List<Type> types = new List<Type> { typeof(SurrogatedData) };
public IEnumerable<Type> GetBinarySerializableTypes()
{
return types;
}
}
The next step is to apply this surrogate on your endpoint. We create an Attribute class which can be attached to the ServiceContracts interface. This attribute iterates throuh all Operation and apply the surrogate by setting the DataContractSerializerOperationBehavior
class DataContractSurrogate
property.
foreach (OperationDescription od in contract.Operations)
{
DataContractSerializerOperationBehavior dataContractBehavior = od.Behaviors.Find<DataContractSerializerOperationBehavior>() as DataContractSerializerOperationBehavior;
if (dataContractBehavior != null)
{
dataContractBehavior.DataContractSurrogate = new BinarySurrogate(typeProvider, surrogateConverter);
}
else
{
dataContractBehavior = new DataContractSerializerOperationBehavior(od);
dataContractBehavior.DataContractSurrogate = new BinarySurrogate(typeProvider, surrogateConverter);
od.Behaviors.Add(dataContractBehavior);
}
}
On server side you only need to apply this attribute on the ISurrogateTestService
service contract interface. The server will serialize the SurrogatedData
class as a BinaryStringContainer
type.
[ServiceContract]
[UseBinarySurrogateBehavior(typeof(TestSurrogateTypeConverter), typeof(BinarySurrogateConverter))]
public interface ISurrogateTestService
{
[OperationContract]
SurrogatedData GetSurrogatedData();
[OperationContract]
NonSurrogatedData GetNonSurrogatedData();
}
On the client side we should apply this surrogate class to deserialize the object correctly. In order to deserialize the objects you should have reference for the concrete types in the client assembly. If you generate proxy classes with Visual Studio you should check the reuse option for that assembly. Otherwise the BinaryFormatter
deserialization will fail because the object is defined in a not referenced assembly. On the client side you cannot use the Attribute, so should apply the surrogate by hand. But the method is the same as on the server side. See ApplyDataContractSurrogate
method.
Now you can start you WCF service and run the client console app. The console app first gets the surrogated data and then gets the non surrogated data. Both data should work as normal, the surrogation is transparent. If you want to check the difference you should log the WCF messages.
The surrogated response only contains the BinaryString
which is the base64 encoded string representation of the object.
<MessageLogTraceRecord Time="2016-03-31T17:02:49.8664839+02:00" Source="TransportSend" Type="System.ServiceModel.Dispatcher.OperationFormatter+OperationFormatterMessage" xmlns="http://schemas.microsoft.com/2004/06/ServiceModel/Management/MessageTrace">
<Addressing>
<Action>http://tempuri.org/ISurrogateTestService/GetSurrogatedDataResponse</Action>
</Addressing>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<GetSurrogatedDataResponse xmlns="http://tempuri.org/">
<GetSurrogatedDataResult xmlns:a="http://schemas.datacontract.org/2004/07/WcfSurrogateTutorial.Surrogate" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<a:BinaryString>AAEAAAD/////AQAAAAAAAAAMAgAAAFJXY2ZTdXJyb2dhdGVUdXRvcmlhbC5Db21tb24sIFZlcnNpb249MS4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj1udWxsBQEAAAAuV2NmU3Vycm9nYXRlVHV0b3JpYWwuVGVzdF...</a:BinaryString>
</GetSurrogatedDataResult>
</GetSurrogatedDataResponse>
</s:Body>
</s:Envelope>
</MessageLogTraceRecord>
The non surrogated object looks like any other normal DataContract serialized object. Contains all the properties and list itmes as xml elements.
<MessageLogTraceRecord Time="2016-03-31T17:02:50.2063213+02:00" Source="TransportSend" Type="System.ServiceModel.Dispatcher.OperationFormatter+OperationFormatterMessage" xmlns="http://schemas.microsoft.com/2004/06/ServiceModel/Management/MessageTrace">
<Addressing>
<Action>http://tempuri.org/ISurrogateTestService/GetNonSurrogatedDataResponse</Action>
</Addressing>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<GetNonSurrogatedDataResponse xmlns="http://tempuri.org/">
<GetNonSurrogatedDataResult xmlns:a="http://schemas.datacontract.org/2004/07/WcfSurrogateTutorial.TestServer" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<a:Id>8669505</a:Id>
<a:ListValue xmlns:b="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<b:string>7568d8e1-6431-4f39-9238-a5948a3bf9e0</b:string>
<b:string>7a53e6e1-fbb7-4ef5-bd6c-aa89c220dd8a</b:string>
<b:string>a82de840-3dff-4586-8cb3-c338d68fa782</b:string>
.
.
.
<b:string>aa2252dd-4952-463c-ac33-973f46f92f90</b:string>
</a:ListValue>
<a:StringValue>Non surrogated data</a:StringValue>
</GetNonSurrogatedDataResult>
</GetNonSurrogatedDataResponse>
</s:Body>
</s:Envelope>
</MessageLogTraceRecord>
Points of Interest
In our project we use this surrogation the improve serialization performance. We don't want the change the WCF serializer but we would like to serialize big object hierarchy faster. We use BinaryFormatter
, protobuf and custom serializers for different types.
History
Keep a running update of any changes or improvements you've made here.