Skip to main content

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

Learn API versioning in ASP.NET Core with practical C# examples. Compare URL, header & query strategies, avoid pitfalls, and version your APIs the right way today.

If you've ever shipped a public or internal REST API, you already know the hard truth: change is inevitable, but breaking your clients is optional. API versioning in ASP.NET Core is the discipline that lets you evolve endpoints, rename fields, and change response shapes without forcing every consumer to update on your timeline. In this complete guide, you'll learn the four main ASP.NET Core API versioning strategies, see runnable C# code for each, and discover the best practices and pitfalls that separate junior implementations from production-grade APIs.

Whether you're a beginner searching for how to version a REST API in C#, an intermediate developer hunting for best practices, or a senior engineer designing an enterprise API governance model, this tutorial has you covered. Let's dive in.

Why API Versioning in ASP.NET Core Matters

An API is a contract. The moment a third party — a mobile app, a partner system, or another microservice — depends on your endpoint, you've made a promise about its shape and behavior. API versioning in ASP.NET Core exists so you can introduce breaking changes safely while keeping the original contract alive for existing clients.

Common changes that demand a new version include:

  • Removing or renaming a JSON property in a response.
  • Changing the data type of a field (e.g., string to int).
  • Altering the meaning of an existing field or status code.
  • Making a previously optional request parameter required.

Non-breaking changes — like adding a new optional field — generally do not require a version bump. Knowing the difference is the first best practice of versioning: version only when you must, because every live version is code you have to maintain, test, and document forever.

Installing the Asp.Versioning Package

Microsoft's official library was renamed from Microsoft.AspNetCore.Mvc.Versioning to the cleaner Asp.Versioning.* family. For controller-based APIs, install:

dotnet add package Asp.Versioning.Mvc
dotnet add package Asp.Versioning.Mvc.ApiExplorer

The ApiExplorer package is what makes versions show up correctly in Swagger/OpenAPI. Now wire it up in Program.cs:

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 =>
{
    // Format like 'v1', 'v2' for route templates and Swagger
    options.GroupNameFormat = "'v'VVV";
    options.SubstituteApiVersionInUrl = true;
});

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

That ReportApiVersions = true setting is a quiet hero — it adds api-supported-versions and api-deprecated-versions headers so clients can discover what's available without reading your docs.

The 4 API Versioning Strategies in ASP.NET Core

There are four ways clients can tell your API which version they want. Let's look at each, with code and a clear take on when to use it.

1. URL Path Versioning (Most Popular)

The version lives directly in the route — /api/v1/products. This is the most common and most discoverable approach, and it's the default recommendation for public APIs because the version is visible in every browser, log, and curl command.

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()
        => Ok(new { Version = "1.0", Products = new[] { "Widget", "Gadget" } });

    [HttpGet]
    [MapToApiVersion(2.0)]
    public IActionResult GetV2()
        => Ok(new
        {
            Version = "2.0",
            Items = new[]
            {
                new { Name = "Widget", InStock = true },
                new { Name = "Gadget", InStock = false }
            }
        });
}

Now GET /api/v1/products and GET /api/v2/products hit different methods in the same controller. The {version:apiVersion} route constraint and [MapToApiVersion] attributes do the routing for you.

Why choose it: maximum visibility, trivially cacheable by CDNs, and dead-simple to test. Pitfall: purists argue a URL should identify a resource, not a representation — versioning the path technically violates strict REST/HATEOAS principles. In practice, the pragmatism usually wins.

2. Query String Versioning

The client appends ?api-version=2.0. This keeps URLs clean and lets the version default gracefully when omitted.

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ApiVersionReader = new QueryStringApiVersionReader("api-version");
});

A request like GET /api/products?api-version=2.0 now resolves to v2. Why choose it: easy to add to an existing unversioned API without changing route templates. Pitfall: query strings are easy to forget, and some proxies strip or reorder them, which can hurt caching.

3. Header Versioning in C#

Header versioning in C# moves the version into a custom HTTP header, keeping the URL completely clean. This is popular for internal microservices where the API surface is hidden from end users.

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");
});

Clients send X-Api-Version: 2.0 as a request header. Why choose it: the cleanest URLs and a clear separation between resource identity and representation. Pitfall: you can't paste a versioned link into a browser, and debugging requires tools like Postman or curl — a real friction cost for developer experience.

4. Media Type (Content Negotiation) Versioning

The most RESTful approach embeds the version in the Accept header: Accept: application/json;v=2.0.

options.ApiVersionReader = new MediaTypeApiVersionReader("v");

Why choose it: it treats different versions as different representations of the same resource, which aligns perfectly with HTTP content negotiation. Pitfall: it's the least intuitive for consumers and the hardest to document, so it's rare outside hypermedia-driven APIs.

Combining Multiple Readers

You don't have to pick just one. Combine readers so clients can use whichever fits their tooling:

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

Versioning Minimal APIs

If you're using minimal APIs instead of controllers, install Asp.Versioning.Http and use a version set. This is increasingly common in modern .NET projects.

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

app.MapGet("/api/v{version:apiVersion}/ping", () => "pong v1")
   .WithApiVersionSet(versionSet)
   .MapToApiVersion(1.0);

app.MapGet("/api/v{version:apiVersion}/ping", () => "pong v2")
   .WithApiVersionSet(versionSet)
   .MapToApiVersion(2.0);

Deprecating Old Versions Gracefully

Versioning isn't just about adding — it's about sunsetting. Mark a version as deprecated so clients get advance warning via response headers instead of a sudden 404.

[ApiVersion(1.0, Deprecated = true)]
[ApiVersion(2.0)]
[Route("api/v{version:apiVersion}/products")]
public class ProductsController : ControllerBase
{
    // v1 still works, but responses now include:
    // api-deprecated-versions: 1.0
}

Pair this with a clear deprecation policy: announce the sunset date, communicate it in docs and changelogs, and give clients a realistic migration window (often 6–12 months for public APIs). The Sunset HTTP header (RFC 8594) is a great companion for signaling the exact retirement date.

API Versioning Best Practices

Here's the distilled wisdom that keeps versioned APIs maintainable as they grow:

  • Default to URL path versioning for public APIs — visibility and cacheability outweigh REST purism.
  • Use major versions only. Avoid v1.1, v1.2 proliferation; reserve new versions for genuine breaking changes and ship additive changes within the current version.
  • Don't duplicate business logic per version. Keep your core domain logic in services and let thin controller methods map old request/response shapes to it. Version the contract, not the engine.
  • Always integrate with Swagger. Use the ApiExplorer package so each version gets its own OpenAPI document and dropdown.
  • Set a default version with AssumeDefaultVersionWhenUnspecified so legacy or careless clients don't break immediately.
  • Test every live version. Each supported version needs its own integration tests; an untested v1 is a regression waiting to happen.
  • Document the deprecation timeline the day you deprecate, not the day you delete.

Common Pitfalls to Avoid

  • Over-versioning. Bumping the version for non-breaking changes multiplies your maintenance burden for zero client benefit.
  • Forgetting the ApiExplorer package. Without it, Swagger shows duplicate or missing routes and confuses consumers.
  • Copy-pasting controllers per version. This leads to logic drift where a bug fix lands in v2 but not v1. Share the service layer instead.
  • Hard 404s on retired versions. Return a 410 Gone with a helpful message and migration link rather than a silent dead end.
  • Mixing the old and new NuGet packages. Migrating from Microsoft.AspNetCore.Mvc.Versioning to Asp.Versioning and leaving both referenced causes maddening conflicts.

Conclusion: Key Takeaways

API versioning in ASP.NET Core is the safety net that lets your API evolve without betraying the clients who depend on it. With the official Asp.Versioning library, you get a clean, declarative way to support URL, query string, header, and media type strategies — and to deprecate old versions gracefully.

Here are the takeaways to remember:

  • Version only for breaking changes; ship additive changes in place.
  • URL path versioning is the safest default for public APIs; header versioning in C# shines for internal microservices.
  • Install Asp.Versioning.Mvc and the ApiExplorer package, then configure a default version and ReportApiVersions.
  • Share your business logic across versions — version the contract, not the core.
  • Deprecate with warning headers and a clear sunset timeline; never delete silently.

Implement these patterns and your ASP.NET Core API versioning strategy will scale cleanly from a single endpoint to an enterprise API platform. Now go version your next API with confidence — your future self, and your API consumers, 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...