MULTIMEDIA

WCF Services : Versioning

9/20/2010 6:02:28 PM
Services should be decoupled from their clients as much as possible, especially when it comes to versioning and technologies. Any version of the client should be able to consume any version of the service and should do so without resorting to version numbers (such as those in assemblies), because those are .NET-specific. When a service and a client share a data contract, an important objective is to allow the service and client to evolve their versions of the data contract separately. To allow such decoupling, WCF needs to enable both backward and forward compatibility, without even sharing types or version information. There are three main versioning scenarios:
  • New members

  • Missing members

  • Round-tripping, in which a new data contract version is passed to and from a client or service with an older version, requiring both backward and forward compatibility

By default, data contracts are version-tolerant and will silently ignore incompatibilities.

1. New Members

The most common change made to a data contract is adding new members on one side and sending the new contract to an old client or service. When deserializing the type, DataContractSerializer will simply ignore the new members. As a result, both the service and the client can accept data with new members that were not part of the original contract. For example, suppose the service is built against this data contract:

[DataContract]
struct Contact
{
[DataMember]
public string FirstName;

[DataMember]
public string LastName;
}

yet the client sends it this data contract instead:

[DataContract]
struct Contact
{
[DataMember]
public string FirstName;

[DataMember]
public string LastName;

[DataMember]
public string Address;
}

Note that adding new members and having them ignored in this way breaks the data contract schema compatibility, because a service (or a client) that is compatible with one schema is all of a sudden compatible with a new schema.

2. Missing Members

By default, WCF lets either party remove members from the data contract. That is, you can serialize a type without certain members and send it to another party that expects the missing members. Although normally you probably won’t intentionally remove members, the more likely scenario is when a client that is written against an old definition of the data contract interacts with a service written against a newer definition of that contract that expects new members. When, on the receiving side, DataContractSerializer does not find in the message the information required to deserialize those members, it will silently deserialize them to their default values; that is, null for reference types and a zero whitewash for value types. In effect, it will be as if the sending party never initialized those members. This default policy enables a service to accept data with missing members or return data with missing members to the client. Example 1 demonstrates this point.

Example 1. Missing members are initialized to their default values
/////////////////////////// Service Side //////////////////////////////
[DataContract]
struct Contact
{
[DataMember]
public string FirstName;

[DataMember]
public string LastName;

[DataMember]
public string Address;
}

[ServiceContract]
interface IContactManager
{
[OperationContract]
void AddContact(Contact contact);
...
}

class ContactManager : IContactManager
{
public void AddContact(Contact contact)
{
Trace.WriteLine("First name = " + contact.FirstName);
Trace.WriteLine("Last name = " + contact.LastName);
Trace.WriteLine("Address = " + (contact.Address ?? "Missing"));
...
}
...
}
/////////////////////////// Client Side //////////////////////////////
[DataContract]
struct Contact
{
[DataMember]
public string FirstName;

[DataMember]
public string LastName;
}

Contact contact = new Contact()
{
FirstName = "Juval",
LastName = "Lowy"
};

ContactManagerClient proxy = new ContactManagerClient();
proxy.AddContact(contact);

proxy.Close();


The output of Example 3-13 will be:

First name = Juval
Last name = Lowy
Address = Missing

because the service received null for the Address data member and coalesced the trace to Missing. The problem with Example 3-13 is that you will have to manually compensate this way at every place the service (or any other service or client) uses this data contract.

2.1. Using the OnDeserializing event

When you do want to share your compensation logic across all parties using the data contract, it’s better to use the OnDeserializing event to initialize potentially missing data members based on some local heuristic. If the message contains values for those members, they will override your settings in the OnDeserializing event. If it doesn’t, the event handling method provides some nondefault values. Using the technique shown here:

[DataContract]
struct Contact
{
[DataMember]
public string FirstName;

[DataMember]
public string LastName;

[DataMember]
public string Address;

[OnDeserializing]
void OnDeserializing(StreamingContext context)
{
Address = "Some default address";
}
}

the output of Example 3-13 will be:

First name = Juval
Last name = Lowy
Address = Some default address

2.2. Required members

Unlike ignoring new members, which for the most part is benign, the default handling of missing members may very likely cause the receiving side to fail further down the call chain, because the missing members may be essential for correct operation. This may have disastrous results. You can instruct WCF to avoid invoking the operation and to fail the call if a data member is missing by setting the IsRequired property of the DataMember attribute to true:

