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:
[DataContract]
public class EmpInfo
{
[DataMember]
public int EmpID;
[DataMember]
public string EmpName;
}
The service implementation is as follows:
namespace MyWcfService
{
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:
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:
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:
Modified service implementation:
We have set the value for the newly added member.
namespace MyWcfService
{
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:
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
:
Modified Service implementation:
We have set the value for the newly added member.
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:
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
:
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:
Now modify the service in order to remove the non-required
member EmpAddress
.
Modified Data Contract:
[DataContract]
public class EmpInfo
{
[DataMember]
public int EmpID;
[DataMember]
public string EmpName;
[DataMember(IsRequired = true)]
public string EmpCity;
}
Modified Service implementation:
namespace MyWcfService
{
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:
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
:
[DataContract]
public class EmpInfo
{
[DataMember]
public int EmpID;
[DataMember]
public string EmpName;
}
Modified Service implementation:
namespace MyWcfService
{
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:
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:
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:
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
:
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:
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:
[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.
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:
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.
Modify the service implementation as follows:
public class MyService : IMyService
{
public EmpInfo UpdateEmployeeData(EmpInfo info)
{
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:
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:
[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:
Now, debug the service and you will see that additional members in ExtensionData
are displayed as null
in the debugger:
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.