The best way to version a RESTful API is a topic of constant debate and every other approach have their own pros and cons. There is no 'one size fits all' solution when it comes to versioning a REST API.
This article will discuss some of the commonly used API Versioning strategies and demonstrate how to implement them in ASP.NET Core Web API.
Challenges of API versioning
The most RESTful way of versioning an API is to not version it at all. However, over time requirements change and API should evolve accordingly to accommodate those changes. It is imperative to anticipate changes and think about a versioning strategy before publishing the first version of the API. Once the API is published, any future changes to the API should not break the existing client app consuming the API.
In typical projects like class libraries or executable programs we could achieve versioning by creating a different version of a package, typically by changing assemblies version. What makes API versioning difficult is that multiple versions of API should be supported at the same time. Old clients may still rely on the older version and new clients would want to make use of the latest API. Side by side deployment of multiple version of API on the server is impractical and inconvenient. The approach to versioning the REST API is to support multiple versions in the same code base.
REST API Versioning in .Net Core
Implementing a versioning strategy into .NET Core web API from the ground up can be a challenge. Microsoft has provided a Nuget package called Microsoft.AspNetCore.Mvc.Versioning to ease the process versioning .Net Core REST APIs.
Getting started
Create a new .NET core web API project. Add Microsoft.AspNetCore.Mvc.Versioning Nuget package as a dependency to the project and add API Versioning package into the service container of the project.
// startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
// Add API Versioning to the service container to your project
services.AddApiVersioning();
}
Configuring the Default Behavior for API Versioning
If a GET request is made to the values resource (/api/values endpoint), 404 Bad Request response is returned. This happens because the default behavior of API versioning package expects to specify the API version in all cases.
GET /api/values
{
"error": {
"code": "ApiVersionUnspecified",
"message": "An API version is required, but was not
specified.",
"innerError": null
}
}
Use a lambda function to configure API versioning and specify the default API version number and use that default API version number when no API version is specified.
// startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
// Add API Versioning to as service to your project
services.AddApiVersioning(config =>
{
// Specify the default API Version as 1.0
config.DefaultApiVersion = new ApiVersion(1, 0);
// If the client hasn't specified the API version in the request, use the default API version number
config.AssumeDefaultVersionWhenUnspecified = true;
});
}
Now if a request is made to the values resource (api/values) without specifying the API version, server will respond with a 200 OK. Since the API version was not specified in the request, the default version of 1.0 is assumed. Furthermore, values controller is not specified with any version number, the values controller is also assumed to be of the default version.
Advertise the accepted API Version to the client
When configuring the API Versioning, set ReportApiVersion property to true to let the consumers know about the supported API versions.
// startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
// Add API Versioning to as service to your project
services.AddApiVersioning(config =>
{
// Specify the default API Version
config.DefaultApiVersion = new ApiVersion(1, 0);
// If the client hasn't specified the API version in the request, use the default API version number
config.AssumeDefaultVersionWhenUnspecified = true;
// Advertise the API versions supported for the particular endpoint
config.ReportApiVersions = true;
});
}
Now a GET request to the values endpoint will respond with a 200 OK and contain response header named api-supported-versions which lists all available API versions for that endpoint.
Advertising the supported versions for an API can be really useful for the API consumers. The consumers could read the api-supported-versions header and figure out what versions are supported for that particular endpoint.
Version specific Controllers and Actions
When the clients make request for a specific version of an API endpoint, the request should be redirected to the appropriate controller or action handling the requested API version. There are multiple approaches on how to assign controllers and actions to handle version specific requests. Separate controllers can be created for each API versions and the request will be directed to a particular controller based on the requested API version. This article however, will demonstrate on creating all the version specific actions within a single controller.
First step is to specify what API versions are supported by the controller using the ApiVersion attribute. Code snippet below specify and advertise that the values controller accepts API v1.0 and v1.1. Separate actions to handle requests based on the API version has not been created yet.
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
namespace apiVersioningDemo.Controllers
{
[Route("api/[controller]")]
[ApiController]
[ApiVersion("1.0")]
[ApiVersion("1.1")]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/values/5
[HttpGet("{id}")]
public ActionResult<string> Get(int id)
{
return "value";
}
}
}
Since no version number is specified to the actions in values controller, all the endpoints are assumed to have the default version of 1.0.
Add a new action method for the GET values endpoint to handle API version 1.1 using MapToApiVersion attribute.
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
namespace apiVersioningDemo.Controllers
{
[Route("api/[controller]")]
[ApiController]
[ApiVersion("1.0")]
[ApiVersion("1.1")]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/values
[HttpGet]
[MapToApiVersion("1.1")] // v1.1 specific action for GET api/values endpoint
public ActionResult<IEnumerable<string>> GetV1_1()
{
return new string[] { "version 1.1 value 1", "version 1.1 value2 " };
}
}
}
In the above code snippet, GET request for v1.0 is handled by the Get() action while GET request for v1.1 is handled by the GetV1_1() action.
Versioning strategies
Now that the API supports multiple version, we need a way to allow clients to specify the API version they are requesting. There few different approaches on how to allow clients to send the versioning information when making the request. Some of these strategies are discussed below:
Using query params
The default versioning scheme provided by the Microsoft.AspNetCore.Mvc.Versioning package makes use of a query param api-version.
Versioning an API using a query string allows for clients to explicitly specify the version number based on their need. Unlike other versioning strategies, clients do not need to include API version info in every request. If the api-version query string is not specified by the client, the default version is implicitly requested.
https://demo.org/api/resource?api-version=1.1
Using Request Headers
Another approach to versioning an API is using request headers where a header value specifies the API version. Many developers advocate this approach because unlike the URL path param and query string approach, using request header doesn't require fiddling around with the URLs on the client side. The downside to using request headers for versioning is that the versioning option is not explicitly visible to the client at a first glance.
By default Microsoft.AspNetCore.Mvc.Versioning package makes use of query param strategy to specify the API version in the request. Configure the API Versioning to use request headers as a versioning strategy using a Version Reader. ApiVersionReader is used to specify the versioning scheme, this class analyzes the request and figure out which version is being requested. If unspecified, the default version reader scheme is QueryStringApiVersionReader, that's why we were able to request v1.1 using the api-version query params earlier.
Changing the version reader to HeaderApiVersionReader enable clients to send the versioning information using the request header instead of query params. API Version can be specified by the client in the X-version header. Note that "X-version" string which is passed as a parameter to the HeaderApiVersionReader() method is an arbitrary name chosen for the request header used for sending API version information.
// startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
// Add API Versioning to as service to your project
services.AddApiVersioning(config =>
{
// Specify the default API Version
config.DefaultApiVersion = new ApiVersion(1, 0);
// If the client hasn't specified the API version in the request, use the default API version number
config.AssumeDefaultVersionWhenUnspecified = true;
// Advertise the API versions supported for the particular endpoint
config.ReportApiVersions = true;
// DEFAULT Version reader is QueryStringApiVersionReader();
// clients request the specific version using the X-version header
config.ApiVersionReader = new HeaderApiVersionReader("X-version");
});
}
Using Media Type (Accept header) Versioning
Another approach of version the API is using the content negotiation process provided by HTTP. When client requests a resource using the Accept header, they could explicitly include the version number in the media type itself.
In GET request to the values endpoint shown below, the client is explicitly mentioning that it accepts the response of media type application/json with a version number of 1.1 from the server.
GET /values
Accept: application/json;v=1.1
Server could read the Accept header from the request, and respond with the appropriate API version.
One of the disadvantages of using Media Type versioning scheme is that it can be quite obscure, difficult to implement and not immediately obvious to clients that they can request different API versions using the Accept header.
To implement versioning using media type set the ApiVersionReader to a instance of MediaTypeApiVersionReader class.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
// Add API Versioning to as service to your project
services.AddApiVersioning(config =>
{
// Specify the default API Version
config.DefaultApiVersion = new ApiVersion(1, 0);
// If the client hasn't specified the API version in the request, use the default API version number
config.AssumeDefaultVersionWhenUnspecified = true;
// Advertise the API versions supported for the particular endpoint
config.ReportApiVersions = true;
// Versioning using media type
config.ApiVersionReader = new MediaTypeApiVersionReader("v");
});
}
Using URL path versioning scheme
Using a version number directly in the URL path is one of the simplest way of versioning an API. URL path versioning approach is more visible since it explicitly states the version number in the URL itself. However, this approach mandates clients to change their URL across their application whenever there is a new API version. Furthermore, embedding the API version into the URL itself would break a fundamental principle of REST API which states that each URL should represent a particular resource and the resource URL should not change over time. This approach is better suited in cases when every new API version has broad major changes.
https://demo.org/api/v2/resource
To implement URL path versioning, modify the Route attribute of the controllers to accept API versioning info in the path param.
The Route attribute is changed to :
[Route("api/v{version:apiVersion}/[controller]")]
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
namespace apiVersioningDemo.Controllers
{
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
[ApiVersion("1.0")]
[ApiVersion("1.1")]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/values
[HttpGet]
[MapToApiVersion("1.1")] // v1.1 specific action for GET api/values endpoint
public ActionResult<IEnumerable<string>> GetV1_1()
{
return new string[] { "version 1.1 value 1", "version 1.1 value2 " };
}
}
}
Supporting multiple versioning Schemes
Supporting for multiple API versioning schemes provide flexibility and give options to the clients to use the versioning scheme of their choice. The code snippet below demonstrate how to provide support for both the query params versioning scheme and request header versioning scheme using the static method Combine avaliable in the ApiVersionReader class.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
// Add API Versioning to as service to your project
services.AddApiVersioning(config =>
{
// Specify the default API Version
config.DefaultApiVersion = new ApiVersion(1, 0);
// If the client hasn't specified the API version in the request, use the default API version number
config.AssumeDefaultVersionWhenUnspecified = true;
// Advertise the API versions supported for the particular endpoint
config.ReportApiVersions = true;
// DEFAULT Version reader is QueryStringApiVersionReader();
// clients request the specific version using the X-version header
//config.ApiVersionReader = new HeaderApiVersionReader("X-version");
// Supporting multiple versioning scheme
config.ApiVersionReader = ApiVersionReader.Combine(new HeaderApiVersionReader("X-version"), new QueryStringApiVersionReader("api-version"));
});
}
Now that two different API versioning schemes are supported, the clients should make sure to use only one of the supported schemes. Technically clients could use both the schemes to specify the API version in a request as long as they specify the same version number. If two different versions are requested in the same request using the different API versioning scheme, it will result in a bad request error.
In the above example, API v1.0 is requested using the query param scheme and v1.1 is requested using the header scheme, which results in the Ambiguous Api Version error.
Advertising the Deprecated Versions
Similar to advertising the supported API versions for an endpoint, API versions which will be deprecated in the near future can also be advertised by setting the deprecated property to true in the ApiVersion attribute. The client could read the api-deprecated-versions in the response header and identify the deprecated API versions.
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
namespace apiVersioningDemo.Controllers
{
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
// DEPRECATING an API Version
[ApiVersion("1.0", Deprecated = true)]
[ApiVersion("1.1")]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/values
[HttpGet]
[MapToApiVersion("1.1")] // v1.1 specific action for GET api/values endpoint
public ActionResult<IEnumerable<string>> GetV1_1()
{
return new string[] { "version 1.1 value 1", "version 1.1 value2 " };
}
}
}
Conclusion
This article discussed about multiple approaches for versioning a REST API in .NET Core. Depending on the given use case and the consumer of the API, an appropriate versioning strategy can be implemented.
Top comments (18)
Good post.
Is it possible to use URL path versioning and header API versioning combination? I added URL versioning support to my API but it breaks access to the old URL(api/authors). now I have to specify version in the URL(api/v1.0/authors). but some of the client apps use that old URL. are there any solutions for that?
Yes you could use multiple versioning schemes. Supporting multiple versioning Schemes section in this article has an example on how to implement both the query params versioning scheme and request header versioning scheme.
You can try adding two url paths like
[Route("api/[controller]")]
[Route("api/v{version:apiVersion}/[controller]")]
for your base controller so that it uses both url
And how do you work with changes on db? for example v1 has one table with 5 fields, v2 you split one field into 2 fields, how can you automate the database changes when an user wants to use the new versiΓ³n?
This was an excellent post, thank you so much for taking the time to put this together!
I wanted to say, on the line where it specifies the API version key and value:
demo.org/api/resource?v=1.1
Having it only be "v=1.1" didn't work for me. I actually had to match that to the image provided directly below, with that parameter being "api-version=1.1"
Once I did that, it worked perfectly!
Thank you for spotting this. The one on the screenshot is correct. I'll update the article.
Question about versioning the datamodel
Lets say you have an API that use AutoMapper on a datamodel.
Now, if the datamodel changes, the API will still compile like nothing happend, but the end result wil not be compatible anymore
I see two different ways out of this:
1:
Project
- DTO
- V1
PersonDTO
- V2
PersonDTO
2:
Project
- DTO
PersonDTO_V1
PersonDTO_V2
What would be considered best practice here?
Just figured it out myself by testing
Adding version to the ClassName is best because else having USING statements to support multiple versions in the same API class wil create problems if ClassNames are the same
That is a great post, in your opinion, what are the best approaches for versioning a REST API?
I think it depends on the nature and complexity of the REST API. In the articles, I've laid out the few approaches. For simpler APIs I'd go with either using path param approach or using the request headers.
Thanks for the answer :)
Great post. I am new to this i am wondering how to do you cater for DTO changes when versioning? Do I duplicate them and cut or add fields needed for a particular version or I use one DTO?
Great article!
Thanks so much for this!
This is great. It's so nice to see these options baked in instead of having to roll our own support. Thanks for writing this up!
Thanks for sharing this. Never knew there was a version management library within asp net core
Great post, thank you!