Skip to main content

API Versioning in ASP.NET Core: A Complete Guide

Learn API versioning in ASP.NET Core with practical C# examples. Master URL, header & query string strategies. Start versioning your APIs the right way today.

API versioning in ASP.NET Core is one of those topics every backend developer eventually runs into — usually the hard way, right after a "small change" breaks a mobile app in production. If you build REST APIs with C# and .NET, learning how to version your endpoints properly is not optional; it is the difference between shipping confidently and dreading every deployment. In this complete guide, we'll cover the main API versioning strategies in ASP.NET Core, show runnable code examples for both controller-based and minimal APIs, and explain the why behind each decision so you can pick the right approach for your project.

Why API Versioning in ASP.NET Core Matters

An API is a contract. The moment a client — a web app, a mobile app, or a third-party integration — starts consuming your endpoints, you have made a promise about how those endpoints behave. The problem is that requirements change. You need to rename a field, change a response shape, remove a deprecated property, or alter validation rules. Any of these can be a breaking change for existing consumers.

Without a versioning strategy, you have only two bad options: break your existing clients, or never evolve your API. API versioning gives you a third, far better path. It lets you introduce new behavior under a new version (for example v2) while keeping the old version (v1) alive and stable for clients that haven't migrated yet. This is especially critical for public APIs and microservices, where you do not control — and often cannot even contact — every consumer.

The good news: the official Asp.Versioning libraries (formerly Microsoft.AspNetCore.Mvc.Versioning) make this remarkably clean in modern .NET. Let's get them installed and explore each strategy.

Getting Started: Installing the Versioning Package

Modern ASP.NET Core API versioning lives in the Asp.Versioning family of NuGet packages, maintained by the .NET team. For controller-based APIs you typically need two packages:

// Run these in your project directory
// dotnet add package Asp.Versioning.Mvc
// dotnet add package Asp.Versioning.Mvc.ApiExplorer

Then register versioning in Program.cs. This single block of configuration is the foundation everything else builds on:

using Asp.Versioning;

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;

    // Return supported/deprecated versions in response headers
    options.ReportApiVersions = true;
})
.AddApiExplorer(options =>
{
    // Formats the version as "v1", "v2", etc. for Swagger
    options.GroupNameFormat = "'v'VVV";
    options.SubstituteApiVersionInUrl = true;
});

var app = builder.Build();
app.MapControllers();
app.Run();

Two options here deserve attention. AssumeDefaultVersionWhenUnspecified keeps old clients working when they send no version — useful when you add versioning to an existing API. ReportApiVersions adds api-supported-versions and api-deprecated-versions response headers, which is a free, low-effort way to communicate deprecation to consumers.

API Versioning Strategies in ASP.NET Core

There are four common ways to tell the server which API version a client wants. Each has trade-offs, and the right answer depends on your audience and constraints.

1. URL Path Versioning (Most Popular)

URL path versioning puts the version directly in the route, like /api/v1/products. This is by far the most popular and discoverable approach — the version is visible, easy to test in a browser, and trivial to document. Most large public APIs (GitHub, Stripe-style designs) lean toward explicit, visible versions.

using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;

[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 { version = "1.0", products = new[] { "Keyboard", "Mouse" } });
    }

    [HttpGet]
    [MapToApiVersion(2.0)]
    public IActionResult GetV2()
    {
        // v2 returns richer objects instead of plain strings
        return Ok(new
        {
            version = "2.0",
            products = new[]
            {
                new { name = "Keyboard", price = 49.99 },
                new { name = "Mouse", price = 19.99 }
            }
        });
    }
}

The v{version:apiVersion} route token and the [MapToApiVersion] attributes let a single controller serve multiple versions. A request to /api/v1/products hits GetV1; /api/v2/products hits GetV2. Why prefer this? Clarity. Anyone reading a log, a curl command, or browser history immediately knows which version was called.

2. Query String Versioning

Query string versioning keeps URLs clean of path segments and passes the version as a parameter: /api/products?api-version=2.0. It's easy to add to an existing API because routes don't change.

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;

    // Read the version from the "api-version" query string parameter
    options.ApiVersionReader = new QueryStringApiVersionReader("api-version");
});

The downside: versions hidden in query strings are easy to forget in documentation and can get stripped by aggressive caching layers or proxies. It's convenient, but less self-documenting than the URL approach.

3. HTTP Header Versioning

Header versioning passes the version in a custom request header, keeping the URL completely free of versioning concerns. Purists like this because the resource URL stays stable across versions — arguably more "RESTful."

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");
});

The trade-off is discoverability and testing friction. You can't paste a URL into a browser to hit v2 — you need a tool like Postman, curl, or code that sets the header. For internal services and SDKs where clients are generated, this is fine. For public, human-facing APIs, it raises the barrier to entry.

4. Media Type (Content Negotiation) Versioning

The most "academically correct" approach embeds the version in the Accept header via content negotiation, e.g. Accept: application/json;v=2.0. You can enable it with MediaTypeApiVersionReader. It's powerful but the least common in practice because it's the hardest for casual consumers to use. Reserve it for hypermedia-driven APIs where you're already deep into content negotiation.

Combining Multiple Readers

You don't have to choose just one. A common production pattern is to accept the version from several places, giving clients flexibility:

options.ApiVersionReader = ApiVersionReader.Combine(
    new UrlSegmentApiVersionReader(),
    new HeaderApiVersionReader("x-api-version"),
    new QueryStringApiVersionReader("api-version"));

API Versioning for Minimal APIs in .NET

Minimal APIs are now a first-class citizen for many .NET teams, and versioning works just as cleanly there. Install Asp.Versioning.Http and use a version set:

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();

ApiVersionSet versionSet = app.NewApiVersionSet()
    .HasApiVersion(new ApiVersion(1, 0))
    .HasApiVersion(new ApiVersion(2, 0))
    .Build();

app.MapGet("/api/v{version:apiVersion}/weather", () =>
        Results.Ok(new { version = "1.0", temp = 20 }))
    .WithApiVersionSet(versionSet)
    .MapToApiVersion(1.0);

app.MapGet("/api/v{version:apiVersion}/weather", () =>
        Results.Ok(new { version = "2.0", tempC = 20, tempF = 68 }))
    .WithApiVersionSet(versionSet)
    .MapToApiVersion(2.0);

app.Run();

The mental model is identical to controllers: declare which versions exist, then map each endpoint to a specific version. This consistency across the two programming models is one of the nicest things about the official versioning library.

Deprecating Old Versions Gracefully

Versioning isn't only about adding new versions — it's about retiring old ones responsibly. Mark a version as deprecated so clients get a heads-up in the response headers:

[ApiVersion(1.0, Deprecated = true)]
[ApiVersion(2.0)]
[Route("api/v{version:apiVersion}/products")]
public class ProductsController : ControllerBase
{
    // ...
}

With ReportApiVersions = true, responses now include api-deprecated-versions: 1.0. Combine this with clear documentation and a published sunset date, and you give consumers a fair, professional migration window instead of a nasty surprise.

API Versioning Best Practices and Common Pitfalls

Once the mechanics are in place, the harder questions are strategic. Here are the practices that separate a maintainable API from a painful one.

  • Version only when you must. Additive, non-breaking changes (adding a new optional field or a brand-new endpoint) do not require a new version. Reserve new versions for genuine breaking changes — removed fields, renamed properties, changed types, or altered behavior. Over-versioning multiplies maintenance cost.
  • Pick one primary strategy and stay consistent. Choose URL path versioning for public APIs (discoverability wins) and stick with it everywhere. Mixing strategies across teams confuses consumers and complicates tooling.
  • Always set a default version. Use AssumeDefaultVersionWhenUnspecified so that adding versioning to a live API doesn't instantly break clients who send no version header.
  • Document every version in Swagger/OpenAPI. The AddApiExplorer configuration shown earlier integrates with Swashbuckle so each version gets its own document. Undocumented versions are versions no one can safely adopt.
  • Avoid leaking versioning into your domain layer. Keep version-specific shaping in DTOs and controllers, not in your business logic or database. Your core services should be version-agnostic; map to the right response shape at the edge.
  • Communicate deprecation early. Use deprecated flags, response headers, and a published timeline. Never silently remove a version.

The most common pitfall? Treating versioning as a code problem when it's really a communication problem. The library handles routing in minutes; the real work is deciding what counts as breaking and keeping consumers informed.

Conclusion: Key Takeaways

API versioning in ASP.NET Core is straightforward to implement with the official Asp.Versioning packages, and getting it right protects both you and your consumers from painful breaking changes. To recap the essentials:

  • Use AddApiVersioning with a sensible default version and ReportApiVersions enabled.
  • URL path versioning (/api/v1/...) is the most discoverable and is the safest default for public APIs; header and query string versioning suit internal services and SDK-driven clients.
  • The same patterns work cleanly for both controller-based and minimal APIs.
  • Version only for true breaking changes, mark old versions deprecated, and document everything in Swagger.
  • Remember that versioning is as much about clear communication with consumers as it is about code.

Start by adding the package to your next .NET API project, pick URL path versioning, and ship a v1 today — your future self, and every developer consuming your API, will thank you. Happy coding!

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