API Versioning in ASP .NET Core

Introduction
An API with a single consumer you control can change shape freely - deploy the API, deploy the client, done. The moment there's more than one consumer, or a mobile app you can't force-update on your schedule, a breaking change to an existing endpoint needs a version boundary so old and new clients can both keep working. This video sets up API versioning in ASP.NET Core using the official Asp.Versioning packages.
🎬 Watch the full video here:
Three common places to put the version
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
});
URL segment (/api/v1/products) is the most visible and cacheable, since the version is part of the URL itself - easy to route to, easy to see in logs, but it means the version leaks into every client's hardcoded paths.
Query string (/api/products?api-version=1.0) keeps the URL path stable and adds versioning as metadata - less visually intrusive, easy to default when omitted.
Header-based (X-Api-Version: 1.0) keeps the URL completely clean and treats the version as what it conceptually is - a property of the request, not the resource path - at the cost of being invisible to anyone just glancing at a URL or a browser address bar.
URL-segment versioning is the most common default for public APIs specifically because it's the most discoverable and the easiest for a consumer to reason about without reading documentation.
Versioning Minimal API endpoints
var versionSet = app.NewApiVersionSet()
.HasApiVersion(new ApiVersion(1, 0))
.HasApiVersion(new ApiVersion(2, 0))
.ReportApiVersions()
.Build();
app.MapGet("/api/v{version:apiVersion}/products", GetProductsV1)
.WithApiVersionSet(versionSet)
.MapToApiVersion(1, 0);
app.MapGet("/api/v{version:apiVersion}/products", GetProductsV2)
.WithApiVersionSet(versionSet)
.MapToApiVersion(2, 0);
Both versions live at the same route template, differentiated by MapToApiVersion - the versioning middleware routes an incoming request to whichever handler matches its declared version.
Deprecating without breaking
ReportApiVersions adds an api-supported-versions response header listing what's currently available, and a version can be marked Deprecated while still functioning - giving consumers a machine-readable signal to migrate before a version is actually removed:
options.HasApiVersion(new ApiVersion(1, 0));
options.HasDeprecatedApiVersion(new ApiVersion(1, 0));
This is the difference between "v1 disappeared with no warning" and "v1 has been reporting itself as deprecated in every response for the last three months."
Common pitfalls
- Making a breaking change to a live endpoint without a new version, silently breaking every existing consumer.
- Picking header-based versioning for a public API where discoverability from the URL matters more than architectural purity.
- No default version, forcing every client to specify one explicitly or fail.
- Removing an old version with no deprecation window, giving consumers no time to migrate.
- Duplicating entire controllers per version instead of versioning only the specific endpoints that actually changed.
Key Takeaways
- API versioning matters the moment more than one consumer depends on the API's shape staying stable.
- URL-segment, query-string, and header-based versioning are the three common schemes - URL-segment is the most discoverable default for public APIs.
Asp.Versioningsupports versioning Minimal API endpoints directly viaWithApiVersionSetandMapToApiVersion.- Mark versions deprecated (not just remove them) to give consumers a migration window, and report supported versions via response headers.
Get the Full Source Code
The complete runnable solution - the versioned Minimal API endpoints and the deprecation setup - is available to Patreon supporters. If you want the versioning scheme ready to apply to your own endpoints instead of rebuilding it from the walkthrough above, you can find it on Patreon.