Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

WCF Backward Compatibility and Versioning Strategies – Part 2

4.82/5 (14 votes)
13 Mar 2012CPOL8 min read 65.4K   475  
WCF’s behavior in different cases of versioning Data Contracts.

Introduction

This article illustrates several aspects of WCF Data Contract versioning and the practices adopted for providing backward compatibility to contracts. Once the service is developed and rolled over to production, any changes in contracts should be backward compatible so that existing clients are not affected when changes are deployed. In the previous article WCF Backwards Compatibility and Versioning Strategies – Part 1 I explored the effects of changes in a Service Contract in different scenarios. In this second article, we will explore WCF’s behavior in different cases of versioning Data Contracts.

Backward compatibility across Data Contracts

Modification in a WCF service can happen in Data Contracts. Let’s explore the different cases through an example. In order to explore WCF’s backward compatibility behavior, we will develop a simple WCF service project. The WCF service has a single method UpdateEmployeeData() which returns a type EmpInfo with updated employee information.

Implementing a WCF service

Let’s develop a simple WCF service project. The WCF service has a single method UpdateEmployeeData() which has a parameter and return type of the user defined type EmpInfo.

The Data Contract EmpInfo is as follows:

C#
[DataContract]
public class EmpInfo
{
   [DataMember]
   public int EmpID;
   [DataMember]
   public string EmpName;
}

The service implementation is as follows:

C#
namespace MyWcfService
{
    // NOTE: You can use the "Rename" command on the "Refactor" menu
    // to change the class name "Service1" in code, svc and config file together.
    public class MyService : IMyService
    {
        public EmpInfo UpdateEmployeeData(EmpInfo info)
        {
            return new EmpInfo { EmpID = info.EmpID, EmpName = info.EmpName };
        }
    }
}

Once you are done with the development of the WCF service, right click -> View in Browser on the file MyService.svc in order to check the WSDL generated by the WCF service.

Develop a console application in order to test the service.

Right click on References->Add Service Reference. Click on Discover or type the service URL in the Address box. Once you click on OK, the stub code will be generated. Now call the service in the console application using the following code:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using WCFTestClient.MyService;

namespace WCFTestClient
{
    class Program
    {
        static void Main(string[] args)
        {
            var sc = proxy.UpdateEmployeeData(new EmpInfo { EmpID = 1, EmpName = "Kausik"});  
             Console.WriteLine("Employee ID: " + sc.EmpID);
             Console.WriteLine("Employee Name: " + sc.EmpName);

        }
    }
}

Run the console application. The following output should be generated:

Image 1

Modification of Data Contracts

Now let’s check the behavior of the client in case of modification of Data Contracts:

Case 1: Adding Add new non-required members

There is an IsRequired property for data members whose default value is false. If the property is set to true for any data member, WCF tells the serialization engine that the value must be presented in the underlying XML. Since we have not mentioned the property, the default value false has been considered.

Modify the Data Contract EmpInfo. Add a non–required member EmpAddress.

Modified Data Contract:

Image 2 

Modified service implementation:

We have set the value for the newly added member.