[DataContract]
struct Contact
{
[DataMember]
public string FirstName;

[DataMember]
public string LastName;

[DataMember(IsRequired = true)]
public string Address;
}

The default value of IsRequired is false; that is, to ignore the missing member. When, on the receiving side, DataContractSerializer does not find the information required to deserialize a member marked as required in the message, it will abort the call, resulting in a NetDispatcherFaultException on the sending side. For instance, if the data contract on the service side in Example 3-13 were to mark the Address member as required, the call would not reach the service. The fact that a particular member is required is published in the service metadata, and when it is imported to the client, the generated proxy definition will also have that member as required.

Both the client and the service can mark some or all of the data members in their data contracts as required, completely independently of each other. The more members that are marked as required, the safer the interaction with a service or a client will be, but at the expense of flexibility and versioning tolerance.

When a data contract that has a required new member is sent to a receiving party that is not even aware of that member, such a call is actually valid and will be allowed to go through. In other words, if Version 2 (V2) of a data contract has a new member for which IsRequired is set to true, you can send V2 to a party expecting Version 1 (V1) that does not even have the member in the contract, and the new member will simply be ignored. IsRequired has an effect only when the V2-aware party is missing the member. Assuming that V1 does not know about a new member added by V2, Table 1 lists the possible permutations of allowed or disallowed interactions as a product of the versions involved and the value of the IsRequired property.

Table 1. Versioning tolerance with required members
IsRequiredV1 to V2V2 to V1
FalseYesYes
TrueNoYes

An interesting situation relying on required members has to do with serializable types. Since serializable types have no tolerance for missing members by default, the resulting data contract will have all data members as required when they are exported. For example, this Contact definition:

[Serializable]
struct Contact
{
public string FirstName;
public string LastName;
}

will have the metadata representation:

[DataContract]
struct Contact
{
[DataMember(IsRequired = true)]
public string FirstName
{get;set;}

[DataMember(IsRequired = true)]
public string LastName
{get;set;}
}

To set the same versioning tolerance regarding missing members as the DataContract attribute, apply the OptionalField attribute on the optional member. For example, this Contact definition:

[Serializable]
struct Contact
{
public string FirstName;

[OptionalField]
public string LastName;
}

will have the metadata representation:

[DataContract]
struct Contact
{
[DataMember(IsRequired = true)]
public string FirstName
{get;set;}

[DataMember]
public string LastName
{get;set;}
}

3. Versioning Round-Trip

The versioning tolerance techniques discussed so far for ignoring new members and defaulting missing ones are suboptimal: they enable a point-to-point client-to-service call, but have no support for a wider-scope pass-through scenario. Consider the two interactions shown in Figure 1.

Figure 1. Versioning round-trip may degrade overall interaction


In the first interaction, a client that is built against a new data contract with new members passes that data contract to Service A, which does not know about the new members. Service A then passes the data to Service B, which is aware of the new data contract. However, the data passed from Service A to Service B does not contain the new members—they were silently dropped during deserialization from the client because they were not part of the data contract for Service A. A similar situation occurs in the second interaction, where a client that is aware of the new data contract with new members passes the data to Service C, which is aware only of the old contract that does not have the new members. The data Service C returns to the client will not have the new members.

This situation of new-old-new interaction is called a versioning round-trip. WCF supports handling of versioning round-trips by allowing a service (or client) with knowledge of only the old contract to simply pass through the state of the members defined in the new contract without dropping them. The problem is how to enable services/clients that are not aware of the new members to serialize and deserialize those unknown members without their schemas, and where to store them between calls. WCF’s solution is to have the data contract type implement the IExtensibleDataObject interface, defined as:

public interface IExtensibleDataObject
{
ExtensionDataObject ExtensionData
{get;set;}
}

IExtensibleDataObject defines a single property of the type ExtensionDataObject. The exact definition of ExtensionDataObject is irrelevant, since developers never have to interact with it directly. ExtensionDataObject has an internal linked list of object references and type information, and that is where the unknown data members are stored. In other words, if the data contract type supports IExtensibleDataObject, when unrecognized new members are available in the message, they are deserialized and stored in that list. When the service (or client) calls out—passing the old data contract type, which now includes the unknown data members inside ExtensionDataObject—the unknown members are serialized out into the message in order. If the receiving side knows about the new data contract, it will get a valid new data contract without any missing members. Example 2 demonstrates implementing and relying on IExtensibleDataObject. As you can see, the implementation is straightforward: just add an ExtensionDataObject automatic property with explicit interface implementation.

