In this article, we’ll discuss API versioning and implement it in ASP.NET Core 6 step by step.

Source code for this article is available on github

We will cover the following:

  • API Versioning – What & Why?
  • When to Version API
  • Query Parameter Versioning
  • URI Versioning
  • Custom HTTP Header Versioning
  • Content Negotiation Versioning (Media Versioning)
  • Combining Multiple Approaches
  • Deprecating Versions
  • Using Conventions
  • Summary

API Versioning – What & Why?

Our knowledge and experience grow with our project, so we can better identify the required changes to improve our API. Also, requirements change over time; thus, our API must evolve. We may need to implement breaking changes to evolve our API, but those changes should not affect our API consumers. Our API should be stable, consistent, well documented, and appropriately managed.

API versioning is the practice of smoothly managing changes to an API without breaking the client applications that consume the API. Versioning allows clients to continue using the existing REST API and only migrate their applications to the newer API versions when they are ready.

When to Version API

As our API evolves, we make changes that can be breaking and non-breaking. When we introduce breaking changes, it is crucial to up-version our API. The following are a few examples of breaking change:

  • Removing or renaming an allowed parameter, request field or, a response field
  • Removing or renaming an endpoint
  • Adding a required field or making a field required on the request
  • Changing the type of request field or response field
  • Changing the existing permission definitions
  • Adding new validations

When we introduce non-breaking changes, this does not require a change in the major version number, but we must keep track of the minor versions of APIs. The following are a few examples of non-breaking changes:

  • Adding new endpoints
  • Adding new fields in responses
  • Adding optional request fields or parameters
  • Adding new required request fields that have default values
  • Adding an optional request header

Different Ways of Versioning API

There are several ways to version an API.

  • Query Parameter Versioning
  • URI Versioning
  • Custom HTTP Header Versioning
  • Content Negotiation Versioning (Media Versioning)

Also, we can combine multiple ways of versioning. We will explore all these ways in the upcoming sections.

Tools Required

Or,

 

Let’s Get Started

Let’s start by creating a new .NET 6 API project. We can use .NET CLI or Visual Studio 2022.

Using .NET CLI:

dotnet new webapi -n "VersioningAPI"

Using Visual Studio 2022: In the “Create a new project” window select ASP.NET Core Web API and follow the below images.

1. Create new project API Versioning in ASP.NET Core

2. Configure new project API Versioning in ASP.NET Core

3. Additional info API Versioning in ASP.NET Core

After creating the project, let’s remove the WeatherForecast.cs class and the WeatherForecastController.cs controller as we’re not going to use those.

Now the project structure looks like this:

api versioning starting project structure API Versioning in ASP.NET Core

Now let’s add some dummy data, create a class called Data and add the following code:

public class Data
{
      public static readonly List<Book> Books = new List<Book>()
        {
            new Book()
            {
                Id = 1,
                Title = "Concurrency in C# Cookbook",
                Author = "Stephen Cleary"
            },
            new Book()
            {
                Id = 2,
                Title = "Designing Data-Intensive Applications",
                Author = "Martin Kleppmann"
            }
        };
 }

In this ‘Data’ class, we’ve added a list of books we’ll use as the data source for the different versions of API.

Now let’s create a new folder called “Models” in the root folder, and inside the “Models” folder, add a new class called User.

public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Author { get; set; }
}

Inside the Controllers folder, we’ll create a controller called BooksController and add a GetBooks action like the following:

[Route("api/[controller]")]
[ApiController]
public class BooksController : ControllerBase
{
    [HttpGet]
    public IActionResult GetBooks()
    {
        var books = Data.Books;
        return Ok(books);
    }
}

Let’s run the project and send a get request to the following endpoint:

https://localhost:7076/api/books

Response:

[
    {
        "id": 1,
        "title": "Concurrency in C# Cookbook",
        "author": "Stephen Cleary"
    },
    {
        "id": 2,
        "title": "Designing Data-Intensive Applications",
        "author": "Martin Kleppmann"
    }
]

Now that we’ve hooked up everything nicely and the data is ready for testing let’s install the required versioning package.

Install Required NuGet Package:

We’ll use the Microsoft.Aspnetcore.Mvc.Versioning NuGet package to implement API versioning.

4. Package API Versioning in ASP.NET Core

We can install it using .NET CLI by executing the following command from our project’s directory:

dotnet add package Microsoft.AspNetCore.Mvc.Versioning

Or by using the Package Manager Console in Visual Studio:

PM> Install-Package Microsoft.AspNetCore.Mvc.Versioning

