Versioning ASP.NET Web API Services Using HTTP Headers

I’ve been doing some work with APIs lately and finally had the chance to dig into the ASP.NET Web API a bit more. While it’s technically brand new (released with .NET 4.5 and Visual Studio 2012), the Web API has been around in beta form for quite a bit now. For those of us who have done a fair amount of work with the WCF framework, the Web API is a welcome addition/replacement. Instead of monstrous configuration files and contract-first demands placed on us by WCF, we can now build RESTful web services using a very lightweight and HTTP-focused framework. As I work on designing a new API, one thing that I’m focused on right now is versioning. In this blog post, I’ll show you how to build HTTP-header-based versioning for ASP.NET Web API services.

Service designers have a few choices when it comes to versioning their services. What seems like the default option for many is to simply replace the existing service with a new one and hope that no consumers get busted. However, that’s pretty rough and hopefully less frequent than it was in the early days of service design. In the must-read REST API Design Handbook (see my review),  author George Reese points out three main options:

  • HTTP Headers. Set the version number in a custom HTTP header for each request.
  • URI Component. This seems to be the most common one. Here, the version is part of the URI (e.g. /customerservice/v1/customers).
  • Query Parameter. In this case, a parameter is added to each incoming request (e.g. /customerservice/customers?version=1).

George (now) likes the first option, and I tend to agree. It’s nice to not force new URIs on the user each time a service changes. George finds that a version in the header fit nicely with other content negotiations that show up in HTTP headers (e.g. “content-type”). So, does the ASP.NET Web API support this natively? The answer is: pretty much. While you could try and choose different controller operations based on the inbound request, it’s even better to be able to select entirely different controllers based on the API version. Let’s see how that works.

First, in Visual Studio 2012, I created a new ASP.NET MVC4 project and chose the Web API template.

2012.09.25webapi01

Next, I wanted to add a new “model” that is the representation of my resource. In this example, my service works with an “Account” resource that has information about a particular service account owner.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Runtime.Serialization;

namespace Seroter.AspNetWebApi.VersionedSvc.Models
{
    [DataContract(Name = "Account", Namespace = "")]
    public class Account
    {
        [DataMember]
        public int Id { get; set; }
        [DataMember]
        public string Name { get; set; }
        [DataMember]
        public string TimeZone { get; set; }
        [DataMember]
        public string OwnerName { get; set; }
    }
}

Note that I don’t HAVE to use the “[DataContract]” and “[DataMember]” attributes, but I wanted a little more control over the outbound naming, so I decided to decorate my model this way. Next up, I created a new controller to respond to HTTP requests.

2012.09.25webapi02

The controller does a few things here. It loads up a static list of accounts, responds to “get all” and “get one” requests, and accepts new accounts via HTTP POST. The “GetAllAccounts” operation is named in a way that the Web API will automatically use that operation when the user requests all accounts (/api/accounts). The “GetAccount” operation responds to requests for a specific account via HTTP GET. Finally, the “PostAccount” operation is also named in a way that it is automatically wired up to any POST requests, and it returns the URI of the new resource in the response header.

