views:

292

answers:

1

I am in the process of upgrading our web services to support versioning. We will be publishing our versioned web services like so:

http://localhost/project/services/1.0/service.asmx
http://localhost/project/services/1.1/service.asmx

One requirement of this versioning is that I am not allowed to break the original wsdl (the 1.0 wsdl). The challenge lies in how to shepherd the newly versioned classes through the logic that lies behind the web services (this logic includes a number of command and adapter classes). Note that upgrading to WCF is not an option at the moment.

To illustrate this, let's consider an example with Blogs and Posts. Prior to the introduction of versions, we were passing concrete objects around instead of interfaces. So an AddPostToBlog command would take in a Post object instead of an IPost.

// Old AddPostToBlog constructor.
public AddPostToBlog(Blog blog, Post post) {
    // constructor body
}

With the introduction of versioning, I would like to maintain the original Post while adding a PostOnePointOne. Both Post and PostOnePointOne will implement the IPost interface (they are not extending an abstract class because that inheritance breaks the wsdl, though I suppose there may be a way around that via some fancy xml serialization tricks).

// New AddPostToBlog constructor.
public AddPostToBlog(Blog blog, IPost post) {
    // constructor body
}

This brings us to my question regarding serialization. The original Post class has an enum property named Type. For various cross-platform compatibility issues, we are changing our enums in our web services to strings. So I would like to do the following:

// New IPost interface.
public interface IPost
{
    object Type { get; set; }
}

// Original Post object.
public Post
{
    // The purpose of this attribute would be to maintain how
    // the enum currently is serialized even though now the
    // type is an object instead of an enum (internally the
    // object actually is an enum here, but it is exposed as
    // an object to implement the interface).
    [XmlMagic(SerializeAsEnum)]
    object Type { get; set; }
}

// New version of Post object
public PostOnePointOne
{
    // The purpose of this attribute would be to force
    // serialization as a string even though it is an object.
    [XmlMagic(SerializeAsString)]
    object Type { get; set; }
}

The XmlMagic refers to an XmlAttribute or some other part of the System.Xml namespace that would allow me to control the type of the object property being serialized (depending on which version of the object I am serializaing).

Does anyone know how to accomplish this?

A: 

I'm not clear on what you want, with the IPost and Post and so on.

It seems like you want to update your webservice. You use the word "version". What I'm not clear on, is what is changing in the external interface. You haven't described that.

We can all agree that the internal implementation of the service should be opaque to consumers of the service, right? So whether you've updated your implemenntation or not, no clients of the service should be aware. Whether you use IPost or Post or IWhatever, clients must not care, must not be aware. The only thing that matters to a service consumer is the on-the-wire signature - the public interface. The WSDL.

You already said that you are not allowed to break the original WSDL. I don't know what that means. Does that mean

  • the new service must use the same WSDL
  • the new service can use a different WSDL, but old clients must continue to run?
  • something else?

Normally when people speak of versioning a web service, they mean changing the messages that get sent on the wire.

Version 1 might look like:

<Post xmlns="http://www.example.com/webservices/2010/04"&gt;
  <AuthorId>217</AuthorId>
  <Posted>2010-04-07T22:02:23.2214747Z</Posted>
  <Title>Hello, I must be going.</Title>
  <Content>...</Content>
  <Type>Rant</Type>
</Post>

To keep compatible, version 2 can only add elements. For example, add a LastEdited element to the message:

<Post xmlns="http://www.example.com/webservices/2010/04"&gt;
  <AuthorId>217</AuthorId>
  <Posted>2010-04-07T22:02:23.2214747Z</Posted>
  <LastEdited>2010-04-07T22:02:23.2214747Z</LastEdited>
  <Title>This is Getting Very Interesting</Title>
  <Content>...</Content>
  <Type>Rant</Type>
</Post>

When I say compatible, I mean that version 1 documents like the above will be de-seriailzable by version 2 services.

Are you doing something like this? If so, elaborate.

You didn't describe any of that. Your description focuses solely on the internal implementation of the service. You haven't said anything about the externally-accessible interface, which is the main area of concern when "versioning" a service.


If you just want a way to serialize a property to a string, and the type of the property doesn't have an appropriate ToString() implementation, you can use a surrogate property. The pattern works like this: mark the actual property with [XmlIgnore]. Create an additional "surrogate" property of String type, with a getter and setter that reads or updates the actual property. Like this:

[XmlRoot("Post", Namespace="http://www.example.com/webservices/2010/04")]
public class Post1_1
{
    public Int32 AuthorId { get; set; }
    public DateTime Posted { get; set; }
    public String Title { get; set; }
    public PostType @Type { get; set; }

    [XmlIgnore]
    public DateTime LastEdited { get; set; }

    [XmlElement("LastEdited")]
    public String LastEdited_Surrogate
    {
        get
        {
            if (LastEdited > (DateTime.UtcNow - new TimeSpan(24,0,0)))
                return "Today";
            else if (LastEdited > (DateTime.UtcNow - new TimeSpan(48,0,0)))
                return "Yesterday";
            else
                return LastEdited.ToString();
        }

        set
        {
            if (value == "Today")
                LastEdited = DateTime.UtcNow;
            else if (value == "Yesterday")
                LastEdited = DateTime.UtcNow - new TimeSpan(24,0,0);
            else
                LastEdited = DateTime.Parse(value);
        }
    }
}
Cheeso