Using versioning in your ASP.NET Core API

Why should you care about versioning your API? Well, writing a web API with ASP.NET Core is easy. Getting it in production also. But what happens when you have your API live, clients are out there consuming it, and the need arises to change something? This is, when you should have thought about versioning your ASP.NET Core web API. This article shows how to version a web API written using ASP.NET Core. It shows different approaches and explains the differences between them.

When to version?

First of all, we should think a bit about when we need to up our version number of our API. When our endpoints offer JSON, under normal circumstances it is not a problem when we add additional properties to the objects we send. So adding something to the response usually does not break clients consuming our API. At least it should not, and clients should be able to handle additional properties.

However, when we remove (or rename) properties I our responses, this will break clients that rely on the information and the old name. So doing this is a breaking change and should introduce a new version.

The same goes for changes to the addresses of the endpoints and the objects that are sent to them in the request body. When we add required additional parameters to our methods (URLs or values), clients don’t magically learn to send that. We could avoid introducing a new API version though, if we still accept the old requests and answer them as we did before. In this case we only change behavior when the new arguments are send from newer clients.

Different ways you can implement versioning

There are multiple ways we could access different versions of our web api. Probably one of the most common ones is dedicating a part of the path to the version, like so: https://mydomain.com/api/v1/someEndpoint. This, however, requires you to take care of versioning at the very beginning of your project and introduce the version to the URL from day 1. Changing the URL to introduce the version later on would be a breaking change. This would allow your clients to manage the API version they work with at a single point: The base path to your API.

Another possibility is, to add the version to the query parameter: https://mydomain.com/api/someEndpoint?api-version=1.0. While you can specifically default to v1 when the query parameter is not set, this somehow makes querying your API a bit tedious, as you have to take care to add the parameter to every query.

The third way to specifically address an API version is a more REST-like way and uses common HTTP content negotiation between the client and the server. In this case, a client should always specifically request the version it is going to access. It sends the version along in the accept header of the API request. Example: accept: application/json;v=2.0 If no version is specified, the server should default to the latest implementation. Like the first approach, this requires to think about versioning from the very beginning, as a client that does not send the version will break when the API changes.

Then there is a fourth way: Custom headers. You can send the requested version in a custom header field, and decide by that. Here you are more free to chose what your APIs default behavior should be in case the header is missing, e.g. falling back to v1.

Caveats and upgrade paths from non-versioned to versioned APIs

If you planned for versioning beforehand, you already chose one of the possibilities, and you can skip to the next section.

If you didn’t plan for versioning from the very beginning, you most likely simply call a path on your API (e.g. https://mydomain.com/api/someEndpoint without any version information. This allows introducing a version part in the path as well as the query string for new clients only. You can add the versioned endpoints to the API under the new path with the version part in it, or with the additional query parameter. The old clients continue to call the old v1 endpoints. If you want to go for one of the header-based options, you should fall back to v1 in case of missing headers. Unless you want to explicitly work against common practices, you shouldn’t go with the accept header. Speaking semantically correct http with your API would default to the latest version, breaking old clients.

Also the header-based options may run into problems when used in certain networks. I encountered strange WAFs (Web Application Firewall). These removed any custom headers, as well as filtered header values if it didn’t knew about them (like the version in the accept header). Depending on the hosting environment, there’s a chance you can’t use these header-based approaches. Luckily, environments that are restricted that harshly aren’t too common.

Implement versioning

In ASP.NET Core, there is already infrastructure available for that. First of all, install the following NuGet package into your API project: Microsoft.AspNetCore.Mvc.Versioning.

Then, for a new (or a small existing) project, add the following code to your Startup class in the ConfigureServices method:services.AddApiVersioning();

On all controllers you already have in your new project, add the following attribute: [ApiVersion( "1.0" )]
Also add this to all new controllers you are going to add in the future, with the correct corresponding version.

If you have an existing project and don’t want to add the [ApiVersion("1.0")] attribute to all your existing controllers, add this instead:services.AddApiVersioning(options => {
options.AssumeDefaultVersionWhenUnspecified = true;
});

This will make the versioning system assume that a controller that does not specify a version implements v1.0 of your API.

Now we need a versioned API call. Let’s assume we did that in a new API project based on the ASP.NET Core API project template, with the known ValuesController, and amend this values controller like so:[ApiVersion("1.0")] // Add this
[ApiVersion("2.0")] // Add this
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
// This is used vor API version 1
public ActionResult<IEnumerable> Get()
{
return Get("v1");
}

In fact, this is already enough to make versioning work. By default, the API versioning will listen on a query parameter. From this moment on, you can add ?api-version=x.y to access any version your API provides. If you request a version that your API does not (yet) support, the system will automatically respond with an HTTP 400 code (Bad Request), and return a JSON object that states that the API does not implement the requested version. This looks like this:{
"error": {
"code": "UnsupportedApiVersion",
"message": "The HTTP resource that matches the request URI 'https://localhost:44363/api/values?api-version=3.0' does not support the API version '3.0'.",
"innerError": null
}
}

To use the path to select the version it is sufficient to add another route to the controller: [Route("api/v{version:apiVersion}/[controller]")]. This will leave the original (unversioned) route in tact, and add a second versioned route to the controller. This will allow both ways, query parameter and path, to work simultaneously. If no version is provided by either query parameter or path, it will default to version 1. You can see this in another branch in the sample repository.

To be able to use the REST-like accept header, slightly other changes are required. Instead of adding the second route the controller, we change the configuration in our Startup class to this:services.AddApiVersioning(options =>
{
options.AssumeDefaultVersionWhenUnspecified = true;
options.ApiVersionReader = new MediaTypeApiVersionReader();
options.ApiVersionSelector = new CurrentImplementationApiVersionSelector(options); // optional, but recommended
});

This will change the system to look in the accept header for the version. Please make sure, that the accept header does not state only the version, as this won’t work. A working value would be accept: application/json;v=2.0. The ApiVersionSelector should be changed too, to make this API behave more REST-like and default to the latest version instead of v1. You can see this in the third branch in the example repo.

I mentioned another way using a custom header. This can be done by replacing the MediaTypeApiVersionReader in the aforementioned constellation with an instance of HeaderApiVersionReader and specifying the custom header name you want to use:// will read from the 'my-api-version' header
options.ApiVersionReader = new HeaderApiVersionReader("my-api-version");

Conclusion

Introducing versioning to your ASP.NET Core API isn’t that difficult. You need to decide on one of the possible ways to address a specific version of your API, and implement that. Probably the most common approaches are path and accept header. Query string and a custom header are also possible, but not that commonly used. The actual implementation is only a matter of adding a NuGet package, configuration, and adding some attributes on your API controllers.