Web API Versioning

 

Why versioning required in Web API?
  • Once a Web API service is made public, different client applications start using Web API services.
  • As the requirements change, we may have to change the services as well, but the changes to the services should be done in way that does not break any existing client applications.
  • This is when Web API versioning helps. We keep the existing services as is, so we are not breaking the existing client applications, and develop a new version of the service that new client applications can start using.
Different options available to version Web API services :

  1. URI's
  2. Query String
  3. Version Header
  4. Accept Header
  5. Media Type



1. URI's


Using Convention Based Routing- We can have multiple routes in WebApiConfig.cs. One route for one version.

config.Routes.MapHttpRoute(
    name: "Version1",
    routeTemplate: "api/v1/Students/{id}",
    defaults: new { id = RouteParameter.Optional, controller = "StudentsV1" }
);

config.Routes.MapHttpRoute(
    name: "Version2",
    routeTemplate: "api/v2/Students/{id}",
    defaults: new { id = RouteParameter.Optional, controller = "StudentsV2" }
);

Using Attribute Routing:- 
Use the [Route] attribute on methods in StudentsV1Controller and StudentsV2Controller as shown below.

public class StudentsV1Controller : ApiController
{
    [Route("api/v1/students")]
    public IEnumerable<StudentV1> Get() {...}

    [Route("api/v1/students/{id}")]
    public StudentV1 Get(int id) {...}
}

public class StudentsV2Controller : ApiController
{
    [Route("api/v2/students")]
    public IEnumerable<StudentV2> Get() {...}

    [Route("api/v2/students/{id}")]
    public StudentV2 Get(int id) {...}
}



2. Query String


For this we need to have our own custom controller selector.Steps to version Web API service using a query string parameter as follows,

Here is what we want
URIShould Return
/api/students?v=1Version 1 Students
/api/students?v=2Version 2 Students

Step 1 : Since the default controller selector implementation provided by Web API does not work for us, we have to provide our own custom controller selector implementation. To do this
1. Add a folder to the web api project. Name it "Custom"
2. Add a class file to the folder. Name it "CustomControllerSelector". Copy and paste the following code. it is self explanatory

using System.Net.Http;
using System.Web;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Dispatcher;

namespace WebAPI.Custom
{
    // Derive from the DefaultHttpControllerSelector class
    public class CustomControllerSelector : DefaultHttpControllerSelector
    {
        private HttpConfiguration _config;
        public CustomControllerSelector(HttpConfiguration config) : base(config)
        {
            _config = config;
        }

  public override HttpControllerDescriptor             SelectController(HttpRequestMessage request)
        {
            // Get all the available Web API controllers
            var controllers = GetControllerMapping();
            // Get the controller name and parameter values from the request URI
            var routeData = request.GetRouteData();

            // Get the controller name from route data.
            // The name of the controller in our case is "Students"
            var controllerName = routeData.Values["controller"].ToString();

            // Default version number to 1
            string versionNumber = "1";
            var versionQueryString = HttpUtility.ParseQueryString(request.RequestUri.Query);
            if (versionQueryString["v"] != null)
            {
                versionNumber = versionQueryString["v"];
            }

            if (versionNumber == "1")
            {
                // if version number is 1, then append V1 to the controller name.
                // So at this point the, controller name will become StudentsV1
                controllerName = controllerName + "V1";
            }
            else
            {
                // if version number is 2, then append V2 to the controller name.
                // So at this point the, controller name will become StudentsV2
                controllerName = controllerName + "V2";
            }

            HttpControllerDescriptor controllerDescriptor;
            if (controllers.TryGetValue(controllerName, out controllerDescriptor))
            {
                return controllerDescriptor;
            }

            return null;
        }
    }
}

Step 2 : The next thing that we need to do is, replace the default controller selector with our custom controller selector. This is done in WebApiConfig.cs file. Notice we are replacing IHttpControllerSelector, with our CustomControllerSelectorDefaultHttpControllerSelector implements IHttpControllerSelector, so that is the reason we are replacing IHttpControllerSelector.

config.Services.Replace(typeof(IHttpControllerSelector),
    new CustomControllerSelector(config));

Step 3 : Include the following default route in WebApiConfig.cs

config.Routes.MapHttpRoute(
    name: "DefaultRoute",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

Step 4 : Remove [Route] attribute, from action methods 



3. Version Header


To implement versioning using a custom version header, we have to change the logic slightly int the CustomControllerSelector class to read the version number from the custom version header instead of from a query string parameter.

As showin in follwing code, we can get vesionNumber from header as follows,

            string customHeader = "X-StudentService-Version";
            if (request.Headers.Contains(customHeader))
            {
                versionNumber = request.Headers.GetValues(customHeader).FirstOrDefault();
            }


4. Accept header

The Accept header tells the server in what file format the browser wants the data. These file formats are more commonly called as MIME-types. MIME stands for Multipurpose Internet Mail Extensions.

Instead of creating a custom header, we can use the standard Accept header. We can add parameters to the Accept header to send any additional data along with the request to the server. 
For example, we can specify the version of the service we want using the version parameter.

Now, we need to read the version parameter value from the Accept header in our CustomControllerSelector class. 

             // Check if any of the Accept headers has a parameter with name version
            var acceptHeader = request.Headers.Accept.Where(a => a.Parameters
                                .Count(p => p.Name.ToLower() == "version") > 0);

            // If there is atleast one header with a "version" parameter
            if (acceptHeader.Any())
            {
                // Get the version parameter value from the Accept header
                versionNumber = acceptHeader.First().Parameters
                                .First(p => p.Name.ToLower() == "version").Value;
            }



5. Media  Type

Instead of using the standard media types like application/xml or application/json, we can use custom media type.

In the media type, we have the version of the service. Custom media types have vnd prefix. vnd indicates that it is a vendor specific media type. 
So from our CustomControllerSelector class we will read the version number from the custom media type we have using regex.

            // Get the version number from the Custom media type
            // Use regular expression for mataching the pattern of the media type
             string regex =
                @"application\/vnd\.mangaltech\.([a-z]+)\.v(?<version>[0-9]+)\+([a-z]+)";

            // Users can include multiple Accept headers in the request.
            // Check if any of the Accept headers has our custom media type by
            // checking if there is a match with regular expression specified
            var acceptHeader = request.Headers.Accept
                .Where(a => Regex.IsMatch(a.MediaType, regex, RegexOptions.IgnoreCase));
            // If there is atleast one Accept header with our custom media type
            if (acceptHeader.Any())
            {
                // Retrieve the first custom media type
                var match = Regex.Match(acceptHeader.First().MediaType,
                    regex, RegexOptions.IgnoreCase);
                // From the version group, get the version number
                versionNumber = match.Groups["version"].Value;
            }








Comments

Post a Comment