After installing the package, we’ll need to add the versioning service to ASP.NET Core’s dependency injection container. Open the Program.cs file and add the following:

// Add services to the container.
builder.Services.AddApiVersioning();
// ...

After adding the service let’s again send a get request to the following endpoint:

https://localhost:7076/api/books

This time we get an error (400 Bad Request) response:

{
    "error": {
        "code": "ApiVersionUnspecified",
        "message": "An API version is required, but was not specified.",
        "innerError": null
    }
}

The message is pretty straightforward. As we’ve enabled versioning, we’ll need to specify a version when sending the request, but we didn’t set it. We’ll get this working in the next section using query string versioning.

Query Parameter Versioning

We can fix the above error by specifying api-version=1.0 as a query param. This way of versioning is called Query Parameter Versioning, and this is the default versioning scheme. Let’s set the version like the following:

https://localhost:7076/api/books?api-version=1.0

Now, this endpoint will work, but in a real-world app, we’ll have many other endpoints and have to append this version to all of those, which isn’t a great developer experience. Also, this change will break the clients’ applications as their implementation doesn’t include the version. To prevent this, we can specify a default version. So, if clients do not set a version in their request, we assume they prefer to use v1.0.

We can pass the ApiVersioningOptions type to our versioning service, which we can use to specify a default version. In Program.cs let’s update AddApiVersioning to the following:

builder.Services.AddApiVersioning(options =>
    {
        options.AssumeDefaultVersionWhenUnspecified = true;
        options.DefaultApiVersion = new ApiVersion(1, 0);
    });

Now if consumers send a request to the https://localhost:7076/api/books they are sending the request to the default version.

Add Multiple Versions for Single Endpoint

We can add multiple versions to the same controller and use [MapToApiVersion] attribute to map Actions to the different versions of endpoints like this:

[Route("api/[controller]")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[ApiController]
public class BooksController : ControllerBase
{
    [MapToApiVersion("1.0")]
    [HttpGet]
    public IActionResult GetBooks()
    {
        var books = Data.Books;
        return Ok(books);
    }

    [MapToApiVersion("2.0")]
    [HttpGet]
    public IActionResult GetBooksV2()
    {
        var books = Data.Books.Select(x => x.Title);
        return Ok(books);
    }
}

Now if we send a get request to https://localhost:7076/api/books or https://localhost:7076/api/books?api-version=1.0 version 1.0 will be executed and we get the following response:

[
    {
        "id": 1,
        "title": "Concurrency in C# Cookbook",
        "author": "Stephen Cleary"
    },
    {
        "id": 2,
        "title": "Designing Data-Intensive Applications",
        "author": "Martin Kleppmann"
    }
]

And if we send a get request to https://localhost:7076/api/books?api-version=2.0 version 2.0 will be executed and we get the following response:

[
    "Concurrency in C# Cookbook",
    "Designing Data-Intensive Applications"
]

So everything is working as expected.

We can also create separate controllers for individual versions.

Let’s first create some folders to organize the controllers of different API versions better. We’ll create two folders called “v1” and “v2” inside the “Controllers” folder. Then we’ll move the BooksController.cs to the “v1” folder and will add .v1 to the namespace. The folder structure should now look like the following:

api versioning end project structure API Versioning in ASP.NET Core

Now let’s remove v2.0 related code from “v1/BooksController.cs” and it should looks like this:

[Route("api/[controller]")]
[ApiVersion("1.0")]
[ApiController]
public class BooksController : ControllerBase
{
    [HttpGet]
    public IActionResult GetBooks()
    {
        var books = Data.Books;
        return Ok(books);
    }
}

Next, create another controller called BooksController inside the “v2” folder and add the following code:

[Route("api/[controller]")]
[ApiVersion("2.0")]
[ApiController]
public class BooksController : ControllerBase
{
    [HttpGet]
    public IActionResult GetBooksV2()
    {
        var books = Data.Books.Select(x => x.Title);
        return Ok(books);
    }
}

Now, if we again send a get request to https://localhost:7076/api/books?api-version=1.0 and https://localhost:7076/api/books?api-version=2.0, everything should work as before.

We should let our consumers know we’re supporting multiple versions. We can do this by adding ReportApiVersions=true inside AddApiVersioning like this:

builder.Services.AddApiVersioning(options =>
    {
        options.AssumeDefaultVersionWhenUnspecified = true;
        options.DefaultApiVersion = new ApiVersion(1, 0);
        options.ReportApiVersions = true;
    });

Now if we send a request ASP.NET Core returns an api-supported-versions response header with all the versions the endpoint supports.

api versioning report versioning API Versioning in ASP.NET Core

As mentioned earlier, query string versioning is the default versioning scheme. This method allows clients to migrate to the new versions when they’re ready. And if no version is specified, clients can rely on the default version.

URI Versioning

URI versioning is the most common and cleaner method. We can easily read which API version are we targeting right from the URI. Here’s an example:

We can easily implement it by modifying the route in our controller:

[ApiVersion("2.0")]
[Route("api/{version:apiVersion}/[controller]")]
[ApiController]
public class BooksController : ControllerBase
{
    [HttpGet]
    public IActionResult GetBooksV2()
    {
        var books = Data.Books.Select(x => x.Title);
        return Ok(books);
    }
}

Now, we can test it. If we send a get request to https://localhost:7076/api/2.0/books we get the following response:

[
    "Concurrency in C# Cookbook",
    "Designing Data-Intensive Applications"
]

One thing to mention, we can’t use the query parameter scheme to call the version 2.0 controller anymore. We can use it for version 1.0, however. Also, as we didn’t configure URI versioning for version 1.0, we can still access it using the normal URL https://localhost:7076/api/books

Custom HTTP Header Versioning

If we don’t want to change the URI of the API, we can send the version in the HTTP Header. To enable this, we have to modify our configuration:

builder.Services.AddApiVersioning(options =>
    {
        options.AssumeDefaultVersionWhenUnspecified = true;
        options.DefaultApiVersion = new ApiVersion(1, 0);
        options.ReportApiVersions = true;
        options.ApiVersionReader = new HeaderApiVersionReader("x-api-version");
    });

And revert the Route change in our version 2.0 controller:

[ApiVersion("2.0")]
[Route("api/[controller]")]
[ApiController]
public class BooksController : ControllerBase
{
//...

api versioning custom header versioning API Versioning in ASP.NET Core

If we use query string versioning, by default ASP.NET core accepts api-version as query parameter if specified. If we want to support different a parameter name, we can use a QueryStringApiVersionReader class instead:

// ...
options.ApiVersionReader = new QueryStringApiVersionReader("x-api-version");
// ...

With this, If we send a request to https://localhost:7076/api/books?x-api-version=2.0 it will execute version 2.0.

Content Negotiation Versioning (Media Versioning)

Similar to custom header versioning, we don’t need to modify the URI in this approach. We only change the Accept header values. In this case, the scheme preserves our URIs between versions.

// ...
options.ApiVersionReader = new MediaTypeApiVersionReader("version");

// …

api versioning media versioning API Versioning in ASP.NET Core

Combining Multiple Approaches

We’re not bound to use only one approach of versioning. We can give consumers multiple ways to choose. ApiVersionReader has a Combine method that we can use to specify multiple readers. Let’s say we want to support the query parameter versioning, the accept header, and the custom header versioning. We can update the versioning service as follows:

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

Deprecating Versions

If we want to deprecate an API version without deleting it, we can use the Deprecated property as follows:

[ApiVersion("2.0", Deprecated = true)]
 [Route("api/[controller]")]
 [ApiController]
 public class BooksController : ControllerBase
 {
...

Now, if we send a get request, ASP.NET core provides a api-deprecated-versions response header with the deprecated versions. We’ll still be able to work with that API but marked it as deprecated.

api versioning depricate version API Versioning in ASP.NET Core

Using Conventions

Instead of adding the [ApiVersion] attribute to the controllers, we can assign these versions to different controllers in the configuration instead.

builder.Services.AddApiVersioning(options =>
    {
        options.AssumeDefaultVersionWhenUnspecified = true;
        options.DefaultApiVersion = new ApiVersion(1, 0);
        options.ReportApiVersions = true;
        options.ApiVersionReader = new HeaderApiVersionReader("x-api-version");
        options.Conventions.Controller<VersioningAPI.Controllers.v1.BooksController>()
            .HasApiVersion(new ApiVersion(1, 0));
        options.Conventions.Controller<VersioningAPI.Controllers.v2.BooksController>()
            .HasDeprecatedApiVersion(new ApiVersion(2, 0));

    });

This approach is helpful when we have many versions of a single controller. Now we can remove the [ApiVersion] attribute from the controllers.

Summary

In this article we’ve learned:

  • What is API Versioning and why it is required
  • When to Version API
  • How to implement different ways of API Versioning in .NET 6
  • How to deprecate an API Version

Although we have covered quite enough to version our APIs, there are many more features than Microsoft.AspNetCore.Mvc.Versioning package provides. To learn more, check out the library’s documentation here,

Thanks!