Change in an API is inevitable as our knowledge and experience of a system improve, but implementing those changes on a system might break existing client interaction. To provide longterm support on constantly evolving APIs, we need versioning. Project versioning is typically accomplished by creating different versions of build packages (assemblies), but REST API versioning is harder because we need to support multiple API versions together in the same project.

Microsoft’s ASP.NET versioning NuGet package (Microsoft.AspNetCore.Mvc.Versioning) gives a powerful, and easy-to-use methods for adding API versioning semantics to the existing REST APIs. Besides, it is compliant with the versioning semantics outlined by the Microsoft REST API. Also, it supports four kinds of API versioning and they are listed below:

  1. Query String Versioning
  2. HTTP Header Versioning
  3. Media Type Versioning
  4. URL Path Versioning

Query String Versioning

It is the default pattern enabled in the versioning package and it is quite simple and easy to configure.

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddApiVersioning(config =>
    {
        config.DefaultApiVersion = new ApiVersion(2, 0);
        config.AssumeDefaultVersionWhenUnspecified = true;
    });
}

DemoController.cs

[ApiController]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/[controller]")]
public class DemoController : ControllerBase
{
    [HttpGet]
    [MapToApiVersion("1.0")]
    public string GetV1() => "Version 1.0";

    [HttpGet]
    [MapToApiVersion("2.0")]
    public string GetV2() => "Version 2.0";
}

HTTP Calls

GET /api/demo?api-version=1.0 HTTP/1.1
Host: localhost:8029

Version 1.0

GET /api/demo?api-version=2.0 HTTP/1.1
Host: localhost:8029

Version 2.0

GET /api/demo HTTP/1.1
Host: localhost:8029

Version 2.0

In the above approach, api-version is used as default query string parameter, but we can customize this name as like below:

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddApiVersioning(config =>
    {
        config.DefaultApiVersion = new ApiVersion(2, 0);
        config.AssumeDefaultVersionWhenUnspecified = true;
        config.ApiVersionReader = new QueryStringApiVersionReader("v");
    });
}

HTTP Header Versioning

In this approach, version information is sent via HTTP header. We can customize this HTTP header name as like below:

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddApiVersioning(config =>
    {
        config.DefaultApiVersion = new ApiVersion(2, 0);
        config.AssumeDefaultVersionWhenUnspecified = true;
        config.ApiVersionReader = new HeaderApiVersionReader("x-version");
    });
}

HTTP Calls

GET /api/demo HTTP/1.1
Host: localhost:8029
x-version: 1.0

Version 1.0

GET /api/demo HTTP/1.1
Host: localhost:8029
x-version: 2.0

Version 2.0

GET /api/demo HTTP/1.1
Host: localhost:8029

Version 2.0

Media Type Versioning

The parameters used in media types for content negotiation can contain custom input that can be used to drive API versioning.

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddApiVersioning(config =>
    {
        config.DefaultApiVersion = new ApiVersion(2, 0);
        config.AssumeDefaultVersionWhenUnspecified = true;
        config.ApiVersionReader = new MediaTypeApiVersionReader("v");
    });
}

HTTP Calls

Note: If we sent both Accept and Content-Type header together, it will read the version information from Accept header.

GET /api/demo HTTP/1.1
Host: localhost:8029
Accept: text/plain;v=1.0

Version 1.0

GET /api/demo HTTP/1.1
Host: localhost:8029
Accept: text/plain;v=2.0

Version 2.0

GET /api/demo HTTP/1.1
Host: localhost:8029
Content-Type: application/json;v=1.0

Version 1.0

GET /api/demo HTTP/1.1
Host: localhost:8029
Content-Type: application/json;v=2.0

Version 2.0

URL Path Versioning

It is also the most commonly used pattern but it depends on routing logic.

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddApiVersioning(config =>
    {
        config.DefaultApiVersion = new ApiVersion(2, 0);
        config.AssumeDefaultVersionWhenUnspecified = true;
        config.ApiVersionReader = new UrlSegmentApiVersionReader();
    });
}

DemoController.cs

[ApiController]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class DemoController : ControllerBase
{
    ...
}

HTTP Calls

GET /api/v1/demo HTTP/1.1
Host: localhost:8029

Version 1.0

GET /api/v2/demo HTTP/1.1
Host: localhost:8029

Version 2.0

By using, ApiVersionReader.Combine() method, we can enable more than one type of versioning in our application. In the below code, I have combined all four types of versioning in a single project.

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddApiVersioning(config =>
    {
        config.DefaultApiVersion = new ApiVersion(2, 0);
        config.AssumeDefaultVersionWhenUnspecified = true;

        config.ApiVersionReader = ApiVersionReader.Combine(
            new HeaderApiVersionReader("x-version"),
            new QueryStringApiVersionReader("version"),
            new UrlSegmentApiVersionReader(),
            new MediaTypeApiVersionReader("v"));
    });
}

You can find the complete source code of this project in my GitHub repository.

In my next blog, I will explain how to configure Swagger along with this ASP.NET versioning package.