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):
- 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.
- 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:
- Service-side:
- Create the XSD
- Provide a version number to be specified at the message level
- Create the web service method receiving an XmlNode
- Client-side:
- Use the XSD distributed by the service provider to generate .NET classes with xsd.exe (or the other cool custom tool ;))
- Implement the client calls using the generated classes
- Generate the proxy for the service
-
Create a helper class that generates an
XmlNodegiven theXmlSerializablemodel - 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!!!