Skip to main content

API Versioning in ASP.NET Core: Complete Guide (2026)

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., int ID to Guid)
  • 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-versions and api-deprecated-versions headers 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.Versioning is no longer maintained; use Asp.Versioning.*.
  • Forgetting SubstituteApiVersionInUrl. Without it, Swagger shows a literal {version} parameter in every route instead of concrete v1/v2 paths.
  • 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.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...

Send an Email via SMTP with MailKit Using .NET 6

How to Send an Email in .NET Core This tutorial show you how to send an email in .NET 6.0 using the MailKit email client library. Install MailKit via NuGet Visual Studio Package Manager Console: Install-Package MailKit How to Send an HTML Email in .NET 6.0 This code sends a simple HTML email using the Gmail SMTP service. There are instructions further below on how to use a few other popular SMTP providers - Gmail, Hotmail, Office 365. // create email message var email = new MimeMessage(); email.From.Add(MailboxAddress.Parse("from_address@example.com")); email.To.Add(MailboxAddress.Parse("to_address@example.com")); email.Subject = "Email Subject"; email.Body = new TextPart(TextFormat.Html) { Text = "<h1>Test HTML Message Body</h1>" }; // send email using var smtp = new SmtpClient(); smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls); smtp.Authenticate("[Username]", "[Password]"); smtp.Se...