Windows Communication Foundation Tutorial - Part 2 (DataContract vs Serializable)
In the previous tutorial in this series we looked at creating our first Windows Communication Foundation service. It was a simple Hello World service that quite literally returned “Hello, [Name]” over the wire, but it allowed us to begin looking at some of the core concepts associated with this new technology and it gave us a chance to explore what it was like to work with WCF. In this article we will look at working with more complex objects on the wire, rather than just simple primitive types (integers, strings, etc.). More specifically, we will look at how WCF allows for the handling of these more complex objects. We will analyze two common, straightforward means of defining the serialization to be used for our objects within the context of a WCF service and then follow this up by looking at the implications to the client of each approach. We will follow this in the next tutorial with a look at creating explicit message objects to represent our communication and this will involve looking at the MessageContract, MessageBody and MessageHeader attributes.
Many of us today have libraries of carefully-crafted business objects to represent our problem domain. Of course, in a services world these objects are only useful if we can get them down to and back from our client. In the past this could be accomplished by simply adding the Serializable attribute to our classes. This was definitely not a considerably challenging nor time-consuming task, but with the ease of use came a severe lack of control over property ordering, versioning or even whether the property went down the wire at all! There were some compromises through some of the attributes in the System.Xml.Serialization namespace, such as XmlIgnore, but these were not entirely adequate nor where they sufficiently explicit. In addition, while the xml classes were more flexible, they of course only worked for xml serialization. There was the flipside – performing custom serialization by implementing ISerializable or IXmlSerializable, for instance, but those of us who went this route found out some of the serious challenges and pitfalls they held, aside from simply being too explicit. A variety of serialization options are available to us in 3.0 and WCF. We will not be examining all of these but will look, in this introductory tutorial, at two simple options - the standard Serializable attribute and the new WCF DataContracts attributes. For a more complete discussion on the various options in WCF, see Sowmy Srinivasan's blog post "WCF Serialization Programming Model" or Aaron Skonnard's MSDN Magazine article "Serialization in Windows Communication Foundation".
For our example we will be using the simple Customer object shown below:
using System;
namespace Elegantware.net.Samples.DotNet3.WCF.Tutorial2.ApplicationLayer
{
public class Customer
{
private string _firstName;
private DateTime _dateOfBirth;
private DateTime _someOtherDate = DateTime.Now;
private bool _isDirty = true;
private DateTime DateOfBirth
{
get { return _dateOfBirth; }
set { _dateOfBirth = value; }
}
private DateTime SomeOtherDate
{
get { return _someOtherDate; }
set { _someOtherDate = value; }
}
public string FirstName
{
get { return _firstName; }
set { _firstName = value; }
}
public bool IsDirty
{
get { return _isDirty; }
set { _isDirty = value; }
}
}
}
In this example, one of the concepts we will test is explicitly removing a property, in this case is the IsDirty property. It is one of those properties that makes sense in an OO context, but not in a Services one. In practice it may sit in a base class, but we will leave it in here to test. In addition, the DateOfBirth property has been made private to see how this is handled between the two approaches.
In the WCF serialization space there are two standard options – you can continue to use the standard Serializable attribute approach, which is useful for pre-existing objects where it may applied already (effectively backwards compatibility), or you can use the new DataContract and DataMember attributes. These latter attributes work in tandem and allow for very simple-to-use but very powerful serialization options. They also, as we shall see, produce different final results in the client. The first difference we notice is that these attributes appear in the System.Runtime.Serialization namespace, because they control the general WCF serialization of the object rather than just its xml-serialized version. The DataContract attribute is applied at the class level and it controls the serialized name of the object as well as the namespace. The DataMember attribute is applied to the specific properties you want to be included. It allows you to specify the serialized name of the property, the order in which it appears within the object as well as whether or not it is a required attribute. There existed a Version property in early beta releases of WCF, but this was removed in favor of explicit versioning guidelines. Quite importantly, the DataMember attribute works independently of the regular scope of the field or property. For instance, a private or internal property could be marked with the attribute and thus made available via a WCF service, even though it was inaccessible from elsewhere within the object hierarchy or client code.
This article will explore the differences between each approach, so we will be using both. The classic Serializable version involves adding the following attribute to the class:
[Serializable]
public class SerCustomer
{
...
}
Notice also that the class name has been changed to reflect the serialization option used. This is only to highlight the differences for later comparison and we will do the same for our DataContract (i.e. WCF) version. Adding this single attribute clearly involves very little effort and very little change to our class. Recall, however, that it also provides very limited features as well. In contrast, the DataContract approach involves the following changes:
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Serialization;
namespace Elegantware.net.Samples.DotNet3.WCF.Tutorial2.ApplicationLayer
{
[DataContract()]
public class WCFCustomer
{
private string _firstName;
private DateTime _dateOfBirth;
private DateTime _someOtherDate = DateTime.Now;
private bool _isDirty = true;
[DataMember]
public string FirstName
{
get { return _firstName; }
set { _firstName = value; }
}
private DateTime DateOfBirth
{
get { return _dateOfBirth; }
set { _dateOfBirth = value; }
}
[DataMember(IsRequired=true)]
private DateTime SomeOtherDate
{
get { return _someOtherDate; }
set { _someOtherDate = value; }
}
public bool IsDirty
{
get { return _isDirty; }
set { _isDirty = value; }
}
}
}
As you can see, we’re specifying that the class itself implements a Data contract (i.e. it is something we want to serialize) and we mark the properties we want to transmit as DataMembers. Notice also that the SomeOtherDate property is marked as a DataMember even though it is private and also that it has the IsRequired attribute property set to true. We will examine the meanings of these options shortly.
Next we need to test how WCF will implement the proxies and transport of these classes, so we need to complete our WCF service. The interface contract appears below. To make the situation a bit more real-world, the “business objects” and the service interface and implementation have been placed in separate assemblies (ApplicationLayer and ServiceImplementation, respectively). We’re dealing with customers, so it is not unreasonably to create a "CMS" service:
using System;
using System.ServiceModel;
using Elegantware.net.Samples.DotNet3.WCF.Tutorial2.ApplicationLayer;
namespace Elegantware.net.Samples.DotNet3.WCF.Tutorial2.ServiceLayer
{
[DataContractFormat]
[ServiceContract]
interface ICMSService
{
[OperationContract]
SerCustomer GetSerCustomer(int id);
[OperationContract]
WCFCustomer GetWCFCustomer(int id);
}
}
Notice that we are just returning each of our object types, and the actual implementation is quite arbitrary:
using System;
using Elegantware.net.Samples.DotNet3.WCF.Tutorial2.ApplicationLayer;
namespace Elegantware.net.Samples.DotNet3.WCF.Tutorial2.ServiceLayer
{
class CMSService : ICMSService
{
public SerCustomer GetSerCustomer(int id)
{
SerCustomer serCustomer = new SerCustomer();
serCustomer.FirstName = "Hilton";
return serCustomer;
}
public WCFCustomer GetWCFCustomer(int id)
{
WCFCustomer wcfCustomer = new WCFCustomer();
wcfCustomer.FirstName = "Hilton";
return wcfCustomer;
}
}
}
The final steps in creating an actual service (configuring the host application, .svc file and config settings) are left to the reader as an exercise as per the previous tutorial. The code to return a customer is reasonably arbitrary, as you can well see, and we won't even examine a sample client. When we generate the client proxy class, however, we get the following:
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "3.0.0.0")]
[System.Runtime.Serialization.DataContractAttribute(Namespace="http://schemas.datacontract.org/2004/07/Elegantware.net
.Samples.DotNet3.WCF.Tutor" +
"ial2.ApplicationLayer")]
[System.SerializableAttribute()]
public partial class SerCustomer : object, System.Runtime.Serialization.IExtensibleDataObject
{
[System.NonSerializedAttribute()]
private System.Runtime.Serialization.ExtensionDataObject extensionDataField;
private System.DateTime _dateOfBirthField;
private string _firstNameField;
private bool _isDirtyField;
private System.DateTime _someOtherDateField;
public System.Runtime.Serialization.ExtensionDataObject ExtensionData
{
get
{
return this.extensionDataField;
}
set
{
this.extensionDataField = value;
}
}
[System.Runtime.Serialization.DataMemberAttribute(IsRequired=true)]
public System.DateTime _dateOfBirth
{
get
{
return this._dateOfBirthField;
}
set
{
this._dateOfBirthField = value;
}
}
[System.Runtime.Serialization.DataMemberAttribute(IsRequired=true)]
public string _firstName
{
get
{
return this._firstNameField;
}
set
{
this._firstNameField = value;
}
}
[System.Runtime.Serialization.DataMemberAttribute(IsRequired=true)]
public bool _isDirty
{
get
{
return this._isDirtyField;
}
set
{
this._isDirtyField = value;
}
}
[System.Runtime.Serialization.DataMemberAttribute(IsRequired=true)]
public System.DateTime _someOtherDate
{
get
{
return this._someOtherDateField;
}
set
{
this._someOtherDateField = value;
}
}
}
We can take note of a number of points on interest on this class. First of all, after all of our explicit work, it is somehow become tagged with the DataContract attribute! In addition, it has also been given a namespace explicitly. The class also has been marked Serializable so that the client can both send and receive serialized instances from our service. The class is marked 'partial', perhaps so that it can be extended by the client code. In addition, it implements an interface called IExtensibleDataObject, as well as what appears to be a matching private field-public property pair called extensionDataField and ExtensionData, respectively. These names and data types (ExtensionDataObject) are pretty self evident, and reflecting on the datatype reveals that it "Stores data from a versioned data contract that has been extended by adding new members". All of this indicates that this is to ensure that the serialization does not break (i.e. that the client will continue to work) if the schema of this class changes in future, but only if these changes involve adding new properties. This class will not be able to resubmit these new properties back to the service, but neither will it break if it suddenly encounters un-planned-for properties in future upon receiving an instance from the service. What is of further interest, however, is that reflection reveals the ExtensionDataObject to have no members at all, so it's use is likely to be rather limited. Earlier implementations implemented what amounted to an untyped key-value HashTable, but this appears to have been removed.
Aside from what has been added to our class, we can also see how our own implementation has been transferred to the client. We can see that the default serialization has in fact totally ignored our properties and simply transferred all of our fields as is, including our internal naming mechanisms. For instance, our FirstName property was backed by a private _firstName property, and the corresponding public property in the client code is named '_firstName'. Similarly, our 'private' implementations, i.e. those fields we perhaps did not want to be exposed publicly (like DateOfBirth) have indeed been transferred, even though they were not even accessible by the service implementation code itself! Our only recourse at the moment is to mark with [System.NonSerialized()] whichever private fields we want to ensure are not transferred. All of this is, though, is actually in line with how the Serializable attribute works generally - it is very simple to use but very inflexible.
Having examined our Serialization customer, we now turn our attention to the DataContract version, shown below.
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "3.0.0.0")]
[System.Runtime.Serialization.DataContractAttribute(Namespace="http://schemas.datacontract.org/2004/07/Elegantware.net.
Samples.DotNet3.WCF.Tutor" +
"ial2.ApplicationLayer")]
[System.SerializableAttribute()]
public partial class WCFCustomer : object, System.Runtime.Serialization.IExtensibleDataObject
{
[System.NonSerializedAttribute()]
private System.Runtime.Serialization.ExtensionDataObject extensionDataField;
[System.Runtime.Serialization.OptionalFieldAttribute()]
private string FirstNameField;
private System.DateTime SomeOtherDateField;
public System.Runtime.Serialization.ExtensionDataObject ExtensionData
{
get
{
return this.extensionDataField;
}
set
{
this.extensionDataField = value;
}
}
[System.Runtime.Serialization.DataMemberAttribute()]
public string FirstName
{
get
{
return this.FirstNameField;
}
set
{
this.FirstNameField = value;
}
}
[System.Runtime.Serialization.DataMemberAttribute(IsRequired=true)]
public System.DateTime SomeOtherDate
{
get
{
return this.SomeOtherDateField;
}
set
{
this.SomeOtherDateField = value;
}
}
}
This version required us to be more explicit within our service code and so, as we would hope, it more closely matches our original definitions. Only two properties (FirstName and SomeOtherDate) are transferred, and they retain the names we would want them to have (this name is overrideable using the DataMember attribute) as well as our additional settings (IsRequired). This latter setting, as its name implies, means that a specific property is required to have a value during serialization, so that a null value is not allowed and it allows us to ensure that a value is supplied for a specific property. Notice that the SerCustomer we examined above implements DataContract automatically on the client and that each of the fields is marked as IsRequired.
Additional points on interest on the DataContract class are that it also implements IExtensibleDataObject and that the FirstNameField is marked as an OptionalField, which reflection reveals is to ensure that the BinaryFormatter and SoapFormatter do not break if it does not exist. Incidentally, the WCF approach is also flexible on the use of properties and fields. Unlike the Serializable approach, you can choose to mark either properties or fields with DataMember or even to mix the use of both in the same class, but this is not recommended.
Additional More Advanced Serialization Options
In this article we have so far examined two basic implementions for Windows Communication Foundation serialization. We have seen the basic options for the most simple serialization and also seen how the new DataContract-DataMember WCF approach works. This latter option provides some additional features that we did examine, such as property ordering, and it is also accompanied by explicit versioning guidelines, as mentioned earlier. However, being a rich framework, WCF also provides a variety of additional serialization options, some of which make use of the svcutil command to further customise existing options as well as additional serialization mechanisms, such as xml serialization that some of us may be more familiar with. The xml serialization options are also quite powerful and finer-grained. They are found in the System.Xml.Serialization namespace and provide a variety of options. These options will not be discussed here, but more info can be found in the articles mentioned in the introduction for a comparison between all of the options with a WCF context. To those experienced with the settings that this approach offers, what is important to note is that a WCF service by default uses the new DataContractFormat serializer which does not utilise the System.Xml.Serialization settings. To leverage these settings, the XmlSerializerFormat attribute must be used in place of the default DataContractFormat option. Either of these attributes can be applied at either the entire service- or just specific operations- (methods) level. For instance, the service interface definition can be left as is to use the default DataContractFormat serializer, with a single method marked with the XmlSerializerFormat attribute. Alternatively, the entire service could be marked with XmlSerializerFormat to make use of existing classes and then new methods marked with DataContractFormat to migrate slowly should this be required. An example is shown below:
using System;
using System.ServiceModel;
using Elegantware.net.Samples.DotNet3.WCF.Tutorial2.ApplicationLayer;
namespace Elegantware.net.Samples.DotNet3.WCF.Tutorial2.ServiceLayer
{
[DataContractFormat] // Not necessary as it is the default - shown here to be explicit
[ServiceContract]
interface ICMSService
{
...
[XmlSerializerFormat]
[OperationContract]
XMLSerCustomer GetXMLSerCustomer(int id);
...
}
}
It must be remembered, however, that unlike asmx web serrvices, Windows Communication Foundation services are far more flexible in the final implementation technologies utilised, so changing the default formatter, and thus forcing xml serialization, is not recommended without good reason.
So far we have looked how to create and invoke a simple Windows Communication Foundation service as well as how to make use of our own custom objects in a WCF context. While this is technically sufficient and will enable you to make use of some of the great features of WCF, it does not fully-employ the rich service-based approach inherent within the framework. In the next tutorial we will begin examining some of this in more detail by looking at the new message attributes, which will also wrap up our analysis of the main attributes within WCF.