| name | versioning |
| description | Use when adding API versioning or managing multiple API versions in a .NET project. |
| metadata | {"category":"api","agent":"api-designer"} |
| when_to_use | When configuring API versioning strategies or sunset policies |
API Versioning
Core Principles
- Version APIs from the start — retrofitting is painful
- URL segment versioning (
/api/v1/orders) is the most common and recommended
- Report available and deprecated versions via response headers
- Maintain backward compatibility within a version
- Use sunset policies to communicate deprecation timelines
Patterns
URL Segment Versioning (Recommended)
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = new UrlSegmentApiVersionReader();
})
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
Controller Versioning
[ApiController]
[Route("api/v{version:apiVersion}/orders")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public sealed class OrdersController(ISender sender) : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
public async Task<ActionResult<List<OrderV1Response>>> GetOrdersV1(
CancellationToken ct)
{
var result = await sender.Send(new ListOrdersV1Query(), ct);
return Ok(result);
}
[HttpGet]
[MapToApiVersion("2.0")]
public async Task<ActionResult<PagedList<OrderV2Response>>> GetOrdersV2(
[FromQuery] OrderFilter filter, CancellationToken ct)
{
var result = await sender.Send(new ListOrdersV2Query(filter), ct);
return Ok(result);
}
}
[ApiController]
[Route("api/v{version:apiVersion}/orders")]
[ApiVersion("1.0", Deprecated = true)]
public sealed class OrdersV1Controller : ControllerBase { }
[ApiController]
[Route("api/v{version:apiVersion}/orders")]
[ApiVersion("2.0")]
public sealed class OrdersV2Controller : ControllerBase { }
Minimal API Versioning
var versionSet = app.NewApiVersionSet()
.HasApiVersion(new ApiVersion(1, 0))
.HasApiVersion(new ApiVersion(2, 0))
.ReportApiVersions()
.Build();
var v1 = app.MapGroup("/api/v{version:apiVersion}")
.WithApiVersionSet(versionSet);
v1.MapGet("/orders", GetOrdersV1)
.MapToApiVersion(new ApiVersion(1, 0));
v1.MapGet("/orders", GetOrdersV2)
.MapToApiVersion(new ApiVersion(2, 0));
Header Versioning (Alternative)
builder.Services.AddApiVersioning(options =>
{
options.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");
});
Query String Versioning (Alternative)
builder.Services.AddApiVersioning(options =>
{
options.ApiVersionReader = new QueryStringApiVersionReader("api-version");
});
Combined Version Reader
builder.Services.AddApiVersioning(options =>
{
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(),
new HeaderApiVersionReader("X-Api-Version"),
new QueryStringApiVersionReader("api-version"));
});
Sunset Policy
builder.Services.AddApiVersioning(options =>
{
options.Policies.Sunset(1.0)
.Effective(new DateTimeOffset(2025, 6, 1, 0, 0, 0, TimeSpan.Zero))
.Link("https://docs.{Company}.com/api/migration-guide")
.Title("v1 to v2 Migration Guide")
.Type("text/html");
});
OpenAPI Document Per Version
builder.Services.AddOpenApi("v1");
builder.Services.AddOpenApi("v2");
app.MapOpenApi();
Anti-Patterns
- Not versioning from the start (forces breaking changes on clients)
- Using version in the response body instead of URL/header
- Major behavior changes within the same version
- Forgetting to deprecate old versions
- Different versioning strategies across the same API
Detect Existing Patterns
- Search for
Asp.Versioning package reference in .csproj
- Look for
AddApiVersioning in Program.cs
- Check for
[ApiVersion] attributes on controllers
- Look for
v{version:apiVersion} in route templates
- Check for version query parameters in existing URLs
Adding to Existing Project
- Install
Asp.Versioning.Http (minimal API) or Asp.Versioning.Mvc.ApiExplorer (controllers)
- Configure
AddApiVersioning with default version and reader
- Add
[ApiVersion] to existing controllers (start with "1.0")
- Update routes to include version segment
- Create v2 endpoints for new behavior; keep v1 for backward compat
- Add sunset policies for deprecated versions
Decision Guide
| Strategy | Pros | Cons | Use When |
|---|
| URL segment | Visible, cacheable, simple | Version in URL | Default choice |
| Header | Clean URLs | Hidden, harder to test | Internal APIs |
| Query string | Easy to add | Pollutes query params | Legacy compat |
References