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.
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.
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.
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.
- Query string
- URL Path
- Custom Request Header
- 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.
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 Url | Matched Controller |
/api/values | WebApiVersioning.Controllers.V1.ValuesController |
/api/values?api-version=1.0 | WebApiVersioning.Controllers.V1.ValuesController |
/api/values?api-version=2.0 | WebApiVersioning.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 Url | Matched Controller |
/api/values | ValuesControllerV1 |
/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
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 Url | Matched Controller |
/api/v1/valuesurl | WebApiVersioning.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.
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
Header | Matched Controller |
api-version:1 | WebApiVersioning.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
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
Header | Matched Controller |
accept:text/plain; v=1 | WebApiVersioning.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 URL | Matched Controller | Matched Action |
/api/valuesmethod?api-version=1.0 | ValuesMethodController | Get |
/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.
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.
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
- Designing RESTful Web APIs
- Documenting an ASP.NET Core API with OpenAPI / Swagger
- Using OpenAPI/Swagger for Testing and Code Generation in ASP.NET Core
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.
Pingback: Web API Versioning Strategies - How to Code .NET
Thank you!
Incredible points. Great arguments. Keep up the great effort.
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!
Thanks in favor of sharing such a pleasant thought, the post is fastidious, thats why i have read it fully