Versioning WCF Services: Part I

ITPro Today logo

This column explores how to build Web services with .NET 3.0 and WCF. In this two-part installment, we ll cover how to version your service contracts and data contracts. If you have questions about migrating your existing ASMX or WSE Web services to WCF, or questions regarding WCF, please send them to [email protected] . For more information, see " What’s New in Microsoft’s .NET Framework 3.0? " and " What's New in WCF 4.0? "

To communicate, clients and services must agree on contracts and policy. Web Service Description Language (WSDL) is used to describe an interoperable contract for a service, defining the operations available at each endpoint the service exposes. For each operation, XSD schema definitions are provided to define associated input, output, and fault messages. Clients depend on the WSDL and schema definitions to generate proxies to communicate with the service. WSDL contracts can optionally include policy sections that describe security, message encoding (such as Text or MTOM support), reliable messaging, transactions, and other protocol requirements for communicating with the service. WS-Policy is the interoperable standard (not yet ratified) used to include such policy requirements in a WSDL contract. The general expectation is that once a WSDL contract and set of policies are published for a public-facing service, clients can generate proxies and related code to communicate with those service endpoints into the foreseeable future.

Ideally, contracts and policy should not be changed to preserve backward compatibility with clients that consume those endpoints. Of course, that s not to say that services and related operations may not require change. In this two-part series, I ll discuss approaches to versioning service contracts and data contracts while preserving backward compatibility.

WCF and Version Tolerance

By default, WCF contracts are version-tolerant. DataContractSerializer makes it possible for service contracts and data contracts to forgive missing, non-required data and to ignore superfluous data. The tables in Figure 1 and 2 summarize the effect that changes to each type of contract have on existing clients.

Service Contract Changes

Impact to Existing Clients

Adding new parameters to an operation signature.

Client unaffected. New parameters initialized to default values at the service.

Removing parameters from an operation signature.

Client unaffected. Superfluous parameters passed by clients are ignored, data lost at the service.

Modifying parameter types.

An exception will occur if the incoming type from the client cannot be converted to the parameter data type.

Modifying return value types.

An exception will occur if the return value from the service cannot be converted to the expected data type in the client version of the operation signature.

Adding new operations.

Client unaffected. Will not invoke operations it knows nothing about.

An exception will occur. Messages sent by the client to the service are considered to be using an unknown action header.

Figure 1: The impact of service contract changes to existing clients

Data Contract Changes

Impact on Existing Clients

Add new non-required members.

Client unaffected. Missing values are initialized to defaults.

Add new required members.

An exception is thrown for missing values.

Remove non-required members.

Data lost at the service. Unable to return the full data set back to the client, for example. No exceptions.

Remove required members.

An exception is thrown when client receives responses from the service with missing values.

Modify existing member data types.

If types are compatible, no exception but may receive unexpected results.

Figure 2: The impact of data contract changes to existing clients

Because of this version tolerance, backward compatibility can be achieved with reasonable changes to the WSDL contract. For service contracts, reasonable changes include adding new parameters that were previously not required, removing parameters that are no longer required by the service, or adding new operations that did not previously exist. Although supported in some cases, modifying data types for parameters and return types is not what I would consider a reasonable change, as it implies a modification to the semantics of the operation. Likewise, you would expect that removing operations would have a negative impact on clients that currently consume them. As for data contracts, reasonable changes include adding or removing non-required members. Removing required members or modifying existing member data types are not reasonable.

IExtensibleDataObject

There are times when it may be desirable to preserve superfluous data received by clients or services during message exchange. For example, a V1 client may call a V2 service where the service returns information to the client according to an updated data contract that contains additional members.

Consider the V1 data contract (LinkItem type) and associated service contract (IContentManagerService) shown in Figure 3. When a V1 client builds a proxy to consume this service, a copy of the V1 data contract and service contract is generated at the client to support serialization. If the LinkItem data contract is modified, as shown in Figure 4, to add a data member that stores DateEnd, a value for DateEnd will be returned to the client when GetItem is called. Because the V1 client is deserializing to the V1 data contract, the value returned for DateEnd will be lost at the client by default.

