Section 3.5. Versioning


3.5. Versioning

Services should be decoupled as much as possible from their clients, 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 allowing 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, where a new version is passed to and from an old version, requiring both backward and forward compatibility

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

3.5.1. New Members

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

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

and yet the client may send 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 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.

3.5.2. Missing Members

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

Example 3-7. Missing members are initialized to their default value

 /////////////////////////// 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( ); contact.FirstName = "Juval"; contact.LastName = "Lowy"; ContactManagerClient proxy = new ContactManagerClient( ); proxy.AddContact(contact); proxy.Close( ); 

The output of Example 3-7 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.

3.5.2.1. Using the OnDeserializing event

You can use the OnDeserializing event to initialize potentially missing data members based on some local heuristic. If the message contains the values, it will override your settings in the OnDeserializing event; if not, you will have some non-default value:

 [DataContract] struct Contact {    [DataMember]    public string FirstName;    [DataMember]    public string LastName;    [DataMember]    public string Address;    [OnDeserializing]    void OnDeserializing(StreamingContext context)    {       Address = "Some default address";    } } 

in which case the output of Example 3-7 will be:

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

3.5.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 DataContractSerializer on the receiving side 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. If the data contract on the service side in Example 3-7 were to mark the Address member as required, the call would not have reached 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 file definition will include the correct setting for it.

Both the client and the service can mark some or all of the data members on their data contracts as required, completely independently of each other. The more members that are marked as required, the safer the interaction is with a service or a client, 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, even if IsRequired is set to true on a new member by Version 2 (V2) of a data contract, you can send V2 to a party expecting Version 1 (V1) that does not even have the member in the contract. The new member will simply be ignored. IsRequired only has an effect when the member is missed by V2-aware parties. Assuming that V1 does not know about a new member added by V2, Table 3-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 3-1. Versioning tolerance with required members

IsRequired

V1 to V2

V2 to V1

False

Yes

Yes

True

No

Yes


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

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

will have the metadata representation of:

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

In order to have the same versioning tolerance regarding missing members as with the DataContract attribute, apply the OptionalField attribute on the member. For example, this Contact definition:

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

will have the metadata representation of:

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

3.5.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 3-4.

Figure 3-4. Versioning round-trip may degrade overall interaction


In the first interaction, a client that is built against a new data contract with new members is passing 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 membersthey were silently dropped during deserialization from the client because they were not part of the data contract for Service A. A similar situation occurs when 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. If Service C returns the data to the client, the data will not have the new members.

This situation of new-old-new interaction is called versioning round-trip. WCF supports handling of versioning round-tripping. A service (or client) that knows about the old contract can just pass though the state of the new members without dropping them. The problem is how to serialize and deserialize the unknown members without their schema, and where to store them in between calls. The 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. If the data contract type supports IExtensibleDataObject, then 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. If the receiving side knows about the new data contract, it will get a valid new data contract without any missing members. Example 3-8 demonstrates implementing and relying on IExtensibleDataObject. As you can see, the implementation is straightforwardjust add an ExtensionDataObject property that accesses a matching member variable.

Example 3-8. Implementing IExtensibleDataObject

 [DataContract] class Contact : IExtensibleDataObject {    ExtensionDataObject m_ExtensionData;    public ExtensionDataObject ExtensionData    {       get       {          return m_ExtensionData;       }       set       {          m_ExtensionData = value;       }    }    [DataMember]    public string FirstName;    [DataMember]    public string LastName; } 

3.5.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. The service can instruct WCF to override the handling of unknown members by IExtensibleDataObject and ignore them even if the data contract supports IExtensibleDataObject. To that end, you need to use the ServiceBehavior attribute. The next chapter will discuss behaviors and this attribute at length. For the purposes of this discussion, 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. By setting it to TRue, 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 SvcUtil or Visual Studio, 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.

There is no need for implementing IExtensibleDataObject when dealing with known types because the subclass is always deserialized without a loss.





Programming WCF Services
Programming WCF Services
ISBN: 0596526997
EAN: 2147483647
Year: 2004
Pages: 148
Authors: Juval Lowy

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net