If you've maintained a versioned ASP.NET Core API for any length of time, you know the dance. Register Swashbuckle. Wire up IConfigureOptions<SwaggerGenOptions>. Loop through ApiVersionDescription to generate a document per version. Register each document name manually. Hope the version formats line up. It worked, but it always felt like you were bolting two systems together with string conventions and hoping they agreed.
.NET 10 changed the OpenAPI story entirely by shipping Microsoft.AspNetCore.OpenApi as a built-in, first-party library that generates OpenAPI 3.1 documents without third-party dependencies. And with the release of Asp.Versioning v10.0.0 in April 2026, the versioning library finally speaks the same language. The result is a setup that takes three method calls where it previously took thirty lines of configuration.
What changed and why it matters
Prior to .NET 10, versioned OpenAPI documentation required a coordination act between three separate libraries: Asp.Versioning for the versioning infrastructure, Swashbuckle for OpenAPI document generation, and a configuration bridge (typically a custom IConfigureOptions<SwaggerGenOptions> implementation) to make the two agree on document names. The bridge was the fragile part — it had to enumerate API versions at startup, register a Swagger document for each one, and match the group name format exactly.
.NET 10's built-in OpenAPI library removed Swashbuckle from the equation. But for the first few months, versioning integration was a gap. Developers had to call builder.Services.AddOpenApi("v1") and builder.Services.AddOpenApi("v2") manually for every version — hardcoding the very thing the versioning library was supposed to manage.
Asp.Versioning v10.0.0 closes that gap with a new Asp.Versioning.OpenApi package that plugs directly into the built-in OpenAPI pipeline. One call replaces the manual registration:
builder.Services.AddApiVersioning()
.AddApiExplorer(options => options.GroupNameFormat = "'v'VVV")
.AddOpenApi();
var app = builder.Build();
app.MapOpenApi().WithDocumentPerVersion();
WithDocumentPerVersion() is the key addition. It inspects the registered API versions at startup and generates a separate OpenAPI document for each one — no loops, no manual document names, no format mismatches.
Setting up from scratch
Start with a .NET 10 minimal API project and add the versioning packages:
dotnet add package Asp.Versioning.Http
dotnet add package Asp.Versioning.Mvc.ApiExplorer
dotnet add package Asp.Versioning.OpenApi --version 10.0.0-rc.1
// NOTE
At the time of writing, Asp.Versioning.OpenApi is at RC1 status. The package is stable for production use but awaits a final upstream dependency alignment before the 10.0.0 stable label ships.
Configure the services and endpoints:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
})
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
})
.AddOpenApi();
var app = builder.Build();
var catalogue = app.NewVersionedApi("Catalogue");
var v1 = catalogue.MapGroup("/api/products")
.HasApiVersion(1.0);
var v2 = catalogue.MapGroup("/api/products")
.HasApiVersion(2.0);
v1.MapGet("/", () => TypedResults.Ok(new[]
{
new { Id = 1, Name = "Mechanical Keyboard" }
}));
v2.MapGet("/", () => TypedResults.Ok(new[]
{
new { Id = 1, Name = "Mechanical Keyboard", Sku = "KB-MX-001", InStock = true }
}));
app.MapOpenApi().WithDocumentPerVersion();
app.Run();
Hitting /openapi/v1.json returns a document containing only the v1 endpoints. /openapi/v2.json returns v2. No manual registration, no document naming collisions.
Adding a documentation UI
The built-in OpenAPI library generates the JSON specification but doesn't ship a visual explorer. You have two solid options.
Scalar is the recommended choice for new projects — it's modern, supports OpenAPI 3.1 natively, and requires minimal configuration:
app.MapScalarApiReference(options =>
{
var descriptions = app.DescribeApiVersions();
for (var i = 0; i < descriptions.Count; i++)
{
var description = descriptions[i];
options.AddDocument(
description.GroupName,
description.GroupName,
isDefault: i == descriptions.Count - 1);
}
});
Swagger UI still works if your team prefers it. Point the endpoint configuration at the versioned JSON paths:
app.UseSwaggerUI(options =>
{
foreach (var description in app.DescribeApiVersions().Reverse())
{
options.SwaggerEndpoint(
$"/openapi/{description.GroupName}.json",
description.GroupName.ToUpperInvariant());
}
});
Both approaches use DescribeApiVersions() to enumerate versions dynamically. No hardcoded version strings required.
Controller-based APIs
The same integration works for controllers. The package set is slightly different:
dotnet add package Asp.Versioning.Mvc
dotnet add package Asp.Versioning.Mvc.ApiExplorer
dotnet add package Asp.Versioning.OpenApi --version 10.0.0-rc.1
The service registration gains an AddMvc() call:
builder.Services.AddApiVersioning()
.AddMvc()
.AddApiExplorer(options => options.GroupNameFormat = "'v'VVV")
.AddOpenApi();
Controllers declare their versions through attributes as before:
[ApiController]
[Route("api/orders")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class OrdersController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
public IActionResult GetV1() =>
Ok(new[] { new { Id = 1, Total = 49.99m } });
[HttpGet]
[MapToApiVersion("2.0")]
public IActionResult GetV2() =>
Ok(new[] { new { Id = 1, Total = 49.99m, Currency = "GBP", Status = "Confirmed" } });
}
Sunset and deprecation policies
One of the most underused features in the Asp.Versioning library is its policy engine, and v10.0.0 makes it considerably more powerful. You can now define both sunset policies (RFC 8594) and deprecation policies (RFC 9745), and the OpenAPI integration surfaces them automatically.
Sunset policies
A sunset policy tells consumers when an API version will stop being available:
builder.Services.AddApiVersioning(options =>
{
options.ReportApiVersions = true;
options.Policies.Sunset(1.0)
.Effective(2026, 9, 1)
.Link("https://api.example.com/docs/versioning-policy")
.Title("Versioning Policy")
.Type("text/html");
})
.AddApiExplorer(options => options.GroupNameFormat = "'v'VVV")
.AddOpenApi();
Responses from v1 endpoints will include a Sunset header:
Sunset: Tue, 01 Sep 2026 00:00:00 GMT
Link: <https://api.example.com/docs/versioning-policy>; rel="sunset"; title="Versioning Policy"; type="text/html"
Deprecation policies
New in v10.0.0, deprecation policies use the RFC 9745 Deprecation header to signal that a version is on its way out but still functional:
options.Policies.Deprecate(0.9)
.Effective(DateTimeOffset.UtcNow)
.Link("https://api.example.com/docs/migration-guide")
.Title("Migration Guide")
.Type("text/html");
When the Asp.Versioning.OpenApi package generates documents, sunset and deprecation policy details are embedded as an x-api-versioning extension, making the lifecycle information available to any tooling that reads the OpenAPI specification.
Marking versions as deprecated
The attribute-level approach also still works and is simpler for basic cases:
[ApiController]
[Route("api/search")]
[ApiVersion("1.0", Deprecated = true)]
[ApiVersion("2.0")]
public class SearchController : ControllerBase
{
// v1 endpoints still function but are marked deprecated in documentation
}
For minimal APIs, the equivalent is HasDeprecatedApiVersion:
var search = api.MapGroup("/api/search")
.HasDeprecatedApiVersion(1.0)
.HasApiVersion(2.0);
Date-based versioning
Asp.Versioning has always supported date-based version formats (e.g., 2026-04-01), but declaring them in attributes required string parsing:
// Bad — string-based date versioning in v8.x
[ApiVersion("2026-04-01")]
public class EventsController : ControllerBase { }
Version 10.0.0 introduces typed date constructors for all version attributes:
// Good — typed date versioning in v10.0.0
[ApiVersion(2026, 4, 1)]
public class EventsController : ControllerBase { }
MapToVersionAttribute and AdvertiseApiVersionsAttribute gain the same constructors. The typed approach catches invalid dates at compile time rather than failing at runtime with a FormatException.
Breaking changes to watch for
Asp.Versioning v10.0.0 aligns its major version with .NET 10's LTS status, and the team took the opportunity to make some long-overdue breaking changes.
Generic policy management. The sunset policy types have been generalised. ISunsetPolicyManager is now IPolicyManager<SunsetPolicy>, and related extension types follow the same pattern. If you wrote custom policy managers, update your implementations.
Extension members replace extension methods. Several GetApiVersion() extension methods have been converted to C# 14 extension properties (ApiVersion { get; }). This affects code that calls GetApiVersion() on HttpRequest, HttpContext, or similar types — the method calls need to become property accesses.
OpenAPI enum format. Versioned parameters in OpenAPI documents now use "enum": ["1.0"] instead of "default": "1.0". This bounds version values to a fixed set rather than allowing free-form input. If you have clients that generate code from your OpenAPI spec, verify they handle the enum representation correctly.
Migrating from Swashbuckle
If you're moving from a Swashbuckle-based setup, the migration is straightforward but involves removing more code than you add.
Remove the IConfigureOptions<SwaggerGenOptions> implementation that looped through IApiVersionDescriptionProvider. Remove the AddSwaggerGen() call. Remove UseSwagger() from the middleware pipeline.
Replace with:
builder.Services.AddApiVersioning()
.AddApiExplorer(options => options.GroupNameFormat = "'v'VVV")
.AddOpenApi();
var app = builder.Build();
app.MapOpenApi().WithDocumentPerVersion();
If you were using Swagger UI, you can keep Swashbuckle.AspNetCore.SwaggerUI and point it at the new endpoint paths (/openapi/{version}.json). Or switch to Scalar for a cleaner experience with OpenAPI 3.1 documents.
// TIP
The GroupNameFormat value "'v'VVV" produces group names like v1, v1.1, v2. The literal 'v' prefix must match whatever your OpenAPI document URLs expect. If your existing Swashbuckle setup used a different format, align the GroupNameFormat with your new URL pattern.
Common pitfalls
Using the wrong AddOpenApi overload. Both Microsoft.AspNetCore.OpenApi and Asp.Versioning.OpenApi expose an AddOpenApi() extension method. If you import both namespaces, the compiler may resolve to the wrong one. Ensure you're calling the one from the Asp.Versioning namespace by checking the return type chains from AddApiVersioning().
Forgetting AddApiExplorer. The OpenAPI integration depends on the API explorer to discover versioned endpoints. Without AddApiExplorer(), the documents will generate but won't contain your versioned operations.
Mismatched GroupNameFormat and document URLs. If GroupNameFormat produces v1.0 but your Scalar or Swagger UI configuration expects v1, the documents won't load. Use "'v'VVV" for v1, v1.1, v2 or "'v'VVVV" for v1.0, v1.1, v2.0 — pick one and stay consistent.
Not calling WithDocumentPerVersion. Calling app.MapOpenApi() without WithDocumentPerVersion() generates a single document for all versions. This is valid for simple APIs but defeats the purpose of version-specific documentation. The single-document mode merges all versions into one specification, which confuses code generators and makes deprecation notices meaningless.
Hardcoding version lists in OpenAPI setup. If you find yourself writing AddOpenApi("v1") and AddOpenApi("v2"), you're bypassing the versioning integration entirely. The whole point of Asp.Versioning.OpenApi is to derive the document list from the registered versions.
Summary
- Asp.Versioning v10.0.0 ships a new
Asp.Versioning.OpenApipackage that integrates directly with .NET 10's built-inMicrosoft.AspNetCore.OpenApilibrary. WithDocumentPerVersion()replaces the manual per-versionAddOpenApi()calls and the Swashbuckle configuration bridge.- Sunset policies (RFC 8594) and deprecation policies (RFC 9745) are now first-class citizens, with automatic OpenAPI extension generation.
- Date-based version attributes gain typed constructors —
[ApiVersion(2026, 4, 1)]instead of[ApiVersion("2026-04-01")]. - Several breaking changes affect custom policy managers and code using the
GetApiVersion()extension method, which is now an extension property. - Migration from Swashbuckle involves removing configuration code rather than replacing it — the new setup is significantly simpler.