Figure 3: V1 data contract and service contract

[DataContract(Name="LinkItem", Namespace=
http://schemas.thatindigogirl.com/samples/2006/06)]
public class LinkItem
<
private long m_id;
private string m_title;
private string m_description;
private DateTime m_dateStart;
private string m_url;
[DataMember(Name = "Id", IsRequired = false, Order = 0)]
public long Id
<
get < return m_id; >
set < m_id = value; >
>
[DataMember(Name = "Title", IsRequired = true, Order = 1)]
public string Title
<
get < return m_title; >
set < m_title = value; >
>
[DataMember(Name = "Description", IsRequired = true,
Order = 2)]
public string Description
<
get < return m_description; >
set < m_description = value; >
>
[DataMember(Name = "DateStart", IsRequired = true,
Order = 3)]
public DateTime DateStart
<

get < return m_dateStart; >
set < m_dateStart = value; >
>
[DataMember(Name = "Url", IsRequired = false, Order = 4)]
public string Url
<
get < return m_url; >
set < m_url = value; >
>
>
[ServiceContract(Name = "ContentManagerContract", Namespace =
http://www.thatindigogirl.com/samples/2006/06)]
public interface IContentManagerService
<
[OperationContract]
void SaveItem(LinkItem item);
[OperationContract]
LinkItem GetItem(string id);

Figure 4: V2 data contract

[DataContract(Namespace=
http://schemas.thatindigogirl.com/samples/2006/06)]
public class LinkItem
<
private long m_id;
private string m_title;
private string m_description;
private DateTime m_dateStart;
private DateTime m_dateEnd;
private string m_url;
// other property accessors
[DataMember(Name = "DateEnd", IsRequired = false,
Order = 4)]
public DateTime DateEnd

Fortunately, the default behavior for proxy generation is to implement IExtensibleDataObject which enables a data contract to preserve unknown XML elements during deserialization. The interface requires the implementation of a property of type ExtensionDataObject, shown implemented on the LinkItem type in Figure 5. This ExtensionDataObject is used by DataContractSerializer to store superfluous elements in a dictionary. Thus, although the V1 client knows nothing of the superfluous elements, after modifying values in the user interface (for example), these unknown elements will be serialized as they were originally received when the modified data contract instance is passed to SaveLink.

Figure 5: Supporting IExtensibleDataObject.

[DataContract(Namespace=
http://schemas.thatindigogirl.com/samples/2006/06)]
public class LinkItem:IExtensibleDataObject
<
// data members
ExtensionDataObject m_extensionData;
public ExtensionDataObject ExtensionData
<
get
set
>
>

It makes sense for proxy generation to implement this interface by default, so that client applications can interact with services as they are updated. In scenarios where data is modified and returned to the service, unknown elements will not be lost during the round trip. However, it may not make sense to implement IExtensibleDataObject on data contracts at the service it can introduce the risk of denial-of-service attacks where malicious clients send large amounts of superfluous data (consuming server resources to deserialize these elements into the dictionary). In addition, the overhead of this deserialization can hamper scalability, as each call takes longer to process unknown elements. I generally recommend you don t implement IExtensibleDataObject at the service unless you are in a dynamic environment where services may in turn pass V2 data to downstream V1 services, where the possibility of this is unknown or out of your control, and you must in turn limit other message size quotas accordingly.

Fortunately, administrators can disable support for IExtensibleDataObject with the following service behavior setting:

Based on the discussion thus far, you can see that WCF supports backward compatibility by default. Still, at some point, changes do require a versioning strategy that will enable you to keep track of changes, influence clients to update their code as services evolve, and eventually retire earlier implementations.

If you adopt a strict versioning policy, any changes to service contracts or data contracts will require formal versioning steps that segregate old clients from new. Formal versioning need not be a complex undertaking; it can be almost mechanically executed once you establish the steps required to properly version service contracts and data contracts. Still, it will add overhead to the development process when change happens.

You may instead opt for a more practical approach to versioning, which does not require formal versioning steps until significant change makes it a requirement. That means you leverage WCF s ability to support backward and forward compatibility preserving unknown elements and forgiving missing elements until a breaking change requires additional steps.

In any case, your first step in producing a V1 service is to define your data contracts, taking these points into consideration:

Provide explicit Name and Namespace properties to the DataContractAttribute (see Figure 3) to decouple WSDL contract values from the CLR type.

Provide explicit Name, IsRequired, and Order values to the DataMemberAttribute (see Figure 3) to decouple WSDL contract values from the CLR type, and to carefully consider the resulting schema for the type up front.

Your service contracts should also be defined taking these points into consideration:

Provide explicit Name and Namespace properties to the ServiceContractAttribute (see Figure 3) to decouple WSDL contract values from the interface definition (CLR type).

You will normally supply data contracts as parameters and return types, except in special circumstances, where IXmlSerializable types or message contracts are involved.

When you provide explicit values to service contract and data contract attributes, developers are less likely to accidentally introduce changes to WSDL contracts by refactoring names of CLR types and related properties. In addition, it provides an explicit way to introduce versioning.

Next, I ll discuss practical and strict approaches to versioning service contracts. In Part II, I ll continue with approaches for data contract versioning.

Versioning Service Contracts

Service contract versioning implies that either the contract definition itself has been altered in some way or the data contracts on which it relies have changed. In either case, you can adopt a non-strict, semi-strict, or strict set of versioning guidelines when changes occur. The type of change will sometimes indicate the required path.

Non-strict Guidelines. To avoid development overhead, it is possible to leverage WCF s built-in version tolerance and avoid formal versioning of service contracts for changes such as adding new operations, or adding and removing operation parameters.

Consider the service contract shown in Figure 6. V1 clients generate proxies that are able to call operations 1 and 2 according to the WSDL contract produced for the service. When a new operation is added, as shown in Figure 7, V1 clients continue to operate on operation 1 and 2, while V2 clients will generate a proxy that includes operation 3. A new endpoint is not required for this, because no changes have been made to impact backward compatibility of V1 clients. In this case, the service contract merely includes a new operation, leaving other operations untouched.


Figure 6: Service contract with V1 client.


Figure 7: Updated service contract with V1 and V2 clients.

In the event operation 1 and 2 are modified, by adding or removing parameters, V1 clients are still unaffected, as WCF will simply ignore superfluous parameters to the operation, or initialize missing parameters to their default values. The point is, these types of changes can be handled gracefully provided you consider how to handle default values, and provided V1 clients will not be negatively affected if (now) superfluous data is lost at the service. If this type of non-strict approach to versioning is acceptable, the same service contract can be exposed at the same service endpoint as shown in Figure 7.

The only changes that cannot be easily forgiven are modifications to data types, or the removal of service operations on which V1 clients may depend. In this case, you simply must version the service contract formally (which I ll discuss shortly). Figure 8 summarizes the non-strict guidelines to versioning service contracts.


Figure 8: Non-strict service contract versioning guidelines.

Semi-strict Guidelines. When the only changes made to a service contract are adding new operations, you can adopt a semi-strict approach to versioning that allows you to track when changes occurred, and potentially monitor which V1 clients have not yet updated their code to leverage new functionality exposed by the service.

In the first variation of the semi-strict approach, new operations are added to a new V2 service contract that inherits the V1 service contract (see Figure 9). While the CLR type of the V2 contract is different, the WSDL contract name remains the same; in this case, ServiceAContract . What distinguishes the operations in the V1 and V2 contracts is the namespace, which in this case follows Web service conventions that include a month and year suffix to the namespace URI. Because the V2 service contract inherits the V1 contract, the original namespace is preserved for operations 1 and 2. The new namespace is used only for operation 3. Thus, even if the original endpoint is updated to expose the V2 contract (see Figure 10), messages sent from V1 clients will still be compatible with the original namespace. Only V2 clients will send messages to operation 3 using the new namespace.

Figure 9: Versioning service contracts with inheritance

[ServiceContract(Name = "ServiceAContract", Namespace =
http://www.thatindigogirl.com/samples/2007/06)]
public interface IServiceA
<
[OperationContract]
string Operation1();
[OperationContract]
string Operation2();
>
[ServiceContract(Name = "ServiceAContract", Namespace =
http://www.thatindigogirl.com/samples/2007/07)]
public interface IServiceA2 : IServiceA
<
[OperationContract]
string Operation3();
>


Figure 10: Versioning service contracts with inheritance at the same endpoint.

The downside of this approach is that as messages arrive to the service, you won t be able to determine if calls to operation 1 and 2 originated from V1 or V2 clients; thus, you can t determine if any V1 clients are still lingering without updating their code to leverage new functionality. In keeping with this semi-strict approach, you still can achieve this by exposing a new endpoint for the V2 service contract (see Figure 11). The namespace of operations 1 and 2 remain that of the original contract, which means that V1 clients can move to the new endpoint without impact to existing code, and update their proxies to reflect new operations at their leisure. Figure 12 illustrates this semi-strict approach to versioning.


Figure 11: Versioning service contracts with inheritance at a new endpoint.


Figure 12: Semi-strict service contract versioning guidelines.

Strict Guidelines. What the non-strict and semi-strict approaches I have described allow you to do is defer the inevitable formal versioning approach that is required as soon as significant changes occur that affect your service contracts. At some point, formal versioning is unavoidable; for example, if you remove service operations or change the data types or semantics of existing operations. By delaying formal versioning, you save on development effort for insignificant changes but you lose the ability to trace versions of services you ve exposed and track the migration of clients to updated service functionality.

In a strict environment, formal versioning is required for any and all changes to service contracts, including adding new operations. As Figure 13 illustrates, all roads lead to formal service contract versioning, which implies a new service contract that includes all operations (no inheritance path) and a new endpoint.


Figure 13: Strict service contract versioning guidelines.

Even if V1 operations haven t changed, with strict versioning, the V2 contract must include all operations under the new namespace (see Figure 14). Notice that the V2 contract can still share the same name as its V1 predecessor if it is semantically equivalent, but uses a new namespace to distinguish messages on the wire.


[ServiceContract(Name = "ServiceAContract", Namespace =
http://www.thatindigogirl.com/samples/2007/06)]
public interface IServiceA
<
[OperationContract]
string Operation1();
[OperationContract]
string Operation2();
>
[ServiceContract(Name = "ServiceAContract", Namespace =
http://www.thatindigogirl.com/samples/2007/07)]
public interface IServiceA2
<
[OperationContract]
string Operation1();
[OperationContract]
string Operation2();
[OperationContract]
string Operation3();
>

Figure 14: Versioning service contracts without inheritance.

Of course, you can share common implementation functionality with a base type Figure 15 illustrates the use of a new contract and endpoint for V2 clients.


Figure 15: Versioning service contracts without inheritance.

By using strict versioning, you have the liberty to make subtle changes to V1 operations, while also adding new operations to the V2 contract. If functionality can be shared by V1 and V2 implementations, so be it; but changes would also be acceptable, as any V1 clients moving to the new V2 service would be required to understand the implications of this move.

To Be Continued.

This concludes the overview of the version tolerance provided by WCF, which allows you to adopt non-strict or semi-strict versioning guidelines to save on development overhead when reasonable changes occur. I also summarized when and how you can approach non-strict and semi-strict guidelines although I generally recommend you adopt a strict versioning policy so you can track changes, explicitly and safely move clients to new functionality, and get developers accustomed to the impact of their changes to service contracts.

I ll continue this discussion in Part II by exploring data contract versioning and the impact of this on service contracts.