Versioning Web API

Versioning Web API

Change is an inevitable factor in the life cycle of an API.  More and more organizations today face a dynamic and changing environment.  The change is driven by factors within the enterprise like implement a new business model, expand to a new market. It can also be external such as disruptive technologies, new government regulations, changes happen in partner/consumer organization, etc. I will talk about versioning in general and versioning Web API using multiple versioning strategies in detail in this post.

Successful software always gets changed. – Frederick P. Brooks

 

Challenges – Evolving API

Handling changes in software systems is not an easy task and it is more challenging particularly in loosely coupled distributed systems, such as APIs or Microservices.  When you make changes to the API, the consumer should be able to continue using the API like they were using before. 

It is unreasonable to force all consumers to adapt or update their apps to access the latest version of the API.  The API provider doesn’t know about the consumer’s implementation that uses the API and the components rely on it. 

A small change is enough to break some of the clients consuming the API.  A change is considered as breaking if the behavior of an existing API is changed in a way such that clients using that feature might not work anymore.  Though we can categorize the changes as breaking and non-breaking, it varies depending on the implementation by the consumer.

Breaking changes

  • Change in the URI
  • New required parameter/fields
  • Removing previously recognized parameter/fields
  • Removing a part of the API
  • Change in the response type (i.e. changing int to float)

Non-breaking changes

  • New URI
  • New optional parameter/fields
  • Additional functions

Solution

When making changes to an API, there’s usually a need to withstand and support the old version for a period.  It is impossible to ask all the consumers to update as most of them are external to the organization.  Generally adapting to the change requires time and money.  From API consumer perspective, longevity and stability are an important aspect of published APIs. 

For maximum versatility and to continue providing support to all consumers, you must keep your APIs backward compatible by versioning them.  In addition, new features should only be added to the new version.  Users who want to can still access the previous version using the versioning strategy implemented by the producer.  This will gradually encourage customers to upgrade.

Prerequisites

  • .NET Core 2.2 SDK
  • VS Code, Visual Studio 2017/2019
  • Familiarity with ASP.NET Web API and REST API

Getting started with ASP.NET Web API application

Let’s start with a new ASP.NET Web API project. First, you will need to install the highlighted NuGet package from your package manager console.

Nuget Packages - API Versioning
(Package versions in the image were latest at the time of writing this post)

Next, go to the startup file and enable Web API versioning.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
    
    // Versioning setup
    services.AddApiVersioning(o => {
        o.ReportApiVersions = true;
        o.AssumeDefaultVersionWhenUnspecified = true;
        o.DefaultApiVersion = new ApiVersion(1, 0);
    });
}

The “ReportApiVersions” flag is set to “true” here.  It is used to add the API versions in the response headers as below.

Postman Snapshot - Api Supported Versions
Postman – Supported versions of the API displayed

The flag “AssumeDefaultVersionWhenUnspecified” helps you from stop breaking any existing clients that aren’t specifying an API version during the call. By the way, you will get an error “An API version is required, but was not specified.” if the flag is not set.

Postman - API Version Unspecified error
Postman – API Version Unspecified error

The ”DefaultApiVersion” flag is used to set the default version number. 

 

Routing methods

Both attribute and convention routing are supported out of the box for versioning Web API.  For convention routing, an ASP.NET MVC controller class must be given a name that ends with the word “Controller”.  If you want to use this and have the same class name for all the versions of your controller, you need to move the classes into different namespaces.  If you want to go with attribute routing, you can have the classes in the same namespace.  The class names must be different, and you will have to set the “Route” attribute with same value for all the controllers.  I have used both attribute and convention routing in “Versioning using Query string”.  For the remaining options, I have used convention routing.

Versioning Strategies

The following four methods are supported out of the box.

  1. Query string
  2. URL Path
  3. Custom Request Header
  4. Accept Header

The decision of choosing one of the approaches for implementation is up to you.  We would discuss each one these strategies with an example, however decision of one over other is not the main purpose of this post.  Also, the main purpose is to show you how to implement versioning in an ASP.NET Core Web API application using the versioning package.

1. Versioning using the Query string

This is probably one of the easiest and the default ones supported.  The controllers are just decorated with the “ApiVersion” attribute.  The default method is to use a query string parameter named api-version.  When no version is mentioned, the client will get the latest version configured in the startup.  You can change the default parameter name while configuring it in the startup class.  We will see an example of custom parameter names later in this post.

e.g. 
Version 1:
HTTP GET
URI: /api/values?api-version=1.0
Accept: application/json

Version 2:
HTTP GET
URI: /api/values?api-version=2
Accept:application/json

Convention routing

Look, the controllers are placed inside two different namespaces, but has the same class name.  Both classes are decorated with the “ApiVersion” attribute.  This helps with the version selection.

namespace WebApiVersioning.Controllers.V1
{
    [ApiVersion("1.0")]
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        [HttpGet]
        public ActionResult<string> Get()
        {
            return "Response from Version-1";
        }
    }
}

