API Versioning with Asp.Net Core Minimal APIs

API Versioning

API versioning is the practice of managing changes to an API without breaking the client’s applications when new versions are developed. Versioning is an integral part of the API design. A good API versioning strategy communicates the changes made and allows clients using an existing REST API and only migrate or update their applications to the recent versions when they are ready.

Types of API Versioning

There are many ways to version an API, each approach has its own set of strengths and weaknesses in addressing changes based on their scope. This article will cover the three following ways.

URI

This is the most natural and straightforward approach. It includes the version number in the URL path and is often done with the prefix "v".

http://www.example.com/api/v1/todos
http://api.example.com/v1/todos

Query Parameter

In this approach the version number is added as a query param making it easier to switch to the newest version in case a query parameter is not specified.

http://www.example.com/api/todos?version=1

Custom Headers

This technique adds custom headers with version numbers as an attribute. This approach differs from query and URI versioning because it doesn’t add filler content to the URI.

Accept-version: v1
Accept-version: v2

Implementation

To start the implementation first it is necessary to add the nugget package Asp.Versioning.Http. Once it is added we will be able to use the extension methods AddApiVersioning to define the strategy, WithApiVersionSet to define the version of the endpoint and HasApiVersion to define with versions are supported by the endpoint.

Configuration

The following code is creating the version numbers that will be used in the version set system.

var version1 = new ApiVersion(1);
var version2 = new ApiVersion(2);

The version set tells the API versioning which versions are available to be used by the endpoints.

var versionSet = app.NewApiVersionSet()
                     .HasApiVersion(version1)
                     .HasApiVersion(version2)
                     .Build();

URI

For this approach it is necessary to modify the route adding a new parameter to MapGet, MapPost or MapPut methods. It will be added a new segment to the URL called version that is limited to the type apiVersion. This is a validation created by the versioning system to ensure that correct values are passed.

app.MapGet("v{version:apiVersion}/todoitems", [Authorize] async (TodoDb db) =>
{
    return await db.Todos.ToListAsync();
})
.WithApiVersionSet(versionSet)
.HasApiVersions(new[] { version1, version2 });

It is also necessary to configure the type of versioning that API will use. The following code shows how.

builder.Services.AddApiVersioning(opt =>
{
    opt.ApiVersionReader = new UrlSegmentApiVersionReader();
});

Screenshot 2022-11-27 094540.png

Query Parameter

For this approach It is not necessary to change the route of the endpoint, it only adds the versioning type that will be used.

builder.Services.AddApiVersioning(opt =>
{
    opt.ApiVersionReader = new QueryStringApiVersionReader("version");
});

Screenshot 2022-11-27 095651.png

Custom Headers

This approach also does not require changing the route and it offers two ways to accomplish the versioning, using an extension to the Accept header or using a custom header.

Media Type

builder.Services.AddApiVersioning(opt =>
{
    opt.ApiVersionReader = new MediaTypeApiVersionReader("version");
});

Screenshot 2022-11-27 100751.png

Custom Header

builder.Services.AddApiVersioning(opt =>
{
    opt.ApiVersionReader = new HeaderApiVersionReader("Api-Version");
});

Screenshot 2022-11-27 101818.png

Testing

So now we can have two different versions of the same endpoint with different behaviors. In the following example the endpoint todoitems has version 1 with the old implementation and version 2 that changes the property CompletedTimestamp.

app.MapPost("v{version:apiVersion}/todoitems", [Authorize] async (IValidator<Todo> validator, Todo todo, TodoDb db) =>
{
    ValidationResult validationResult = await validator.ValidateAsync(todo);

    if (!validationResult.IsValid)
    {
        return Results.ValidationProblem(validationResult.ToDictionary());
    }

    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
})
.WithApiVersionSet(versionSet)
.HasApiVersion(version1);

Screenshot 2022-11-27 165312.png

app.MapPost("v{version:apiVersion}/todoitems", [Authorize] async (IValidator<Todo> validator, Todo todo, TodoDb db) =>
{
    ValidationResult validationResult = await validator.ValidateAsync(todo);

    if (!validationResult.IsValid)
    {
        return Results.ValidationProblem(validationResult.ToDictionary());
    }

    todo.CompletedTimestamp = DateTime.Now;
    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todo.Id}", todo);
})
.WithName("newtodoitems")
.WithApiVersionSet(versionSet)
.HasApiVersion(version2);

Screenshot 2022-11-27 165359.png

Errors

The API will return an error when a version is required and was not specified or when the endpoint does not support the version used.

Screenshot 2022-11-27 102239.png

Screenshot 2022-11-27 102215.png

When the API version is not specified, it can be defined a default version that will be used.

builder.Services.AddApiVersioning(opt =>
{
    opt.ApiVersionReader = new HeaderApiVersionReader("Api-Version");
    opt.DefaultApiVersion = version1;
    opt.AssumeDefaultVersionWhenUnspecified = true;
});

Wrapping Up

You can find the full code on my GitHub.