C#
namespace MyWcfService
{
    // NOTE: You can use the "Rename" command on the "Refactor" menu
    // to change the class name "Service1" in code, svc and config file together.
    public class MyService : IMyService
    {
        public EmpInfo UpdateEmployeeData(EmpInfo info)
        {
            return new EmpInfo { EmpID = info.EmpID, 
                       EmpName = info.EmpName, EmpAddress = info.EmpAddress };        
        }
    }

Build the service.

Now run the existing consumer client console application without updating the service reference. We want to call the service using the old stub code. You should get the following output:

Image 3

So, we can see the existing client of the service remains unaffected on adding new non-required members to the service Data Contracts.

Case 2: Adding new required members

Modify the Data Contract EmpInfo. Add a required member EmpCity:

Image 4

Modified Service implementation:

We have set the value for the newly added member.

C#
public class MyService : IMyService
{
    public EmpInfo UpdateEmployeeData(EmpInfo info)
    {
        return new EmpInfo { EmpID = 1, EmpName = "Kausik",
               EmpAddress = info.EmpAddress, EmpCity = info.EmpCity };
    }
}

Build the service.

Now run the existing consumer client console application without updating the service reference. An exception is supposed to occur as follows:

Image 5 

As we have added a required Data Member EmpCity, WCF expects the value of EmpCity must be present while a message is being traversed from the client to the service. Since we are not providing the value of the required member EmpCity from the client, the application encounters an error.

Case 3: Remove non-required members

Update the service reference and run the client. The client should successfully run. Also our stub code got updated and the newly added required/non-required members are included in the EmpInfo class in stub code. Change the client code in order to send the value for the updated members EmpAddress and EmpCity:

C#
namespace WCFTestClient
{
    class Program
    {
        static void Main(string[] args)
        {
            using (MyServiceClient proxy = new MyServiceClient())  
            {
                var sc = proxy. UpdateEmployeeData(
                  new EmpInfo { EmpID = 1, EmpName = "Kausik",
                  EmpAddress="Salt Lake", EmpCity="Kolkata"});  
                Console.WriteLine("Employee ID: " + sc.EmpID);
                Console.WriteLine("Employee Name: " + sc.EmpName);
                Console.WriteLine("Employee Name: " + sc.EmpAddress);
                Console.WriteLine("Employee Name: " + sc.EmpCity);
            }
            Console.ReadLine();  
        }
    }
}

Run the client. You should get the following output:

Image 6 

Now modify the service in order to remove the non-required member EmpAddress.

Modified Data Contract:

C#
[DataContract]
public class EmpInfo
{
    [DataMember]
    public int EmpID;
    [DataMember]
    public string EmpName;
    [DataMember(IsRequired = true)]
    public string EmpCity;
}

Modified Service implementation:

C#
namespace MyWcfService
{
    // NOTE: You can use the "Rename" command on the "Refactor"
    // menu to change the class name "Service1" in code, svc and config file together.
    public class MyService : IMyService
    {
        public EmpInfo UpdateEmployeeData(EmpInfo info)
        {
            return new EmpInfo { EmpID = info.EmpID, 
                   EmpName = info.EmpName,  EmpCity = info.EmpCity };
        }
    }
}

Build the service.

Run the client  application. You should get the following output:

Image 7

In this case, the service is unable to return the full dataset back to the client. The value sent for the member EmpAddress gets lost at the service end.

Case 4: Removing required members

Modify the service in order to remove the required member EmpCity:

C#
[DataContract]
public class EmpInfo
{
    [DataMember]
    public int EmpID;
    [DataMember]
    public string EmpName;
}

Modified Service implementation:

C#
namespace MyWcfService
{
    // NOTE: You can use the "Rename" command on the "Refactor" menu
    // to change the class name "Service1" in code, svc and config file together.
    public class MyService : IMyService
    {
        public EmpInfo UpdateEmployeeData(EmpInfo info)
        {
            return new EmpInfo { EmpID = info.EmpID, EmpName = info.EmpName};            
        }
    }
}

Build the service.

Now run the existing consumer client console application without updating the service reference. An exception is supposed to be occurred as follows:

Image 8 

So, in this case, an exception is thrown when the client receives responses from the service with missing values.

Case 5: Modify existing member data types

While modifying Data Contracts, there could be a case where the types of individual Data Members are modified. Before exploring the case, let’s make the application stable.

Update the service reference and remove the EmpAddress/EmpCity manipulation part from the client code as follows:

C#
namespace WCFTestClient
{
    class Program
    {
        static void Main(string[] args)
        {
            using (MyServiceClient proxy = new MyServiceClient())  
            {
                var sc = proxy.UpdateEmployeeData(new EmpInfo { EmpID = 1, EmpName = "Kausik"});  
                Console.WriteLine("Employee ID: " + sc.EmpID);
                Console.WriteLine("Employee Name: " + sc.EmpName);
                                  
            }
            Console.ReadLine();  

        }
    }
}

Run the client. The client should successfully run.

In order to explore the case, let’s modify the type of the EmpID Data Member of the EmpInfo Data Contract as follows:

Image 9

Build the service.

Now run the client. Since we have modified primitive types (from int to string) the client might run successfully and produce the desired output. The client sends the EmpID value as int which is then converted to a string at the service end. But in most cases, if types are compatible, no exception is thrown but unexpected results might be received.

Versioning round trip

Let’s consider the Case - 1 scenario and try to investigate about the newly added data member EmpAddress. It is understood that the newly added Data Member has been serialized and passed over the wire. Only the EmpInfo class in our old stub code has not been modified to include the new member EmpAddress. So in  this case, a "versionNew" service sends data with some newly added members to a "versionOld" client. The client ignores the newly added members when processing the message, but it resends that same data, including the newly added members, back to the versionNew service. This is called versioning round-trip. The typical scenario for this is data updates where data is retrieved from the service, changed, and returned. When the WCF Framework encounters data that is not part of the original data contract, the data is stored in the property and preserved. It is not processed in any other way except for temporary storage. If the object is returned back to where it originated, the original (unknown) data is also returned. Therefore the data has made a round trip to and from the originating endpoint without loss.

To enable round-trip for a particular type, the type must implement the IExtensibleDataObject interface. The interface contains one property, ExtensionData that returns the ExtensionDataObject type. The property is used to store any data from future versions of the data contract that is unknown to the current version.

The svcutil tool generated stub code also implements the Data Contract classes from the IExtensibleDataObject interface. In the Visual Studio IDE, explore the service reference and open the stub code in the file Reference.cs. You can see that the Data Contract class EmpInfo implements IExtensibleDataObject:

Image 10

The ExtensionDataObject type contains no public methods or properties. Thus, it is impossible to get direct access to the data stored inside the ExtensionData property. Although it can viewed in the debugger:

Image 11

In order to prevent data loss at service (Case -3), we can implement IExtensibleDataObject at the service as well. To achieve this, we can derive Data Contracts from the IExtensibleDataObject interface:

C#
[DataContract]
public class EmpInfo: IExtensibleData
{
    [DataMember]
    public int EmpID;
    [DataMember]
    public string EmpName;
   
    #region IExtensibleDataObject Members
    private ExtensionDataObject extensionData;
    public ExtensionDataObject ExtensionData
    {
        get { return extensionData; }
        set { extensionData = value; }
    }
    #endregion
}

On doing this, we can temporarily preserve data at the service end, while additional data members are sent from the client due to mismatch in the Data Contract version.

Modify the client code in order to send unknown Data Members. The EmpInfo stub code should have an additional Data Member EmpAddress which is unknown to the server.

C#
namespace WCFTestClient
{
    class Program
    {
        static void Main(string[] args)
        {
            using (MyServiceClient proxy = new MyServiceClient())  
            {
                var sc = proxy.UpdateEmployeeData(
                    new EmpInfo { EmpID = 1, EmpName = "Kausik", EmpAddress="Salt Lake"});  
                Console.WriteLine("Employee ID: " + sc.EmpID);
                Console.WriteLine("Employee Name: " + sc.EmpName);
                Console.WriteLine ("Employee Address: " + sc.EmpAddress);
            }
            Console.ReadLine();  

        }
    }
}

Now debug the service and you can view the additional members in the debugger:

Image 12

On running the client, you can see that although the service is preserving the unknown Data Member EmpAddress, the value of “Employee Address” is displayed as blank at the client.

Image 13

Modify the service implementation as follows:

C#
public class MyService : IMyService
{
   public EmpInfo UpdateEmployeeData(EmpInfo info)
   {
       //return new EmpInfo { EmpID = info.EmpID, EmpName = info.EmpName};
       return new EmpInfo { EmpID = info.EmpID, EmpName = info.EmpName, ExtensionData=info.ExtensionData };
    }
}

Now run the client and you can see the value of “EmpAddress” is changed after version round trip from the service:

Image 14

Also, the IExtensibleDataObject behavior can protect the service from potential DoS attacks. This is done in code so by setting the IgnoreExtensionDataObject property of ServiceBehaviorAttribute to true, as shown below:

C#
[ServiceBehavior(IgnoreExtensionDataObject = true)]
public class MyService : IMyService
{
    public EmpInfo UpdateEmployeeData(EmpInfo info)
    {
        return new EmpInfo { EmpID = info.EmpID, EmpName = info.EmpName};
    }
}

The behavior can also be configured in web.config as follows:

Image 15

Now, debug the service and you will see that additional members in ExtensionData are displayed as null in the debugger:

Image 16

It is recommended that all your types implement this interface to accommodate new and unknown future members. This way the WCF data contract system can evolve over time in non-breaking ways and provide Data Contract forward compatibility.

I have tried to discuss some possible cases of versioning of operations in Data Contracts. In the next and last part of the article, I will explore versioning strategies supported by the WCF framework in different cases.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)