namespace WebApiVersioning.Controllers.V2
{
    [ApiVersion("2.0")]
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        [HttpGet]
        public ActionResult<string> Get()
        {
            return "Response from Version-2";
        }
    }
}

Following is the list of request URL and the matched controller list after the implementation of versioning

Request UrlMatched Controller
/api/valuesWebApiVersioning.Controllers.V1.ValuesController
/api/values?api-version=1.0WebApiVersioning.Controllers.V1.ValuesController
/api/values?api-version=2.0WebApiVersioning.Controllers.V2.ValuesController

Attribute routing

Here the classes are inside the same namespace but with different names.  The attribute “Route” is set to same value for both.

namespace WebApiVersioning.Controllers
{
    [ApiVersion("1.0")]
    [Route("api/values")]
    [ApiController]
    public class ValuesControllerV1 : ControllerBase
    {
        [HttpGet]
        public ActionResult<string> Get()
        {
            return "Response from Version-1";
        }
    }

    [ApiVersion("2.0")]
    [Route("api/values")]
    [ApiController]
    public class ValuesControllerV2 : ControllerBase
    {
        [HttpGet]
        public ActionResult<string> Get()
        {
            return "Response from Version-2";
        }
    }
}

URL and the matched controller list

Request UrlMatched Controller
/api/valuesValuesControllerV1
/api/values?api-version=1.0 ValuesControllerV1
/api/values?api-version=2.0 ValuesControllerV2

2. Versioning using URL Path

Using the URL is the most straightforward approach and often used.  Here the “Route” attribute is added with additional details related to version.  Unlike versioning using query string, this approach doesn’t support the default API version.  The version must be explicitly mentioned in the URL

e.g. 
Version 1:
HTTP GET
URI: /api/v1/values
Accept: application/json

Version 2:
HTTP GET
URI: /api/v2/values
Accept:application/json

namespace WebApiVersioning.Controllers.V1
{
    [ApiVersion("1.0")]
    [Route("api/v{version:apiVersion}/[controller]")]
    [ApiController]
    public class ValuesURLController : ControllerBase
    {
        [HttpGet]
        public ActionResult<string> Get()
        {
            return "Response from Version-1";
        }
    }
}

namespace WebApiVersioning.Controllers.V2
{
    [ApiVersion("2.0")]
    [Route("api/v{version:apiVersion}/[controller]")]
    [ApiController]
    public class ValuesURLController : ControllerBase
    {
        [HttpGet]
        public ActionResult<string> Get()
        {
            return "Response from Version-2";
        }
    }
}

URL and the matched controller list

Request UrlMatched Controller
/api/v1/valuesurlWebApiVersioning.Controllers.V1.ValuesURLController
/api/v2/valuesurl WebApiVersioning.Controllers.V2.ValuesURLController

3. Versioning using Custom Request Header

Using the header to pass the version information is another common method for Web API versioning.  It helps the URL to be clean and unique without any version information.  To add support for versioning using header, you must define the header name in startup class which contains the API version information.  To make versioning works with both query and header in the same project, you must modify the configure services method in the startup class.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    services.AddApiVersioning(options =>
    {
        options.ReportApiVersions = true;
        options.AssumeDefaultVersionWhenUnspecified = true;
        options.DefaultApiVersion = new ApiVersion(1, 0);
        // Using ApiVersionReader.Combine to use both Query string and Header based versioning
        options.ApiVersionReader =
            ApiVersionReader.Combine(new QueryStringApiVersionReader(),
                                        new HeaderApiVersionReader("api-version"));
    });

}

The version information must be passed in the header like below using the configured version parameter.

e.g. 
Version 1:
HTTP GET
URI: /api/values
Accept: application/json
api-version:1

Version 2:
HTTP GET
URI: /api/values
Accept:application/json
api-version:2.0

namespace WebApiVersioning.Controllers.V1
{
    [ApiVersion("1.0")]
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesHeaderController : ControllerBase
    {
        [HttpGet]
        public ActionResult<string> Get()
        {
            return "Response from Version-1";
        }
    }
}

namespace WebApiVersioning.Controllers.V2
{
    [ApiVersion("2.0")]
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesHeaderController : ControllerBase
    {
        [HttpGet]
        public ActionResult<string> Get()
        {
            return "Response from Version-2";
        }
    }
}

Header information and the matched controller list

HeaderMatched Controller
api-version:1WebApiVersioning.Controllers.V1.ValuesHeaderController
api-version:2.0 WebApiVersioning.Controllers.V2.ValuesHeaderController

4. Versioning using Accept Header

Content negotiation lets us preserve the URL from versioning information.  You can configure the name of the parameter to read the API version in startup class while configure the Media type API Version reader.  If you see the “ConfigureServices” code below, multiple “ IApiVersionReader” implementations are combined to work with all the styles.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    services.AddApiVersioning(options =>
    {
        options.ReportApiVersions = true;
        options.AssumeDefaultVersionWhenUnspecified = true;
        options.DefaultApiVersion = new ApiVersion(1, 0);
        options.ApiVersionReader =
            ApiVersionReader.Combine(new QueryStringApiVersionReader(),
                                        new HeaderApiVersionReader("api-version"),
                                        new MediaTypeApiVersionReader("v"));
    });

}

