Versioning messages in WebServices using XmlSerializer (was One parameter to rule them all)

Note: this entry has moved.

Some time ago, there was an exchange of opinions between Don Smith and Dare Obasanjo about the merits of having coarse-grained web service interfaces (e.g. XmlNode Execute(XmlNode message)). Many believe this is an approach that eases long-term versioning of your service. The reason is that you can simply have a version number on the message itself and follow one of the following approaches when messages arrive (and you already support multiple message versions):

  1. Apply a transformation on the fly based on the message version: this can be done to upgrade an incoming version to the one that is supported by a single internal implementation, and/or to downgrade the response so that an older client can understand it.
  2. Route the message to the internal implementation that understands that particular message version.

Dare argued that this was pretty much a question of usability, as having an untyped interface receiving an XmlNode doesn’t make for a good API for your clients. I partly agree, but he was neglecting the existence of the xsd.exe/XmlSerializer combo. The way I see it, nobody will ever want to code against an untyped XmlDocument. For these coarse-grained web service methods, I’d distribute the corresponding schema that the payload is expected to comply with, which would be used on the client side to generate the .NET classes for a friendlier API. So the process would be:

  1. Service-side:
    1. Create the XSD
    2. Provide a version number to be specified at the message level
    3. Create the web service method receiving an XmlNode
  2. Client-side:
    1. Use the XSD distributed by the service provider to generate .NET classes with xsd.exe (or the other cool custom tool ;))
    2. Implement the client calls using the generated classes
    3. Generate the proxy for the service
    4. Create a helper class that generates an XmlNode given the XmlSerializable model
    5. Call the proxy with the node.

Given that doing 1.a. first is the recommended way of doing web services (a.k.a. contract-first), it seems not only logical but also something you would like to encourage your developers to do. Providing a version number at the message level is just a matter of adding a new attribute, which I like to call SchemaVersion (to avoid conflicts with potential payload Version attribute that may also be required, like Product.Version)

On the client side, if you’re using the custom tool the process is trivial. The helper class would simply serialize the object to a stream that you can later load into an XmlDocument ready to pass to the proxy. Note that the client API will remain strongly-typed until that last step of getting an XmlNode view of the object:

MessageRoot root = new MessageRoot();
root.SchemaVersion = "1.0"; 
 
// Build other objects/properties required by the message.
 
proxy.Execute(XmlHelper.GetNode(root));

Of course if the service returns a node in turn, another call to the helper could give you back a strongly typed object again.

NOTE: in another post I’ll show you how to avoid constructing an XmlDocument just to get an XmlNode view of an XmlSerializable object to pass to a web service.

What’s missing

Ideally, I would have defined the SchemaVersion attribute on the message root to have a required fixed value:

<xs:attribute; 
name="SchemaVersion" type="xs:string" fixed="1.0"; 
use="required" />

When you do this, the xsd.exe tool will generate a property like the following:

public MessageRoot() { 
    this.schemaVersionField = "1.0"; 

/// <remarks/> 
[System.Xml.Serialization.XmlAttributeAttribute()] 
[System.ComponentModel.DefaultValueAttribute("1.0")] 
public string SchemaVersion { 
    get { 
        return this.schemaVersionField; 
    } 
    set { 
        this.schemaVersionField = value; 
    } 
}

This implies that you wouldn’t need the line in the client that sets the version:

root.SchemaVersion = "1.0";

This would imply that when a new version of the schema is redistributed, the client wouldn’t need to go and change that line according to the new version. Failure to do so will cause the message to be processed as an older version, so it becomes a very important detail.

The only problem is that, as I reported a while back, the XmlSerializer will NOT serialize the SchemaVersion as it’s annotated with the DefaultValueAttribute and it happens to match the current value of the property.

So the only way (as of v1.x and until the bug I reported is fixed) is to define the SchemaVersion as a required attribute, with a simple type with a pattern that validates the version we want to ensure:

<xs:attribute 
name="SchemaVersion" type="SchemaVersionType" 
use="required" /> 
<xs:simpleType name="SchemaVersionType"> 
     <xs:restriction base="xs:string"> 
          <xs:pattern value="1.0" /> 
     </xs:restriction> 
</xs:simpleType>

The only problem is that now the generated class contains no hint whatsoever about the specific version that needs to be assigned to the property. Moreover, it’s up to the client code to go find the particular value, and set it to the object before passing it to the proxy. Not good at all, right?

It’s not too late to get this bug fixed in time for v2 and improve the versioning story for message-based web services, so hurry up and vote for the bug!

Update: the bug has been fixed in .NET 2.0, and now you should be able to have a fixed value serialized properly. Great!!!

3 Comments

  • The standarized way is the XSD itself ;).

    The problem is that with coarse-grained web services, you will necessarily have a single endpoint that can process multiple schemas. I don't think there's a way to describe such a service...

  • In the article you make this note &quot;NOTE: in another post I’ll show you how to avoid constructing an XmlDocument just to get an XmlNode view of an XmlSerializable object to pass to a web service.&quot; Have I missed this post? Would really like to see this.



    Steven Roberts

  • Yep... I owe you that one :( ... stay tuned

Comments have been disabled for this content.