Versioning APIs in ASP.NET Core Explained - Strategies and Implementations

ASP.NET Core Posted 21 days ago

Imagine a scenario where you need to make changes to your API in its structure or in its functionality, but at the same time ensure that the existing API clients shouldn't face any issues. In other words, you would want to upgrade your API, but ensure that it's still backward compatible. How'd you solve this problem? The solution is something we use very extensively in software applications - versioning. You can simply mark your existing APIs as the current (older version), while all the changes you intend to do on the APIs move to the next (or the latest) version.

This solves two problems for us - one, you no longer need to worry about backward compatibility, as the current state of the APIs still exist (or say, co-exist) so your clients aren't going to face any issues and two, you get the capability of giving optional functionalities to your clients in the form for versions, where a client can subscribe to a particular version for new or improved functionalities or features without having to break the existing. Sounds like an application of the Open Closed Principle right? But it is often easier said than done.

When you're building version capabilities into your APIs, you need to ensure certain things, such as -

  1. whether the system is able to correctly parse the requested version.
  2. whether the requests are being routed to the correct endpoints based on their versions - not like a v2 request goes to v1 endpoint.
  3. choosing the right way to pass the expected version information in the requests.

In ASP.NET Core, versioning APIs is a simple and straightforward affair - thanks to the support for API versioning provided by the APIVersioning library provided for AspNetCore. In this article, let's look in detail about to how setup API Versioning for your applications, the versioning strategies employed in general and a few issues we encounter on our way.

For better understanding of things, let's take the example of building a Heroes API, that manages all the registered Heroes in a Hero Association. The story goes like this - there was a plan to build a Heroes API by the team, and they wanted it to be a simple affair with less complexity. So they created a Hero entity with attributes like below:

public class Hero
{
	public int Id { get; set; }
	public string Name { get; set; }
	public string[] Powers { get; set; }
	public bool HasCape { get; set; }
	public DateTime Created { get; set; }
	public bool IsAlive { get; set; }
	public Category Category { get; set; }
}

public enum Category
{
	Anime,
	Comic,
	History,
	Mythology
}

They then used this Entity directly in their CRUD endpoint as below.

[ApiController]
[Route("api/[controller]")]
public class HeroesController : ControllerBase
{
	private readonly IHeroesRepository _data;

	public HeroesController(IHeroesRepository data)
	{
		_data = data;
	}

	[HttpGet]
	public IEnumerable<Hero> Get()
	{
		return _data.All();
	}

	[HttpGet, Route("{id}")]
    public Hero Get(int id)
    {
    	return _data.Single(x => x.Id == id);
    }

	[HttpPost]
	public Hero Post(Hero model)
	{
		return _data.Create(item);
	}
}

The setup seemed pretty much working, and the APIs were pushed for to be used by the clients. Sometime later, as the data grew in size, the team wanted to bring in some more functionalities into this API, such as searching by name and also wanted to change the way in which the requests worked, because the APIs were directly giving out the data from entity Hero which also contains some data unnecessary for the clients to access. But if they are to make such structural changes to the API already in production, the changes might cause issues with the clients and they didn't want the new functionalities they wanted to bring out to be readily available to all their clients - it was a feature requested for some of them.

To solve this, they implemented the API versioning mechanism as below:

  1. Install the APIVersioning library available for use by installing the nuget:
dotnet add package Microsoft.AspNetCore.Mvc.Versioning
  1. Add The APIVersioning service to the IServiceCollection. This adds the necessary capabilities to detect and branch out the requests based on their version.
public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IHeroesRepository, HeroesRepository>();
    
	services.AddControllers();
    
    services.AddApiVersioning(setup =>
    {
		setup.DefaultApiVersion = new ApiVersion(1, 0);
		setup.AssumeDefaultVersionWhenUnspecified = true;
		setup.ReportApiVersions = true;
    });
}

If we look at the options we've configured inside the AddApiVersioning() service:

  • we're setting the DefaultApiVersion to specify to which the requests fallback to.
  • The AssumeDefaultVersionWhenUnspecified lets the router fallback to the default version (specified by the DefaultApiVersion setting) in cases where the router is unable to determine the requested API version. The ReportApiVersions setting sends out the available versions for the API in all the responses in the form of "api-supported-versions" header.
api-supported-versions: 1.0, 2.0
  1. We'd then decorate the API Controller with the version numbers that it'd be called when a request is made for that specific version.
namespace Heroes.Api.Controllers.V2
{
	[ApiController]
	[ApiVersion("1.0")]
	[Route("api/v{version:apiVersion}/[controller]")]
	[Route("api/[controller]")] // for backward compatibility
	public class HeroesController : ControllerBase
	{
		// heroes CRUD operations
	}
}

The [ApiVersion] describes which version the controller needs to be mapped for, the version is specified in the route (in this case), using the fragment "v{version:apiVersion}" - which adds the version number defined in the ApiVersion to the route while matching. In this case, both "/api/v1.0/heroes/1" and "/api/v1/heroes/1" are mapped to this controller.

An API version contains a major and a minor version, and the minor version is sometimes omitted as a convention and its upto the API developers on how the version needs to be specified.

When we mark a controller with both the [Route("api/v{version:apiVersion}/[controller]")] and [Route("api/[controller]")], it is to ensure that the requests from older route templates are also supported (since this is an upgrade from the regular routing, we need to make sure that the old route paths are also supported).

This means that both the requests /api/v1/heroes/1 and /api/heroes/1 are passed onto the above controller.

  1. When we require to push the same controller with some more additions or modifications, we can simply write a new controller (or extend the current one, if we're not modifying the existing) and let the new controller be decorated with the next version number.
namespace Heroes.Api.Controllers.V2
{
    [ApiController]
    [ApiVersion("2.0")]
    [Route("api/v{version:apiVersion}/[controller]")]
    public class HeroesController : ControllerBase
    {
        // new version 
		// new features
	}
}

API Versioning Strategies - How to pass the Version Information:

There are different approaches in passing version information to the API while requesting for data. We specify the source from which the ApiVersion service looks up for version information while configuring it as below:

services.AddApiVersioning(setup => 
	setup.ApiVersionReader = // the source for the version to be extracted
);

_via URL Segment:

The default is the URL Segment approach, where we add the version information within the URL path. The AspNetCore ApiVersion service looks up to this version inside the request path and determines which controller the incoming request be mapped to. Most of the popular API providers such as Google, Facebook use URL Segments to determine the version of the API to be mapped.

To explicitly specify a URL Segment, we'd set it as the ApiVersionReader:

services.AddApiVersioning(setup => 
	setup.ApiVersionReader = new UrlSegmentApiVersionReader());

via Query:

In this approach, the version is attached to the URL as a query parameter. The advantage is that the URL can be kept clean, while the version can be changed as a query parameter. The downside is that if we are to change the version number, we need to get it done on all the URLs where we populate the query parameters.

an instance of the QueryStringApiVersionReader is set to the AddApiVersioning() to enable query parameters based versioning. By default, the version is passed using the "api-version" query parameter in the query string, while we can customize it by passing the parameter name in an overloaded constructor of the QueryStringApiVersionReader().

// /api/heroes/?api-version=2.0 -- default
services.AddApiVersioning(
    options => options.ApiVersionReader = new QueryStringApiVersionReader());

// /api/heroes/?v=2.0
services.AddApiVersioning(
    options => options.ApiVersionReader = new QueryStringApiVersionReader("v"));

One good example of this is the AWS APIs where we pass on the version of the service we're intended to invoke through the query parameter.

via Header:

The third approach is that we pass our version information via the request headers. This makes the URL completely free of any versioning information and can be kept constant on all times, while using the request header we determine the version to be mapped.

an instance of HeaderApiVersionReader is set to the ApiVersionReader to let the headers be considered for version information.

services.AddApiVersioning(
    options => options.ApiVersionReader = new HeaderApiVersionReader("api-version"));

via ContentType:

The four approach is by extending the media types we use in our request headers to pass on the version information. To achieve this, we assign ApiVersionReader to an instance of MediaTypeApiVersionReader(). By default, the version is passed as "v={versionNumber}" which can be overriden similar to the QueryStringApiVersionReader() by passing the desired key in the constructor.

// Content-Type: application/json;v=2.0 -- default
services.AddApiVersioning(
    options => options.ApiVersionReader = new MediaTypeApiVersionReader());

// Content-Type: application/json;version=2.0
services.AddApiVersioning(
    options => options.ApiVersionReader = new MediaTypeApiVersionReader("version"));

Reading from More than one Sources:

Sometimes we might want our version information be obtained from various sources instead of sticking to just one common constraint on all times. In such cases we can actually pass multiple ApiVersionReader instances as shown below:

services.AddApiVersioning(setup =>
{
    setup.ApiVersionReader = ApiVersionReader.Combine(
		new UrlSegmentApiVersionReader(), 
		new HeaderApiVersionReader("api-version"), 
		new QueryStringApiVersionReader("api-version"),
    	new MediaTypeApiVersionReader("version"));
});

Final Words:

API Versioning is an important part of the API design to create extensible APIs which can be updated or upgraded with new functionalities over the time, while keeping it backward compatible. AspNetCore comes with a useful library for ApiVersioning which makes things easier for us.

Although the setup works for us, its not yet completed - it'd be satisfying if we can provide a proper API documentation that comes with version support.

In the next article, we shall look at how we can integrate our versioned API with SwaggerUI and let Swagger dynamically generate documentation for us based on the versions.

api versioning aspnetcore dotnetcore swagger swaggerui swaggergen api versioning

Join the Newsletter

Subscribe to get our latest content by email.
    We won't send you spam. Unsubscribe at any time.
    We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept