A few days ago, I and my friends were developing an API using ASP.NET Core where from a GET method we returned some data to our client app. We did pagination in the front end. This means that first, we sent all the data to the client and then did some data.length operations on that to count the items. Then we decided that we should move the logic to the back-end (server-side pagination) because it will decrease the payload of the HTTP request. In our case, it wasn't any problem since we had extreme control over our client app. We changed all the logic here, there, and everywhere and we were fine with it.

However, the possibility is you will have additional clients and only a single source of truth (API). Introducing a breaking change in one API can support one client and meanwhile break others. For example, let's say your mobile team is on vacation and your web team is working their asses off for that server-side pagination stuff. To support the web team, you made a simple change (who cares!) in the API. You and your web team, both are happy with that change.

If you are happy and you know it clap your hands 👏

The nightmares begin when you find out that your million-dollar mobile client is not working for that very simple (yet breaking) change and you are losing customers. More nightmares come later when you find out that neither you are a mobile app developer nor you have access to its source code. Now you only have the option of downgrading both your API and web app. But the team who made the web app is now on vacation too. I'll stop there now cause too many nightmares going around here.

Maybe (not maybe. It is!) API versioning is a good practice in that case. With API versioning, you can not only be safe against those breaking changes but also can support those. It's a win-win situation for everyone.

Let's see how to configure API versioning in ASP.NET Core.

Note: I'm using an empty ASP.NET Core Web API project (.NET Core 3.*)

Download .NET (Linux, macOS, and Windows)
Free downloads for building and running .NET apps on Linux, macOS, and Windows. Runtimes, SDKs, and developer packs for .NET Framework, .NET Core, and ASP.NET.

Install this package via NuGet: Microsoft.AspNetCore.Mvc.Versioning.

Microsoft.AspNetCore.Mvc.Versioning 4.0.0
A service API versioning library for Microsoft ASP.NET Core.

Now, configure it as a service in the ConfigureServices() method of Startup.cs, (duh! Like I didn't know that was coming)

public void ConfigureServices(IServiceCollection services)
{
   /* code omitted for brevity */
   services.AddApiVersioning();
}
Startup.cs

Next, you have to decorate (i.e with ApiVersion attribute) the controller on which you want support for API versioning (multiple versions). Likewise, you would also have to decorate  your actions with a specific API version number (i.e. with MapToApiVersion attribute):

[ApiVersion("1.0")]
[ApiController]
[Route("api/[controller]")]
public class ValuesController : BaseController
{
    [HttpGet, MapToApiVersion("1.0")]
    public IEnumerable<string> Get()
    {
        return Json(new string[] { "value1", "value2" });
    }
}
ValuesController.cs

Now to get the GET action result you have to specify the API version of it. For the time being, use the query string versioning way. In this way, you will specify the API version directly in the query string. Something like,

http://localhost:5000/api/values?api-version=1.0

If you are adding API versioning to your existing API project, you can tell ASP.NET Core to treat the undercoated controllers and actions to have version 1.0 by default. To do that, configure the AddApiVersioning() service like the below:

services.AddApiVersioning(options => options.AssumeDefaultVersionWhenUnspecified = true);
Startup.cs

You can now make a call to the API like, http://localhost:5000/api/values with no harm made.

In the above configuration (default to version 1.0), note that you will have to explicitly specify the version number when you want to use other versions along with it. However, you don't have to decorate the action in this case. Below is an example of the situation:

[ApiVersion("2.0")]
[ApiController]
[Route("api/[controller]")]
public class ValuesController : BaseController
{
    [HttpGet]
    public IActionResult Get()
    {
        return Json(new string[] { "value1", "value2" });
    }

    [HttpGet, MapToApiVersion("2.0")]
    public IActionResult GetWithTotal()
    {
        var data = new string[] { "value1", "value2" };
        var total = data.Length;
        return Json(new { data = data, total = total });
    }
}
ValuesController.cs

There are three ways you can specify the API version. They can be set via:

In URL Path Versioning, the way you pass the version number as a segment to your URL path. This for example,

http://localhost:5000/api/v1/values

By the way, you have to modify your Route attribute to accommodate version segment in it like below:

[ApiVersion("1.0")]
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public class ValuesController : BaseController
{
    [HttpGet, MapToApiVersion("1.0")]
    public IActionResult Get()
    {
        return Json(new string[] { "value1", "value2" });
    }
}
ValuesController.cs

Note that the letter v is not mandatory to add in front of the version number. It's just a convention.

You can configure your service to read the API version number from a specific Media Type (By default it reads from the content-type request header. You can configure your own media type). Configure your service like the below to activate media type visioning,

public void ConfigureServices(IServiceCollection services)
{
    /* code omitted for brevity */
    services.AddApiVersioning(options =>
    {
        options.ApiVersionReader = new MediaTypeApiVersionReader();
        options.AssumeDefaultVersionWhenUnspecified = true;
        options.ApiVersionSelector = new CurrentImplementationApiVersionSelector(options);
    });
}
Startup.cs

Now when you want to make an HTTP request, just specify the API version as a part of the content-type request header like below,

GET api/values HTTP/1.1
host: localhost
accept: text/plain;v=1.0

Response
--------
["value1", "value2"]

By the way, the CurrentImplementationApiVersionSelector the option will take the most recent API version if there is no version defined as a part of the content-type request header. In the following example, I didn't mention any version number so it's taking the most recent version among all the versions.

GET api/values HTTP/1.1
host: localhost
accept: text/plain;

Response
--------
{ data: ["value1", "value2"], count: 2 }

Last, you can also pass the version number using a header key. You have to configure the service like,

options => options.ApiVersionReader = new HeaderApiVersionReader("x-ms-version");

Now when you want to make a HTTP request, just specify the API version as a part of the header key,

GET api/values HTTP/1.1
host: localhost
x-ms-version: 1.0

Response
--------
["value1", "value2"]

And that's not all. There are other cool features available and you can find those here in this official git repository of the project,

microsoft/aspnet-api-versioning
Provides a set of libraries which add service API versioning to ASP.NET Web API, OData with ASP.NET Web API, and ASP.NET Core. - microsoft/aspnet-api-versioning