Chapter 20. Versioning

<feature><title>In This Chapter</title> </feature>

Introduction

Microsoft’s field personnel often express interest in hearing about how to manage the lifecycle of a service and, in particular, how to deal with the changes that will inevitably need to be made to services over time. The reason for their interest is that their customers are seeking guidance on the matter. Unfortunately, the literature on service-oriented programming, including the first edition of this book, neglects to pay any attention to the topic. One book that devotes a few paragraphs to the subject is Service-Oriented Architecture Compass: Business Value, Planning, and Enterprise Roadmap, by Norbert Bieberstein and others (Bieberstein and others 2006, 39). The brief discussion there refers to the most useful contribution on the subject of service version management hitherto: the article, Best Practices for Web services versioning by Kyle Brown and Michael Ellis (Brown and Ellis 2004). They make an observation that might well explain why so little has been written about versioning services when so much has been written about building them:

[T]he brutal fact [...] is that versioning has not been built into the Web services architecture. Current products from [the leading vendors] do not directly address the versioning issue, requiring developers to solve the problem through the application of patterns and best practices (Brown and Ellis 2004).

The Windows Communication Foundation actually does have some versioning facilities. This chapter will cover those mechanisms, of course. However, its main purpose is to remedy the shortage of guidance on managing the versioning of services. The chapter aims to do so by using the language the Windows Communication Foundation provides for modeling services, to yield a logically complete set of versioning problems, with the solutions to each one of them.

Versioning Nomenclature

The challenge in managing modifications to a service over time is to limit the cost the changes incur. The obvious way of accomplishing that is to avoid having to change any existing client applications as a consequence of changing the service.

Brown and Ellis refer to changes to services that would not require changes to existing clients as backwards-compatible changes, and changes to services that would require changes to existing clients as non-backwards-compatible changes (Brown and Ellis 2004). The same nomenclature will be used here.

The Universe of Versioning Problems

A versioning decision tree is presented in Figure 20.1. It depicts all of the logically possible ways in which one might have to modify a service, how to implement each change, and what the consequences will be.

Versioning decision tree.

Figure 20.1. Versioning decision tree.

The diamonds represent alternative ways in which a service might have to change. The rectangles and the circle represent procedures for implementing changes. The procedures represented by the rectangles yield backwards-compatible changes, whereas the procedure represented by the circle results in a non-backwards-compatible change.

Thus, the versioning decision tree provides two interesting insights at a glance. The first is that there is exactly one thing one might do that would result in a non-backwards-compatible change. The second is that one can have considerable leeway to modify one’s services while still avoiding that outcome.

Trace the routes through the decision tree now to understand the implications of each alternative. Start by imagining that there is a service that has to be modified.

Adding a New Operation

The first decision that the decision tree requires one to make is whether it will suffice to simply add a new operation to the service. If that is indeed all that is required, that change can be accomplished using a procedure called service contract inheritance, and the result will be a backwards-compatible change.

To understand service contract inheritance, suppose that the service that has to be modified has an endpoint with this service contract:

[ServiceContract]
public interface IEcho
{
    [OperationContract]
    string Echo(string input);

Assume that this service contract is implemented by this service type:

public class Service : IEcho
{
    public string Echo(string input)
    {
        return input;
    }

The first step in service contract inheritance is to define a new service contract, with the new operations to be added to the service, that derives from the original service contract:

[ServiceContract]
public interface IExtendedEcho: IEcho
{
    [OperationContract]
    string[] ExtendedEcho(string[] inputs);

The next step is to have the service type implement the new contract in addition to the original one:

public class Service : IEcho, IExtendedEcho
{
    public string Echo(string input)
    {
        return input;
    }
    public string[] ExtendedEcho(string[] input)
    {
        return input;
    }

The final step is to modify the configuration of the service endpoint so that where it referred to the original contract

<endpoint
        address="Echo"
        binding="basicHttpBinding"
        contract="IEcho"
/>

it now refers to the derived contract with the additional methods:

<endpoint
        address="Echo"
        binding="basicHttpBinding"
        contract="IExtendedEcho"
/>

Now new clients that are aware of the additional operations of the derived contract can make use of those operations. Yet existing clients, which might know about only the original service contract, could still have the operations of that original contract executed at the same endpoint. Thus, service contract inheritance has the happy consequence of a backwards-compatible change.

Changing an Operation

The second decision posed by the versioning decision tree is whether or not an existing operation of a service has to be modified. If so, the next decision to make is whether or not the change that is required is a change to the data contracts of one or more parameters.

Changing the Data Contract of a Parameter

If the change that has to be made to an existing operation is a change to the data contracts of one or more parameters, the versioning decision tree asks whether that change is restricted to the addition of data members to those data contracts. As the decision tree shows, that change can be accomplished by adding optional members to the data contracts, and the change is a backwards-compatible one.

Adding Optional Members to Data Contracts

To add an optional member to a data contract, add the member and set the value of the IsRequired property of its System.Runtime.Serialization.DataMember attribute to false. Thus, given an original data contract

[DataContract]
public class DerivativesCalculation
{
     [DataMember(IsRequired=true)]
     public string[] Symbols;
     [DataMember(IsRequired=true)]
     public decimal[] Parameters;
     [DataMember(IsRequired=true)]
     public string[] Functions;
     public DataTime Date
}

one could add a new optional member:

[DataContract]
public class DerivativesCalculation
{
     [DataMember(IsRequired=true)]
     public string[] Symbols;
     [DataMember(IsRequired=true)]
     public decimal[] Parameters;
     [DataMember(IsRequired=true)]
     public string[] Functions;
     public DataTime Date
     [DataMember(IsRequired=false)]
     public decimal LastValue;
}

The addition of optional data members is a backwards-compatible change. The Windows Communication Foundation’s System.Runtime.Serialization.DataContractSerializer can deserialize from XML streams that omit values for the optional members. It can also deserialize from XML streams that have values for optional members into instances of types that do not include the optional members. Thus, if new optional members get added to the data contracts of a service, clients using versions of those data contracts that do not have the new optional members will still be able to send messages to the service, and even receive responses that might include values for the optional members, values that the Windows Communication Foundation will quietly ignore.

Consider this case, though. Messages are being passed from one node through an intermediary node to a third node. The first and third nodes have been modified so that their versions of the data contracts that define the messages include optional members of which the intermediary node is unaware. In that case, it would be desirable for the values of those optional members of which the intermediary node is unaware, to still pass from the first node, through the intermediary node, and on to the third. All that is required for that to happen is for the data contracts of the intermediary node to implement the System.Runtime.Serialization.IExtensibleDataObject interface. Implementing that simple interface ensures that memory is set aside where the System.Runtime.Serialization.DataContractSerializer can store data in from an input XML stream that is not defined by a data contract, so that the data can later be reserialized into an output XML stream. A data contract that implements the System.Runtime.Serialization.IExtensibleDataObject interface is shown in Listing 20.1. It is always wise to implement that interface on data contracts so as to anticipate the possibility of the values of unknown members having to pass through them.

Example 20.1. A Data Contract that Implements IExtensibleDataObject

[DataContract]
public class DerivativesCalculation: IExtensibleDataObject
{
        [DataMember(IsRequired=true)]
        public string[] Symbols;
        [DataMember(IsRequired=true)]
        public decimal[] Parameters;
        [DataMember(IsRequired=true)]
        public string[] Functions;
        public DataTime Date

        private ExtensionDataObject unknownData = null;