public class AccountsController : ApiController
    {
        /// <summary>
        /// instantiate list of accounts
        /// </summary>
        Account[] accounts = new Account[]
        {
            new Account { Id = 100, Name = "Big Time Consulting", OwnerName = "Harry Simpson", TimeZone = "PST"},
            new Account { Id = 101, Name = "BTS Partners", OwnerName = "Bobby Thompson", TimeZone = "MST"},
            new Account { Id = 102, Name = "Westside Industries", OwnerName = "Ken Finley", TimeZone = "EST"},
            new Account { Id = 103, Name = "Cricket Toys", OwnerName = "Tim Headley", TimeZone = "PST"}
        };

        /// <summary>
        /// Returns all the accounts; happens automatically based on operation name
        /// </summary>
        /// <returns></returns>
        public IEnumerable<Account> GetAllAccounts()
        {
            return accounts;
        }

        /// <summary>
        /// Returns a single account and uses an explicit [HttpGet] attribute
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        [HttpGet]
        public Account GetAccount(int id)
        {
            Account result = accounts.FirstOrDefault(acct => acct.Id == id);

            if (result == null)
            {
                HttpResponseMessage err = new HttpResponseMessage(HttpStatusCode.NotFound)
                {
                    ReasonPhrase = "No product found with that ID"
                };

                throw new HttpResponseException(err);
            }

            return result;
        }

        /// <summary>
        /// Creates a new account and returns HTTP code and URI of new resource representation
        /// </summary>
        /// <param name="a"></param>
        /// <returns></returns>
        public HttpResponseMessage PostAccount(Account a)
        {
            Random r = new Random(1);

            a.Id = r.Next();
            var resp = Request.CreateResponse<Account>(HttpStatusCode.Created, a);

            //get URI of new resource and send it back in the header
            string uri = Url.Link("DefaultApi", new { id = a.Id });
            resp.Headers.Location = new Uri(uri);

            return resp;
        }
    }

At this point, I had a working service. Starting up the service and invoking it through Fiddler made it easy to interact with. For instance, a simple “get” targeted at http://localhost:6621/api/accounts returned the following JSON content:

2012.09.25webapi03

If I did an HTTP POST of some JSON to that same URI, I’d get back an HTTP 201 code and the location of my newly created resource.

2012.09.25webapi04

Neato. Now, something happened in our business and we need to change our API. Instead of just overwriting this one and breaking existing clients, we can easily add a new controller and leverage the very cool IHttpControllerSelector interface to select the right controller at runtime. First, I made a few updates to the Visual Studio project.

  • I added a new class (model) named AccountV2 which has additional data properties not found in the original model.
  • I changed the name of the original controller to AccountsControllerV1 and created a second controller named AccountsControllerV2. The second controller mimics the first, except for the fact that it works with the newer model and new data properties. In reality, it could also have entirely new operations or different plumbing behind existing ones.
  • For kicks and giggles, I also created a new model (Invoice) and controller (InvoicesControllerV1) just to show the flexibility of the controller selector.

2012.09.25webapi05

I created a class, HeaderVersionControllerSelector, that will be used at runtime to pick the right controller to respond to the request. Note that my example below is NOT efficiently written, but just meant to show the moving parts. After seeing what I do below, I strongly encourage you to read this great post and very nice accompanying Github code project that shows a clean way to build the selector.

