Versioning Strategies

Articles in this series

Once services are pushed to production their associated WSDL documents – which represent service endpoints, protocols and related contracts describing operations and messaging – must not be changed. Or, at a minimum, any changes should be backward compatible so that existing clients are not affected when changes are published. In this section I will discuss how WCF contracts support backward compatibility, and explain a few versioning strategies that you might consider for your WCF applications.

WCF Contracts and Backward Compatibility

WCF contracts are version tolerant by default. Figure 1 and Figure 2 summarize typical changes to service contracts and data contracts and describe the impact to existing clients. In short, the DataContractSerializer allows missing, non-required data and ignores superfluous data for service contracts, data contracts and similarly, message contracts. Only the removal of operations or the addition or removal of required data causes problems with existing clients.

Figure 1: Service contracts and backward compatibility

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 pass 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.

Removing operations

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

Figure 2: Data contracts and backward compatibility

Data Contract Changes Impact to 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.

Choosing a Versioning Strategy

Version tolerance is a good thing since it gives developers a flexible way to handle change. Since the DataContractSerializer is version tolerant, in theory you can refrain from officially versioning contracts and endpoints that expose those contracts until a breaking change is introduced such as removing operations from a service contract, or adding or removing required members from a data contract.

There are also potential side-effects of version tolerance:

  • If you add new operations to a service contract only clients that reference the latest WSDL will know about those operations and you will not be able to track which of your existing clients have updated their WSDL unless you expose a new endpoint when changes are made.
  • If you add non-required data members to a data contract you may have to write extra code to initialize missing values to something meaningful – as opposed to using the default value.
  • If you remove non-required data members from a data contract you may have round-trip issues where data passed to services or returned to clients is lost.
  • It may be difficult to track problems if you do not keep track of different versions of contracts, as changes are made to production.

It is important to choose an appropriate versioning strategy that satisfies the sometimes conflicting need for agility and productivity vs. change control. Here are a few versioning strategies to consider:

  • Agile Versioning: Rely on backward compatibility for as long as possible and avoid formal contract and endpoint versioning until compatibility is broken. This approach is useful in agile environments that require frequent updates to production code.
  • Strict Versioning: Perform formal contract and endpoint versioning for any change to a service contract, data contract, message contract or other contract-related or endpoint-related changes. This approach is best in environments what have less frequent production updates or that require detailed tracking of any and all changes.
  • Semi-Strict Versioning: Perform formal contract and endpoint versioning when contracts or endpoints are modified in a way that your policy requires tracking the change. For example, your policy might be to allow new operations to be added to a service contract, but if any changes are made to existing operations, or if any data contracts change such that they are semantically different, it requires versioning. This approach lies somewhere between agile and strict versioning.

The agile approach to versioning – shown in Figure 3 – means making changes to existing data contracts and service contracts without versioning them, or supplying new endpoints. Strict versioning means providing new data contracts, service contracts and endpoints as shown in Figure 4. 

Figure 3: Agile versioning through the same service contract and endpoint

Figure 4: Strict versioning through a new service contract and unique endpoint

It is best to decide on a versioning policy before you release the first version to production.

Versioning Service Contracts

To formally version a service contract you should do the following:

  • Make a copy of the original contract with a new interface type name, the same contract name and a new namespace.
  • Make any required modifications to the contract definition.
  • Implement the new service contract on the same service type if possible. You can use explicit contract implementation to funnel requests to the different method implementations if necessary.
  • Add a new endpoint for the new service contract to the existing <service> configuration.
  • As an alternative, you can use a common base class that implements core service operations and provide overrides in each version’s derived type. This can be a better approach if there are significant changes to the service implementation, or if the service security behaviors will be different from the original service. This will require a separate <service> entry in the configuration file.

Figure 5 shows an example of strict service contract versioning with a shared service implementation. Note that the Name property of the ServiceContractAttribute is the same for both versions of the contract, while the interface type name changes from IServiceA to IServiceA2. Also notice that two service endpoints are configured – one for each contract.

Figure 5: Version 1 and 2 of ServiceAContract with related endpoint configuration