        public ExtensionDataObject ExtensionData
        {
                get
                {
                        return this.extensionData;
                }

                set
                {
                        this.extensionData = value;
                }
        }
}

Other Changes to Data Contracts

As the versioning decision tree shows, any change to a data member other than the addition of an optional member would require the definition of a new version of the data contract. Such changes would include altering the name or the type of a member, or deleting a member.

A new version of a data contract would have to be disambiguated from the original. Disambiguation is necessary so that references to the new version of the data contract in the metadata for the modified service would not be mistaken by clients as references to the original version. That mistake could result in an error if the client was to send a message incorporating data that was structured in accordance with the original version of the data contract to the service when data structured in accordance with the revised version was expected.

Brown and Ellis offer sound advice on how to disambiguate versions of contracts: “[t]o ensure that the various editions of a [contract] are unique, we would recommend a simple naming scheme that appends a date or version stamp to the end of a namespace definition. This follows the general guidelines given by the W3C for XML namespace definitions” (Brown and Ellis 2004). Following that advice, starting with this contract

namespace Fabrikam.Derivatives
{
        [DataContract(
                Namespace="http://www.fabrikam.com/derivatives/v1.0.0.0",
                Name="DerivativesCalculation")]
        public class DerivativesCalculation
        {
                [DataMember(IsRequired=true)]
                public string[] Symbols;
                [DataMember(IsRequired=true)]
                public decimal[] Parameters;
                [DataMember(IsRequired=true)]
                public string[] Functions;
                public DataTime Date
        }
}

one could unambiguously define a new version, modified by the omission of one of the original members, in this way, using a version-specific namespace:

namespace Fabrikam.Derivatives
{
        [DataContract(
                Namespace="http://www.fabrikam.com/derivatives/v2.0.0.0",
                Name="DerivativesCalculation")]
        public class SimplifiedDerivativesCalculation
        {
                [DataMember(IsRequired=true)]
                public string[] Symbols;
                [DataMember(IsRequired=true)]
                public decimal[] Parameters;
                public DataTime Date
        }
}

The versioning decision tree shows that once a new version of a data contract has had to be defined, that new version will have to be incorporated into the definition of a revised service contract. Just as new versions of data contracts should be disambiguated from earlier versions by defining them in new version-specific namespaces, so too should revised versions of service contracts.

After a new version of a service contract has been defined, then, as the versioning decision tree shows, it should be exposed at a new service endpoint. So, to summarize the complete sequence of steps through the tree, if a service has to be modified, and the modification requires some change to a data contract other than the addition of an optional member, a new data contract and service contract must be defined, and the new service contract exposed at a new service endpoint. Interestingly, as the tree shows, none of this would entail a non-backwards-compatible change. The original client applications, unaware of the modified data contract, would continue to use the original service endpoint, whereas new client applications that are aware of the new version of the data contract would use the new service endpoint.

Other Changes to Operations

Changes to existing operations that are not restricted to changing the data contracts of one or more parameters—changing the name of the operation or adding or deleting a parameter—can be dealt with by defining a new service contract incorporating the modified operation. As indicated already, revised versions of service contracts must be disambiguated from earlier versions by defining them in new version-specific namespaces. Also as indicated before, after a new version of a service contract has been defined, it should be exposed at a new service endpoint. Again, exposing a new service endpoint is a backwards-compatible change.

Deleting an Operation

The versioning decision tree shows that deciding to delete an operation from a service contract has the same benign consequences. The backwards-compatible consequences are having to define to a new service contract that omits the deleted operation, within a new, version-specific namespace, and exposing that service contract at a new service endpoint.

Changing a Binding

If the change to be made to a service requires changing the binding of one its endpoints, the versioning decision tree indicates that the modified binding should be exposed at a new service endpoint. As noted several times, exposing a new service endpoint is a backwards-compatible change.

Deciding to Retire an Endpoint

The most important insight offered by the versioning decision tree is that there is exactly one type of change that one might decide to make that would be non-backwards-compatible, and that is the decision to retire an existing endpoint. Furthermore, the only change that necessarily entails the retiring of an existing endpoint is changing an endpoint’s address. Changes to the operations of service contracts and changes to bindings do not require that existing endpoints which use those contracts and bindings be retired—the modified contracts and bindings can be exposed at new endpoints alongside the existing ones.

If an endpoint is to be retired, there are two ways of easing the consequences of this non-backwards-compatible change to a service. However, both require anticipating having to make that change and planning ahead.

The first alleviator is to add a System.ServiceModel.FaultContract attribute to all the operations of a contract to indicate that the operation might return a fault indicating that the endpoint has been retired:

[DataContract]
public class RetiredEndpointFault
{
        [DataMember]
        public string NewEndpointMetadataLocation;
}

[ServiceContract]
public interface IEcho
{
    [FaultContract(typeof(RetiredEndpointFault))]
    [OperationContract]
    string Echo(string input);
}

Doing so will ensure that the possibility of the endpoint being retired will be described in the metadata for the service as a potential cause of a fault from the outset. Consequently, developers building clients will be able to anticipate the prospect of the endpoint being retired at some point. To retire the existing endpoint, the service developer would have the methods implementing the operations exposed at that endpoint do nothing more than throw the fault exceptions indicating that the endpoint was no longer in use.

The second way of alleviating the consequences of retiring an endpoint is to closely monitor the use of a service. One should know the number of clients using an endpoint and also the operators of those clients. Then the implications of retiring the endpoint can be properly assessed and the parties that will be affected can be notified.

Changing the Address of a Service Endpoint

Deciding to change the address of a service endpoint is equivalent to deciding to retire an existing endpoint. However, if all one was doing was changing the address of an endpoint, the cost of that non-backwards-compatible change would be alleviated by having the clients locate the service via a UDDI registry rather than relying on an address that might vary. Chapter 2, “The Fundamentals,” explained how to write Windows Communication Foundation clients that use address and binding information retrieved from metadata, and that metadata could be indeed be retrieved from a UDDI registry.

Centralized Lifecycle Management

In Chapter 2, service-oriented architecture was defined as an approach to organizing the software of an enterprise by providing service facades for all of that software, and publishing the WSDL for those services in a central repository. Among the perceived virtues of that approach is the prospect of centralized lifecycle management. Specifically, proponents of service-oriented architecture anticipate the registry serving as a central point of control through which system administrators could determine how the software entities in their organizations function by associating policies with the services that provide facades for all of the other software resources.

For that prospect to be realized, it would be necessary for services to monitor the registry for changes in policy applicable to them, and then automatically reconfigure themselves in accordance with those policies. As remarkable as that might sound, the Windows Communication Foundation actually can be made to reconfigure itself according to stipulated policies.

A policy, in the sense in which that term is being used here, refers to a collection of policy assertions, and a policy assertion is a nonfunctional requirement of a service endpoint (Weerawarana and others 2005, 128). WS-Policy provides an interoperable, standard way of expressing policies.

Because policies define the nonfunctional requirements of a service endpoint, they pertain to the binding of the endpoint, in Windows Communication Foundation terms, rather than to the contract, which defines the functional characteristics. So, for Windows Communication Foundation applications to be able to configure themselves in accordance with policies retrieved from a registry, they would have to be able to convert policies into bindings. They can. Follow these steps to learn how.

