
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.,
stringtoint). - 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.2proliferation; 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
ApiExplorerpackage so each version gets its own OpenAPI document and dropdown. - Set a default version with
AssumeDefaultVersionWhenUnspecifiedso 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 Gonewith a helpful message and migration link rather than a silent dead end. - Mixing the old and new NuGet packages. Migrating from
Microsoft.AspNetCore.Mvc.VersioningtoAsp.Versioningand 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.Mvcand theApiExplorerpackage, then configure a default version andReportApiVersions. - 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.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment