Skip to content

Add API versioning

Granit.ApiVersioning integrates Asp.Versioning.Mvc to provide URL-segment versioning for all API endpoints. Combined with Granit.ApiDocumentation, it generates per-version OpenAPI documents and exposes them through Scalar UI.

Terminal window
dotnet add package Granit.ApiVersioning
dotnet add package Granit.ApiDocumentation

Add the module dependencies:

using Granit.Core.Modularity;
using Granit.ApiDocumentation;
[DependsOn(typeof(GranitApiDocumentationModule))]
public sealed class MyAppHostModule : GranitModule { }

GranitApiDocumentationModule depends on GranitApiVersioningModule transitively, so you do not need to declare both.

Add the configuration to appsettings.json:

{
"ApiVersioning": {
"DefaultMajorVersion": 1,
"ReportApiVersions": true
},
"ApiDocumentation": {
"Title": "My API",
"MajorVersions": [1]
}
}
OptionTypeDefaultDescription
DefaultMajorVersionint1Version assumed when client omits it
ReportApiVersionsbooltrueAdds api-supported-versions and api-deprecated-versions response headers

Create an ApiVersionSet and a versioned route group in Program.cs:

using Asp.Versioning;
var app = builder.Build();
await app.UseGranitAsync();
app.UseAuthentication();
app.UseAuthorization();
// Declare supported API versions
var apiVersionSet = app.NewApiVersionSet()
.HasApiVersion(new ApiVersion(1))
.ReportApiVersions()
.Build();
// Create versioned route group
var api = app.MapGroup("api/v{version:apiVersion}")
.WithApiVersionSet(apiVersionSet);
// All endpoints registered on this group inherit the version
api.MapInventoryItemEndpoints();
api.MapPatientEndpoints();
// Enable Scalar UI at /scalar/v1
app.UseGranitApiDocumentation();
app.Run();

Clients access endpoints using URL-segment versioning:

GET /api/v1/inventory-items
GET /api/v1/patients/abc-123

When you need to introduce breaking changes, declare a new API version:

var apiVersionSet = app.NewApiVersionSet()
.HasApiVersion(new ApiVersion(1))
.HasApiVersion(new ApiVersion(2))
.ReportApiVersions()
.Build();
var api = app.MapGroup("api/v{version:apiVersion}")
.WithApiVersionSet(apiVersionSet);
// Available on both v1 and v2 (same behavior)
api.MapGet("/patients", GetAllPatients);
// Available only on v2
api.MapGet("/patients/summary", GetPatientsSummary)
.MapToApiVersion(2);
// Different implementations per version
api.MapGet("/patients/{id}", GetPatientV1).MapToApiVersion(1);
api.MapGet("/patients/{id}", GetPatientV2).MapToApiVersion(2);

Without .MapToApiVersion(), an endpoint is available on all versions declared in the ApiVersionSet. Use it to restrict an endpoint to a specific version.

Update the documentation to generate both OpenAPI documents:

{
"ApiDocumentation": {
"MajorVersions": [1, 2]
}
}

This generates /openapi/v1.json and /openapi/v2.json, each containing only the endpoints available on that version.

Mark a version as deprecated to signal clients they should migrate:

var apiVersionSet = app.NewApiVersionSet()
.HasApiVersion(new ApiVersion(1))
.HasDeprecatedApiVersion(new ApiVersion(1))
.HasApiVersion(new ApiVersion(2))
.ReportApiVersions()
.Build();

Clients receive the api-deprecated-versions: 1.0 response header on every v1 request, signaling that migration to v2 is expected.

For finer control, deprecate specific endpoints with RFC 8594 headers:

api.MapGet("/patients/legacy", GetLegacyPatients)
.Deprecated(
sunsetDate: "2026-11-01",
link: "https://docs.example.com/migration/v1-to-v2");

Each response from this endpoint includes:

Deprecation: true
Sunset: Sat, 01 Nov 2026 00:00:00 GMT
Link: <https://docs.example.com/migration/v1-to-v2>; rel="deprecation"

A warning is also logged for every call to a deprecated endpoint using [LoggerMessage] source-generated logging.

If your application uses MVC controllers instead of Minimal APIs:

[ApiController]
[Route("api/v{version:apiVersion}/patients")]
[ApiVersion("1.0")]
public sealed class PatientController : ControllerBase
{
[HttpGet]
public IActionResult GetAll() => Ok();
}
[ApiController]
[Route("api/v{version:apiVersion}/patients")]
[ApiVersion("2.0")]
public sealed class PatientV2Controller : ControllerBase
{
[HttpGet]
public IActionResult GetAll() => Ok();
}
StrategyURL formatISO 27001 audit trail
URL segment (recommended)/api/v1/patientsVersion in access logs
Query string (fallback)/api/patients?api-version=1.0Version in access logs
Header (not supported)X-Api-Version: 1.0Often omitted by reverse proxies
using Asp.Versioning;
using Granit.Core.Extensions;
using MyApp.Host;
var builder = WebApplication.CreateBuilder(args);
await builder.AddGranitAsync<MyAppHostModule>();
var app = builder.Build();
await app.UseGranitAsync();
app.UseAuthentication();
app.UseAuthorization();
var apiVersionSet = app.NewApiVersionSet()
.HasApiVersion(new ApiVersion(1))
.HasApiVersion(new ApiVersion(2))
.HasDeprecatedApiVersion(new ApiVersion(1))
.ReportApiVersions()
.Build();
var api = app.MapGroup("api/v{version:apiVersion}")
.WithApiVersionSet(apiVersionSet);
api.MapGet("/patients", GetAllPatients);
api.MapGet("/patients/{id}", GetPatientV1).MapToApiVersion(1);
api.MapGet("/patients/{id}", GetPatientV2).MapToApiVersion(2);
api.MapGet("/patients/summary", GetPatientsSummary).MapToApiVersion(2);
app.UseGranitApiDocumentation();
app.MapHealthChecks("/healthz");
app.Run();