Versioning Options
Scott Seely
Microsoft Corporation
October 15, 2002
Summary: Scott Seely walks through the steps of implementing a new version of a Web service, showing how to add extra methods, change method signatures, and update the data model. (11 printed pages)
Download Ays15oct2002-setup.msi.
Introduction
Back in August, a few of you wrote in asking me to explain how to version an interface. When implementing a new version of an interface, there are some instances where you can simply enhance the existing class, and others where you need to implement a new class that may use the previous version. I want to address the combinations that I believe most readers will encounter. Here are the most common tasks you will face when updating a Web service:
- Adding extra methods. The new methods are conceptually related to the existing Web service and should be implemented on the same endpoint.
- Changing method signatures. In this instance, the number and or types for input elements changes.
- Updating the data model. In this instance, data types will be expanded and data members may change names.
To set the stage, I want to lay out a simple Web service capable of undergoing all sorts of changes. This Web service represents version 1. Classes and the Web service will start out synchronized with one namespace. Everything will evolve throughout the article.
Version 1
I want to present a starting point for the modifications that will be made later. Each section builds on this initial Web service. The following sections do not, however, build on each other. The service is set up to handle messages to add two numbers together and to convert some basic personal data to a string.
[WebServiceAttribute(Namespace = NamespaceConsts.AYS15Oct2002)]
public class VersionOne : System.Web.Services.WebService {
[WebMethodAttribute]
public string GetDisplayValue( Person person ) {
return person.ToString();
}
[WebMethodAttribute]
public int Add( int a, int b ) {
return a + b;
}
}
The namespace strings are stored in one location, the NamspaceConsts class, to reduce problems with typos. I'll be reusing the namespace value a lot, and I want to reduce the chances of typing in the wrong string.
public class NamespaceConsts {
/// <summary>
/// This value contains the string used for the XML Namespace
/// https://msdn.microsoft.com/samples/AYS/2002/10/15/
/// </summary>
public const string AYS15Oct2002 =
"https://msdn.microsoft.com/samples/AYS/2002/10/15/";
/// <summary>
/// This value contains the string used for the XML Namespace
/// https://msdn.microsoft.com/samples/AYS/2002/10/22/
/// </summary>
public const string AYS22Oct2002 =
"https://msdn.microsoft.com/samples/AYS/2002/10/22/";
}
Finally, the first version of the Web service uses a few other classes:
- PersonName: This class contains three string member variables for a person's first, middle, and last name.
- USAddress: This class contains other member variables representing the street address, city, state, and zip code.
- Person: This class contains two public members, a PersonName and a USAddress. (Big surprise, right?)
All the classes use the System.Xml.Serialization.XmlTypeAttribute to make sure that when the class is serialized to XML, the classes are in the same XML namespace. For example, here is the Person class.
[XmlTypeAttribute(Namespace=NamespaceConsts.AYS15Oct2002)]
public class Person {
public PersonName Name;
public USAddress Address;
public override string ToString() {
return string.Format( "{0}\n{1}",
Name.ToString(),
Address.ToString() );
}
}
This pretty much details version 1 of the Web service.
Adding Extra Messages
One of the ways to update a Web service is to add extra messages that the Web service is able to accept. The Web service supports addition of numbers. How about adding the ability to subtract as well? How do we add this new message without breaking existing clients? First, let's see what happens if we try the naïve approach and just add the new Web method. For this experiment, I created a new Web service endpoint called Service2a.asmx. In this situation, the Version2a Web service was created only to show how to implement the changes. These changes could have also been applied to the VersionOne Web service. I then copied the code from ServiceOne.asmx.cs and added the new Web method. Here's the result.
[WebServiceAttribute(Namespace = NamespaceConsts.AYS15Oct2002)]
public class Version2a : System.Web.Services.WebService {
[WebMethodAttribute]
public string GetDisplayValue( Person person ) {
return person.ToString();
}
[WebMethodAttribute]
public int Add( int a, int b ) {
return a + b;
}
[WebMethodAttribute]
public int Subtract( int a, int b ) {
return a - b;
}
}
If I then point the client at this new endpoint, the Microsoft® .NET client continues to run just fine. What does this tell me? This tells me that if I add new messages without altering the existing ones, the deployed clients will continue to work without any modifications. A more important question to ask: "Is this the right thing to do?" From my point of view, the answer to that question is "No." I am speaking strictly from the point of view of a fully deployed Web service, not how you choose to work through a beta.
So, what is the right thing to do? In general, you should change the qualified name of the portType whenever you change the definition of a message. A "qualified name" is the XML Namespace plus portType name.
In this situation, the operations are all still logically related. It makes perfect sense to have an Add and Subtract operation as part of the same WSDL portType and binding. A user of the Web service would expect any proxy generation tools to keep the operations together. In this instance, we want to manage the XML namespaces used by the input and output messages while keeping all the operations in one binding. We also want to keep Add and GetDisplayValue accessible to existing clients. Finally, we want to indicate that the Web service is using a new XML Namespace. To do this, we need to add some attributes to set the namespace used by the request and response messages.
[WebServiceAttribute(Namespace = NamespaceConsts.AYS22Oct2002)]
public class Version2a : System.Web.Services.WebService {
[WebMethodAttribute]
[SoapDocumentMethodAttribute(
NamespaceConsts.AYS15Oct2002 + "GetDisplayValue",
RequestNamespace=NamespaceConsts.AYS15Oct2002,
ResponseNamespace=NamespaceConsts.AYS15Oct2002 )]
public string GetDisplayValue( Person person ) {
return person.ToString();
}
[WebMethodAttribute]
[SoapDocumentMethodAttribute(
NamespaceConsts.AYS15Oct2002 + "Add",
RequestNamespace=NamespaceConsts.AYS15Oct2002,
ResponseNamespace=NamespaceConsts.AYS15Oct2002 )]
public int Add( int a, int b ) {
return a + b;
}
[WebMethodAttribute]
[SoapDocumentMethodAttribute(
NamespaceConsts.AYS22Oct2002 + "Subtract",
RequestNamespace=NamespaceConsts.AYS22Oct2002,
ResponseNamespace=NamespaceConsts.AYS22Oct2002 )]
public int Subtract( int a, int b ) {
return a - b;
}
}
By default, Microsoft® ASP.NET Web services route the message based on the SOAPAction. The SOAP action is created by concatenating the Web service XML Namespace with the operation being invoked. So, if a Web service uses the XML Namespace http://tempuri.org/ and contains an operation named foo, the default SOAPAction is http://tempuri.org/foo. Each exposed operation sets the SOAPAction and namespaces to create a more up-to-date set of operations. The WSDL generated by ASP.NET will place all of the operations in the same binding. As a reader of the WSDL, I can tell that Subtract was added sometime after Add and GetDisplayValue. As a user of the old WSDL, the original client will continue to function. Any new clients will be able to call Subtract as well, and will use the updated XML Namespaces. The endpoint is able to correctly respond to old and new clients without breaking. The endpoint does, however, fail to do one important thing: To document the fact that the one endpoint supports two bindings. How do we do this?
Because of how we chose to build the Web service, the existing attribution methods, which allow the developer to specify which binding a particular operation belongs to, do not work in this situation. The Add and GetDisplayValue messages did not change in any way, meaning that I cannot just attribute things "correctly" and move on. At this point, you have two options:
- Write some more code and give up on having one endpoint support both versions.
- Save the binding information to separate files for both versions, write a custom WSDL file for the endpoint, and turn off documentation for the Web application.
Let's take a look at these two options in turn.
Writing More Code
The code for this option lives in Version2b.asmx. If the idea of writing WSDL by hand repulses you, you can choose to write more code instead. (Hey, writing WSDL by hand is not exactly an easy thing to do, and you just may not want to bother.) If you would rather let ASP.NET do the work for you, the option of writing more code requires you to deploy a new endpoint. This option does not maintain namespace compatibility with existing clients, meaning that the Web application will have two .ASMX endpoints: one for version 1 and one for version 2.
When I went to do this, my first instinct was to derive the version 2 Web service class from VersionOne, add methods in the new XML namespace, then redirect requests to Add in the new namespace to Add in the base class. For better or worse, the methods in VersionOne are made available through inheritance, and the message names for Add and GetDisplayValue in both namespaces wind up clashing. Why? ASP.NET winds up with some problems around clashes in identical message names. I could change the message name, but setting the message name to Addv2 is completely unappealing to me. The targetNamespace of the WSDL file indicates the version. I don't want to take that information into the operation name—it clutters things up.
My next attempt used delegation. Since functionality is not changing, the Web service should make use of the previous version methods. Delegation wound up working. The code has the three functions: Add, GetDisplayValue, and the new Subtract.
[WebServiceAttribute(Namespace = NamespaceConsts.AYS22Oct2002)]
public class Version2b : WebService {
[WebMethodAttribute]
public string GetDisplayValue( Person person ) {
// Call the original function
VersionOne v1 = new VersionOne();
return v1.GetDisplayValue( person );
}
[WebMethodAttribute]
public int Add( int a, int b ) {
// Call the original function
VersionOne v1 = new VersionOne();
return v1.Add( a, b );
}
[WebMethodAttribute]
public int Subtract( int a, int b ) {
return a - b;
}
}
As you can see, the calls to Add and GetDisplayValue delegate the requests to the original version.
Custom WSDL
Let's take a look at what we could do to show that Service2a.asmx really does implement the version 1 and 2 bindings. A WSDL file indicates its version in the /documentation/@targetNamespace attribute. For version 1, the value of the targetNamespace attribute is https://msdn.microsoft.com/samples/AYS/2002/10/15/. Version 2 uses https://msdn.microsoft.com/samples/AYS/2002/10/22/.
As far as I could tell, there is no way I could see to make ASP.NET generate the right WSDL automatically. Here are the steps we will follow:
- Save the WSDL for the VersionOne.asmx Web service.
- Save the WSDL for the Version2a.asmx Web service.
- Remove the service elements from the WSDL documents.
- Place the schema for the version 1 namespace into a separate XSD file.
- Write and save the WSDL file for the Version2a.asmx Web service.
- Turn off documentation.
Steps 1–3 can be done by navigating to the .ASMX page, viewing the WSDL, and saving the presented WSDL to disk. Then just open the saved file and remove the service element. For step 4, I created a new file, MessageTypes.xsd. I saved the contents of the types section from VersionOne.wsdl to this file, and added namespace declarations for the XSD XML Namespace and for the version 1 XML Namespace. In the end, the types section for VersionOne.wsdl was reduced to the following:
<import
namespace="https://msdn.microsoft.com/samples/AYS/2002/10/15/"
location="https://localhost/AYS15Oct2002/MessageTypes.xsd" />
<types />
The new WSDL for the Web service is:
<?xml version="1.0" encoding="utf-8" ?>
<definitions
xmlns:s1="https://msdn.microsoft.com/samples/AYS/2002/10/22/"
xmlns:soap="https://schemas.xmlsoap.org/wsdl/soap/"
xmlns:s0="https://msdn.microsoft.com/samples/AYS/2002/10/15/"
targetNamespace="https://msdn.microsoft.com/samples/AYS/2002/10/Impl"
xmlns="https://schemas.xmlsoap.org/wsdl/">
<import namespace="https://msdn.microsoft.com/samples/AYS/2002/10/15/"
location="https://localhost/AYS15Oct2002/VersionOne.WSDL" />
<import namespace="https://msdn.microsoft.com/samples/AYS/2002/10/22/"
location="https://localhost/AYS15Oct2002/Version2a.WSDL" />
<types />
<service name="Version2a">
<port name="Version1" binding="s0:Version1">
<soap:address
location="https://localhost/AYS15Oct2002/Version2a.asmx" />
</port>
<port name="Version2aSoap" binding="s1:Version2aSoap">
<soap:address
location="https://localhost/AYS15Oct2002/Version2a.asmx" />
</port>
</service>
</definitions>
Notice that the location for both ports is the same. Finally, the documentation was turned off. To do this, use the /configuration/system.web/webServices/protocols section of web.config and add the following line:
<webServices>
<protocols>
<remove name="Documentation" />
</protocols>
</webServices>
So, what did this option accomplish? We edited WSDL that accurately reflects the messages accepted by both versions of the Web service. A new WSDL was created to show that the endpoint understands the messages defined in both documents. The underlying Web service is able to accept messages for both versions due to the way it was coded.
Adding new methods isn't the only way to evolve a Web service. Next, let's take a look at what to do if you change the signature of a method.
Changing Method Signatures
For this case, I am assuming that you want to leave the existing version operational, and that you want to change the contents of a specific message. Let’s take a look at changing the Add message to take an array of integers and return the sum of those integers. This new Add message and the old one are incompatible.
I could get both messages to operate on the same endpoint by giving them different names, but that isn't what I want to do. The WSDL needs to reflect that this method is a newer, better form of Add. To make the break clean and keep the other functions together, I created a new Web service, Version2c. I also chose to move GetDisplayValue into the new XML Namespace. This function still calls into the internal GetDisplayValue method to take advantage of the existing code and any future bug fixes.
[WebServiceAttribute(Namespace=NamespaceConsts.AYS22Oct2002)]
[WebServiceBinding("Version2", NamespaceConsts.AYS22Oct2002 )]
public class Version2c : System.Web.Services.WebService {
[WebMethodAttribute]
[SoapDocumentMethodAttribute(Binding="Version2")]
public string GetDisplayValue( Person person ) {
VersionOne v1 = new VersionOne();
return v1.GetDisplayValue( person );
}
[WebMethodAttribute]
[SoapDocumentMethodAttribute(Binding="Version2")]
public int Add( int[] values ) {
int retval = 0;
if ( ( values == null ) ||
( values.Length < 1 ) ) {
// No values passed in, return 0.
return retval;
}
foreach( int val in values ) {
retval += val;
}
return retval;
}
}
In this situation, the version 1 and version 2 client run side-by-side at different URLs. Add methods cannot be confused, since one version takes an array of integers whereas the older version takes two integers.
To wrap up, let's take a look at what to do when you add new derived types and want the Web methods to handle the new data.
Updating the Data Model
In case you were wondering why GetDisplayValue was in the Web service, you are about to find out. In our scenario, we decide to add a new type of Person called Penpal. Penpal extends Person by adding an e-mail address. The definition for Penpal is:
[XmlType(Namespace=NamespaceConsts.AYS22Oct2002)]
public class Penpal : Person {
public string Email;
public override string ToString() {
return string.Format( "{0}\n{1}",
base.ToString(),
Email );
}
}
I'd like to enable the Web service to accept either a Person or a Penpal for GetDisplayValue. For the Web service, I do the following:
- Add a new type.
- Change the types GetDisplayValue accepts.
Considering my requirements, it appears I am somewhere between where I was when I added a new method and where I changed the signature. However, I did change the set of data types that the GetDisplayValue method accepts. If versioning is truly important (and it is), this change requires GetDisplayValue to be in a new XML Namespace. So, once again, this new version should go to a separate URL and class.
[WebServiceAttribute(Namespace=NamespaceConsts.AYS22Oct2002)]
[WebServiceBindingAttribute("Version2", NamespaceConsts.AYS22Oct2002)]
public class Version2d : System.Web.Services.WebService {
[WebMethodAttribute]
[SoapDocumentMethodAttribute(Binding="Version2")]
public string GetDisplayValue(
[XmlElement("Person", typeof(Person))]
[XmlElement("Penpal", typeof(Penpal))]
Person person ) {
VersionOne v1 = new VersionOne();
return v1.GetDisplayValue( person );
}
[WebMethodAttribute]
[SoapDocumentMethodAttribute(Binding="Version2")]
public int Add( int a, int b ) {
VersionOne v1 = new VersionOne();
return v1.Add( a, b );
}
}
Internally, the revision calls into the old Web service since the functionality didn't change—just the WSDL.
Conclusion
When creating a new version of a Web service, you will need to make one or more changes:
- Add a new method.
- Change the signature of a method.
- Update the data model.
In every case, it's easiest to create a new Web service endpoint that delegates to the previous version when possible, and implements the new functionality otherwise. This isn't the first time I've discussed versioning, but it is the first time I've actually shown what I was doing in code to get the job done. A few readers posted some great questions that drove most of the content in this article. Thanks for the great feedback!
On another note, this is the last At Your Service column that I will write. My job duties have changed and I will no longer have the time to devote to this column. I've really enjoyed being able to present in this space and show my readers some of the neat things I've learned about ASMX and SOAP interoperability. Adios, amigos.
At Your Service
Scott Seely is a member of the MSDN Architectural Samples team. Besides his work there, Scott is the author of SOAP: Cross Platform Web Service Development Using XML (Prentice Hall—PTR) and the lead author for Creating and Consuming Web Services in Visual Basic (Addison-Wesley).