Wednesday, September 13, 2017

Microsoft 70-487: Version a WCF service

Exam Objectives

Version different types of contracts (message, service, data); configure address, binding, and routing service versioning


Quick Overview of Training Materials


Versioning Contracts


The "Versioning Strategies" document explains the version tolerance properties of the service and data contracts in WCF.  Certain changes to the service contract will not affect existing clients, such as adding and removing parameters (added parameters get default values, extra parameters are ignored) and adding new operations (existing clients just won't use).  Other changes will have an effect on existing clients (or at least can), such as modifying parameters or return values, or removing existing operations.  For Data contracts, adding or removing required members will break existing clients, and modifying existing members can lead to unexpected results.

One note on data version tolerance (brought up in Programming WCF Services), is that the version tolerance qualities apply when a data contract is defined with [DataContract] and [DataMember].  Custom types marked as [Serializable] are not version tolerant by default, as the generated data contract will have all the members marked as required.  Marking fields with the [OptionalField] attribute can get around this.

The version strategies range from "Agile" versioning strategies that rely heavily on backward compatibility, introducing formal version changes only when breaking changes are introduced, and "Strict" versioning, which introduces a new formal version for any change.  These strategies represents either extreme on a spectrum of versioning strategies.  Between these two extremes are "Semi-strict" strategies where introducing a new formal version is determined by policy.

The following diagrams are from the Versioning document:
Agile versioning.  Clients call the same service


Strict Versioning.  Each client version calls a unique endpoint

One drawback of a more "Agile" versioning strategy relates to the problem of round tripping and lost data.  When a client with a newer data contract (perhaps with new members) calls an old service that does not expect the new field, the data in the new field is lost. This can lead to issues in subsequent calls.  One way to work around this is the use of the IExtensibleDataObject interface, which introduces a "catch all" bucket to to the data contract that the data contract serializer can use to put extra fields it encounters that aren't in the contract.  If it doesn't make sense for a service to utilize this bucket, the default behavior (ignoring unknown members) can be enabled by setting the IgnoreExtensionDataObject property to true in a service behavior.

Message contract versioning behaves in much the same way as data contract versioning.  Extra or missing header or body elements are ignored in most cases.  However, if a header is marked as "MustUnderstand" but is not part of the contract (i.e. it is an unexpected header), then the service will throw an exception.



Version Routing


To get an idea of how the WCF routing service can be used in versioning scenarios, I created a demo based on the ideas in the "Versioning Strategies" document under "Versioning Service Contract".  I create two service contract interfaces, set the Name property the same on both, and created several operations.  Both had an operation called "OperationB", while one had "OperationA" and the other "OperationC", simulating a scenario in which a new version of the service has added a new operation and removed an old one.  I then created an implementing class that implemented all the operations for both contracts:


    [ServiceContract(Name ="IService")]
    public interface IService1
    {
        [OperationContract(Action = "OperationA")]
        string OperationA();

        [OperationContract(Action = "OperationB")]
        string OperationB();
    }
 
    [ServiceContract(Name = "IService")]
    public interface IService2
    {
        [OperationContract(Action = "OperationC")]
        string OperationC();

        [OperationContract(Action = "OperationB")]
        string OperationB();
    }
 
    public class Service : IService1, IService2
    {
        string endpoint = OperationContext.Current.Channel.LocalAddress.Uri.ToString();

        public string OperationA()
        {
            string message = "Called OperationA on endpoint " + endpoint;
            Console.WriteLine(message);
            return message;
        }

        public string OperationB()
        {
            string message = "Called OperationB on endpoint " + endpoint;
            Console.WriteLine(message);
            return message;
        }

        public string OperationC()
        {
            string message = "Called OperationC on endpoint " + endpoint;
            Console.WriteLine(message);
            return message;
        }
    }


Since I'm treating IService1 as the "old" version, I want the requests for OperationA to go to this legacy service, while all the requests supported by the new IService2 contract should be routed to that service.  This involves using an Action filter:

    <routing>
      <filters>
        <filter name="legacy" filterType="Action" filterData="OperationA"/>
        <filter name="else" filterType="MatchAll" />
      </filters>
      <filterTables>
        <filterTable name="RoutingTable">
          <add filterName="legacy" endpointName="Service1" priority="2"/>
          <add filterName="else" endpointName="Service2" priority="0"/>
        </filterTable>
      </filterTables>
    </routing>


The rest of the routing setup is covered in my previous post, as well as an article on CodeProject on WCF Routing, so I'm not going to dig that deep.  The point is that the router will direct all the calls to OperationA to the old service, and everything else will come to the new one:



A couple pitfalls I ran into while putting the demo together related to contract compatibility.  Before I set the ServiceContract "Name" property, the contracts were being treated as two (incompatible) contracts, one called IService1 and the other IService2.  I set the "Action" properties on the OperationContract attributes while trying to debug this issue, and while this did make it simpler, setting the name and updating the service reference in the clients should have been sufficient.  With the Action property blank, the "Action" header looks like "http://tempuri.org/IService/OperationA".

The other obstacle I ran into was that I could not find a way to set a wildcard contract on a programmatically generated ServiceEndpoint.  I got around this by using the app.config file (which seems to be the accepted norm for WCF services anyway), but it was just annoying that I couldn't set it in code (unless I missed something).  Ultimately the problem I was trying to solve had nothing to do with the contract on the endpoints... figures.

Of course, the only reason routing was needed in this case is because the contracts were mutually incompatible.  If a third contract, containing all three operations, were created, they both the version 1 and version 2 clients would be able to use it without modification to their service reference code.  If this superset "version 3" is hosted in place of the routing service (on port 10000 in my case), the client config doesn't even need to be changed.  They just work out of the box:




The full demo code can be found on my GitHub repo.

No comments:

Post a Comment