Blog from Saravanan Arumugam

Let us talk about Technologies

Versioning of Data Contracts in WCF


The main advantage of using Service Oriented architecture is that the server and the client can be completely decoupled. This is achieved by making the client know only the XML format of the message being sent to the server; they do not have a binary level code dependency.

To achieve the complete decoupling, both the server and client need to be version tolerant of its count part.

There are 3 scenarios from which the version conflict can occur between a service and a client. Let us see the scenarios one by one and understand the offer from WCF to over come them.

For our discussion, let us consider the following service.

namespace BookShop
{
    [ServiceContract(Name = "BookShop", Namespace = "http://www.8fingergenie.com/")]
    public interface IBookShopService
    {
        [OperationContract]
        Book GetBookByTitle(string title);
        [OperationContract]
        Book GetBook(long id);
        [OperationContract]
        long CheckAvailability(Book book);
        [OperationContract]
        long AddBook(string title, string author);
        [FaultContract(typeof(Fault))]
        [OperationContract]
        void BuyBook(Book book);
    }
}

 

The definition of the service contract is as follows.

namespace BookShop
{
     public class BookShopService : IBookShopService
    {
        #region Private fields
        private static List<BookStock> bookShelf = new List<BookStock>();
        private static long nextBookId = 1;
        #endregion
        #region IBookShop Members
        public long AddBook(string title, string author)
        {
            BookStock bookStock = bookShelf.Find(delegate(BookStock b)
            {
                return (b.Book.Title == title);
            });
            if (bookStock == null)
            {
                bookStock = new BookStock();
                bookStock.Book = new Book();
                bookStock.Book.Author = author;
                bookStock.Book.Title = title;
                bookStock.Book.BookID = nextBookId++;
                bookStock.Quantitiy = 1;
            }
            else
            {
                bookStock.Quantitiy++;
            }
            bookShelf.Add(bookStock);
            return bookStock.Book.BookID;
        }
        public void BuyBook(Book book)
        {
            BookStock bookStock = bookShelf.Find(delegate(BookStock b)
            {
                return b.Book == book;
            });
            if (bookStock == null)
            {
                Fault f = new Fault();
                f.FaultReason = "Requested Book is not available";
                throw new FaultException<Fault>(f);
            }
            bookStock.Quantitiy–;
        }
        public Book GetBookByTitle(string title)
        {
            BookStock bookStock = bookShelf.Find(delegate(BookStock b)
            {
                return b.Book.Title == title;
            });
            return bookStock.Book;
        }
        public Book GetBook(long id)
        {
            BookStock bookStock = bookShelf.Find(delegate(BookStock b)
            {
                return b.Book.BookID == id;
            });
            return bookStock.Book;
        }
        public long CheckAvailability(Book book)
        {
            BookStock bookStock = bookShelf.Find(delegate(BookStock b)
            {
                return b.Book.Title == book.Title;
            });
            return bookStock.Quantitiy;
        }
        #endregion
    }
}

 

Main attraction is on the Data Contract here.

namespace BookShop
{
    [DataContract(Namespace = "http://8fingergenie.com&quot;)]
    public class Book
    {
        [DataMember(IsRequired = false)]
        public long BookID;
        [DataMember(IsRequired = true)]
        public string Title;
        [DataMember(IsRequired = false)]
        public string Author;
    }
    [DataContract(Namespace = "http://8fingergenie.com&quot;)]
    public class BookStock
    {
        [DataMember(IsRequired = false)]
        public Book Book;
        [DataMember(IsRequired = true, EmitDefaultValue = true)]
        public long Quantitiy;
    }
}

 

Adding New Member to a Data Contract

Adding a new Member in the Data contract is the most common version change that occurs.

But the DataContractSerializer will simply ignore the new members while deserializing the type. So both the client and server can operate with the data having the new members that were not present in the original contract.

Here in the Book type, let us add a new member (Edition) and analyze the impact of this.

So the result becomes,

[DataContract(Namespace = "http://8fingergenie.com")]
   public class Book
   {
       [DataMember(IsRequired = false)]
       public long BookID;

       [DataMember(IsRequired = true)]
       public string Title;

       [DataMember(IsRequired = false)]
       public string Author;

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

Note that the Edition is mentioned as a Required field.

But the client has the original version of the contract i.e, without the new member.

<xs:schema xmlns:tns="http://8fingergenie.com&quot; elementFormDefault="qualified" targetNamespace="http://8fingergenie.com&quot; xmlns:xs="http://www.w3.org/2001/XMLSchema"&gt;
   <xs:complexType name="Book">
     <xs:sequence>
       <xs:element minOccurs="0" name="Author" nillable="true" type="xs:string" />
       <xs:element minOccurs="0" name="BookID" type="xs:long" />
       <xs:element name="Title" nillable="true" type="xs:string" />
     </xs:sequence>
   </xs:complexType>
   <xs:element name="Book" nillable="true" type="tns:Book" />

</xs:schema>

However with the older version of the data contract, the communication still works well because of the inbuilt version tolerance capacity of WCF. As described previously the new member would be ignored while deserialization process.

The resulting on wire message is shown below.

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<ActivityId CorrelationId="ecc9eedb-c493-49f8-a508-e935099321ac" xmlns="http://schemas.microsoft.com/2004/09/ServiceModel/Diagnostics">8ad0e625-587e-4484-b92c-e4ef4bd9fba7</ActivityId>
</s:Header>
<s:Body>
<GetBookResponse xmlns="http://www.8fingergenie.com/">
<GetBookResult xmlns:a="http://8fingergenie.com" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<a:Author>Spencer Johnson</a:Author>
<a:BookID>1</a:BookID>
<a:Edition>1.0</a:Edition>
<a:Title>Who Moved My Cheese</a:Title>
</GetBookResult>
</GetBookResponse>
</s:Body>
</s:Envelope>

The Edition is present in the message. But while deserializing it to an old version, the new member is ignored.

 

Members Missing from a Data Contract

In situations when a sender (service or client) sends a data to a receiver with missing member/element, the default behavior of WCF is to accept the data with Missing member.

How does it do it?

When a member is missing, if that is an optional member in the receiving end, DataContractSerializer fills the missing member with default value. That is null for reference types and 0 for value types.

Note: In the data contract by default every member is an optional member, except if explicitly specified as IsRequired = true.

When the missing member is a required field, WCF would throw an exception. This is possibly the best thing to do at this situation.

From my example, if we’ll come across this situation if the client passes the Book object to Server.

Here the client has the Author, BookID and Title as members, whereas the service has an additional member to it “Edition”.

    [DataContract(Namespace = "http://8fingergenie.com&quot;)]
    public class Book
    {
        [DataMember(IsRequired = false)]
        public long BookID;

        [DataMember(IsRequired = true)]
        public string Title;

        [DataMember(IsRequired = false)]
        public string Author;

        [DataMember(IsRequired = false)]
        public string Edition;
    }

When the client makes a call to the server without Edition in the member filled as null.

If the Edition is marked with IsRequired = true, then any such communication would result in exception.

 

Round tripping

The scenarios discussed so far are adequate for most scenarios. However there is an interesting scenario called roundtripping.

Let us say v1 is the original contract. v2 is a new contract version with newer members in it.

Suppose there is a communication happens from an entity (client or server), let us call it Point-A, with v2 version of contract to an entity, Point-B, with v1 version of the contract. And then after some manipulation,  the receiver Point-B has to send back the data to the original sender Point-A.

Here there are two communications happening. When Point-A sends the data contract, Point-B would not have any idea of the additional members.

Then how would Point-A hold the additional member values and send them back to Point-A?

This can be done by implementing IExtensibleDataObject in the Point-B. This interface implements only one Property called ExtensionData.

When the DataContractSerializer deserializes data at Point-B, the unknown members are accumulated into the System.Runtime.Serialization.ExtensionDataObject.

During the second communication when the data is transmitted from Point-B to Point-A, the ExtensionDataObject is serialized into the v2 members.

In my example, in the client side I have a search button with which I can either search by Title of the book or id of the book.

        protected void SearchButton_Click(object sender, EventArgs e)
        {
            using (BookShopClient client = new BookShopClient(preferedBinding))
            {
                Book bookSearched = null;
                if (!string.IsNullOrEmpty(SearchByTitleText.Text))
                {
                    GetBookByTitleRequest request = new GetBookByTitleRequest(SearchByTitleText.Text);
                    GetBookByTitleResponse response = client.GetBookByTitle(request);
                    bookSearched = response.GetBookByTitleResult;
                }
                else if (!string.IsNullOrEmpty(SearchByIdText.Text))
                {
                    long bookId;
                    long.TryParse(SearchByIdText.Text, out bookId);
                    GetBookRequest request = new GetBookRequest(bookId);
                    GetBookResponse response = client.GetBook(request);
                    bookSearched = response.GetBookResult;
                }
                if (bookSearched != null)
                {
                    CheckAvailabilityRequest request = new CheckAvailabilityRequest(bookSearched);
                    CheckAvailabilityResponse response = client.CheckAvailability(request);
                    ResultLabel.Text = String.Format("{0} books are available!", response.CheckAvailabilityResult);
                }
            }
        }

When I search using the id or title, GetBook() operation is called which in turn returns a Book object.

Here in the client side, Book doesn’t have the member “Edition”. But when the server returns the response, the Book object returned would contain Edition.

During deserialization, BookID, Title, Author are mapped to the respective members of the Book object in client. Edition value received from server is stored in ExtentionData property of the Book object.

When the CheckAvailabilty() operation is called, the same Book object is sent as parameter. Here during the serialization, “Edition” is retrieved from ExtensionData and put as a Book’s member, so that the service would get back the original data without any loss.

Here is the on wire data.

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<ActivityId CorrelationId="b6955eb2-06ba-4f98-a1c0-fa128ab9fe8a" xmlns="http://schemas.microsoft.com/2004/09/ServiceModel/Diagnostics">db08b8e9-94d3-4849-9fb6-3703a2572d7e</ActivityId>
</s:Header>
<s:Body>
<GetBookResponse xmlns="http://www.8fingergenie.com/">
<GetBookResult xmlns:a="http://8fingergenie.com" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<a:Author>Spencer Johnson</a:Author>
<a:BookID>1</a:BookID>
<a:Edition>1.0</a:Edition>
<a:Title>Who Moved My Cheese</a:Title>
</GetBookResult>
</GetBookResponse>
</s:Body>
</s:Envelope>

 

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://www.8fingergenie.com/BookShop/CheckAvailability</Action>
<ActivityId CorrelationId="4c4d8eda-a7ed-4b2b-97e2-4bb875d39292" xmlns="http://schemas.microsoft.com/2004/09/ServiceModel/Diagnostics">5a4db2e8-5dc6-4dec-89ac-308f46c1538c</ActivityId>
</s:Header>
<s:Body>
<CheckAvailability xmlns="http://www.8fingergenie.com/">
<book xmlns:d4p1="http://8fingergenie.com" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<d4p1:Author>Spencer Johnson</d4p1:Author>
<d4p1:BookID>1</d4p1:BookID>
<d4p1:Edition>1.0</d4p1:Edition>
<d4p1:Title>Who Moved My Cheese</d4p1:Title>
</book>
</CheckAvailability>
</s:Body>
</s:Envelope>

You can notice that Edition is available in GetBook() operation’s response and the same value can be seen in CheckAvailability() operation’s request. Between these the value is actually preserved in ExtentionData Property.

 

Summary

WCF has made all the necessary moves for decoupling the service provider and consumer by providing various Version Tolerance mechanisms.

We have seen in this paper about three scenarios where version tolerance mechanism of WCF ensures that the service is intact regardless of data contracts’ version changes.

Advertisements

One response to “Versioning of Data Contracts in WCF

  1. naresh December 20, 2012 at 7:08 am

    Good explanation

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: