Learn API versioning in ASP.NET Core with practical examples. Master URL, header & query string strategies plus best practices. Start building versioned APIs now!
API versioning in ASP.NET Core is one of those topics every backend developer eventually runs into — usually the moment a mobile app, a third-party partner, or another team starts depending on an endpoint you need to change. Once an API is public, you cannot simply rename a field or remove a property without breaking someone. That is exactly the problem versioning solves. In this complete guide, you will learn the main API versioning strategies, how to implement each one with the official Asp.Versioning libraries, and the best practices that keep your Web API maintainable for years.
Whether you are a beginner searching for "how to version an API", an intermediate developer comparing best practices, or a senior architect weighing advanced trade-offs, this tutorial walks through runnable C# examples and explains the why behind every decision.
Why API Versioning in ASP.NET Core Matters
An API is a contract. Consumers — web front-ends, mobile clients, partner integrations, and internal microservices — write code against the shape of your responses and the behavior of your endpoints. The instant you ship a breaking change, every one of those consumers can fail. Common breaking changes include:
- Renaming or removing a JSON property.
- Changing a data type (for example, turning a
stringstatus into anenuminteger). - Adding a required request parameter.
- Changing the meaning of an existing field or status code.
Versioning lets you introduce these changes under a new version (v2) while keeping the old version (v1) running for clients that have not migrated yet. This is the core reason API versioning in ASP.NET Core is considered a fundamental production practice rather than an optional extra. It buys you the freedom to evolve without coordinating a synchronized release across every consumer on the planet.
What Counts as a Non-Breaking Change?
Not every change needs a new version. Adding a new optional field, a new endpoint, or a new optional query parameter is typically backward compatible — existing clients ignore what they do not understand. A good rule of thumb: add, don't mutate. Reserve new versions for changes that would break a well-behaved client.
Installing the Asp.Versioning Library
The modern, officially supported package is Asp.Versioning.Mvc (the successor to the old Microsoft.AspNetCore.Mvc.Versioning). For controller-based APIs, install both the core MVC package and the API Explorer package, which integrates versioning with Swagger/OpenAPI.
// Install via the .NET CLI
// dotnet add package Asp.Versioning.Mvc
// dotnet add package Asp.Versioning.Mvc.ApiExplorer
// Program.cs — register API versioning services
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddApiVersioning(options =>
{
// Assume v1.0 when the client doesn't specify a version
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
// Advertise supported versions in the "api-supported-versions" response header
options.ReportApiVersions = true;
})
.AddApiExplorer(options =>
{
// Formats the version as "'v'major[.minor]" e.g. v1, v2
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
var app = builder.Build();
app.MapControllers();
app.Run();
Those three options — DefaultApiVersion, AssumeDefaultVersionWhenUnspecified, and ReportApiVersions — are the foundation. ReportApiVersions in particular is a small touch that pays off: it tells consumers which versions exist and which are deprecated, directly in the response headers.
API Versioning Strategies in ASP.NET Core
There are four mainstream API versioning strategies, and the Asp.Versioning library supports all of them. Let's look at each with code, then compare trade-offs.
1. URL Path Versioning (Most Popular)
URL path versioning puts the version directly in the route: /api/v1/products. It is the most common and most discoverable approach — the version is visible in the browser address bar, in logs, and in every curl command.
[ApiController]
[ApiVersion(1.0)]
[ApiVersion(2.0)]
[Route("api/v{version:apiVersion}/products")]
public class ProductsController : ControllerBase
{
[HttpGet]
[MapToApiVersion(1.0)]
public IActionResult GetV1()
{
return Ok(new { Id = 1, Name = "Keyboard" });
}
[HttpGet]
[MapToApiVersion(2.0)]
public IActionResult GetV2()
{
// v2 adds a Price field and renames Name -> Title
return Ok(new { Id = 1, Title = "Keyboard", Price = 49.99 });
}
}
The {version:apiVersion} route constraint binds the URL segment to the version, and MapToApiVersion routes each request to the correct method. A call to /api/v1/products hits GetV1; /api/v2/products hits GetV2.
2. Query String Versioning
Query string versioning keeps a clean base URL and selects the version with a parameter: /api/products?api-version=2.0. It is easy to test in a browser and requires no route changes.
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
// Read the version from a query string parameter named "api-version"
options.ApiVersionReader = new QueryStringApiVersionReader("api-version");
});
3. HTTP Header Versioning
Header versioning keeps the URL completely clean by moving the version into a custom request header such as X-Api-Version. Purists like this because the resource identifier (the URL) stays stable while the representation is negotiated separately.
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
// Clients send: X-Api-Version: 2.0
options.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");
});
4. Media Type (Content Negotiation) Versioning
The most RESTful approach embeds the version in the Accept header: Accept: application/json;v=2.0. It treats the version as part of content negotiation, which is conceptually elegant but the least discoverable and hardest for casual consumers to use.
options.ApiVersionReader = new MediaTypeApiVersionReader("v");
Combining Multiple Readers
You rarely have to pick just one. Use ApiVersionReader.Combine to accept several strategies at once — handy during migrations when some clients use the URL and others use a header.
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(),
new HeaderApiVersionReader("X-Api-Version"),
new QueryStringApiVersionReader("api-version"));
API Versioning with Minimal APIs
If you are building with Minimal APIs instead of controllers, the same library works through version sets. This is increasingly common in modern .NET projects, so it is worth knowing the pattern.
// dotnet add package Asp.Versioning.Http
var versionSet = app.NewApiVersionSet()
.HasApiVersion(new ApiVersion(1, 0))
.HasApiVersion(new ApiVersion(2, 0))
.ReportApiVersions()
.Build();
app.MapGet("/api/v{version:apiVersion}/products", () =>
Results.Ok(new { Id = 1, Name = "Keyboard" }))
.WithApiVersionSet(versionSet)
.MapToApiVersion(1.0);
app.MapGet("/api/v{version:apiVersion}/products", () =>
Results.Ok(new { Id = 1, Title = "Keyboard", Price = 49.99 }))
.WithApiVersionSet(versionSet)
.MapToApiVersion(2.0);
Deprecating an API Version
Versioning is not just about adding new versions — it is about retiring old ones gracefully. Mark a version as deprecated so clients get advance warning through the api-deprecated-versions response header, while the endpoint keeps working during the sunset window.
[ApiController]
[ApiVersion(1.0, Deprecated = true)]
[ApiVersion(2.0)]
[Route("api/v{version:apiVersion}/products")]
public class ProductsController : ControllerBase
{
// v1 still responds, but clients are warned it is going away
}
Pair this with clear documentation and a published sunset date. A deprecation header that nobody reads helps no one — communicate the timeline through your developer portal and changelog as well.
Swagger / OpenAPI Integration
When you expose multiple versions, your API documentation must reflect them. The AddApiExplorer call shown earlier generates a separate OpenAPI document per version. Wire those documents into the Swagger UI so each version gets its own dropdown entry.
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());
}
});
API Versioning Best Practices
Knowing the strategies is half the battle; applying them well is the other half. These API versioning best practices come from real production systems.
- Pick one primary strategy and stay consistent. URL path versioning is the safest default for public APIs because it is the most discoverable and cache-friendly. Mixing strategies without reason confuses consumers.
- Version only when you must. Prefer additive, non-breaking changes. A new version is a long-term maintenance commitment — every active version is code you must keep testing and securing.
- Use major versions for breaking changes. Don't bump versions for bug fixes or new optional fields. Semantic intent matters:
v2should mean "the contract changed." - Always set a sensible default version.
AssumeDefaultVersionWhenUnspecifiedprevents older clients from breaking the day you add versioning. - Report and deprecate explicitly. Turn on
ReportApiVersionsand mark dying versionsDeprecatedso consumers self-discover the migration path. - Keep version-specific logic out of controllers. Push transformation logic into services or mappers so a v1 controller doesn't become a tangle of
if (version == ...)branches. - Document and communicate sunset dates. Technical headers are not a substitute for a human-readable changelog.
Common Pitfalls to Avoid
- Forgetting the API Explorer package. Without
Asp.Versioning.Mvc.ApiExplorer, Swagger won't understand your versions and your docs will be wrong. - Versioning every endpoint individually. Apply versions at the controller level for consistency rather than scattering attributes per action.
- Never retiring old versions. Supporting
v1throughv9forever is a maintenance nightmare. Define a deprecation policy from day one. - Confusing the old and new packages. Tutorials referencing
Microsoft.AspNetCore.Mvc.Versioningare outdated; the maintained library isAsp.Versioning.
Which Versioning Strategy Should You Choose?
For most teams, URL path versioning is the pragmatic winner: it is explicit, testable in a browser, plays nicely with HTTP caches, and is instantly understood by every developer who reads a log line. Choose header or media-type versioning when you have strict REST requirements and a technical audience that controls its own clients. Query string versioning sits in between — convenient for quick testing but easy to forget. The good news is that the Asp.Versioning library lets you switch or combine these with a single line of configuration, so you are never locked in.
Conclusion and Key Takeaways
API versioning in ASP.NET Core is the discipline that lets your Web API evolve without breaking the clients that depend on it. With the official Asp.Versioning libraries, adding robust versioning takes only a few lines of configuration, yet it protects you from one of the most painful failure modes in backend development: shipping a breaking change to consumers you cannot control.
- Version to manage breaking changes — add new fields freely, but bump a major version when the contract changes.
- URL path versioning is the safe default for public APIs thanks to its discoverability and caching behavior.
- Use
Asp.Versioning.Mvc, not the deprecatedMicrosoft.AspNetCore.Mvc.Versioningpackage. - Always configure a default version and enable
ReportApiVersionsso clients can discover what's available. - Deprecate gracefully with explicit headers, documentation, and a published sunset date.
Start by adding versioning to a single controller in your next project, enable Swagger integration, and adopt a clear deprecation policy. Your future self — and every developer consuming your API — will thank you.
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