Basically, there are a few key parts here. First, I created a dictionary to hold the controller (descriptions) and load that within the constructor. These are all the controllers that the selector has to choose from. Second, I added a helper method (thanks to the previously mentioned blog post/code) called “GetControllerNameFromRequest” that yanks out the name of the controller (e.g. “accounts”) provided in the HTTP request. Third, I implemented the required “GetControllerMapping” operation which simply returns my dictionary of controller descriptions. Finally, I implemented the required “SelectController” operation which determines the API version from the HTTP header (“X-Api-Version”), gets the controller name (from the previously created helper function), and builds up the full name of the controller to pull from the dictionary.

 /// <summary>
    /// Selects which controller to serve up based on HTTP header value
    /// </summary>
    public class HeaderVersionControllerSelector : IHttpControllerSelector
    {
        //store config that gets passed on on startup
        private HttpConfiguration _config;
        //dictionary to hold the list of possible controllers
        private Dictionary<string, HttpControllerDescriptor> _controllers = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="config"></param>
        public HeaderVersionControllerSelector(HttpConfiguration config)
        {
            //set member variable
            _config = config;

            //manually inflate controller dictionary
            HttpControllerDescriptor d1 = new HttpControllerDescriptor(_config, "AccountsControllerV1", typeof(AccountsControllerV1));
            HttpControllerDescriptor d2 = new HttpControllerDescriptor(_config, "AccountsControllerV2", typeof(AccountsControllerV2));
            HttpControllerDescriptor d3 = new HttpControllerDescriptor(_config, "InvoicesControllerV1", typeof(InvoicesControllerV1));
            _controllers.Add("AccountsControllerV1", d1);
            _controllers.Add("AccountsControllerV2", d2);
            _controllers.Add("InvoicesControllerV1", d3);
        }

        /// <summary>
        /// Implement required operation and return list of controllers
        /// </summary>
        /// <returns></returns>
        public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
        {
            return _controllers;
        }

        /// <summary>
        /// Implement required operation that returns controller based on version, URL path
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        public HttpControllerDescriptor SelectController(System.Net.Http.HttpRequestMessage request)
        {
            //yank out version value from HTTP header
            IEnumerable<string> values;
            int? apiVersion = null;
            if (request.Headers.TryGetValues("X-Api-Version", out values))
            {
                foreach (string value in values)
                {
                    int version;
                    if (Int32.TryParse(value, out version))
                    {
                        apiVersion = version;
                        break;
                    }
                }
            }

            //get the name of the route used to identify the controller
            string controllerRouteName = this.GetControllerNameFromRequest(request);

            //build up controller name from route and version #
            string controllerName = controllerRouteName + "ControllerV" + apiVersion;

            //yank controller type out of dictionary
            HttpControllerDescriptor controllerDescriptor;
            if (this._controllers.TryGetValue(controllerName, out controllerDescriptor))
            {
                return controllerDescriptor;
            }
            else
            {
                return null;
            }
        }

        /// <summary>
        /// Helper method that pulls the name of the controller from the route
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        private string GetControllerNameFromRequest(HttpRequestMessage request)
        {
            IHttpRouteData routeData = request.GetRouteData();

            // Look up controller in route data
            object controllerName;
            routeData.Values.TryGetValue("controller", out controllerName);

            return controllerName.ToString();
        }
    }

Nearly done. All that was left was to update the global.asax.cs file to ignore the default controller handling (where it looks for the controller name from the URI and appends “Controller” to it) and replace it with our new controller selector.

public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            //added to support runtime controller selection
            GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector),
                                                           new HeaderVersionControllerSelector(GlobalConfiguration.Configuration));
        }
    }

That’s it! Let’s try this bad boy out. First, I tried retrieving an individual record using the “version 1” API. Notice that I added an HTTP header entry for X-Api-Version.

2012.09.25webapi06

Did you also see how easy it is to switch content formats? Just changing the” Content-Type” HTTP header to “application/xml” resulted in an XML response without me doing anything to my service. Next, I did a GET against the same URI, but set the X-Api-Version to 2.

2012.09.25webapi07

The second version of the API now returns the “sub accounts” for a given account, while not breaking the original consumers of the first version. Success!

Summary

The ASP.NET Web API clearly multiple versioning strategies, and I personally like this one the best. You saw how it was really easy to carve out entirely new controllers, and thus new client experiences, without negatively impacting existing service clients.

What do you think? Are you a fan of version information in the URI, or are HTTP headers the way to go?

Author: Richard Seroter

Richard Seroter is currently the Chief Evangelist at Google Cloud and leads the Developer Relations program. He’s also an instructor at Pluralsight, a frequent public speaker, the author of multiple books on software design and development, and a former InfoQ.com editor plus former 12-time Microsoft MVP for cloud. As Chief Evangelist at Google Cloud, Richard leads the team of developer advocates, developer engineers, outbound product managers, and technical writers who ensure that people find, use, and enjoy Google Cloud. Richard maintains a regularly updated blog on topics of architecture and solution design and can be found on Twitter as @rseroter.