[ServiceContract(Name = "ServiceAContract",

Namespace = "urn:WCFEssentials/Samples/2008/12")]

public interface IServiceA

{

   [OperationContract]

    void Operation1();

    [OperationContract]

    void Operation2();

}

[ServiceContract(Name = "ServiceAContract",

Namespace = "urn:WCFEssentials/Samples/2009/01")]

public interface IServiceA2

{

    [OperationContract]

    void Operation1();

    [OperationContract]

    void Operation2();

    [OperationContract]

    void Operation3();

}

<service name="BusinessServices.ServiceA"

behaviorConfiguration="serviceBehavior">

  <endpoint address="ServiceA" binding="wsHttpBinding"

contract="BusinessServiceContracts.IServiceA"  />

  <endpoint address="ServiceA2" binding="wsHttpBinding"

contract="BusinessServiceContracts.IServiceA2"  />

  <host>

    <baseAddresses>

      <add baseAddress="https://localhost:8000"/>

    </baseAddresses>

  </host>

</service>

In the accompanying code download for this paper, the following directories contain samples that illustrate service contract versioning: WCFEssentials\ContractVersioning\ServiceContractVersioningAgile, WCFEssentials\ContractVersioning\ServiceContractVersioningStrict.

Versioning Data Contracts

To formally version data contracts is a little more complicated since each data contract is likely tied to business entities that are used by the service tier, the business tier and the data access tier in the application. I recommend the following approach:

  • Make a copy of the original data contract and type, but rename the type to V1 since this will no longer be actively used each application tier.
  • Modify the business entity implementation as needed to work with the application, and update its data contract providing a new namespace.
  • Make a copy of the original service contract and implementation and update it to use the V1 type name. Note that this will not alter the service implementation since the V1 type name should have the original data contract with an explicit name and namespace.
  • Version any service contracts that rely on this new data contract following instructions in the previous section. That includes exposing new endpoints.
  • Modify the original service implementation to forward calls to a V2 service instance after mapping the V1 business entity to the V2 business entity.

Figure 6 illustrates this approach.

Figure 6: Versioning data contracts

Client-side data contracts should also implement IExtensibleDataObject so that V1 clients will preserve unknown data from a V2 service – assuming that non-strict versioning is used. Although you can implement IExtensibleDataObject at the service as well, it is less likely that a service needs to preserve unknown data since it is the system of record for the application. Suppressing IExtensibleDataObject behavior can protect the service from potential DoS attacks alongside message size and reader quotas set on the binding. If you are sharing metadata such as data contract definitions between clients and services – you can implement IExtensibleDataObject on the data contract types and disable support for this extensibility at the service only. This is best done in code so that it can’t be inadvertently changed on production – by setting the IgnoreExtensionDataObject property of the ServiceBehaviorAttribute to true, as shown here:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall,

IgnoreExtensionDataObject=true)]

public class ArticlesManagerService : IArticlesManager

{…}

This can also be configured declaratively in the <behaviors> section as follows:

<behaviors>

  <serviceBehaviors>

    <behavior name="serviceBehavior"  >

      <dataContractSerializer ignoreExtensionDataObject="true"/>

    </behavior>

  </serviceBehaviors>

</behaviors>

In the accompanying code download for this paper, the following directories contain samples that illustrate data contract versioning and IExtensibleDataObject: WCFEssentials\ContractVersioning\DataContractVersioning, WCFEssentials\ContractVersioning\IExtensibleDataObject.

Recommendations: Versioning Strategies

  1. Use an agile versioning strategy that relies heavily on backward compatibility for environments that must tolerate frequent change.
  2. Use a strict versioning policy for environments that are less frequently updated or require strict change control.
  3. Feel free to come up with a customized, semi-strict versioning policy that meets your application's needs.
  4. Establish a versioning policy ahead of time so that contracts and endpoints can be defined accordingly.
  5. Provide explicit name and namespace properties to contracts to support formal versioning.
  6. Implement IExtensibleDataObject on data contracts for clients and suppress support for this at the service.