MessageHandlers and service Versions in Web API

The article demonstrates how different versions of the same service could be supported or not in a web API. Underneath are good links of different possibilities, which are better for different business scenarios. Depending on the client application/implementation, an update strategy could be implemented using Message Handlers.

Code: https://github.com/damienbod/WebAPIVersioningAnd-MessageHandlers

Possibility One: The old version of the service is supported but returns a HTTP 426 to inform the end client that the client must be updated. This could work well for closed systems, but never for public services as the HTTP status is not OK!

To demonstrate this, 2 controllers need to be created.

This is the old controller version with the URL api/myservice

using System;
using System.Collections.Generic;
using System.Web.Http;

namespace WebAPIVersioning.Controllers
{
    [RoutePrefix("api/myservice")]
    public class MyServiceController : ApiController
    {
        // OLD methods still required for legacy clients. The client is informed 
        // with a 426 in a message handler but still receives content.
        [Route("")]
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "V1-1", "V1-2" };
        } 
    }
}

And this is the new service version with the URL api/v2/myservice

using System;
using System.Collections.Generic;
using System.Web.Http;

namespace WebAPIVersioning.Controllers
{
    [RoutePrefix("api/v2/myservice")]
    public class MyServiceV2Controller : ApiController
    {
        [Route("")]
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "V2-1", "V2-2" };
        }
    }
}

Now create a Message Handler. The Message Handler allows the request to be executed as before but changes the status to UpgradeRequired. This will set the HTTP status OK to false;

using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;

namespace WebAPIVersioning
{
    public class VersioningHandler : DelegatingHandler
    {
        protected async override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var response = await base.SendAsync(request, cancellationToken);
            
            if (request.RequestUri.ToString().Contains("api/myservice"))
            {
                // Recieved request for old service implementation. Will return the object and send a 426
                response.StatusCode = HttpStatusCode.UpgradeRequired;
                response.Headers.Add("Location", "/api/v2/myservice");
            }

            return response;
        }
    }
}

The Web API config needs to be changed. The VersioningHandler is added.

using System.Web.Http;

namespace WebAPIVersioning
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            config.MapHttpAttributeRoutes();
            config.MessageHandlers.Add(new VersioningHandler());
        }
    }
}

When the HTTP request is sent, the client receives a 426 with the correct old content.
MessageHandlerWebAPiVersion01

Possibility 2: The old version is not supported anymore. The request is returned directly in the Message Handler with a 426 and no payload. The Message Handler is as follows:

using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;

namespace WebAPIVersioning
{

    public class VersioningHandlerReturn : DelegatingHandler
    {
        // stop proccessing return directly to client
        protected override async Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var resp = new HttpResponseMessage(HttpStatusCode.UpgradeRequired)
            {
                ReasonPhrase = "Use /api/v2/myservice this is very old and not supported",
                Content = new StringContent("Use /api/v2/myservice this is very old and not supported")

            };
            throw new HttpResponseException(resp);

        }
    }
}

Only the URL with the old service should cause the above handler to be called. Add the Message Handler to the route config for the controller which does not exist anymore.

       config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/veryold",
                defaults: null,
                constraints: null,
                handler: new VersioningHandlerReturn()
                );

When executed a 426 is returned but with no payload and no controller processing.
MessageHandlerWebAPiVersion02

What if you need to support 2 versions of a service at the same time:

Jon Galloway provides an example on how to do this. See ASP.NET Web API 2.1 Custom Route Attributes example: Versioning By Header

Or also :
VersionConstraint.cs

VersionedRoute.cs

Note: If you use this, you cannot use the default RoutePrefix Attribute.
Here’s how your controller could look like:

using System.Collections.Generic;
using System.Web.Http;

namespace WebAPIVersioning.Controllers
{
    public class VersionTestController : ApiController
    {
        [VersionedRoute("api/versiontest/a", 1)]
        [HttpGet]
        public IHttpActionResult GetA()
        {
            return  SetVersionOk(new string[] { "returning A Version 1" });
        }

