Skip to content

Add API Versioning — Asp.Versioning Setup

Granit.Http.ApiDocumentation integrates Asp.Versioning.Mvc to provide URL-segment versioning for all API endpoints and generates one OpenAPI document per declared major version. Add the optional Granit.Http.ApiDocumentation.Scalar companion to expose them through the Scalar UI.

Terminal window
dotnet add package Granit.Http.ApiDocumentation
dotnet add package Granit.Http.ApiDocumentation.Scalar # optional: interactive UI

Add the module dependency:

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

GranitHttpApiDocumentationScalarModule depends on GranitHttpApiDocumentationModule, so a host that wants the UI declares only the Scalar module. Headless hosts depend on GranitHttpApiDocumentationModule directly.

Versioning and documentation share a single configuration section — Http:ApiDocumentation:MajorVersions drives both the route constraint and the per-version documents:

{
"Http:ApiDocumentation": {
"Title": "My API",
"MajorVersions": [1],
"DefaultMajorVersion": 1,
"ReportApiVersions": true
}
}
OptionTypeDefaultDescription
MajorVersionsint[][1]Major versions to route and document
DefaultMajorVersionint1Version assumed when client omits it (must be in MajorVersions)
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();
// Scalar package: maps /openapi/v1.json + the Scalar UI.
// Headless hosts call app.MapGranitOpenApiDocuments() instead.
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:

{
"Http: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. Attaching the metadata is all it takes — the response middleware is auto-registered by the module, and the OpenAPI operation is marked deprecated: true:

api.MapGet("/patients/legacy", GetLegacyPatients)
.Deprecated(
sunsetDate: new DateOnly(2026, 11, 1),
link: "https://docs.example.com/migration/v1-to-v2");
// Equivalent — .Deprecated() only attaches this metadata
api.MapGet("/patients/legacy-export", ExportLegacyPatients)
.WithMetadata(new DeprecatedAttribute
{
SunsetDate = new DateOnly(2026, 11, 1),
Link = "https://docs.example.com/migration/v1-to-v2",
});

Each response from these endpoints includes:

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

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.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();