Skip to main content

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

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 string status into an enum integer).
  • 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: v2 should mean "the contract changed."
  • Always set a sensible default version. AssumeDefaultVersionWhenUnspecified prevents older clients from breaking the day you add versioning.
  • Report and deprecate explicitly. Turn on ReportApiVersions and mark dying versions Deprecated so 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 v1 through v9 forever is a maintenance nightmare. Define a deprecation policy from day one.
  • Confusing the old and new packages. Tutorials referencing Microsoft.AspNetCore.Mvc.Versioning are outdated; the maintained library is Asp.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 deprecated Microsoft.AspNetCore.Mvc.Versioning package.
  • Always configure a default version and enable ReportApiVersions so 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.

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 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...

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...

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...