Part 2:WCF Versioning
Part 1: WCF Versioning-Understanding the versioning and common issues
Before understanding how to version the services, let us understand WSDL Contracts and policies.
Every client must agree on some of the policies and contracts with the service to utilize the operations provided by the services. These are defined in the WSDL file.
Basically WSDL file is used to describe a service and its endpoints, what are the protocols used to access the operations and types of the messages. It also may or may not comprise of policy sections.
WS-Policy dictates the security features used by the service, if the service supports reliable messaging or transactions and the message encoding supported and other information on the protocols supported by a service.
After sharing WSDL file with the clients, the service should not be changed unless the client is willing to handle the changes. But, in a real world the services always should evolve by supporting the backward compatibility. This is one of the requirements of the SOA principles. The policies may change in order to provide extra security policies and other reliability features. This should not force the clients to change their coding and of course, no client would like to subscribe to the services if they have to change their code every time. So, in order to make the versioning possible we may have to think on the versioning strategies before developing the services.
With reference to on the Microsoft articles on WCF, there are two types of strategies for versioning. One is Strict Versioning and the other one Practical Versioning.
Let us understand the versioning by example.
Create a WCF Library and a host as discussed in the Part 1. We are not going to use HTTPBinding for this to make the test simple. So, we do not need to change the app.config in the host project.
using System;
usingSystem.Collections.Generic;
usingSystem.Linq;
usingSystem.Text;
using System.ServiceModel;
usingSystem.Runtime.Serialization;
namespace HelloWorld
{
[DataContract]
public class Customer
{
privateint m_Id;
privatestring m_Name;
privatestring m_Address;
[DataMember(IsRequired=true, Order=0)]
public int Id
{
get{ return m_Id; }
set{ m_Id = value; }
}
[DataMember(IsRequired = true, Order = 1)]
public string Name
{
get{ return m_Name; }
set{ m_Name = value; }
}
[DataMember(IsRequired = false, Order = 2)]
public string Address
{
get{ return m_Address; }
set{ m_Address = value; }
}
}
}
We have created a custom class called Customer and decorated it with [DataContract]. To create a data contract, we need to use the namespace“System.Runtime.Serialization”. We added few properties to the class. In WCF, each member of the data contract is designated with attribute [DataMember].
A very good information for Data contract from Microsoft is as follows:
A data contract is a formal agreement between a service and a client that abstractly describes the data to be exchanged. That is, to communicate, the client and the service do not have to share the same types, only the same data contracts. A data contract precisely defines, for each parameter or return type, what data is serialized (turned into XML) to be exchanged.
Windows Communication Foundation (WCF) uses a serialization engine called the Data Contract Serializer by default to serialize and deserialize data (convert it to and from XML). All .NET Framework primitive types, such as integers and strings, as well as certain types treated as primitives, such as DateTimeand XmlElement, can be serialized with no other preparation and are considered as having default data contracts. Many .NET Framework types also have existing data contracts. For a full list of serializable types, see Types Supported by the Data Contract Serializer.
New complex types that you create must have a data contract defined for them to be serializable. By default, the DataContractSerializerinfers the data contract and serializes all publicly visible types. All public read/write properties and fields of the type are serialized. You can opt out members from serialization by using the IgnoreDataMemberAttribute. You can also explicitly create a data contract by using DataContractAttributeand DataMemberAttributeattributes. This is normally done by applying the DataContractAttributeattribute to the type. This attribute can be applied to classes, structures, and enumerations. The DataMemberAttribute attribute must then be applied to each member of the data contract type to indicate that it is a data member, that is, it should be serialized.
More information on this can be had from MSDN link http://msdn.microsoft.com/en-us/library/ms733127%28v=vs.90%29.aspx
And also observe that each data member has got a property“isRequired”. This instructs the serialization engine that the member must be present when reading or deserializing. That means, if the client do not set the property, the service throws an error when the member is decorated with“isRequired=true”. Second thing to notice in the attribute properties is the order. The property Order dictates the order of serialization and deserialization of a member.
If any of the data member attribute has a property“isRequired=false”, this will not enforce the serialization engine to check if the value is set while reading the property. Meaning that client never need to set a value for this member.
By default WCF contracts are version tolerant. That means service contracts, data contracts and message contracts if the client does not send the non-required data or any missing information from the client or any extra information from the client. So, we do not need to worry if we have to change the contracts by adding new operations or new members for data contracts or removing the non-required members or parameters.
There is a table provided in one of the Microsoft web casts on the versioning and how it matters based on the type of change. This gives a clear picture on the effect of versioning the existing services without taking care of proper procedures or guide lines.
Let us see the above said in practice.
Now modify the IHelloWorld interface created in the previous example.
using System;
usingSystem.Collections.Generic;
usingSystem.Linq;
using System.Runtime.Serialization;
usingSystem.ServiceModel;
usingSystem.Text;
namespace HelloWorld
{
// NOTE: If you change the interface name "IService1" here, you must also update the reference to "IService1" in App.config.
[ServiceContract(SessionMode=SessionMode.Allowed)]
public interface IHelloWorld
{
[OperationContract]
voidSetCustomer(Customer customer);
[OperationContract]
CustomerGetCustomer();
}
}
And implement the interface in the service class HelloWorld.cs:
using System;
usingSystem.Collections.Generic;
usingSystem.Linq;
using System.Runtime.Serialization;
usingSystem.ServiceModel;
usingSystem.Text;
namespace HelloWorld
{
// NOTE: If you change the class name "Service1" here, you must also update the reference to "Service1" in App.config.
[ServiceBehavior(InstanceContextMode=InstanceContextMode.PerSession)]
public class HelloWorld :IHelloWorld
{
privateCustomer customer;
#region IHelloWorld Members
public void SetCustomer(Customercustomer)
{
this.customer = customer;
Console.WriteLine("Setting Customer");
}
public Customer GetCustomer()
{
returnthis.customer;
}
#endregion
}
}
Now rebuild the solution and run an instance of host project to check if everything is good till now.
Now we need to refer the service hosted on TCP/IP from the client application. Create a console application for client and add the Customer.cs class from the HelloWorld project and add a reference to System.Runtime.Serialization. Since we are not generating any WSDL, we cannot reference the service directly from visual studio à Add service reference mechanism.
In real world, we are going to generate WSDL and share the same with our clients.
So, your solution should like as below:
Program.cs in your client application should look as in the below:
using System;
usingSystem.Collections.Generic;
usingSystem.Linq;
usingSystem.Text;
usingSystem.ServiceModel;
usingHelloWorld;
namespace Client
{
class Program
{
[ServiceContract]
public interface IHelloWorld
{
[OperationContract]
voidSetCustomer(Customer customer);
[OperationContract]
CustomerGetCustomer();
}
static void Main(string[] args)
{
IHelloWorldproxy = ChannelFactory<IHelloWorld>.CreateChannel(new NetTcpBinding(),new EndpointAddress("net.tcp://localhost:9000/HelloWorld"));
Customercustomer = new Customer();
customer.Id = 1;
customer.Name = "John";
customer.Address = "NYC";
proxy.SetCustomer(customer);
CustomerlocalCustomer = proxy.GetCustomer();
Console.WriteLine("Customer ID is {0}", localCustomer.Id);
Console.WriteLine("Customer Name is {0}", localCustomer.Name);
Console.WriteLine("Customer Address is {0}", localCustomer.Address);
Console.ReadLine();
}
}
}
Now run the instances of host and client and check if you are able to send and get the values back. Put break points if needed in the service to check the communication.
Since the property “Address” is not marked as required in the Data Contract class in the WCF Service Library project, even if we comment the property, client still do not run into any exceptions. Rebuild the solution again.
//[DataMember(IsRequired = false, Order = 2)]
//public string Address
//{
// get { return m_Address; }
// set { m_Address = value; }
//}
Now run the service host application and client again.
Everything went well except client did not get the address back. This is OK if the client does not have any issue if the property value is not returned. But, in real world scenario we cannot comment something client expects it to be return.
Now uncomment the above and this time try to comment out the property with “isRequired = true”. Let us comment the property “Name” and check what happens:
//[DataMember(IsRequired = true, Order = 1)]
//public string Name
//{
// get { return m_Name; }
// set { m_Name = value; }
//}
You see the following error:
The formatter threw an exception while trying to deserialize the message: There was an error while trying to deserialize parameter http://tempuri.org/:GetCustomerResult. The InnerException message was ''Element' 'Address' from namespace 'http://schemas.datacontract.org/2004/07/HelloWorld' is not expected. Expecting element 'Name'.'. Please see InnerException for more details.
As we have already discussed at the beginning of the article, isRequired = true for any property instructs the serialization engine that the member must be present when reading or deserializing.
Now uncomment the one we did earlier and now try to add extra parameter to one of the operations and check again.
Change the interface and service class:
[ServiceContract(SessionMode=SessionMode.Allowed)]
public interface IHelloWorld
{
[OperationContract]
void SetCustomer(Customer customer, stringIamNotRequired);
[OperationContract]
CustomerGetCustomer();
}
public void SetCustomer(Customercustomer, string IamNotRequired)
{
this.customer = customer;
Console.WriteLine("Setting Customer");
}
Everything went fine. You might have remembered that the WCF services are version tolerant. They can ignore superfluous data.
Even if you add another operation to the service and the client does not consume the operation, still no exception occurs.
Add a new operation to the service and run the service host and client again.
In the interface:
[OperationContract]
void WasteMethod();
In the service class:
public void WasteMethod()
{
Console.WriteLine("I am waste");
}
Now test the changes and still the application is doing good with no errors. Similarly if you add a new non-required property nothing happens. But if you add any of the required properties in the data contract and client is not aware of the changes and if the client consumes the service, the service will get into an exception.
Test this again by adding a required method in the service.
[DataMember(IsRequired = true, Order = 3)]
public string Description
{
get{ return m_Description; }
set{ m_Description = value; }
}
The service has thrown the following exception:
The formatter threw an exception while trying to deserialize the message: There was an error while trying to deserialize parameter http://tempuri.org/:customer. The InnerException message was ''EndElement' 'customer' from namespace 'http://tempuri.org/' is not expected. Expecting element 'Description'.'. Please see InnerException for more details.
So, we cannot change the service without effecting the client every time and we need to think about the strategies of versioning the services when change is needed.
You can test the above without adding Customer.cs and modifying the Program.cs class in the client project with the interface definition, by adding basicHttpBinding in the service configuration and referring the service in the client project.
We will discuss the versioning strategies in the next article.
No comments:
Post a Comment