pr0g33k

 collapse all
  1. Four Ways to Version Your MVC Web API

    MSDN defines a WCF Contract as "a collection of Operations that specifies what the Endpoint communicates to the outside world" and that's appropriate considering Dictionary.com defines a contract as "an agreement between two or more parties for the doing or not doing of something specified." Whenever you create a REST endpoint and it gets consumed by a client, you're establishing a contract with that client. REST is all about resources and a Web API is a contract promising that a given resource will exist at the specified URI. Like most things in life, however, things change. When a resource that is the subject of a Web API changes, you have a few options: you can take the position that the behavior of a resource is one of its intrinsic properties, hence if that changes, you really have a different resource and therefore a different URI or you can establish a version for that resource. Since the former requires clients to update their code to take advantage of the new resources and each new resource will exponentially grow and complicate the API, we'll focus on the latter and discuss versioning in this article.

    Now the question becomes, "How do I version my Web API?" There's a lot of debate on this these days. The main debate comes down to identifying the version via the URI versus identifying the version in the header. There are pro's and con's to both. With Microsoft's MVC Web Api, though, you don't have to limit yourself to just one versioning scheme. I'm going to show you four ways to version your Web API within the same project:

    1. URI using Routing Attributes
    2. Custom HTTP Header
    3. HTTP Accept Header with a header parameter
    4. HTTP Accept Header with a Vendor MIME Type

    Project Setup

    Create a new Web Project and call it "Web" with a Solution name "WebApiVersioning." Make sure "Create directory for solution" is checked. Also note that I'm placing the Solution in a folder named "Projects."

    Select the Web API template and click "OK."

    When creating a new project, it's a good idea to update all of the NuGet packages before you get started so take a few moments to do that. As of this date, Web API 2.1 was the main update I installed. Run the project to make sure everything is working properly.

    URI-based versioning

    The templated project gives us a controller named "ValuesController" by default so we'll just work with that. To make it simple for this tutorial, I'm just going to work with the GET actions but everything we do here will apply to the other actions as well. Change the ValuesController class to this:

    namespace Web.Controllers
    {
        public class ValuesController : ApiController
        {
            // GET api/values
            public IEnumerable<string> Get()
            {
                return new string[] { "value1", "value2" };
            }
    
            // GET api/values/5
            public string Get(int id)
            {
                return "value";
            }
        }
    }

    Run the project and click on "API" in the menu (optionally, you can browse to "/help"). You should see this:

    Let's update our ValuesController and add a new version of the API. Then let's use MVC's Route attribute to add a "V1" and "V2" version indicator to the URI's.

    namespace Web.Controllers
    {
        public class Values_V1Controller : ApiController
        {
            [Route("api/v1/values")]
            public IEnumerable<string> Get()
            {
                return new string[] { "value1 (V1)", "value2 (V1)" };
            }
    
            [Route("api/v1/values/{id:int}")]
            public string Get(int id)
            {
                return String.Format("value {0} (V1)", id);
            }
        }
    
        public class Values_V2Controller : ApiController
        {
            [Route("api/v2/values")]
            public IEnumerable<string> Get()
            {
                return new string[] { "value1 (V2)", "value2 (V2)" };
            }
    
            [Route("api/v2/values/{id:int}")]
            public string Get(int id)
            {
                return String.Format("value {0} (V2)", id);
            }
        }
    }

    When you run the project and browse to the API help view, you'll notice the new URI's:

    As you can see, it's quite simple to version using the URI. This way of versioning is very helpful when your clients do not have access to any other means of reaching distinct versions of your API. For example, browsers or other applications that have only a URI address option can easily reach the different versions.

    Versioning in the URI can give your clients the wrong impression, however. According to some of the REST community, a URI defines a distinct resource. In our case, we have two distinct URI's and therefor two distinct resources. This may not be what we're intending.

    It's important to note that, in this tutorial, I'm only changing the data in the ValuesController to distinguish the 2 versions. In reality, it would be the structure of the objects returned that would dictate the need to have a different version. I didn't want to get bogged down in creating a complicated scenario. Instead, I'm just focusing on several mechanisms to enable versioning.

    Using a Custom HTTP Header

    With Web API 2 and above, you can add custom constraints to your routes. A custom route constraint enables you to prevent a route from being matched unless some custom condition is matched - in this case, the value of a custom HTTP header. The official samples list on Codeplex has a custom Route Constraint that does exactly what we need.

    First, add a new Class Library project called "Core.Web." Be sure to add it to the Projects directory, at the same directory level as the WebApiVersioning solution:

    Add a folder to the Core.Web project called "Routing" and add the Microsoft Web API 2.1 Core Libraries (or whatever is the most recent version) from NuGet.

    Next, add the following classes:

    namespace Core.Web.Routing
    {
        public class VersionConstraint : IHttpRouteConstraint
        {
            private const Int32 DEFAULT_VERSION = 1;
    
            public VersionConstraint(Int32 allowedVersion)
            {
                AllowedVersion = allowedVersion;
            }
    
            public Int32 AllowedVersion
            {
                get;
                private set;
            }
    
            public Boolean Match(HttpRequestMessage request, IHttpRoute route, String parameterName, IDictionary<String, Object> values, HttpRouteDirection routeDirection)
            {
                if (routeDirection == HttpRouteDirection.UriResolution)
                {
                    Int32 version = GetVersionHeader(request) ?? DEFAULT_VERSION;
    
                    if (version == AllowedVersion)
                        return true;
                }
    
                return false;
            }
    
            private Int32? GetVersionHeader(HttpRequestMessage request)
            {
                String versionAsString = String.Empty;
                IEnumerable<String> headerValues;
    
                if (request.Headers.TryGetValues("api-version", out headerValues) && headerValues.Count() == 1)
                    versionAsString = headerValues.First();
    
                Int32 version;
    
                if (!String.IsNullOrEmpty(versionAsString) && Int32.TryParse(versionAsString, out version))
                    return version;
    
                return null;
            }
        }
    }
    
    namespace Core.Web.Routing
    {
        public class VersionedRoute : RouteFactoryAttribute
        {
            public VersionedRoute(String template, Int32 allowedVersion)
                : base(template)
            {
                AllowedVersion = allowedVersion;
            }
    
            public Int32 AllowedVersion
            {
                get;
                private set;
            }
    
            public override IDictionary<String, Object> Constraints
            {
                get
                {
                    var constraints = new HttpRouteValueDictionary();
                    constraints.Add("version", new VersionConstraint(AllowedVersion));
                    return constraints;
                }
            }
        }
    }

    Add a Solution reference to the Web project for Core.Web and update the the Values_V1Controller and Values_V2Controller to this:

    namespace Web.Controllers
    {
        public class Values_V1Controller : ApiController
        {
            [VersionedRoute("api/values", 1)]
            [Route("api/v1/values")]
            public IEnumerable<string> Get()
            {
                return new string[] { "value1 (V1)", "value2 (V1)" };
            }
    
            [VersionedRoute("api/values/{id:int}", 1)]
            [Route("api/v1/values/{id:int}")]
            public string Get(int id)
            {
                return String.Format("value {0} (V1)", id);
            }
        }
    
        public class Values_V2Controller : ApiController
        {
            [VersionedRoute("api/values", 2)]
            [Route("api/v2/values")]
            public IEnumerable<string> Get()
            {
                return new string[] { "value1 (V2)", "value2 (V2)" };
            }
    
            [VersionedRoute("api/values/{id:int}", 2)]
            [Route("api/v2/values/{id:int}")]
            public string Get(int id)
            {
                return String.Format("value {0} (V2)", id);
            }
        }
    }

    Now, run the project and open Fiddler. Switch Fiddler to the Composer tab and type the URI for the Values controller ("http://localhost:[YOUR PORT]/api/values"). In the "Request Headers" box, add "api-version: 1":

    Next, click the "Execute" button. You should have a response in the grid on the left. Select the response for "/api/values" and switch to the "Inspectors" tab. In the bottom half of the Inspectors tab, select the JSON tab. It should look like this:

    Now switch back to the Composer tab and change the "api-version" to 2. Execute it again, select the new response, and look at it in the Inspectors tab.

    If you take a look at the API help view, you'll notice that the routes defined in the VersionedRoute attributes appear only with Values_V1:

    It will be important for you to add your own documentation to let your clients know they can optionally set the "api-version" custom header to access the other versions. Another thing to consider is that you might want to name the first version of your controller without the "_V1" and start naming the subsequent versions with "_V2". This way, the subject of your API's (e.g. "Values" in this case) appears to stay the same from version-to-version (e.g. "/api/values") for the majority of the requests only to be differentiated by the headers.

    Using a HTTP Accept Header with a header parameter

    A lot of people favor using the HTTP Accept header to request the version of the resource. The HTTP Accept header defines the Content-Types that are acceptable for the response. Or, in other words, it is a way for a client to specify the media type of the response content it is expecting. Examples are "text/plain," "image/jpeg," "application/xml," and "application/json." The HTTP Accept header optionally allows you to add parameters to define other aspects of the request. We're going to use "version=[SOME NUMBER]" as a parameter to specify the version we want.

    Update the GetVersionHeader method of the VersionConstraint class to this:

    private Int32? GetVersionHeader(HttpRequestMessage request)
    {
        String versionAsString = String.Empty;
        IEnumerable<String> headerValues;
    
        if (request.Headers.TryGetValues("api-version", out headerValues) && headerValues.Count() == 1)
            versionAsString = headerValues.First();
        else
        {
            var accept = request.Headers.Accept.Where(a => a.Parameters.Count(p => p.Name == "version") > 0);
    
            if (accept.Any())
                versionAsString = accept.First().Parameters.Single(s => s.Name == "version").Value;
        }
    
        Int32 version;
    
        if (!String.IsNullOrEmpty(versionAsString) && Int32.TryParse(versionAsString, out version))
            return version;
    
        return null;
    }
    

    Now run the project and open Fiddler again. This time, add an Accept header for "application/json" and a parameter of "version=1"

    Execute the request and examine the result in the Inspectors tab like you did previously.

    Now let's change the request to XML. Replace the "Accept: application/json;version=1" header with "Accept: application/xml;version=2" and execute the request. The result in the Inspectors tab should look like this:

    Using a HTTP Accept Header with a Vendor MIME Type

    Many in the REST camp do not like to use the generic MIME Types to request a resource (e.g. "application/xml"). Their argument is that they aren't requesting any ol' resource, they're requesting a resource with a specific schema or representation. Vendor MIME Types provide a way to specify the resource in a more granular way. Personally, I'm not a fan of custom Vendor MIME Types. It feels like a solution in search of a problem. But, for those of you that want to go this route, here's a way to implement versioning for these types of requests.

    Update the GetVersionHeader method of the VersionConstraint class to this:

    private Int32? GetVersionHeader(HttpRequestMessage request)
    {
        String versionAsString = String.Empty;
        IEnumerable<String> headerValues;
    
        if (request.Headers.TryGetValues("api-version", out headerValues) && headerValues.Count() == 1)
            versionAsString = headerValues.First();
        else
        {
            var accept = request.Headers.Accept.Where(a => a.Parameters.Count(p => p.Name == "version") > 0);
    
            if (accept.Any())
                versionAsString = accept.First().Parameters.Single(s => s.Name == "version").Value;
            else
            {
                accept = request.Headers.Accept.Where(a => Regex.IsMatch(a.MediaType, @"^application\/vnd\..*-v\d+\+.*$", RegexOptions.Singleline));
    
                if (accept.Any())
                    versionAsString = Regex.Match(accept.First().MediaType, @"^application\/vnd\..*-v(?<version>\d+)\+.*$", RegexOptions.Singleline).Groups["version"].Value;
            }
        }
    
        Int32 version;
    
        if (!String.IsNullOrEmpty(versionAsString) && Int32.TryParse(versionAsString, out version))
            return version;
    
        return null;
    }

    The Vendor MIME Type follows a specific pattern of "application/vnd.some.namespaced.resource+json". To version this, the convention is shaping up to look like this: application/vnd.some.namespaced.resource-v1+json

    The regular expressions in the GetVersionHeader method checks for this pattern and extracts the version if present.

    Run the project, open Fiddler, and compose a request like this:

    When you inspect the response, you should see the V2 version in the JSON tab. Now change the Accept to "application/vnd.webapiversioning.string-v2+xml" and execute the request. When you inspect the response you'll notice that it's still JSON and not XML! The reason is that MVC Web API doesn't parse Vendor MIME Types like it does the standard, generic MIME Types. Instead, Microsoft leaves it for you to create you own media type formatters to handle such cases.

    Add a new folder to the Core.Web project called "Extensions" and add the following class:

    namespace Core.Web.Extensions
    {
        public static class TypeExtensions
        {
            public static Type GetEnumerableType(this Type type)
            {
                if (IsIEnumerable(type))
                    return type.GetGenericArguments()[0];
                else
                {
                    foreach (var i in type.GetInterfaces())
                    {
                        if (IsIEnumerable(i))
                            return i.GetGenericArguments()[0];
                    }
                }
    
                return null;
            }
    
            private static Boolean IsIEnumerable(Type type)
            {
                return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>);
            }
        }
    }

    Next, add a new folder called "Http" to the Core.Web project and add the following classes:

    namespace Core.Web.Http
    {
        public class TypedJsonMediaTypeFormatter : JsonMediaTypeFormatter
        {
            private readonly Type _ResourceType;
    
            public TypedJsonMediaTypeFormatter(Type resourceType, MediaTypeHeaderValue mediaType)
            {
                _ResourceType = resourceType;
                SupportedMediaTypes.Clear();
                SupportedMediaTypes.Add(mediaType);
            }
    
            public override Boolean CanReadType(Type type)
            {
                return _ResourceType == type || _ResourceType == type.GetEnumerableType();
            }
    
            public override Boolean CanWriteType(Type type)
            {
                return _ResourceType == type || _ResourceType == type.GetEnumerableType();
            }
        }
    }
    namespace Core.Web.Http
    {
        public class TypedXmlMediaTypeFormatter : XmlMediaTypeFormatter
        {
            private readonly Type _ResourceType;
    
            public TypedXmlMediaTypeFormatter(Type resourceType, MediaTypeHeaderValue mediaType)
            {
                _ResourceType = resourceType;
                SupportedMediaTypes.Clear();
                SupportedMediaTypes.Add(mediaType);
            }
    
            public override Boolean CanReadType(Type type)
            {
                return _ResourceType == type || _ResourceType == type.GetEnumerableType();
            }
    
            public override Boolean CanWriteType(Type type)
            {
                return _ResourceType == type || _ResourceType == type.GetEnumerableType();
            }
        }
    }

    You will need to add a using for Core.Web.Extensions to each of these classes.

    These formatters, when registered with the application for a particular type, will format the response appropriately. Here is how you would perform that registration in Global.asax:

    namespace Web
    {
        public class WebApiApplication : System.Web.HttpApplication
        {
            protected void Application_Start()
            {
                AreaRegistration.RegisterAllAreas();
                GlobalConfiguration.Configure(WebApiConfig.Register);
                FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
                RouteConfig.RegisterRoutes(RouteTable.Routes);
                BundleConfig.RegisterBundles(BundleTable.Bundles);
    
                GlobalConfiguration.Configuration.Formatters.Add(new TypedXmlMediaTypeFormatter(typeof(String), new MediaTypeHeaderValue("application/vnd.webapiversioning.string-v1+xml")));
                GlobalConfiguration.Configuration.Formatters.Add(new TypedJsonMediaTypeFormatter(typeof(String), new MediaTypeHeaderValue("application/vnd.webapiversioning.string-v1+json")));
                GlobalConfiguration.Configuration.Formatters.Add(new TypedXmlMediaTypeFormatter(typeof(String), new MediaTypeHeaderValue("application/vnd.webapiversioning.string-v2+xml")));
                GlobalConfiguration.Configuration.Formatters.Add(new TypedJsonMediaTypeFormatter(typeof(String), new MediaTypeHeaderValue("application/vnd.webapiversioning.string-v2+json")));
            }
        }
    }

    What we're saying with these registrations is that when a request is made for the type of resource specified in the Accept header and the response is of that type, then use the appropriate media type formatter to serialize the response.

    Now run the project, open Fiddler, and compose a request with an Accept header value "application/vnd.webapiversioning.string-v2+xml". You should now see an XML response.

    Again, I'm just focusing on the versioning mechanisms in this tutorial and not a typical, real-life scenario. Your Web API's will likely deal with resources that are more complex so your registrations would likely look something like this:

    GlobalConfiguration.Configuration.Formatters.Add(new TypedJsonMediaTypeFormatter(typeof(Product), new MediaTypeHeaderValue("application/vnd.mycompany.product-v1+json")));
    GlobalConfiguration.Configuration.Formatters.Add(new TypedJsonMediaTypeFormatter(typeof(Product_V2), new MediaTypeHeaderValue("application/vnd.mycompany.product-v2+json")));
    GlobalConfiguration.Configuration.Formatters.Add(new TypedXmlMediaTypeFormatter(typeof(Product), new MediaTypeHeaderValue("application/vnd.mycompany.product-v1+xml")));
    GlobalConfiguration.Configuration.Formatters.Add(new TypedXmlMediaTypeFormatter(typeof(Product_V2), new MediaTypeHeaderValue("application/vnd.mycompany.product-v2+xml")));
    GlobalConfiguration.Configuration.Formatters.Add(new TypedJsonMediaTypeFormatter(typeof(Customer), new MediaTypeHeaderValue("application/vnd.mycompany.customer-v1+json")));
    GlobalConfiguration.Configuration.Formatters.Add(new TypedJsonMediaTypeFormatter(typeof(Customer_V2), new MediaTypeHeaderValue("application/vnd.mycompany.customer-v2+json")));
    GlobalConfiguration.Configuration.Formatters.Add(new TypedXmlMediaTypeFormatter(typeof(Customer), new MediaTypeHeaderValue("application/vnd.mycompany.customer-v1+xml")));
    GlobalConfiguration.Configuration.Formatters.Add(new TypedXmlMediaTypeFormatter(typeof(Customer_V2), new MediaTypeHeaderValue("application/vnd.mycompany.customer-v2+xml")));
    

    I hope this tutorial gives you enough options to successfully version your Web API. If you have any questions or suggestions on how I can make this better, please leave a comment.

    You can download the source for this article here.