.NET & C# Development · Lesson 17 of 92
Version Your API So You Never Break Clients Again
Why Versioning Matters
Shipping a breaking change to /api/products will take down every client you have. API versioning lets you evolve your contract while existing consumers keep working on the version they were built against.
Install the package first:
dotnet add package Asp.Versioning.Mvc
dotnet add package Asp.Versioning.Mvc.ApiExplorerConfigure Versioning in Program.cs
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true; // adds api-supported-versions header
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(),
new HeaderApiVersionReader("X-Api-Version"),
new QueryStringApiVersionReader("api-version")
);
})
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});ReportApiVersions = true adds api-supported-versions: 1.0, 2.0 and api-deprecated-versions: 1.0 response headers — clients can programmatically detect when to upgrade.
URL Segment Versioning
This is the most visible and the easiest to test in a browser.
[ApiController]
[Route("api/v{version:apiVersion}/products")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class ProductsController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
public IActionResult GetV1()
{
return Ok(new { version = "1.0", products = new[] { "Widget A" } });
}
[HttpGet]
[MapToApiVersion("2.0")]
public IActionResult GetV2()
{
// V2 returns richer payload
return Ok(new
{
version = "2.0",
products = new[]
{
new { id = 1, name = "Widget A", sku = "WGT-001", inStock = true }
}
});
}
}Routes: GET /api/v1/products and GET /api/v2/products.
Header Versioning
No URL pollution — useful for internal APIs where you control all clients.
GET /api/products
X-Api-Version: 2.0The reader is already registered above via HeaderApiVersionReader("X-Api-Version"). No controller changes needed — the version reader does the dispatch.
Query String Versioning
GET /api/products?api-version=2.0Registered via QueryStringApiVersionReader("api-version"). Good for quick testing and webhooks where headers are awkward.
Deprecating an Old Version
Mark the version deprecated on the controller. Clients still get responses — nothing breaks — but the api-deprecated-versions header tells them to migrate.
[ApiController]
[Route("api/v{version:apiVersion}/orders")]
[ApiVersion("1.0", Deprecated = true)]
[ApiVersion("2.0")]
public class OrdersController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
public IActionResult GetV1() => Ok(new { warning = "v1 is deprecated, migrate to v2" });
[HttpGet]
[MapToApiVersion("2.0")]
public IActionResult GetV2() => Ok(new { orders = Array.Empty<object>() });
}Response headers on a v1 call:
api-supported-versions: 1.0, 2.0
api-deprecated-versions: 1.0Version Sets for Minimal API
Controller-based versioning does not apply to Minimal API endpoints. Use ApiVersionSet instead.
var versionSet = app.NewApiVersionSet()
.HasApiVersion(new ApiVersion(1, 0))
.HasApiVersion(new ApiVersion(2, 0))
.ReportApiVersions()
.Build();
app.MapGet("/api/v{version:apiVersion}/inventory", (ApiVersion version) =>
{
return version.MajorVersion == 1
? Results.Ok(new { items = 42 })
: Results.Ok(new { items = 42, lastUpdated = DateTime.UtcNow });
})
.WithApiVersionSet(versionSet)
.MapToApiVersion(1, 0)
.MapToApiVersion(2, 0);Versioned OpenAPI Docs
Wire up Swagger to generate a separate spec per version:
// Program.cs
var descriptions = app.DescribeApiVersions();
app.UseSwagger();
app.UseSwaggerUI(options =>
{
foreach (var description in descriptions)
{
options.SwaggerEndpoint(
$"/swagger/{description.GroupName}/swagger.json",
description.GroupName.ToUpperInvariant());
}
});Register AddSwaggerGen to pick up the version groups:
builder.Services.AddSwaggerGen(options =>
{
var provider = builder.Services.BuildServiceProvider()
.GetRequiredService<IApiVersionDescriptionProvider>();
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, new OpenApiInfo
{
Title = "My API",
Version = description.ApiVersion.ToString(),
Description = description.IsDeprecated
? "This version is deprecated. Migrate to the latest."
: null
});
}
});This gives you /swagger/v1/swagger.json and /swagger/v2/swagger.json — separate, accurate specs per version.
Key Rules
- Start versioning on day one — retrofitting it is painful.
- Never remove a version without a sunset period and
api-deprecated-versionsheader in place. - Keep V1 and V2 controllers in separate folders (
Controllers/V1/,Controllers/V2/) to avoid a single massive file. - Only version at the API boundary. Your domain and application layers should not know about API versions.