14 thoughts

  1. Thanks Richard, great post!

    I prefer to have the version information in the URI. It seems more natural for me (like using the version as part of the xml namespace). Then again, I don’t have rock solid arguments 🙂 you could go either way.

  2. Good stuff. I prefer to use HTTP headers for version identification and negotiation. I also prefer to use the standard Accepts header with vendor MIME media types in the request and the Content-Type in the response. The Accepts header contains an ordered list of media types, so the request can specify that it prefers (for example) v3, but will accept v1 or v2 in that order. Here version is being negotiated in a single request. See http://barelyenough.org/blog/2008/05/versioning-rest-web-services/ for more discussion. I haven’t done this with Web API services yet, but do it quite a bit with WCF HTTP (RESTful) services. I’d be curious why you chose to create a custom header in this example.

    Putting version identifiers in the URI is stating that the different versions are fundamentally different resources. That’s not what we want different versions of the same resource to mean. A new version might (e.g.) add some data to a Car resource, it is not a completely different Car resource.

    Forcing new URIs on the client can be very disruptive to the client. When a version identifier is placed in the URI the request can only identify the version, not negotiate it. I also find the implementation using version identifiers in the URI when a service manages many resources with many versions tends to get complex, creating an artificial force limiting the addition of new versions.

    Query string? Nah, that’s just wrong. Feels like a hack afterthought rather than a planned versioning capability.

  3. Thanks, Richard. If someone uses the Accepts HTTP header, one nice thing to do is have the service implementation do something reasonable when the request is generated from a browser’s address bar (the request will always have */* at the end of the list of media types in the Accepts header) or from a utility like curl.exe (which might have an empty or missing Accepts header). For example, one might have any of those conditions always return the latest version. That facilitates testing and quick sanity checks.

    Personal preference, but I’d also write the HeaderVersionControllerSelector to instantiate _controllers inside the ctor and use initializer syntax to avoid creating the intermediate variables (d1, …) and then adding them to the dictionary. It’s not just that this is more compact, but that will make it easier to add and remove controllers over time, since the code would no longer have to match up intermediate variables and dictionary Adds (you know that is a typo just waiting to happen.)

  4. Great read.

    It’s a pity that Web Api doesn’t support namespace parameter in a route, as MVC does. It would be nice if you could define Api controllers with same names in different namespaces. The controller selector would have much simpler logic in that case.

  5. nice stuff….how can i test whether my selector is working fine, i mean client getting info according to which service version used?

    1. Sonu, in my case, the other service version returns different data and a different data format. The client can definitely tell which one it is calling. You could also have trace statements to confirm what’s happening on the server side.

  6. Hello Richard, i tried to use your method but i have a problem, I can switch from a controllerV1 to a ControllerV2, but i get an error when trying to satisfy the request (key was not present in the dictionary). In my Apis i add a route attribute: [Route(“api/ping”)]. It seems it’s in the correct controller (because it is: i see it enters the constructor) but it can’t “resolve” the api.

    error message is:

    Message: “An error has occurred.”
    ExceptionMessage: “The given key was not present in the dictionary”
    ExceptionType: “System.Collections.Generic.KeyNotFoundException”
    StackTrace: ” in System.Collections.Generic.Dictionary`2.get_Item(TKey key) in System.Web.Http.Controllers.ApiControllerActionSelector.ActionSelectorCacheItem.FindActionMatchRequiredRouteAndQueryParameters(IEnumerable`1 candidatesFound) in System.Web.Http.Controllers.ApiControllerActionSelector.ActionSelectorCacheItem.FindMatchingActions(HttpControllerContext controllerContext, Boolean ignoreVerbs) in System.Web.Http.Controllers.ApiControllerActionSelector.ActionSelectorCacheItem.SelectAction(HttpControllerContext controllerContext) in System.Web.Http.Controllers.ApiControllerActionSelector.SelectAction(HttpControllerContext controllerContext) in System.Web.Http.ApiController.ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken) in System.Web.Http.Dispatcher.HttpControllerDispatcher.SendAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken) in System.Web.Http.Dispatcher.HttpControllerDispatcher.d__0.MoveNext()”

    Can you help me?
    thanks
    Max

    1. Same here, try to use this with attribute routing, got the same exception later in ApiControllerActionSelector, any idea?

      thanks,

      Ryan

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.