ServiceStack: RESTful Resource Versioning

Steve Stevenson picture Steve Stevenson · Sep 13, 2012 · Viewed 9.3k times · Source

I've taken a read to the Advantages of message based web services article and am wondering if there is there a recommended style/practice to versioning Restful resources in ServiceStack? The different versions could render different responses or have different input parameters in the Request DTO.

I'm leaning toward a URL type versioning (i.e /v1/movies/{Id}), but I have seen other practices that set the version in the HTTP headers (i.e Content-Type: application/vnd.company.myapp-v2).

I'm hoping a way that works with the metadata page but not so much a requirement as I've noticed simply using folder structure/ namespacing works fine when rendering routes.

For example (this doesn't render right in the metadata page but performs properly if you know the direct route/url)

  • /v1/movies/{id}
  • /v1.1/movies/{id}

Code

namespace Samples.Movies.Operations.v1_1
{
    [Route("/v1.1/Movies", "GET")]
    public class Movies
    {
       ...
    } 
}
namespace Samples.Movies.Operations.v1
{
    [Route("/v1/Movies", "GET")]
    public class Movies
    {
       ...
    }   
}

and corresponding services...

public class MovieService: ServiceBase<Samples.Movies.Operations.v1.Movies>
{
    protected override object Run(Samples.Movies.Operations.v1.Movies request)
    {
    ...
    }
}

public class MovieService: ServiceBase<Samples.Movies.Operations.v1_1.Movies>
    {
        protected override object Run(Samples.Movies.Operations.v1_1.Movies request)
        {
        ...
        }
    }

Answer

mythz picture mythz · Sep 13, 2012

Try to evolve (not re-implement) existing services

For versioning, you are going to be in for a world of hurt if you try to maintain different static types for different version endpoints. We initially started down this route but as soon as you start to support your first version the development effort to maintain multiple versions of the same service explodes as you will need to either maintain manual mapping of different types which easily leaks out into having to maintain multiple parallel implementations, each coupled to a different versions type - a massive violation of DRY. This is less of an issue for dynamic languages where the same models can easily be re-used by different versions.

Take advantage of built-in versioning in serializers

My recommendation is not to explicitly version but take advantage of the versioning capabilities inside the serialization formats.

E.g: you generally don't need to worry about versioning with JSON clients as the versioning capabilities of the JSON and JSV Serializers are much more resilient.

Enhance your existing services defensively

With XML and DataContract's you can freely add and remove fields without making a breaking change. If you add IExtensibleDataObject to your response DTO's you also have a potential to access data that's not defined on the DTO. My approach to versioning is to program defensively so not to introduce a breaking change, you can verify this is the case with Integration tests using old DTOs. Here are some tips I follow:

  • Never change the type of an existing property - If you need it to be a different type add another property and use the old/existing one to determine the version
  • Program defensively realize what properties don't exist with older clients so don't make them mandatory.
  • Keep a single global namespace (only relevant for XML/SOAP endpoints)

I do this by using the [assembly] attribute in the AssemblyInfo.cs of each of your DTO projects:

[assembly: ContractNamespace("http://schemas.servicestack.net/types", 
    ClrNamespace = "MyServiceModel.DtoTypes")]

The assembly attribute saves you from manually specifying explicit namespaces on each DTO, i.e:

namespace MyServiceModel.DtoTypes {
    [DataContract(Namespace="http://schemas.servicestack.net/types")]
    public class Foo { .. }
}

If you want to use a different XML namespace than the default above you need to register it with:

SetConfig(new EndpointHostConfig {
    WsdlServiceNamespace = "http://schemas.my.org/types"
});

Embedding Versioning in DTOs

Most of the time, if you program defensively and evolve your services gracefully you wont need to know exactly what version a specific client is using as you can infer it from the data that is populated. But in the rare cases your services needs to tweak the behavior based on the specific version of the client, you can embed version information in your DTOs.

With the first release of your DTOs you publish, you can happily create them without any thought of versioning.

class Foo {
  string Name;
}

But maybe for some reason the Form/UI was changed and you no longer wanted the Client to use the ambiguous Name variable and you also wanted to track the specific version the client was using:

class Foo {
  Foo() {
     Version = 1;
  }
  int Version;
  string Name;
  string DisplayName;
  int Age;
}

Later it was discussed in a Team meeting, DisplayName wasn't good enough and you should split them out into different fields:

class Foo {
  Foo() {
     Version = 2;
  }
  int Version;
  string Name;
  string DisplayName;
  string FirstName;
  string LastName;  
  DateTime? DateOfBirth;
}

So the current state is that you have 3 different client versions out, with existing calls that look like:

v1 Release:

client.Post(new Foo { Name = "Foo Bar" });

v2 Release:

client.Post(new Foo { Name="Bar", DisplayName="Foo Bar", Age=18 });

v3 Release:

client.Post(new Foo { FirstName = "Foo", LastName = "Bar", 
   DateOfBirth = new DateTime(1994, 01, 01) });

You can continue to handle these different versions in the same implementation (which will be using the latest v3 version of the DTOs) e.g:

class FooService : Service {

    public object Post(Foo request) {
        //v1: 
        request.Version == 0 
        request.Name == "Foo"
        request.DisplayName == null
        request.Age = 0
        request.DateOfBirth = null

        //v2:
        request.Version == 2
        request.Name == null
        request.DisplayName == "Foo Bar"
        request.Age = 18
        request.DateOfBirth = null

        //v3:
        request.Version == 3
        request.Name == null
        request.DisplayName == null
        request.FirstName == "Foo"
        request.LastName == "Bar"
        request.Age = 0
        request.DateOfBirth = new DateTime(1994, 01, 01)
    }
}