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.
Prerequisites
Section titled “Prerequisites”- A working Granit application with at least one endpoint
- Familiarity with Minimal API endpoints
Step 1 — Install the packages
Section titled “Step 1 — Install the packages”dotnet add package Granit.Http.ApiDocumentationdotnet add package Granit.Http.ApiDocumentation.Scalar # optional: interactive UIAdd 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.
Step 2 — Configure versioning
Section titled “Step 2 — Configure versioning”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 }}| Option | Type | Default | Description |
|---|---|---|---|
MajorVersions | int[] | [1] | Major versions to route and document |
DefaultMajorVersion | int | 1 | Version assumed when client omits it (must be in MajorVersions) |
ReportApiVersions | bool | true | Adds api-supported-versions and api-deprecated-versions response headers |
Step 3 — Set up versioned route groups
Section titled “Step 3 — Set up versioned route groups”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 versionsvar apiVersionSet = app.NewApiVersionSet() .HasApiVersion(new ApiVersion(1)) .ReportApiVersions() .Build();
// Create versioned route groupvar api = app.MapGroup("api/v{version:apiVersion}") .WithApiVersionSet(apiVersionSet);
// All endpoints registered on this group inherit the versionapi.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-itemsGET /api/v1/patients/abc-123Step 4 — Add a new version
Section titled “Step 4 — Add a new version”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 v2api.MapGet("/patients/summary", GetPatientsSummary) .MapToApiVersion(2);
// Different implementations per versionapi.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.
Step 5 — Deprecate an old version
Section titled “Step 5 — Deprecate an old 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.
Deprecate individual endpoints
Section titled “Deprecate individual endpoints”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 metadataapi.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: trueSunset: Sat, 01 Nov 2026 00:00:00 GMTLink: <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.
Step 6 — MVC controller versioning
Section titled “Step 6 — MVC controller versioning”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();}Versioning strategies summary
Section titled “Versioning strategies summary”| Strategy | URL format | ISO 27001 audit trail |
|---|---|---|
| URL segment (recommended) | /api/v1/patients | Version in access logs |
| Query string (fallback) | /api/patients?api-version=1.0 | Version in access logs |
| Header (not supported) | X-Api-Version: 1.0 | Often omitted by reverse proxies |
Complete example
Section titled “Complete example”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();Next steps
Section titled “Next steps”- Add an endpoint — create endpoints with validation and error handling
- Configure multi-tenancy — add tenant headers to versioned APIs
- API & Web reference — full API documentation module details