API Versioning in ASP.NET Core: A Friendly, Complete Guide
Learn API versioning in ASP.NET Core with simple examples. URL, query string, header, and media type versioning explained with diagrams, code, and OpenAPI tips.
A train with different platforms
Picture a busy railway station in your city. The same train line runs every day, but the railway keeps improving it. The old train still stops at Platform 1 for people who are used to it. The new, faster train stops at Platform 2. Both trains take you to the same city, but they work a little differently inside — new seats, new doors, new rules.
The station does not force everyone onto the new train on the same morning. That would cause chaos. Instead, both platforms run side by side for a while. People move to the new train when they are ready. Slowly, the old train is retired.
Your web API is just like that station. Over time you change how it works — you rename a field, you add a new rule, you remove an old one. But other people's apps already depend on the old behaviour. If you change everything at once, their apps break. API versioning is the station master who keeps both platforms open, points each traveller to the right train, and gives everyone time to move.
Let us learn how to build those platforms in ASP.NET Core.
Why versioning matters
When you build an API, other programs start to depend on it. A mobile app, a partner's website, a background job — they all send requests and expect a certain shape of answer back. The moment you change that shape, you risk breaking them.
There are two kinds of changes:
- Additive changes are safe. Adding a new endpoint, a new optional field, or a new optional query parameter usually does not break anyone. Old clients simply ignore the new bits.
- Breaking changes are not safe. Renaming a field, removing a field, changing a data type, or changing the meaning of a value can crash an old client.
You only need a new version for breaking changes. If a change is purely additive, keep the same version. This keeps your version numbers meaningful and your life simple.
The four ways to choose a version
A client needs a way to tell your server which version it wants. There are four common strategies, and ASP.NET Core supports all of them. Here they are side by side.
| Strategy | Example request | Good for | Trade-off |
|---|---|---|---|
| URL segment | GET /v1/orders | Public APIs, easy to read and cache | Version lives in the path, so routes change |
| Query string | GET /orders?api-version=1.0 | Quick to start, easy to test | Clutters the URL, easy to forget |
| Header | GET /orders with X-Api-Version: 1.0 | Internal services, clean URLs | Hidden, harder to test in a browser |
| Media type | Accept: application/json;v=1.0 | Strict REST fans | Hardest to use and explain |
Most teams pick URL segment versioning for public APIs because anyone can see the version just by reading the address. For internal microservices, header versioning is popular because it keeps URLs tidy.
How a versioned request flows
Steps
Client
Sends version in URL, header, or query
Reader
Framework reads the version value
Selector
Picks the matching endpoint
Handler
Runs the right code for that version
Getting started: install the packages
For .NET 10, the versioning toolkit lives in the community-maintained dotnet/aspnet-api-versioning project. The package names start with Asp.Versioning. Pick the one that matches how you build your API.
- Use
Asp.Versioning.Httpfor Minimal APIs. - Use
Asp.Versioning.Mvcfor controllers. - Add
Asp.Versioning.Mvc.ApiExplorerif you want one Swagger or OpenAPI document per version.
You can add them from the command line:
// Run these in your project folder (they are dotnet CLI commands)
// dotnet add package Asp.Versioning.Mvc --version 10.0.0
// dotnet add package Asp.Versioning.Mvc.ApiExplorer --version 10.0.0
// Then register versioning in Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services
.AddApiVersioning(options =>
{
// If a client does not ask for a version, assume this one.
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
// Add api-supported-versions and api-deprecated-versions headers.
options.ReportApiVersions = true;
})
.AddMvc(); // wires versioning into MVC controllers
var app = builder.Build();
app.MapControllers();
app.Run();Three options do most of the work here:
DefaultApiVersionis the version used when a client does not say which one it wants.AssumeDefaultVersionWhenUnspecifiedlets old clients that never knew about versions keep working.ReportApiVersionsadds helpful response headers so clients can see what is available.
URL segment versioning with controllers
This is the friendliest style for most people. The version sits right in the route, like /v1/weather and /v2/weather. Let us version a simple controller.
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[ApiVersion(1.0)]
[ApiVersion(2.0)]
[Route("v{version:apiVersion}/weather")]
public class WeatherController : ControllerBase
{
// Responds to GET /v1/weather
[HttpGet]
[MapToApiVersion(1.0)]
public IActionResult GetV1()
{
return Ok(new { temp = 30, note = "Version 1: temp only" });
}
// Responds to GET /v2/weather
[HttpGet]
[MapToApiVersion(2.0)]
public IActionResult GetV2()
{
// Version 2 adds humidity. That is a breaking change in shape,
// so it earns a new version.
return Ok(new { temp = 30, humidity = 70, note = "Version 2: richer data" });
}
}The magic is the route template v{version:apiVersion}. The {version:apiVersion} part is a special route constraint added by the versioning library. It reads the number from the URL and matches it to the [ApiVersion] attributes on the controller. The [MapToApiVersion] attribute on each method says which version that method serves.
So GET /v1/weather runs GetV1, and GET /v2/weather runs GetV2. Same controller, two platforms.
Minimal API versioning
If you prefer Minimal APIs, the idea is the same but the code is lighter. You build a version set and attach it to your endpoints.
using Asp.Versioning;
using Asp.Versioning.Builder;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
});
var app = builder.Build();
// A version set groups the versions an endpoint family supports.
ApiVersionSet versionSet = app.NewApiVersionSet()
.HasApiVersion(new ApiVersion(1, 0))
.HasApiVersion(new ApiVersion(2, 0))
.ReportApiVersions()
.Build();
var group = app.MapGroup("v{version:apiVersion}")
.WithApiVersionSet(versionSet);
group.MapGet("/weather", () => new { temp = 30 })
.MapToApiVersion(1, 0);
group.MapGet("/weather", () => new { temp = 30, humidity = 70 })
.MapToApiVersion(2, 0);
app.Run();Here NewApiVersionSet describes which versions exist. MapGroup("v{version:apiVersion}") puts the version into the URL, just like the controller route. Each MapGet then picks the version it answers with MapToApiVersion. The result is the same two platforms, /v1/weather and /v2/weather, with much less ceremony.
Reading the version from a header or query string
The URL is not the only place a version can live. The ApiVersionReader decides where ASP.NET Core looks. You can use one reader, or combine several so a client may choose any of them.
using Asp.Versioning;
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
// Accept the version from a query string, a header, OR the URL.
options.ApiVersionReader = ApiVersionReader.Combine(
new QueryStringApiVersionReader("api-version"),
new HeaderApiVersionReader("X-Api-Version"),
new UrlSegmentApiVersionReader());
});With this setup, all three of these requests reach version 2.0:
GET /orders?api-version=2.0GET /orderswith headerX-Api-Version: 2.0GET /v2/orders
Combining readers is handy while you migrate. New clients can use the clean style you prefer, while older clients still work with the style they already use.
| Reader class | Where it looks | Example |
|---|---|---|
UrlSegmentApiVersionReader | A part of the path | /v2/orders |
QueryStringApiVersionReader | A query parameter | /orders?api-version=2.0 |
HeaderApiVersionReader | A request header | X-Api-Version: 2.0 |
MediaTypeApiVersionReader | The Accept header | Accept: application/json;v=2.0 |
Deprecating a version the kind way
You should never pull the rug out from under your clients. When a version is on its way out, mark it deprecated first. This does not turn it off. It just signals "this still works, but please move soon."
// The old version still runs, but it is flagged as deprecated.
[ApiVersion(1.0, Deprecated = true)]
[ApiVersion(2.0)]
[Route("v{version:apiVersion}/orders")]
public class OrdersController : ControllerBase { /* ... */ }Because ReportApiVersions is on, a response now includes headers like:
api-supported-versions: 2.0api-deprecated-versions: 1.0
A well-behaved client reads these and knows to upgrade. After enough time has passed and traffic on the old version has dropped, you can safely remove it. This slow, polite path is sometimes called sunset discipline: announce, deprecate, wait, then retire.
The deprecation lifecycle
Steps
Announce
Tell clients a new version exists
Deprecate
Flag old version with headers
Wait
Give clients months to migrate
Retire
Remove the old version safely
Showing every version in Swagger and OpenAPI
When you have several versions, your API documentation needs to show each one clearly. The Asp.Versioning.Mvc.ApiExplorer package builds a separate OpenAPI document for every version, so readers can switch between them.
In .NET 10 there is one important detail. After you turn on versioning, you must call the AddOpenApi method from the Asp.Versioning namespace, not the plain one. This special variant knows how to split your endpoints into one document per version.
using Asp.Versioning;
builder.Services
.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.ReportApiVersions = true;
})
.AddApiExplorer(options =>
{
// Group name format: 'v'major[.minor], e.g. v1, v2.
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
// Use the versioning-aware AddOpenApi from the Asp.Versioning namespace.
builder.Services.AddOpenApi();The GroupNameFormat = "'v'VVV" setting names each group v1, v2, and so on. SubstituteApiVersionInUrl = true replaces the {version} placeholder in the path with the real number in the generated docs, so the examples look clean.
Picking a strategy: a simple guide
You do not have to overthink this. Here is a short, practical guide for what to choose.
- Building a public API that outside teams call? Use URL segment versioning. It is visible, cacheable, and works in any tool, even a plain browser.
- Building internal microservices? Use header versioning so your URLs stay clean and stable.
- Just experimenting or building a small tool? Use query string versioning. It is the quickest to set up and test.
- A strong REST purist on the team? Media type versioning is the most "correct" by the book, but it is the hardest for others to use, so weigh that cost.
Whatever you pick, stay consistent. Mixing styles across one API confuses the people who call it.
Common mistakes to avoid
A few traps catch many beginners. Knowing them ahead of time saves hours.
- Versioning additive changes. If you only added an optional field, you did not break anyone. Do not bump the version. Save versions for real breaking changes.
- Forgetting
AssumeDefaultVersionWhenUnspecified. Without it, old clients that never sent a version suddenly get errors. Turn it on so they keep working. - Removing a version with no warning. Always deprecate first and watch the traffic. Pulling a live version is how you get angry phone calls.
- Using the wrong
AddOpenApi. In .NET 10, the plain one will not split your docs by version. Import fromAsp.Versioning. - Mixing styles randomly. Pick one reader strategy as your main one. Combine readers only on purpose, usually to ease a migration.
A note on related libraries
While you set up an API, you may reach for messaging or mediator libraries too. Be aware that MediatR and MassTransit moved to commercial licensing in their recent versions. They are still excellent, but check the license terms and pricing before you add them to a production project. For pure versioning, you do not need them at all. The Asp.Versioning packages are open source and free under the dotnet foundation umbrella.
Quick recap
- API versioning lets old and new clients use your API at the same time, like two trains running from different platforms.
- You only need a new version for breaking changes. Additive changes keep the same version.
- Install
Asp.Versioning.Httpfor Minimal APIs orAsp.Versioning.Mvcfor controllers. For .NET 10 these are version 10.0.0. - There are four ways to read a version: URL segment, query string, header, and media type. Use
ApiVersionReader.Combineto accept several. - Set
DefaultApiVersion,AssumeDefaultVersionWhenUnspecified, andReportApiVersionsto keep things smooth and clients informed. - Mark old versions
Deprecated = truefirst, wait, then remove them. Never pull a live version with no warning. - For docs, add
Asp.Versioning.Mvc.ApiExplorerand call theAddOpenApifrom theAsp.Versioningnamespace to get one document per version.
References and further reading
- Combining API versioning with OpenAPI in .NET 10 — .NET Blog
- dotnet/aspnet-api-versioning — GitHub
- Asp.Versioning.Mvc 10.0.0 — NuGet Gallery
- API Versioning in ASP.NET Core — Milan Jovanović
- API Versioning in ASP.NET Core — Code Maze
Related Posts
90% of APIs Are Not RESTful: What You're Missing and When It Matters
Most APIs called RESTful are really Level 2. Learn what real REST means, the Richardson Maturity Model, HATEOAS in ASP.NET Core, and when it matters.
API Key Authentication in ASP.NET Core: The Secure Way
Learn how to add API key authentication to your ASP.NET Core API the right way. Use an AuthenticationHandler, hash keys, compare safely, and follow 2026 security best practices, with diagrams and code.
Caching in ASP.NET Core: Make Your App Fast (The Easy Way)
Learn caching in ASP.NET Core with simple examples. Understand in-memory cache, distributed Redis cache, HybridCache, and output cache, with diagrams, code, and clear advice on which to use and when.
Top 15 Mistakes Developers Make When Creating Web APIs
A warm, beginner-friendly tour of the 15 most common Web API mistakes in ASP.NET Core, with simple fixes, diagrams, tables, and clear C# examples.
How to Increase the Performance of Web APIs in .NET
A friendly, step-by-step guide to making your ASP.NET Core Web APIs fast: async, caching, query tuning, compression, and pooling in .NET 10.
CORS in ASP.NET Core: A Comprehensive Guide
A simple, friendly guide to CORS in ASP.NET Core. Learn how the browser, preflight requests, and policies work, with clear diagrams, tables, and code.