Your guide to REST API versioning in ASP.NET Core

This post was originally published on the Telerik Developer Blog.

When you ship an API, you're inviting developers to consume it based on an agreed-upon contract. So what happens if the contract changes? Let's look at a simple example.

If I call an API with a URL of https://mybandapi.com/api/bands/4, I'll get the following response:

{
  "id": 4,
  "name": "The Eagles, man"
}

Now, let's say I decide to update my API schema with a new field, YearFounded. Here's how the new response looks now:

{
  "id": 4,
  "name": "The Eagles, man",
  "YearFounded": 1971
}

With this new field, existing clients still work. It's a non-breaking change, as existing apps can happily ignore it. You should document the new field in The Long Run, for sure, but it isn't a huge deal at the end of the day.

Let's say you want the name to be instead a collection of names associated with the band, like this:

{
  "id": 4,
  "names": 
  [
    "The Eagles, man",
    "The Eagles"
  ],
  "FoundedYear": 1971
}

You introduced a breaking change. Your consumers expected the name field to be a string value, and now you're returning a collection of strings. As a result, when consumers call the API, their applications will not work as intended, and your users are not going to Take It Easy.

Whether it's a small non-breaking change or one that breaks things, your consumers need to know what to expect. To help manage your evolving APIs, you'll need an API versioning strategy.

Since ASP.NET Core 3.1, Microsoft has provided libraries to help with API versioning. It provides a simple and powerful way to add versioning semantics to your REST services and is also compliant with the Microsoft REST Guidelines. In this post, I'll show you how you can use the Microsoft.AspNetCore.Mvc.Versioning NuGet package to apply API versioning to ASP.NET Core REST web APIs.

We're going to look through various approaches to versioning your APIs and how you can enable the functionality in ASP.NET Core. Which approach should you use? As always, It Depends™. There's no dogmatic "one size fits all" approach to API versioning. Instead, use the approach that works best for your situation.

Get Started

To get started, you'll need to grab the Microsoft.AspNetCore.Mvc.Versioning NuGet package. The easiest way is through the .NET CLI. Execute the following command from your project's directory:

dotnet add package Microsoft.AspNetCore.Mvc.Versioning

With the package installed in your project, you'll need to add the service to ASP.NET Core's dependency injection container. From Startup.ConfigureServices, add the following:

public void ConfigureServices(IServiceCollection services)
{
   services.AddApiVersioning();
   // ..
}

What happens when I make a GET request on https://mybandapi.com/api/bands/4? I'm greeted with the following 400 Bad Request response:

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

By default, you need to append ?api-request=1.0 to the URL to get this working, like this:

https://mybandapi.com/api/bands/4?api-request=1.0

This isn't a great developer experience. To help this, we can introduce a default version. If consumers don't explicitly include a version in their request, we'll assume they want to use v1.0. Our library takes in an ApiVersioningOptionstype, which we can use to specify a default version. So you're telling the consumers, "If you don't opt-in to another version, you'll be using v1.0 of our API."

In Startup.ConfigureServices, update your AddApiVersioning code to this:

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

With this in place, your consumers should be able to call a default version from https://mybandapi.com/api/bands/4.

Introducing Multiple Versions for a Single Endpoint

Let's say we want to work with two versions of our API, 1.0 and 2.0. We can use this by decorating our controller with an ApiVersionAttribute.

[Produces("application/json")]
[Route("api/[controller]")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class BandsController : ControllerBase
{}

Once I do that, I can use the MapToApiVersionAttribute to let ASP.NET Core know which action methods are mapped to my API versions. In my case, I've got two methods wired up to a GET on api/bands, GetById and GetById20.

Here's how the annotations look:

[MapToApiVersion("1.0")]
[HttpGet("{id}")]
public async Task<ActionResult<Band>> GetById(int id)
{}

[MapToApiVersion("2.0")]
[HttpGet("{id}")]
public async Task<ActionResult<Band>> GetById20(int id)
{}

With this, the controller will execute the 1.0 version of the GetById with the normal URI (or /api/bands/4?api-version=1.0) and the controller executes the 2.0 version when the consumer uses https://mybandapi.com/api/bands/4?api-version=2.0.

Now that we're supporting multiple versions, it's a good idea to let your consumers know which versions your endpoints are supporting. To do this, you can use the following code to report API versions to the client.

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

When you do this, ASP.NET Core provides an api-supported-versions response header that shows which versions an endpoint supports.

By default, the library supports versioning from query strings. I like this method. Your clients can opt-in to new versions when they're ready. And if no version is specified, consumers can rely on the default version.

Of course, this is not the only way to version an API. Aside from query strings, we'll look at other ways you can version your APIs in ASP.NET Core:

  • URI/URL path
  • Custom request headers
  • Media versioning with Accept headers

Versioning with a URI Path

If I want to version with the familiar /api/v{version number} scheme, I can do it easily. Then, from the top of my controller, I can use a RouteAttribute that matches my API version. Here's how my controller annotations look now:

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

Then, when I call my API from api/v2/bands/4, I will be calling version 2 of my API. While this is a popular method and is easy to set up, it doesn't come without drawbacks. It doesn't imply a default version, so clients might feel forced to update the URIs throughout their apps whenever a change occurs.

Whether it's using a query string or the URI path, Microsoft.AspNetCore.Mvc.Versioning makes it easy to work with versioning from the URI level. Many clients might prefer to do away with URI versioning for various reasons. In these cases, you can use headers.

Custom Request Headers

If you want to leave your URIs alone, you can have consumers pass a version from a request header. For example, if consumers want to use a non-default version of my API, I can have them pass in a X-Api-Version request header value.

When moving to headers, also consider that you're making API access a little more complicated. Instead of hitting a URI, clients need to hit your endpoints programmatically or from API tooling. This might not be a huge jump for your clients, but is something to consider.

To use custom request headers, you can set the library's ApiVersionReader to a HeaderApiVersionReader, then passing in the header name. (If you want to win Trivia Night at the pub, the answer to "What is the default ApiVersionReader?" is "QueryStringApiVersionReader." )

services.AddApiVersioning(options =>
{
   options.DefaultApiVersion = new ApiVersion(2, 0);
   options.AssumeDefaultVersionWhenUnspecified = true;
   options.ReportApiVersions = true;
   options.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");
});

In Postman, I can pass in an X-Api-Version value to make sure it works. And it does.

Media Versioning with Accept Headers

When a client passes a request to you, they use an Accept header to control the format of the request it can handle. These days, the most common Accept value is the media type of application/json. We can use versioning with our media types, too.

To enable this functionality, change AddApiVersioning to the following:

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

Clients can then pass along an API version with an Accept header, as follows.

Combining Multiple Approaches

When working with the Microsoft.AspNetCore.Mvc.Versioning NuGet package, you aren't forced into using a single versioning method. For example, you might allow clients to choose between passing in a query string or a request header. The ApiVersionReader supports a static Combine method that allows you to specify multiple ways to read versions.

services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader =
    ApiVersionReader.Combine(
       new HeaderApiVersionReader("X-Api-Version"),
       new QueryStringApiVersionReader("version"));
});

With this in place, clients get v2.0 of our API by calling /api/bands/4?version=2 or specifying a X-Api-Version header value of 2 .

Learn More

This post only scratches the surface—there's so much more you can do with the Microsoft.AspNetCore.Mvc.Versioning NuGet package. For example, you can work with OData and enable a versioned API explorer, enabling you to add versioned documentation to your services. If you don't enjoy decorating your controllers and methods with attributes, you can use the controller conventions approach.

Check out the library's documentation for details, and make sure to read up on known limitations (especially if you have advanced ASP.NET Core routing considerations).