Skip to main content

ASP.NET Core Minimal APIs: Quick Guide to API Versioning

· By Matthias Güntert · 9 min read

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/customers

Advantages

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=1

Advantages

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/products

Versioning 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=2

Versioning 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, 2

This 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

GitHub - dotnet/aspnet-api-versioning: Provides a set of libraries which add service API versioning to ASP.NET Web API, OData with ASP.NET Web API, and ASP.NET Core.
Provides a set of libraries which add service API versioning to ASP.NET Web API, OData with ASP.NET Web API, and ASP.NET Core. - dotnet/aspnet-api-versioning
Home
Provides a set of libraries which add service API versioning to ASP.NET Web API, OData with ASP.NET Web API, and ASP.NET Core. - dotnet/aspnet-api-versioning
How are REST APIs versioned?
I am currently working on a REST API, and the question was raised, how are, and how should, REST APIs be versioned? Here are the results of my research. It seems that there are a number of people r…
Updated on Nov 11, 2025