Example 2. Implementing IExtensibleDataObject
[DataContract]
class Contact : IExtensibleDataObject
{
ExtensionDataObject IExtensibleDataObject.ExtensionData
{get;set;}

[DataMember]
public string FirstName;

[DataMember]
public string LastName;
}

3.1. Schema compatibility

While implementing IExtensibleDataObject enables round-tripping, it has the downside of enabling a service that is compatible with one data contract schema to interact successfully with another service that expects another data contract schema. In some esoteric cases, the service may decide to disallow round-tripping and enforce its own version of the data contract on downstream services. Using the ServiceBehavior attribute, services can instruct WCF to override the handling of unknown members by IExtensibleDataObject and ignore them even if the data contract supports IExtensibleDataObject. The ServiceBehavior attribute offers the Boolean property IgnoreExtensionDataObject, defined as:

[AttributeUsage(AttributeTargets.Class)]
public sealed class ServiceBehaviorAttribute : Attribute,...
{
public bool IgnoreExtensionDataObject
{get;set;}
//More members
}

The default value of IgnoreExtensionDataObject is false. Setting it to true ensures that all unknown data members across all data contracts used by the service will always be ignored:

[ServiceBehavior(IgnoreExtensionDataObject = true)]
class ContactManager : IContactManager
{...}

When you import a data contract using Visual Studio 2010, the generated data contract type always supports IExtensibleDataObject, even if the original data contract did not. I believe that the best practice is to always have your data contracts implement IExtensibleDataObject and to avoid setting IgnoreExtensionDataObject to true. IExtensibleDataObject decouples the service from its downstream services, allowing them to evolve separately.

Other  
 
Top 10
iPhone Programming : Simplifying the Template Classes
Troubleshooting Guide - Internet Connection Problems
Getting the Most Out of the Microsoft Outlook Client : Highlighted Features in Outlook 2007
Angry Bird Space - They are back (Part 2)
Xen Virtualization : Installing Xen from Source
Windows Server 2008: Domain Name System and IPv6 - DNS in an Active Directory Domain Services Environment
Installing Windows Server 2008 R2 and Server Core : Managing and Configuring a Server Core Installation
Understanding IIS 7.0 Architecture : Request Processing in Application Pool
ASP.NET and AJAX
Microsoft SQL Server 2005 : Report Definition and Design (part 2) - Business Intelligence Development Studio
Most View
iTunes Entertainment Weekly - Movies
iPhone 3D Programming : Reflections with Cube Maps
Customizing the Browser User Interface
Programming the Mobile Web : HTML 5 (part 1)
Programming Microsoft SQL Serve 2005 : An Overview of SQL CLR - Security
Deploying an ASP.NET Application in Windows Azure
Synchronizing Mobile Data : Using RDA
Sharepoint 2010 : Business Connectivity Services Deployment Types (part 2) - Creating a Profile Page to Display BCS Results
Buyer’s Guide - Keyboard and mice (Part 1) - Logitech Wireless Solar Keyboard K750, Rosewill RKM-800RF 2.4 GHz, AVS Gear Zippy BT-637
Windows Server 2008 : Domain Name System and IPv6 - Performing Zone Transfers
SQL Server 2005 : Implementing Service Broker
Multifaceted Tests : Attempting Server-Side Includes (SSI) Injection Systematically, Attempting Log Injection Interactively & Attempting LDAP Injection Interactively
iPhone Application Development : Building a Multi-View Tab Bar Application (part 2) - Implementing the Area View
Philips Fidelio AS851 Docking Speaker for Android
The ASP.NET AJAX Infrastructure : The Script Manager Control
Programming Microsoft SQL Server 2005: Using the Data Mining Wizard and Data Mining Designer (part 2) - Creating a Mining Model
Java Mobile Edition Security : Configurations, Profiles, and JSRs
Parallel Programming : Parallel Loops
Windows Server 2003 : Implementing a GPO (part 2) - Modifying a GPO
Designing a Windows Server 2008 R2 Active Directory : Renaming an AD DS Domain