SYSK 266: SOA Versioning Best Practices and on the Importance of Using Nullable Types and IExtensibleDataObject Interface in Data Contracts

“Interfaces are immutable…” Yes, we’ve heard that for years... The reality, however, is that contracts, both -- service and data, do change as more requirements are discovered, etc. Moreover, it’s not reasonable to expect that all clients of your V1 service will migrate to V2 service at the same time; so, you need to be able to release a new version without breaking old clients.

The rule of thumb is simple: if it’s a new data member on a data type (i.e. data contract) and it’s not a required element or it’s a new method (operation) on a service type and it’s not a callback interface, then you can add it to same class; otherwise, the versioning best practices recommend that you create a new data type in a new versioned namespace (versioned namespace allows you to keep the class name the same).

Here are some “good practices” I follow:

  1.  Use versioned namespace, e.g.

namespace YourCompany.Services .V1

{

[ServiceContract(Namespace="urn:yourcompany-com :v1")]

public interface ISomeService

{

}

}

  1.  When adding a new data member, use Order named parameter on the DataMember attribute
  2. Add new data members at the end of the list
  3. When using DataContract serializer, use nullable types so you can detect those situations when V1 client called V2 service and didn’t pass required data. Missing data elements will be mapped to nulls and you can assign default values in OnDeserialized event
  4. To handle extra data (e.g. when V2 client calls V1 service), implement IExtensibleDataObject interface

namespace YourCompany.Services.V1

{

 [DataContract(Namespace="urn:yourcompany-com:v1")]

public class SomeData : System.Runtime.Serialization.IExtensibleDataObject

{

private int _dataX; // this data element was part of the version 1 release

private int _dataY; // this data element was part of the version 1 release

private int? _dataZ; // this data element was added after version 1 release

[DataMember(Order=1)] public int DataX

{

get { return _dataX; }

set { _dataX = value; }

}

[DataMember(Order=1)] public int? DataY

{

get { return _dataY; }

set { _dataY = value; }

}

// Note that order = 2 as DataZ was added after version 1 (dataX and dataY only) was already released

[DataMember(Order=2, IsRequired = false)] public int DataZ

{

get { return _dataZ; }

set { _dataZ = value; }

}

[OnDeserialized]

internal void OnDeserialized(StreamingContext ctx)

{

if (DataZ.HasValue == false)

DataZ = 0;

}

#region IExtensibleDataObject Members

private System.Runtime.Serialization.ExtensionDataObject _extData;

public virtual System.Runtime.Serialization.ExtensionDataObject ExtensionData

{

get { return _extData; }

set { _extData = value; }

}

#endregion

}

}

  1. If it’s a breaking change, e.g. a required data member is added, a signature of an existing method has to be changed, etc., then create a new type under next version namespace (e.g. YourCompany.Services.V2)
  2. Specify the default formatter on the interface, e.g.

[ServiceContract(Namespace = "urn:YourCompany-com:v1"), DataContractFormat]

public interface ISomeService

{

    [OperationContract]

    string SomeMethod(YourDataClass data);

}

  1. Use properties, not fields. E.g.

private int _dataX;

[DataMember]

public int DataX

{

    get { return _dataX; }

    set { _dataX = value; }

}

instead of

[DataMember]

public int DataX;

Note: you could choose to use XmlSerializer rather than DataContractSerializer. If that’s your choice, then:

  1. Use XmlType attribute instead of DataContract on the data class
  2. Use XmlElement attribute instead of DataMember on data properties/fields
  3. Specify element order by using Order named parameter on XmlElement attribute; e.g.

private int _dataX;

private int _dataY;

[XmlElement(Order = 1)]

public int DataX

{

    get { return _dataX; }

    set { _dataX = value; }

}

[XmlElement(Order = 2)]

public int DataY

{

    get { return _dataY; }

    set { _dataY = value; }

}

  1. For nullable members, either use .NET’s nullable types or use XmlIgnore attribute
  2. For required data members, assign default values to support Client V1 -> Service V2 situation.
  3. To handle extra data members (Client V2 -> Service V1 case), add

[XmlAnyElement(Order = 999999)]

public XmlElement[] ExtensionData;

at the end of your data class definitions