API Versioning in .NET: A Guide to URL, Query, Header, and Media Types

API Versioning is a technique that allows you to create multiple versions of your app or APIs, ensuring that changes (such as new features or breaking updates) don’t disrupt existing consumers of the API.

API Versioning is really important because it allows developers can introduce improvements, bug fixes, or new features without forcing existing clients to upgrade immediately. By maintaining multiple versions, developers can provide a stable experience for users relying on older versions, while also enabling the adoption of newer functionality at their own pace. This strategy not only ensures backward compatibility but also helps in managing long-term API evolution efficiently.

Types of API Versioning

There are several ways to implement API versioning in a .NET Web API application, but the four most commonly used methods are:

  1. URL versioning
  2. Query String Versioning
  3. Header Versioning
  4. Media Type Versioning

Media Type Versioning may not be as commonly used as the other three methods, but it is still a widely recognized technique, so we will cover it as well.

Setting Up Versioning in ASP.NET WEB API

To implement API versioning, you first need to set up the project to support it, which means you need to install some packages and update the configuration file. I will be using .NET 8, the latest stable version of .NET, but you can use any version of .NET greater than 5.0.

First, you need to install the required NuGet packages to enable versioning in your API project. The primary package is Asp.Versioning.Mvc, which allows you to implement API versioning. Additionally, Asp.Versioning.Mvc.ApiExplorer is useful for versioning support in API documentation tools like Swagger.

You can install these packages either using the NuGet Package Manager or by using the dotnet CLI commands:

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

After installing the packages, go to Program.cs and update the default configuration to include API versioning. You can use the code below:

using Asp.Versioning;

var builder = WebApplication.CreateBuilder(args);
// ...other code here

//Add API Versioning support
builder.Services.AddApiVersioning(options =>
{
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.ReportApiVersions = true;
}).AddApiExplorer(options => {
    options.GroupNameFormat = "'v'VVV"; 
    options.SubstituteApiVersionInUrl = true; 
});

var app = builder.Build();

// ...other code here

app.Run();

How does the code above work? Let’s break it down step by step.

builder.Services.AddApiVersioning(options => { ... })

This line adds and configures the API versioning service, allowing your API to support multiple versions. Here’s what the inner options do:

  • AssumeDefaultVersionWhenUnspecified = true: If a client request doesn’t specify an API version, the application assumes the default version defined below.
  • DefaultApiVersion = new ApiVersion(1, 0): Sets the default API version to v1.0. If no version is specified in the request, the system will default to this version.
  • ReportApiVersions = true: This enables the reporting of API versions in the response headers. When enabled, clients can see which versions of the API are supported.
.AddApiExplorer(options => { ... })

This line adds and configures API versioning to integrate with the API explorer, which is often used in Swagger documentation. The options specified here are:

  • SubstituteApiVersionInUrl = true: When set to true, the API version will be substituted into the route template. For example, if your route template includes {version}, it will be replaced by the actual API version in the request.
  • GroupNameFormat = “‘v’VVV”: Defines the format of the version group names used in Swagger. The "v" is a literal, and VVV is replaced by the API version number (e.g., v1, v2).

As you can see, the setup is quite simple. Now, let’s dive into each versioning method in more detail.

URL Versioning

URL path versioning, or simply URL versioning, is a method where the API version is embedded directly in the URL path. This technique is intuitive for developers since the version number is part of the endpoint’s URL structure.

In this versioning type to call a different versions of the API you must specify the version in the URL itself. Example:

GET https://api.dotnethow.net/api/v1.0/items
GET https://api.dotnethow.net/api/v2.0/items

As you can see, the version of the API is determined by the vx.y value you include in the URL, allowing you to call different versions. To illustrate this, I will create a controller named ItemsController that supports both versions. Inside this controller, there will be two actions: one for v1 and another for v2. The controller would look like this:

[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[Controller]")]
[ApiController]
public class ItemsController : ControllerBase
{
    [HttpGet]
    [MapToApiVersion("1.0")]
    public IActionResult GetV1() => Ok("Version 1.0 - Items");

    [HttpGet]
    [MapToApiVersion("2.0")]
    public IActionResult GetV2() => Ok("Version 2.0 - Items");
}

Now let us break down this code line by line

[ApiVersion("1.0")]
[ApiVersion("2.0")]

These two lines define that this controller will support two versions, v1.0 and also the v2.0, if you try to call it with a different version it will be ignored. Another important line is the route line:

[Route("api/v{version:apiVersion}/[Controller]")]

This line defines that the route will include an API version as part of the URL, where {version:apiVersion} is a placeholder for the version number (like v1 or v2), and [Controller] will be replaced by the controller’s name, which in this example is Items (ItemsController). But, how does the ruoting mechanism know which action (or api endpoint) to call?

Let check one of them out

[HttpGet]
[MapToApiVersion("1.0")]
public IActionResult GetV1() => Ok("Version 1.0 - Items");
  • [HttpGet]: Indicates this method responds to HTTP GET requests.
  • [MapToApiVersion(“1.0”)]: Maps this method to version 1.0 of the API. This means when the client requests v1 in the URL (api/v1/Items), this method will be executed.
  • Ok(“Version 1.0 – Items”): Returns an HTTP 200 (OK) response with the message “Version 1.0 – Items”.

The second API endpoint (action) is similar, but it is mapped to version 2.0. Once you run the project, you can access both versions. Since we specified in Program.cs that the GroupNameFormat = "'v'VVV", you can call the v1 action using the following patterns:

  1. api/v1/items
  2. api/v1.0/items

You can get the source code from here: https://github.com/etrupja/ApiVersioning.Demo – branch name: versioning-url

Query String Versioning

In query string versioning, instead of passing the version in the URL path, you include it in the query string or request URL. For example

GET /api/books?api-version=1.0
GET /api/books?api-version=2.0

To enable query string versioning, you need to update Program.cs with the appropriate configuration. The updated code should look like this:

//Add API Versioning support
builder.Services.AddApiVersioning(options =>
{
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.ReportApiVersions = true;
    options.ApiVersionReader = new QueryStringApiVersionReader("api-version");  // Use query string to pass the version
}).AddApiExplorer(options => {
    options.GroupNameFormat = "'v'VVV"; 
    options.SubstituteApiVersionInUrl = true; 
});

The line

options.ApiVersionReader = new QueryStringApiVersionReader("api-version");

configures ASP.NET Core to look for the API version in the query string using the parameter api-version, which is essential for setting up query string versioning.

The api-version keyword isn’t mandatory, but it needs to match whatever parameter you plan to use in the URL. In our example, since we want the request to look like this:

GET /api/books?api-version=1.0

you must pass api-version as the QueryStringApiVersionReader parameter.

Now, I will create a controller named BooksController, which will look like this:

[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/[controller]")]
[ApiController]
public class BooksController : ControllerBase
{
    // Action for version 1.0
    [HttpGet]
    [MapToApiVersion("1.0")]
    public IActionResult GetV1()
    {
        return Ok("Version 1.0 - Books");
    }

    // Action for version 2.0
    [HttpGet]
    [MapToApiVersion("2.0")]
    public IActionResult GetV2()
    {
        return Ok("Version 2.0 - Books");
    }
}

Now let us break down this code line by line:

[ApiVersion("1.0")]
[ApiVersion("2.0")]

These attributes indicate that this controller supports both version 1.0 and version 2.0 of the API. These versions will be matched based on the versioning mechanism configured which in this case is the query string. When a request specifies version 1.0 or 2.0 it will be routed to this controller.

The [MapToApiVersion(“x.y”)] attribute maps a particular action to version x.y of the API. So, if the request specifies API version 1.0 (ex: via query string or URL), the method that is decorated with [MapToApiVersion(“1.0”)] will be executed.

If I run the project and call the api endpoint using any of the following:

[HttpGet] api/books?api-version=1
[HttpGet] api/books?api-version=1.0

I will get the following response

Version 1.0 - Books

You can get the source code from here: https://github.com/etrupja/ApiVersioning.Demo – branch name: versioning-query

Header Versioning

Now, let us talk about another important versioning type, which is header versioning. As the name already indicates the api version is passed in the request header, not in the url (url versioning) or in the url as a query (query versioning). A typical request would look like this:

GET /api/books
Host: yourserver.com
api-version: 1.0

GET /api/books
Host: yourserver.com
api-version: 2.0

As always the first thing to do is to add support for this versioning type. So, in Program.cs you need to replace this line

options.ApiVersionReader = new QueryStringApiVersionReader("api-version");

with this other one

options.ApiVersionReader = new HeaderApiVersionReader("api-version");

This line configures the API to read the version from a custom HTTP header called api-version. api-version can be any text, but for this to work you have to pass the same text value in the request header as well. Now, the complete code would look like this

//Add API Versioning support
builder.Services.AddApiVersioning(options =>
{
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.ReportApiVersions = true;
    options.ApiVersionReader = new HeaderApiVersionReader("api-version"); 
}).AddApiExplorer(options => {
    options.GroupNameFormat = "'v'VVV"; 
    options.SubstituteApiVersionInUrl = true; 
});

So, to support header versioning, we had to replace the existing query string versioning configuration line. But what if you want to support both? In .NET, that’s possible. You simply need to modify the line to:

options.ApiVersionReader = new HeaderApiVersionReader("api-version");

to this

// Combine Query String and Header versioning
    options.ApiVersionReader = ApiVersionReader.Combine(
        new QueryStringApiVersionReader("api-version"),  // Query string versioning
        new HeaderApiVersionReader("api-version")        // Header versioning
    );

Now, I will just run my project and try to call the version 1.0 with query string and the version 2.0 with header version using Postman. The result looks like below

Example requests
Query string versioning (left) and Header versioning (right)

You can get the source code from here: https://github.com/etrupja/ApiVersioning.Demo – branch name: versioning-header

Media Type Versioning

To support Media Type versioning (also known as Content-Type or Accept header versioning) in your API, the version of the API is specified in the media type, typically in the Accept or Content-Type HTTP headers. A typical request would look like this:

GET /api/books
Host: yourserver.com
Accept: application/json;v=1.0

GET /api/books
Host: yourserver.com
Content-Type: application/json;v=2.0

As always the first thing to do is to add support for this versioning type. Now that we know we can support multiple versioning types at the same time, we are just going to add support to media type versioning as well. For that I am going to modify the Program.cs configuration section to look like this:

builder.Services.AddApiVersioning(options =>
{
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.ReportApiVersions = true;

    // Combine Query String and Header versioning
    options.ApiVersionReader = ApiVersionReader.Combine(
        new QueryStringApiVersionReader("api-version"),  // Query string versioning
        new HeaderApiVersionReader("api-version"),        // Header versioning
        new MediaTypeApiVersionReader("v")  // Media type versioning (expects version as 'v')
    );

}).AddApiExplorer(options => {
    options.GroupNameFormat = "'v'VVV"; 
    options.SubstituteApiVersionInUrl = true; 
});

The line that was added here is

new MediaTypeApiVersionReader("v")  // Media type versioning (expects version as 'v')

and this line configures the API to extract the version number from the media type in the Accept or Content-Type header. The version is specified using a parameter like v in the media type (ex: application/json;v=1.0).

Now, when you run the project, you might encounter a 400 error, which is expected because the initial request doesn’t include any of the required media types. To verify that everything is working correctly, you can use a tool like Postman.

In Postman, I will pass the version in the Accept and Content-Type headers and observe the results.

For the Content-Type header, I set the version to 1.0, and the result was as follows

Media Type Versioning – Content-Type 1.0

and for Accept header I set the version to 2.0, and the result was as follows

Media Type Versioning – Accept 2.0

As you can see, everything has worked as expected. You can get the source code from here: https://github.com/etrupja/ApiVersioning.Demo – branch name: versioning-media-type

Conclusion

API versioning is a really important feature that allows developers to improve their APIs without breaking existing features. .NET supports multiple versioning methods, including URL Path, Query String, Header, and Media Type versioning, each with its own advantages. Query String and URL Path versioning are easy to implement and highly visible, while Header and Media Type versioning provide more flexibility and clean URLs by shifting versioning responsibility to HTTP headers. Choosing the right versioning approach depends on the specific requirements and preferences of the API consumers.


Enjoyed this post? Subscribe to my YouTube channel for more great content. Your support is much appreciated. Thank you!


Check out my Udemy profile for more great content and exclusive learning resources! Thank you for your support.
Ervis Trupja - Udemy



Enjoyed this blog post? Share it with your friends and help spread the word! Don't keep all this knowledge to yourself.