REST API versioning with ASP.NET Core

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?v=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.

Last updated