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();
});
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");
});
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");
});
Custom Header
builder.Services.AddApiVersioning(opt =>
{
opt.ApiVersionReader = new HeaderApiVersionReader("Api-Version");
});
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);
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);
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.
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.