Introduction
In most scenarios, when we design a service, we create a service contract and data contracts for the types used in the service. We let the service description be generated by the service contract, and we won’t ever have to look at the underlying XML, but sometimes, things don’t go that well in situations like preexisting types which implement SerializableAttribute
and ISerializable
, mapping an incoming message to types which are not serializable or which don’t match the predefined schema.
Internally, WCF represents all the messages with the Message
class which is defined in the System.ServiceModel.Channels
namespace. The Message
class generate a SOAP envelop which consists of a message header and a message body. The message body contains the payload. The Message
class provides an interface to interact with the message body or the message header.
The most common way of serialization in a WCF service is to mark the type with the DataContract
attribute and mark each member which is to be serialized with the DataMember
attribute.
Background
The reader should be familiar with WCF, and how to create a WCF service and data contracts. The reader should also be aware of web services.
Types that support serialization
- Types marked with
DataContractAttribute
or CollectionDataContractAttribute
. - Types decorated with
SerializableAttribute
which can optionally implement the ISerializable
or IXmlSerializable
interface. MessageContract
decorated with MessageContractAttribute
and containing the data contracts or the serializable types as header or body. - We can include any of these types, but the resulting message format will change based on the choice of the serializer.
There are two types of serializers: DataContractSerializer
and XmlSerializer
. By default, WCF uses DataContractSerializer
for message serialization. Here are the differences between both the serializers.
- In the case of
DataContract
, the DataContractSerializer
serializes the fields marked with the DataMember
attribute (irrespective of public or private), while XmlSerializer
serializes only public properties and fields. - In the case of
SerializableAttribute
, the DataContractSerializer
serializes all fields, while XmlSerializer
serializes only the public fields. - In the case of the
IXmlserializable
interface, we provide the schema and control the serialization. - In the case of
MessageContract
, the DataContractSerializer
serializes members marked with the header or body attribute, while XmlSerializer
serializes only the public properties or fields. - In the case of
DataContractSerializer
, constructors are not called during deserialization. However, if you need to perform initialization, DataContractSerializer
supports the [OnDeserializing]
, [OnDeserialized]
, [OnSerializing]
, and [OnSerialized]
callbacks that were also supported on the Binary/SoapFormatter
classes.
Message encoding
There exists two types of encoding, Document/literal and RPC. Actually, they are two programming models, one is the RPC programming model and the other is the Message Passing programming model. One programming model says that if you are talking to a service, you should always explicitly form a message and use explicit message-passing infrastructure to send the message to the service and, optionally, to receive the response. The following is an example of a potential implementation of such a mechanism (from the caller's perspective):
Channel chan = new ChannelFactory.CreateChannel(endpointAddress);
ActionInvocationMessage msg = new ActionInvocationMessage();
msg.Action = "SayHello";
msg.Parameters.Add(new StringParameter("Forename", "Rich"));
msg.Parameters.Add(new StringParameter("Surname", "Turner"));
Console.WriteLine(chan.SendMessageSync(msg, new SayHelloCallback(HandleResponse)));
The other camp aims to simplify the developer experience by reusing development constructs in wide use today, that results in code such as the following:
HelloWorldProxy proxy = new HelloWorldProxy(endpointAddress);
Console.WriteLine(proxy.SayHello("Rich", "Turner"));
In both the DataContractSerializer
and the XmlSerializer
, default the value of style
is document/literal format, but we can use RPC encoding using the OperationFormatStyle
enumeration in the event you need it. Each serializer has a style
property based on the enumeration, and can be set to one of its two elements, Document
and RPC
.
DataContractFormatAttribute
We can use the RPC style like this:
[ServiceContract(Name="ServiceContract",
Namespace="http://www.ServiceContract.com/Samples/RPCExample")]
[DataContractFormat(Style=OperationFormatStyle.Rpc)]
public interface IRPCStyleTest
XmlSerializerFormatAttribute
We can support the RPC style in the case of XmlSerializer
like this:
[ServiceContract(Name="ServiceContract",
Namespace="http://www.ServiceContract.com/Samples/RPCExample")]
[XmlSerializerFormat(Style=OperationFormatStyle.Rpc,
Use=OperationFormatUse.Encoded)]
public interface IRPCStyleTest
Serialization/Encoding
Actually, there exists two contracts when discussing .NET types in the context of serialization. First is the .NET contract which has properties, methods, and constructors, and the other is the contract which has the serializable properties and which would be exposed to the word. Each serializer comes with some algorithms which define the mapping details, and includes attributes that let us customize the mapping. This mapping is stored with the metadata which serializers can access at runtime and decide which property to serialize.
There exists an inbuilt tool (svc.exe) for moving between these contract representations. We can use xsd.exe while working with XmlSerializer
. Svc.exe can generate the .NET data type with all the attributes from the XML schema definition. If we pass a .NET assembly to svc.exe, it will automatically generate all the types which support serialization. So, we can write the data contracts in either XML schema or in .NET code.
Windows Communication Foundation lets us specify the encoding to be used. Serialization defines how the .NET object maps to XML, while Encoding defines how the XML is written out to a stream of bytes. WCF supports three types of encoding: text, binary, and message transmission optimization mechanism (MTOM). We can also add custom encoding.
If you represent the same logical data by the three encodings, the stream will be totally different, but they all represent the same logical data. Encoding isn’t considered to be part of the service contract, but it is the configuration entry. We can change it dynamically, and we can control it by endpoint binding. Different kinds of encoding are used in different types of word scenarios. If interoperability is a concern, then text type encoding is used, while if performance is the concern, then binary encoding can be used.
When WCF creates a message at runtime, it chooses the serializer based on the service contract, and selects the encoding based on the binding configuration. Let’s see how this is accomplished. We can take the following type for understanding the serializers:
[DataContract]
public class Name
{
[DataMember]
public string firstName;
public string middlename;
[DataMember]
public string lastName;
}
DataContractSerializer
We can use the following method to generate the serialized stream from DataContractSerializer
:
public static void SerializeClass()
{
Name name = new Name { firstName = "John",
middlename = "Fuma", lastName = "last" };
using (Message message = Message.CreateMessage(MessageVersion.Soap11WSAddressing10,
"http://www.ServiceContract.com/Samples/RPCExample/Name", name,
new DataContractSerializer(typeof(Name))))
{
using (FileStream fs = new FileStream("binary.bin", FileMode.Create))
{
using (XmlDictionaryWriter writer =
XmlDictionaryWriter.CreateBinaryWriter(fs))
{
message.WriteMessage(writer);
}
}
}
}
The CreateMessage
method’s last parameter specifies the serializer. The serializer controls how the message body has the Name
object as XML. The writer determines which encoding will be used to write the message out to the byte stream. XmlDictionaryWriter
and XmlDictionaryReader
can be used to read and write the type in any encoding (text, binary, MTOM).
Here is the generated file view in Visual Studio:
The following method can be used to generate the Name
type again from the bin file. We can use XmlDictionaryReader
and the corresponding decoder to do the reverse engineering, and get the original message.
public static void ReadSerializeClass()
{
using (FileStream fs = File.OpenRead("binary.bin"))
{
using (XmlDictionaryReader reader =
XmlDictionaryReader.CreateBinaryReader(fs, XmlDictionaryReaderQuotas.Max))
{
using (Message message = Message.CreateMessage(
reader, 1024, MessageVersion.Soap11WSAddressing10))
{
Name name = message.GetBody<name />(
new DataContractSerializer(typeof(Name)));
}
}
}
}
In case we don’t want to read the XML infoset directly, we can call GetReaderAtBodyContents
instead of GetBody
, which returns the XmlReader
and reads the specific elements.
XmlSerializer
XmlSerializer
is quite an old technology which is used in web services. Windows Communication Foundation supports it for backwards compatibility. I am not going to discuss a lot about it as there are a lot of articles about it available already. Here is the sample code which serializes the same Name
object. We can see that it includes the middle name too as middle name is the public property.
Serialization method:
public static void WriteNameXmlSerializer()
{
Name name = new Name { firstName = "John",
middlename = "Fuma", lastName = "last" };
using (FileStream fs = new FileStream("Name.xml", FileMode.Create))
using (XmlDictionaryWriter writer =
XmlDictionaryWriter.CreateTextWriter(fs))
{
XmlSerializer serializer = new XmlSerializer(typeof(Name));
serializer.Serialize(writer, name);
}
}
Serialized stream:
<Name xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<firstName>John</firstName>
<middlename>Fuma</middlename>
<lastName>last</lastName>
</Name>
Deserialization method:
public static void ReadNameXmlSerializer()
{
using (FileStream fs = new FileStream("Name.xml", FileMode.Open))
using (XmlDictionaryReader reader =
XmlDictionaryReader.CreateTextReader(fs, XmlDictionaryReaderQuotas.Max))
{
XmlSerializer serializer = new XmlSerializer(typeof(Name));
Name person = (Name)serializer.Deserialize(reader);
}
}
NetDataContractSerializer
DataContractSerializer
has two modes: SharedContract
and SharedType
. The modes reflect whether you want to share the schema contract across the wire or the .NET type information.
In general, sharing schema contract is the preferred and recommended approach, as it loosens the coupling across the wire, but WCF still wants to support .NET Remoting style applications where maintaining type across the wire is important. WCF spilts both the modes into separate classes, so now, WCF has DataContractSerializer
(SharedContract
) and NetDataContractSerializer
(SharedType
). Since WCF wants to encourage DataContractSerializer
, it is the default serializer.
NetDataContractSerializer
supports the same mapping algorithm and the same customization attributes, but in the case of NetDataContractSerializer
, type information is not required ahead of time as in DataContractSerializer
. We can see it from the following code:
NetDataContractSerializer serializer = new NetDataContractSerializer();
serializer.WriteObject(writer, p);
We can use the following method to serialize the Name
class:
public static void SerializeClass()
{
Name name = new Name { firstName = "John", middlename = "Fuma", lastName = "last" };
NetDataContractSerializer netData = new NetDataContractSerializer();
using (FileStream fs = new FileStream("NetData.xml", FileMode.Create))
{
netData.Serialize(fs, name);
}
}
Here is the serialized stream. It contains the type information, so there is no need for specifying the type while doing the deserialization.
<Name z:Id="1" z:Type="HelloIndigo.Name" z:Assembly="HelloIndigo,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
xmlns="http://schemas.datacontract.org/2004/07/HelloIndigo"
xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/">
<firstName z:Id="2">John</firstName>
<lastName z:Id="3">last</lastName>
</Name>
Following are some precautions that we have to take while doing the serilalization:
- When serializing types that use inheritance, all the types in the hierarchy should be serializable either via the
DataContract
or the Serializable
attribute. - WCF also provide a
KnowType
attribute which can be used to substitute the type which serializers will recognize at runtime. This lets us specify the Serializable
attribute to some base type and send some derived type. - We can use
IDataContractSurrogate
in case we find that there is some property which is not serializable. We can implement this interface, and the serializer will call it while doing the serialization.
Conclusion
Windows Communication Foundation takes care of everything, but it is important to understand how each serializer works and which one is the best on a particular scenario. You should use DataContractSerializer
whenever possible in WCF. It provides interoperability. NetDataContractSerializer
should be used when you need type fidelity across the wire. You should user XmlSerializer
when you need backwards compatibility with ASMX pages, or you are starting with the existing types which don’t follow the DataContract
rules.
References