The version information must be passed in the header like below

e.g. 
Version 1:
HTTP GET
URI: /api/values
Accept: application/json;v=1.0

Version 2:
HTTP GET
URI: /api/values
Accept:application/json;v=2.0

namespace WebApiVersioning.Controllers.V1
{
    [ApiVersion("1.0")]
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesMediaTypeController : ControllerBase
    {
        [HttpGet]
        public ActionResult<string> Get()
        {
            return "Response from Version-1";
        }
    }
}

namespace WebApiVersioning.Controllers.V2
{
    [ApiVersion("2.0")]
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesMediaTypeController : ControllerBase
    {
        [HttpGet]
        public ActionResult<string> Get()
        {
            return "Response from Version-2";
        }
    }
}

Header information and the matched controller list

HeaderMatched Controller
accept:text/plain; v=1WebApiVersioning.Controllers.V1.ValuesMediaTypeController
accept:text/plain; v=2.0 WebApiVersioning.Controllers.V2. ValuesMediaTypeController

Apply versioning in specific action methods

Now we know about versioning and how to configure and implement that in a controller.  Usually, we apply the version at a controller level.  For a major change, it is ok to do that.  What if, you have a very minor change and only one of your action methods is going to change.  Let’s see how to implement versioning in one of the action methods.

namespace WebApiVersioning.Controllers
{
    [ApiVersion("1.0")]
    [ApiVersion("2.0")]
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesMethodController : ControllerBase
    {
        [HttpGet]
        public ActionResult<string> Get()
        {
            return "Response from Version-1 method";
        }

        [HttpGet, MapToApiVersion("2.0")]
        public ActionResult<string> GetV2()
        {
            return "Response from Version-2 method";
        }
    }
}

If you look in the code, you can see that both version 1 and version 2 are configured in the controller level using the “ApiVersion” attribute.  In addition to that “MapToApiVersion” attribute is used in the method to map that particular method to a different version.

We can pass the version information to the controller in both the query string and header.

URL, Header and the matched controller list

Request URLMatched ControllerMatched Action
/api/valuesmethod?api-version=1.0ValuesMethodControllerGet
/api/valuesmethod?api-version=2.0 ValuesMethodController GetV2
api-version: 1.0 ValuesMethodController Get
api-version: 2.0 ValuesMethodController GetV2

Deprecating a Version

When your API has multiple versions, you have to regularly check whether all the old versions are still accessed by the consumers.  Maintaining the APIs is a huge task that requires infrastructure and monitoring efforts.  If the old version of an API is not accessible anymore by the consumer, you can depreciate those. 

namespace WebApiVersioning.Controllers.V1
{
    [ApiVersion("1.0", Deprecated = true)]
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesDeprecatedController : ControllerBase
    {
        [HttpGet]
        public ActionResult<string> Get()
        {
            return "Response from Version-1";
        }
    }
}

namespace WebApiVersioning.Controllers.V2
{
    [ApiVersion("2.0")]
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesDeprecatedController : ControllerBase
    {
        [HttpGet]
        public ActionResult<string> Get()
        {
            return "Response from Version-2";
        }
    }
}

You can see the supported and deprecated versions in the below screenshot.

Postman - API Version Deprecated Versions
Postman – API Version Deprecated Versions

API Explorer Options

Now we have seen the versioning strategies in detail, It is also important to document all the versions of your API for consumption.  You can use the “Swashbuckle” package to implement swagger specifications for the APIs.  I have explained how to set up a swagger specification for ASP.NET Core Web API in another post.  Please refer to the following link.

Swagger with ASP.NET Core Web API

Challenges

Besides the benefits an API provider and consumer get from versioning web API, there are certain challenges as well.  The following are some of them

  • Having multiple versions is a costly option as the provider must take care of maintenance, monitoring, infrastructure and support
  • Confuse new consumers with the documentation when there are multiple versions
  • Get more complicated with changes in database or schema
  • Additional testing effort (i.e. regression & integration)

Summary

This article tried to provide a summary of the very diverse and difficult problem of evolving a Web API.  We discussed the challenges, solution, versioning strategies and an implementation example with ASP.NET Web API. 

The full implementation of the sample code used in this post can be found on Github

 

Recommended Courses

We may receive a commission for purchases made through these links.

Further Reading

The following are some of the links you can refer to if you want to learn more about versioning Web API.

This Post Has 5 Comments

  1. Thank you!

  2. Incredible points. Great arguments. Keep up the great effort.

  3. Hello! I could have sworn I’ve visited this web site before but after browsing through a few of the posts I realized it’s new to me. Regardless, I’m definitely happy I came across it and I’ll be bookmarking it and checking back frequently!

  4. Thanks in favor of sharing such a pleasant thought, the post is fastidious, thats why i have read it fully

Leave a Reply

Close Menu
Share via
Copy link
Powered by Social Snap