Learn API versioning in ASP.NET Core with real code examples — URL, header, and query string strategies. Build APIs that never break clients. Start now!
API versioning in ASP.NET Core is one of those topics every backend developer eventually runs into — usually the hard way. You ship an API, mobile apps and third-party clients start depending on it, and then the business asks for a change that would break every one of them. Versioning is how you evolve your API without burning down the integrations built on top of it. In this guide, you'll learn the four main API versioning strategies, how to implement each one in ASP.NET Core using the official Asp.Versioning packages, how to version Minimal APIs, and the best practices (and pitfalls) that separate a maintainable API from a support nightmare.
Why API Versioning Matters
An API is a contract. Once a client — a React frontend, an iOS app, a partner's integration — starts consuming it, you no longer fully own its shape. Some changes are safe (adding a new optional field to a response), but many are not:
- Renaming or removing a property in a response
- Changing a data type (e.g.,
intID toGuid) - Making a previously optional request field required
- Changing status codes or error formats
- Restructuring resource URLs
Without versioning, every breaking change forces all clients to update simultaneously — which is impossible when you don't control the clients. With versioning, v1 keeps working exactly as it always did while v2 introduces the new behavior. Clients migrate on their own schedule, and you retire v1 when telemetry shows traffic has drained.
The why is important here: versioning isn't about being able to change your API — you can always do that. It's about decoupling your release schedule from your consumers' release schedules. That's the whole game.
Setting Up API Versioning in ASP.NET Core
The old Microsoft.AspNetCore.Mvc.Versioning package is deprecated. The maintained successor lives under the Asp.Versioning namespace (the "ASP.NET API Versioning" project). Install the packages that match your API style:
// For controller-based APIs
dotnet add package Asp.Versioning.Mvc
dotnet add package Asp.Versioning.Mvc.ApiExplorer
// For Minimal APIs
dotnet add package Asp.Versioning.Http
Then register versioning in Program.cs:
using Asp.Versioning;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true; // adds api-supported-versions response header
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(),
new HeaderApiVersionReader("X-Api-Version"),
new QueryStringApiVersionReader("api-version"));
})
.AddMvc()
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV"; // v1, v2, v2.1
options.SubstituteApiVersionInUrl = true; // replaces {version:apiVersion} in routes
});
var app = builder.Build();
app.MapControllers();
app.Run();
Three options here do a lot of heavy lifting:
- ReportApiVersions adds
api-supported-versionsandapi-deprecated-versionsheaders to every response, so clients can discover what's available. - AssumeDefaultVersionWhenUnspecified keeps old clients (who predate your versioning) working by routing them to a default version.
- ApiVersionReader.Combine lets clients specify the version via URL, header, or query string — useful during migrations.
The Four API Versioning Strategies
1. URL Path Versioning (Most Popular)
The version lives in the route: GET /api/v1/products. This is the most widely used strategy — it's what GitHub, Twitter/X, and Stripe-style APIs expose publicly, and what most developers expect.
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
public IActionResult GetV1()
=> Ok(new[] { new { Id = 1, Name = "Keyboard" } });
[HttpGet]
[MapToApiVersion("2.0")]
public IActionResult GetV2()
=> Ok(new[] { new { Id = 1, Name = "Keyboard", Price = 49.99m, Currency = "USD" } });
}
Pros: Explicit, visible in logs and browser, cache-friendly, trivially testable with curl. Cons: Purists argue the URL should identify a resource, not a version of it — /v1/products/1 and /v2/products/1 are the same product at two different URLs.
2. Query String Versioning
The version is passed as a parameter: GET /api/products?api-version=2.0. This is the default in the Asp.Versioning library and is common in Azure REST APIs.
[ApiController]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
public IActionResult GetV1() => Ok("Orders v1");
[HttpGet]
[MapToApiVersion("2.0")]
public IActionResult GetV2() => Ok("Orders v2");
}
Pros: URLs stay stable; easy to default when omitted. Cons: Easy for clients to forget; query strings sometimes get stripped by proxies or excluded from cache keys.
3. Header Versioning
The version travels in a custom request header: X-Api-Version: 2.0. The URL stays completely clean.
builder.Services.AddApiVersioning(options =>
{
options.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");
});
Pros: RESTfully "pure" — one URL per resource. Cons: Invisible in browser testing and basic logs; harder for consumers to discover; you can't just paste a link to demonstrate v2 behavior.
4. Media Type Versioning (Content Negotiation)
The version rides inside the Accept header: Accept: application/json;v=2.0. This is the most HTTP-idiomatic approach — you're negotiating a representation, which is exactly what content negotiation is for.
builder.Services.AddApiVersioning(options =>
{
options.ApiVersionReader = new MediaTypeApiVersionReader("v");
});
Pros: Theoretically correct REST. Cons: Highest friction for consumers, hardest to debug, weakest tooling support. In practice, few public APIs choose this unless they have strong hypermedia requirements.
Which should you pick? For public APIs, URL path versioning wins on discoverability and developer experience. For internal microservices where you control both sides, header or query string versioning keeps routes simpler. Whatever you choose, choose one primary strategy and be consistent across your whole API surface.
API Versioning in ASP.NET Core Minimal APIs
Since .NET 7, the Asp.Versioning.Http package supports Minimal APIs through version sets. Here's a complete, runnable example:
using Asp.Versioning;
using Asp.Versioning.Builder;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.ReportApiVersions = true;
options.ApiVersionReader = new UrlSegmentApiVersionReader();
});
var app = builder.Build();
ApiVersionSet versionSet = app.NewApiVersionSet()
.HasApiVersion(new ApiVersion(1, 0))
.HasApiVersion(new ApiVersion(2, 0))
.ReportApiVersions()
.Build();
app.MapGet("api/v{version:apiVersion}/weather", () =>
new { Summary = "Sunny" })
.WithApiVersionSet(versionSet)
.MapToApiVersion(new ApiVersion(1, 0));
app.MapGet("api/v{version:apiVersion}/weather", () =>
new { Summary = "Sunny", TemperatureC = 31, TemperatureF = 88 })
.WithApiVersionSet(versionSet)
.MapToApiVersion(new ApiVersion(2, 0));
app.Run();
Requesting /api/v1/weather returns the lean v1 payload; /api/v2/weather returns the richer shape. The version set is defined once and shared by every endpoint in the group, which keeps the supported-version list in a single place.
Deprecating Old Versions Gracefully
Versioning is only half the story — the other half is retiring versions. Mark a version as deprecated and ASP.NET Core will advertise it:
[ApiController]
[ApiVersion("1.0", Deprecated = true)]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class CustomersController : ControllerBase
{
// ...
}
With ReportApiVersions = true, responses now include:
api-supported-versions: 2.0
api-deprecated-versions: 1.0
A good deprecation lifecycle looks like this: announce the sunset date in your docs and changelog, emit the deprecation headers, monitor traffic per version (log HttpContext.GetRequestedApiVersion() to your metrics system), contact the remaining heavy users, and only then return 410 Gone. Deleting a version while it still serves meaningful traffic is how you end up on a partner's incident report.
Documenting Versions with Swagger / OpenAPI
Multiple versions need multiple Swagger documents. The AddApiExplorer call you registered earlier groups endpoints by version; then you emit one OpenAPI doc per group:
builder.Services.AddSwaggerGen();
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI(options =>
{
var provider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerEndpoint(
$"/swagger/{description.GroupName}/swagger.json",
description.GroupName.ToUpperInvariant());
}
});
Swagger UI now shows a dropdown — V1, V2 — so consumers can browse each contract independently. This matters more than it sounds: undocumented versions effectively don't exist to your consumers.
Best Practices for API Versioning
- Version from day one. Retrofitting versioning onto an unversioned API is a breaking change in itself. Shipping
/api/v1/...on launch day costs nothing. - Version only on breaking changes. Adding a new optional response field or a new endpoint doesn't need a new version. Minting v3, v4, v5 for additive changes multiplies your maintenance surface for no benefit.
- Version the whole API surface, not one endpoint at a time. A client on "v2" should be able to assume every endpoint speaks v2. Mixed per-endpoint version matrices are miserable to document and consume.
- Keep old versions thin. Don't fork your business logic per version. Route both versions to the same service layer and map to version-specific DTOs at the edge — the controller/endpoint layer is the versioned part, not your domain.
- Support at most two or three live versions. Every live version is a permanent test and support obligation. Aggressively drain and retire.
- Measure usage per version. You can't deprecate what you can't measure. Tag your logs and metrics with the requested API version.
Common Pitfalls to Avoid
- Using the deprecated package.
Microsoft.AspNetCore.Mvc.Versioningis no longer maintained; useAsp.Versioning.*. - Forgetting
SubstituteApiVersionInUrl. Without it, Swagger shows a literal{version}parameter in every route instead of concretev1/v2paths. - Duplicating logic per version. If fixing one bug means fixing it in three controllers, your versioning boundary is in the wrong layer.
- Breaking v1 while building v2. Your v1 integration tests must keep running unchanged. If a v2 refactor makes v1 tests fail, v1 clients are about to fail too.
- No default version policy. Decide explicitly what happens when a client sends no version — default to the oldest supported version (safest for legacy clients) and document it.
Conclusion: Key Takeaways
API versioning in ASP.NET Core is straightforward to implement and painful to skip. The Asp.Versioning packages give you first-class support for all four strategies — URL path, query string, header, and media type — across both controllers and Minimal APIs, with deprecation reporting and Swagger integration built in.
- Version from day one; it's free at launch and expensive later.
- URL path versioning is the pragmatic default for public APIs; header/query string work well internally.
- Only breaking changes deserve a new version — keep the live-version count low.
- Version at the edge (DTOs and routes), share the business logic underneath.
- Deprecate with headers, telemetry, and a published sunset date — never by surprise.
Get these fundamentals right and your API can evolve for years while the apps built on it keep working — which is exactly what a good contract is supposed to do. Now open your Program.cs, add AddApiVersioning, and ship /api/v1 before your first external client shows up.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment