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.

Shiva Wahi

Shiva Wahi

Shiva Wahi is a Technical Lead at 3Pillar Global. He is a Microsoft Certified Professional with over 10 years of experience in software development. He possess good problem solving skills and has a well-proven ability to convert business requirement into working functional components. He has a strong passion for learning new technologies and using them to solve complex problems with innovative ideas. He holds a Bachelor of Technology degree from GGSIP university, Delhi.

17 Responses to “WebAPI Versioning – Using Routing Attributes”
  1. Himanshu Bhankar on

    Hi Shiva,

    Nice article indeed, there is Nuget package available for the controller selector utility https://github.com/Sebazzz/SDammann.WebApi.Versioning I hope we can use this package and insert dependencies for controller selector.

    I really like the approach of encapsulating the API/V1 part not to copy and paste in all controllers.

    Cheers.
    Himanshu Bhankar
    Engineering Lead ARI Network Services.

    Reply
  2. Hiren Tailor on

    Hello SHIVA WAHI,

    I have gone through this article but but it didn’t work me, am facing some issue.
    can you please help me as soon as possible.

    Thanks,
    Hiren Tailor .

    Reply
    • Shiva Wahi on

      What’s not working for you?

      Reply
      • Hiren Tailor on

        Hello Shiva,

        I am facing below error:-

        An error has occurred.
        The given key was not present in the dictionary.
        System.Collections.Generic.KeyNotFoundException

        at System.Collections.Generic.Dictionary`2.get_Item(TKey key) at System.Web.Http.Controllers.ApiControllerActionSelector.ActionSelectorCacheItem.FindActionMatchRequiredRouteAndQueryParameters(IEnumerable`1 candidatesFound) at System.Web.Http.Controllers.ApiControllerActionSelector.ActionSelectorCacheItem.FindMatchingActions(HttpControllerContext controllerContext, Boolean ignoreVerbs) at System.Web.Http.Controllers.ApiControllerActionSelector.ActionSelectorCacheItem.SelectAction(HttpControllerContext controllerContext) at System.Web.Http.Controllers.ApiControllerActionSelector.SelectAction(HttpControllerContext controllerContext) at System.Web.Http.ApiController.ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken) at System.Web.Http.Dispatcher.HttpControllerDispatcher.d__1.MoveNext()

        Can you please help me for that.

        Hiren Tailor
        Thanks

        Reply
      • Hiren Tailor on

        Hello Shiva,

        Can you please provide me your email ID so we can discuss more related to this error or else if possible then please share your project of this article if you have.

        Waiting for your positive prompt.

        Hiren Tailor,
        Thanks

        Reply
  3. Hiren Tailor on

    Hello Shiva,

    Shiva i need your one help, as per your solution the API version concept is working as expected but here i need one more things like as below.

    scenarios 1:-

    Controllers -> v1 -> TestController
    Controllers -> v2 -> TestController

    Access using :- api/v1/Test & api/v2/Test & its working properly

    scenarios 2:-

    Controllers -> TestAPIController

    Access using :- api/TestAPI this is not working in your solution which you have shared with me so it is possible to achieve above both scenarios in same solution ?

    Help me as soon as possible.

    Hiren Tailor,
    Thanks.

    Reply
  4. Hiren Tailor on

    Hello Shiva,

    I have achieved that both scenarios within the same solution which i have discussed above.

    Hiren Tailor

    Reply
  5. Ramanathan on

    Hello Shiva,

    I am facing below error:-

    An error has occurred.

    The given key was not present in the dictionary.

    System.Collections.Generic.KeyNotFoundException

    at System.Collections.Generic.Dictionary`2.get_Item(TKey key) at System.Web.Http.Controllers.ApiControllerActionSelector.ActionSelectorCacheItem.FindActionMatchRequiredRouteAndQueryParameters(IEnumerable`1 candidatesFound) at System.Web.Http.Controllers.ApiControllerActionSelector.ActionSelectorCacheItem.FindMatchingActions(HttpControllerContext controllerContext, Boolean ignoreVerbs) at System.Web.Http.Controllers.ApiControllerActionSelector.ActionSelectorCacheItem.SelectAction(HttpControllerContext controllerContext) at System.Web.Http.Controllers.ApiControllerActionSelector.SelectAction(HttpControllerContext controllerContext) at System.Web.Http.ApiController.ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken) at System.Web.Http.Dispatcher.HttpControllerDispatcher.d__1.MoveNext()

    Can you please help me for that.

    Reply
  6. Radhesham Shah on

    I thought one of the requirements was that existing consumers of the API should not be affected. By modifying the route to have a v1 in it, will not the consumers of the API have to modify the url (from hitting just /customers now they have to hit /api/v1/customers) to get their resources?

    Reply
  7. Shahid on

    What if I don’t need to create V2 of the controllers yet? I mean in my project, I don’t need to create version 2 yet but in future, i will have to. Will my project run successfully with V1?

    Reply
Leave a Reply