  1. Copy the code associated with this chapter downloaded from http://www.cryptmaker.com/WindowsCommunicationFoundationUnleashed to the folder C:WCFHandsOn. The code is all in a folder called Versioning.

  2. Open the Visual Studio 2005 solution, Versioning.sln. This solution contains a single project, called Host. The project is for building a console application that is to serve as the host of a derivatives calculator service like the one described in Chapter 2.

  3. Note that the project does not include any application configuration file.

  4. Examine the code in the static Main() method of the console application, which is in the Program.cs of the Host project. It is reproduced in Listing 20.2. A single statement that uses the Windows Communication Foundation’s System.ServiceModel.WsdlImporter class, reads a stream of metadata that includes policy assertions expressed using WS-Policy, and yields from that stream a collection of binding objects. That collection of endpoints is then used to add service endpoints to the host programmatically.

    Example 20.2. A Host that Configures Itself from Metadata

    using (ServiceHost host = new ServiceHost(
        serviceType,
        new Uri[] {new Uri("http://localhost:8000/Derivatives/")}
        ))
    {
        Collection<Binding> bindings =
                    new WsdlImporter(
                            MetadataSet.ReadFrom(
                                    new XmlTextReader(
                                           new FileStream(
                                              "metadata.xml",
                                              FileMode.Open)))).ImportAllBindings();
        int index = 0;
        foreach (Binding binding in bindings)
        {
            host.AddServiceEndpoint(
                            typeof(IDerivativesCalculator),
                            binding,
                            string.Format("Calculator{0}",index++));
        }
    
        [...]
        host.Open();
    
        Console.WriteLine(
            "The derivatives calculator service is available."
        );
        Console.ReadKey(true);
        host.Close();
    }
    
  5. Choose Debug, Start Debugging from the Visual Studio 2005 menus, to start the service host application.

  6. When that application’s console asserts that the service is available, confirm that it is by directing a browser to the service’s base address, http://localhost:8000/Derivatives/. A page describing the service should appear.

  7. Choose Debug, Stop Debugging from the Visual Studio 2005 menus to terminate the host application.

In this case, the stream of metadata from which the Windows Communication Foundation’s System.ServiceModel.WsdlImporter retrieved a collection of bindings included policies, and those policies consisted of standard policy assertions defined by the WS-SecurityPolicy specification. The System.ServiceModel.WsdlImporter has the facilities for understanding such standard policy assertions and configuring bindings accordingly. However, the WS-Policy language allows one to formulate an unlimited variety of policy assertions, and the System.ServiceModel.WsdlImporter is naturally not inherently capable of understanding every policy assertion one might devise. One can readily extend its capabilities to understand any given policy assertion, though. To do so, one creates a type that implements the System.ServiceModel.Description.IPolicyImportExtension interface that can read the policy assertion and apply it to a binding, and then one adds an instance of that type to the System.ServiceModel.WsdlImporter object’s PolicyImportExtensions collection.

It should be apparent, then, that the Windows Communication Foundation does allow services to reconfigure themselves in accordance with stipulated policies. However, while the notion of services being able to do this has some currency, it is ill-advised. It has already been explained that having a service reconfigure itself to conform to new policies means having the service change its binding. The versioning decision tree showed that the least costly way to implement a change to a binding is to expose a new endpoint with the modified binding, and to avoid the option of retiring the old endpoint. However, when a service reconfigures its binding to conform to a new policy, the existing endpoint is changed, and any clients that used that endpoint will very likely no longer be able to do so until they are reconfigured as well. Chapter 2 already showed that Windows Communication Foundation clients can configure themselves dynamically from updated binding information that they could retrieve from the metadata of the modified service, but one must consider how many potential points of failure this scenario incorporates.

Summary

This chapter provided a versioning decision tree that depicts all the logically possible ways in which one might have to modify a service, how to implement each change, and what the consequences will be. The tree shows that one has considerable leeway to modify a service without incurring the cost of having to update all its clients.

References

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset