Introduction
When building RESTful APIs with ASP.NET Core, you'll sooner or later find yourself in the place where you need to add versioning to your API. Mostly because you need to introduce breaking changes but need to keep backwards compatibility.
This is where API versioning comes into play, and allows you to evolve your application without breaking existing clients. As you add new features or change data contracts, older versions of the API can remain stable and compatible for consumers who depend on them.
This allows you to introduce improvements safely, manage deprecations in a controlled way, and maintain backward compatibility across mobile apps, integrations, or partner systems.
Let's have a look at the different types of API versioning and compare them to each other. Later, we'll get to see some code.
Types of API versioning
Versioning by URL segment
This is by far the most popular way to version a public API, where the version itself is part of the URL, e.g.
https://api.kloudshift.net/v1/products
https://api.kloudshift.net/v2/customersAdvantages
This approach clearly and explicitly communicates which version a client is using. Also, requiring an explicit service version helps ensure existing clients don't break.
Disadvantage
However, with this way of versioning, it's not possible to select a default API version, when clients don't specify one explicitly. To enable such scenarios, double route registration would be required providing multiple routes for the same controller action, that can clutter your code.
Also, clients must change URLs, when upgrading to a new API version, which increases maintenance friction.
Further, some REST purists might argue, that this approach breaks resource identity. In pure REST, a resource's URI should be stable - adding /v2 means a "new" resource, even if it's semantically the same entity.
When to use
This type of versioning is best suited if you're building a public API with multiple long-lived versions and you want maximum visibility and simplicity for clients.
You should avoid it if you are a REST purist and need strict semantics or prefer transparent evolution. If the later is the case, consider header or media-type versioning...
Versioning by header
When versioning an API by header, clients need to specifiy which version of the API they are talking to by adding a custom header.
curl https://api.kloudshift.net/products -H "X-Api-Version: 1" ...Advantage
With this approach, the URLs and resource paths remain clean and free of version strings. It aligns well with REST principles in that the URL represents the resource not its version. This also implies that API versioning is decoupled from routing.
Also I like the fact, that default API versions can be controlled backend-side and independelty from the client.
It further allows clients to switch versions withoug having to change URLs - just the header value. This can be beneficial, e.g. when building an SDK.
Disadvantage
However on the downside, I think this approach is less discoverable to clients. Which header should I set? Which versions are supported by this endpoint?
Some APIs just return a 400 Bad Request, without letting you know that a version header was missing in the request. Don't be like that, this is enoying! Luckily Asp.Versioning.Http has some revelation to that, we'll get to that later.
When to use
You should use header-based API versioning when you want to keep your URLs clean and version agnostic, and when your clients (e.g., mobile apps, backends, SDKs) can easily control their requests/headers.
However, when you rely on caching (CDNs, proxies), you should better choose URL based versioning, since caches often key by URL, not headers. Further, when your API is accessed primarily from browsers or forms you are better of with versioning by URL path segment or query strings...
Versioning by query string
When versioning an API by query string, you include the version as a query parameter in the URL, for example:
https://api.kloudshift.net/products?version=1Advantages
For clients, this approach is very simple to use and understand, it doesn't require modifying headers or complex request formats.
Disadvantages
Just like the URL path segment versioning approach, REST purist might argue it doesn't follow strict REST semantics. The resource should be identified by the URL - the version is more of a representational concern. Including it as a query parameter mixes concerns.
When to use
Query-based versioning works well for internal APIs, low-traffic services, or early-stage projects where simplicity and ease of testing outweigh strict REST compliance or caching concerns.
Versioning by media type
When using API versioning by media type, a client needs to set the requested version in the Accept header that is used by the HTTP content negotiation mechanism.
curl https://api.kloudshift.net/api/products -H "Accept: application/json; charset=utf-8; version=1.3-rc" ...Advantages
Just like the header-based versioning approach, this comes with the benefit of clean and stable URLs. It keeps the URL free of version information and aligns well with REST principles.
Although being the most complex approach, it also provides the best flexibility, in that it allows versioning per representation, not per endpoint.
Disadvantages
This approach comes with the same disadvantages as the header-based versioning, in that it's not obvious for a client how it needs to select a version and which are available (the Asp.Versioning.Http NuGet package helps us with in this aspect, more later).
Also, it can be seen as some overhead to the client, since the client needs to manage headers carefully making it harder for simple integrations or frontend apps that expect straightforward URLs.
When to use
You should consider media-type versioning if you're building a highly RESTful enterprise-level, or hypermedia-driven API, where representations evolve independently of endpoints. Otherwise, prefer URL or header-based versioning for ease of maintenance.
Versioning with ASP.NET Core
Getting started
To add versioning to your project you need to add the following packages. Since this blog post deals with Minimal APIs only, its sufficient to install Asp.Versioning.Http.
| Package | Description |
|---|---|
| Asp.Versioning.Http | Adds API versioning to your ASP.NET Core Minimal API applications |
| Asp.Versioning.Mvc | Adds API versioning to your ASP.NET Core MVC (Core) applications |
| Asp.Versioning.OData | Adds API versioning to your ASP.NET Core applications using OData v4.0 |
Then add these calls to your Program.cs, where AddProblemDetails adds services required for creation of ProblemDetails for failed requests and AddApiVersioning adds the versioning capabilities.
builder.Services.AddProblemDetails();
builder.Services.AddApiVersioning();Versioning by URL segment
To enable versioning by URL segment I initialize the ApiVersionReader with an instance of UrlSegmentApiVersionReader.
Also, I create one ApiVersionSet per endpoint, that allows defining which versions are supported. Finally, a route template is required that is defined with /api/v{version:apiVersion}/products and map each endpoint to a specific version.
public static class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
builder.Services.AddApiVersioning(o =>
{
o.ApiVersionReader = new UrlSegmentApiVersionReader();
});
var app = builder.Build();
var products = app.NewApiVersionSet()
.HasApiVersion(new(1))
.HasApiVersion(new(2))
.Build();
app.MapPost("/api/v{version:apiVersion}/products", (HttpContext ctx, ProductRequest req) =>
{
return TypedResults.Ok(new
{
version = ctx.GetRequestedApiVersion()?.ToString(),
request = req
});
})
.WithApiVersionSet(products)
.MapToApiVersion(1);
app.MapPost("/api/v{version:apiVersion}/products", (HttpContext ctx, ProductRequestV2 req) =>
{
return TypedResults.Ok(new
{
version = ctx.GetRequestedApiVersion()?.ToString(),
request = req
});
})
.WithApiVersionSet(products)
.MapToApiVersion(2);
app.Run();
}
}Example request
https://api.kloudshift.net/v2/productsVersioning by header
To enable versioning by header, we initialize the ApiVersionReader with an instance of HeaderApiVersionReader and pass in the expected header name.
builder.Services.AddApiVersioning(o =>
{
o.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");
});Example request
https://api.kloudshift.net/products -H "X-Api-Version: 2"Versioning by query string
To enable versioning by query string, we use the QueryStringApiVersionReader type and optionally pass a name for the parameter name to use. The default is api-version.
builder.Services.AddApiVersioning(o =>
{
// defaults to "api-version"
o.ApiVersionReader = new QueryStringApiVersionReader("version");
});Example request
https://api.kloudshift.net/products?version=2Versioning by media-type
To enable versioning by media type we use the MediaTypeApiVersionReader and optionally pass a name for the parameter, which defaults to v.
builder.Services.AddApiVersioning(o =>
{
// paramter name defaults to "v"
o.ApiVersionReader = new MediaTypeApiVersionReader();
});Example request
https://api.kloudshift.net/products -H "Accept: application/json; charset=utf-8; v=1 "Version discovery
Most likely you want to communicate to a client which versions each endpoint supports. This can be achieved by using the ReportApiVersions option, which adds the api-supported-versions header to the response.
builder.Services.AddApiVersioning(o =>
{
o.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");
o.ReportApiVersions = true;
});As you can see in the example request/response below, the client requests version 2 by setting the header x-api-version: 2. The client response then contains the api-supported-versions header indicating it supports both version 1, and 2.
POST http://api.kloudshift.net/products HTTP/1.1
X-Api-Version: 2
{
"Name": "Backend development",
"Price": 42.4,
"Description": "Some description"
}
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 11 Nov 2025 14:30:39 GMT
Server: Kestrel
Transfer-Encoding: chunked
api-supported-versions: 1, 2This option can also be set on the ApiVersionSet. In the example, only the products endpoint will advertise available versions.
var products = app.NewApiVersionSet()
.HasApiVersion(new(1))
.HasApiVersion(new(2))
.ReportApiVersions()
.Build();
var customers = app.NewApiVersionSet()
.HasApiVersion(new(1))
.HasApiVersion(new(2))
.HasApiVersion(new(3))
.Build();
app.MapPost("/api/products", ...)
.WithApiVersionSet(products)
.MapToApiVersion(2);
app.MapPost("/api/customers", ...)
.WithApiVersionSet(customers)
.MapToApiVersion(3);Handling requests without a specified API version
The last feature, that I'd like to highlight is the ApiVersionSelector option. This setting defines the behavior of how an API version is selected by the backend, when a client has not requested an explicit API version and you don't want such calls to fail with a 400 Bad Request response.
There are four types of ApiVersionSelectors, these are:
DefaultApiVersionSelector
This selector always selects the configured DefaultApiVersion regardless of the available API version available.
builder.Services.AddApiVersioning(o =>
{
o.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");
o.DefaultApiVersion = new ApiVersion(2);
o.AssumeDefaultVersionWhenUnspecified = true;
o.ApiVersionSelector = new DefaultApiVersionSelector(o);
});ConstantApiVersionSelector
Always selects the defined API version.
builder.Services.AddApiVersioning(o =>
{
o.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");
o.AssumeDefaultVersionWhenUnspecified = true;
o.ApiVersionSelector = new ConstantApiVersionSelector(o);
});CurrentImplementationApiVersionSelector
Selects the maximum API version available which doesn't have a version status. For example, if the version 1, 2 and 3-alpha are available, then 2 will be selected.
builder.Services.AddApiVersioning(o =>
{
o.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");
o.AssumeDefaultVersionWhenUnspecified = true;
o.ApiVersionSelector = new CurrentImplementationApiVersionSelector(o);
});LowestImplementedApiVersionSelector
Selects the minimum API version available which does not have a version status. For example, if the version 0.9-beta, 1, 2 and 3-alpha are available, then 1 will be selected.
builder.Services.AddApiVersioning(o =>
{
o.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");
o.AssumeDefaultVersionWhenUnspecified = true;
o.ApiVersionSelector = new LowestImplementedApiVersionSelector(o);
});Conclusion
- There are four common approaches to version your API, each having it's advantage and disadvantage.
- Selecting the right versioning approach depends on the clients your writing your API for.
- I'd recommend implementing API versioning right from the beginning of your project. Sooner or later you'll need to implement breaking changes, but need to keep backwards compatibility.
- My personal favorite is the header-based versioning, since I like to keep endpoints stable. YMMV.
That's it for today. Happy hacking 😎
Further reading
