August 10, 2016

WebAPI Versioning – Using Routing Attributes

Business Problem

In one of my projects, the client had a requirement to maintain versions of his RESTful APIs. They had a few APIs that were being consumed by some vendors and they later added a few other vendors in their kitty who wanted some additional features in those APIs. Our client thus decided to maintain versions of their APIs and asked us to strategize this change so that it wouldn't affect existing vendors. This article is all about the strategy we adopted to cater to their need.

Approach

There are 4 ways to version RESTful APIs 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

The question is, "How should we version our RESTful API(s)?" There's a lot of debate on what is the best strategy. The main debate comes down to identifying the version via the URI versus identifying the version in the header. One must explore all the options and select the best possible strategy for their business problem.

Based on the existing architecture of our client’s architecture, we decided to version the APIs via Attributes-based routing. Supporters of the Attributes-based routing strategy claim that REST is all about resources and a Web API is a contract promising that a given resource will exist at the specified URI. For example, a URI pointing to version V1 should give resources corresponding to version V1, and a URI pointing to version V2 should give resources corresponding to version V2.

The Idea Behind the Strategy

We will create folders called V1, V2, etc. within the Controllers folder. The V1 folder will contain existing APIs and V2 will be used for new APIs.

To start with, we can consider existing controllers as V1 and will move them inside the V1 folder. Let’s assume the namespace of this file is <<ProjectName>>.Controllers.V1

Now, create a copy of that controller inside folder V2. The namespace of this file is <<ProjectName>>.Controllers.V2. Feel free to add more methods in this controller as per your needs.

Is it this simple?

The answer is NO. MVC identifies the controller to be called based on the unique name identified by the route attributes. It doesn‘t matter in what namespace the controllers exist because MVC just identifies the correct controller with the unique name.

If there are two controller classes in the project with the same name, the project will still compile because they are in different namespaces. However, requests will always be routed to the controller in the V1 folder/namespace because the framework only matches on the controller class name without regard to the controller class namespace, and the V1 controller is the first match it finds. This is the case with both convention-based and attribute-based routing. For more on this, see the following representation:

web_api_1

What shall we do then?

To work around this shortcoming, we will use attribute-based routing with custom constraints, and we will also add a custom controller selector that takes the namespace into account when looking for the matching controller class. We thus decided to inject our logic into the MVC controller selection pipeline to enable MVC to consider namespace while selecting the correct controller. We had to write a custom logic for this.

Implementation

For coding, we used Microsoft Visual Studio 2013, C# 4.0, and WebAPI2.

Prerequisite

  1. Create a new MVC project and add all appropriate references.
  2. In the Controllers folder, add two folders - V1 and V2 - and create APIs in it with same name. Let’s call them IndexController as shown in the following snapshot.
    web_api_2
  3. Create a method with the same name in each controller. See the following snapshot for the controller in namespace V1:web_api_3
  4. Create a similar method within the controller of namespace V2.

Implementing Custom HTTPRouteConstraint

Create a Routes folder in your solution, add a class ApiVersionConstraint, and implement it as follows:

web_api_4

This class implements the IHttpRouteConstraint.Match method. The match will return true if the specified parameter name equals the AllowedVersion property, which is initialized in the constructor. Where does the constructor get this value? It gets it from a RoutePrefixAttribute, which we’ll implement now.

Implementing Custom RoutePrefixAttribute

Add a class named ApiVersion1RoutePrefixAttribute in the Routes folder and implement it as follows:

web_api_5

The main purpose of this attribute class is to encapsulate the API/V1 part of the route template so that we don’t have to copy and paste it over all of the controllers. Let’s add this ApiVersion1RoutePrefixAttribute to the IndexController of V1. Here’s the controller with the attribute applied to it:

web_api_6

Basically, the RoutePrefix attribute is configured to match a URL path of API/{apiVersion}/customers and the apiVersion parameter is constrained by our custom IHttpRouteConstraint to a value of “v1.”

Now create a similar class ApiVersion2RoutePrefixAttribute in the Routes folder and use it similarly on the IndexController API in the V2 namespace.

All that's left is to implement the custom controller selector and wire it up with the custom constraint we just created.

Implementing Custom HTTPControllerSelector

In the following code, we are basically creating a custom controller selector that first identifies all of the controllers available in our assembly. After that, it creates a dictionary and maps the controller with a string key that is comprised of the controller’s fully qualified name (including the namespace).

Later, when the MVC framework calls the SelectController method, it chooses the appropriate controller from the dictionary mapping depending on the WebAPI URL. Here is the code demonstrating this:

public class NamespaceHttpControllerSelector : IHttpControllerSelector
    {
        private readonly HttpConfiguration _configuration;
        private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers;

        public NamespaceHttpControllerSelector(HttpConfiguration config)
        {
            _configuration = config;
            _controllers = new Lazy<Dictionary<string, HttpControllerDescriptor>>(InitializeControllerDictionary);
        }
        
        public HttpControllerDescriptor SelectController(HttpRequestMessage request)
        {
            var routeData = request.GetRouteData();
            if (routeData == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            var controllerName = GetControllerName(routeData);
            if (controllerName == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            var namespaceName = GetVersion(routeData);
            if (namespaceName == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            var controllerKey = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", namespaceName, controllerName);
            HttpControllerDescriptor controllerDescriptor;
            if (_controllers.Value.TryGetValue(controllerKey, out controllerDescriptor))
            {
                return controllerDescriptor;
            }
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }
        
        public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
        {
            return _controllers.Value;
        }
        
        private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary()
        {
            var dictionary = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);
            var assembliesResolver = _configuration.Services.GetAssembliesResolver();
            var controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();
            var controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);
            foreach (var controllerType in controllerTypes)
            {
                var segments = controllerType.Namespace.Split(Type.Delimiter);
                var controllerName = controllerType.Name.Remove(controllerType.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length);
                var controllerKey = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", segments[segments.Length - 1], controllerName);

                if (!dictionary.Keys.Contains(controllerKey))
                {
                    dictionary[controllerKey] = new HttpControllerDescriptor(_configuration,
                    controllerType.Name,
                    controllerType);
                }
            }
            return dictionary;
        }
        
        private T GetRouteVariable<T>(IHttpRouteData routeData, string name)
        {
            object result;
            if (routeData.Values.TryGetValue(name, out result))
            {
                return (T)result;
            }
            return default(T);
        }
       
        private string GetControllerName(IHttpRouteData routeData)
        {
            var subroute = routeData.GetSubRoutes().FirstOrDefault();
            if (subroute == null) return null;
            //((HttpActionDescriptor[])subroute.Route.DataTokens["actions"]).First()
            var dataTokenValue = subroute.Route.DataTokens["actions"];
            
            if (dataTokenValue == null) 
                return null;
            
            var controllerName = ((HttpActionDescriptor[])dataTokenValue).First().ControllerDescriptor.ControllerName.Replace("Controller", string.Empty);

            return controllerName;
        }
       
        private string GetVersion(IHttpRouteData routeData)
        {
            var subRouteData = routeData.GetSubRoutes().FirstOrDefault();
            if (subRouteData == null) return null;
            return GetRouteVariable<string>(subRouteData, "apiVersion");
        }
    }

Configure the API

Now at last, we need to register our constraint with ASP.NET Web API so that it gets applied to incoming requests. We also need to configure our custom controller selector. We accomplish this by implementing the WebApiConfig class.

The first part of the Register method configures the version constraint. Our ApiVersionConstraint is registered with a constraint resolver, which the framework uses to find and instantiate the appropriate constraint at runtime. The last part of the method wires-in our custom controller selector, replacing the default, namespace-unaware, and controller selector.

This is shown in the code snippet below:

web_api_7

Testing the Code

Run your application and type <<base address>>/api/v1/customers/customers in your browser (the port number and base address will vary for you). This will call the Customers method of the IndexController in the V1 namespace.

web_api_8

Similarly, <<base address>>/api/v2/customers/customers will call the Customers method of the IndexController in the V2 namespace. If you add more methods in your controllers, they will work seamlessly.

web_api_9

Conclusion

This demonstration shows how we can implement versioning in ASP.Net WebAPI by injecting our custom logic into the MVC controller selection pipeline to force the MVC consider namespace while selecting the controller. This strategy allows us to avoid writing repeated RoutePrefix on each API and adding any hardcoded strings.