        [VersionedRoute("api/versiontest/b", 1)]
        [HttpGet]
        public IHttpActionResult GetB()
        {
            return SetVersionOk(new string[] { "returning B Version 1" });
        }

        [VersionedRoute("api/versiontest/{id}/c", 1)]
        [HttpGet]
        public IHttpActionResult GetC(int id)
        {
            return SetVersionOk(new List<string> { "returning C Version 1, id:" + id });
        }

        private IHttpActionResult SetVersionOk(object body)
        {
            return new SetVersionInResponseHeader<object>(Request, "1", body, true);
        }
    }
}

And the Version 2 controller:

using System.Web.Http;

namespace WebAPIVersioning.Controllers
{
    public class VersionTestV2Controller : ApiController
    {
        [HttpGet]
        [VersionedRoute("api/versiontest/a", 2)]
        public IHttpActionResult GetA()
        {
            return SetVersionOk(new string[] { "returning A Version 2" });
        }

        [HttpGet]
        [VersionedRoute("api/versiontest/b", 2)]
        public IHttpActionResult GetB()
        {
            return SetVersionOk(new string[] { "returning B Version 2" });
        }

        [VersionedRoute("api/versiontest/{id}/c", 2)]
        [HttpGet]
        public IHttpActionResult GetC(int id)
        {
            return SetVersionOk(new string[] { "returning C Version 2, id:" + id });
        }

        private IHttpActionResult SetVersionOk(object body)
        {
            return new SetVersionInResponseHeader<object>(Request, "2", body);
        }
    }
}

The IHttpActionResult implementation is used to set the headers of the responses. Different Headers are set depending on the version.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;

namespace WebAPIVersioning
{
    public class SetVersionInResponseHeader<T> : IHttpActionResult where T : class
    {
        private HttpRequestMessage _request;
        private T _body;
        private string _version;
        private bool _willBeRetiredInNextVersion;
        private bool _isObsolete;

        public SetVersionInResponseHeader(HttpRequestMessage request, string version, T body, bool willBeRetiredInNextVersion = false, bool isObsolete = false )
        {
            _request = request;
            _version = version;
            _body = body;
            _isObsolete = isObsolete;
            _willBeRetiredInNextVersion = willBeRetiredInNextVersion;
        }

        public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
        {
            var response = _request.CreateResponse(_body);
            response.Headers.Add("api-version", _version);
            if (_isObsolete) response.Headers.Add("api-version-obsolete", "THIS_VERSION_IS_OBSOLETE");
            if (_willBeRetiredInNextVersion) response.Headers.Add("api-version-retiring", "WILL_BE_RETIRED_IN_NEXT_VERSION");
            return Task.FromResult(response);
        }
    }
}

Of course there are many others ways to support different versions of services in Web API. See the links below. This post just demonstrates some possibilities using Message Handlers or request header version handling.

Links:

http://weblogs.asp.net/jgalloway/archive/2014/01/24/looking-at-asp-net-mvc-5-1-and-web-api-2-1-part-2-attribute-routing-with-custom-constraints.aspx

http://aspnet.codeplex.com/SourceControl/latest

http://bitoftech.net/2013/12/16/asp-net-web-api-versioning-accept-header-query-string/

http://seroter.wordpress.com/2012/09/25/versioning-asp-net-web-api-services-using-http-headers/

http://blogs.msdn.com/b/webdev/archive/2013/03/08/using-namespaces-to-version-web-apis.aspx

http://www.codeproject.com/Articles/741326/Introduction-to-Web-API-Versioning

http://byterot.blogspot.ch/2012/05/aspnet-web-api-series-messagehandler.html

http://www.asp.net/web-api/overview/working-with-http/http-message-handlers

http://www.asp.net/web-api/overview/web-api-clients/httpclient-message-handlers

http://benfoster.io/blog/aspnet-web-api-compression

One comment

  1. Wow that was unusual. I just wrote an incredibly long
    comment but after I clicked submit my comment didn’t show up.

    Grrrr… well I’m not writing all that over again. Anyhow,
    just wanted to say fantastic blog!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: