.NET Nakama

Improving your .NET skills

All about Web API Versioning in ASP.NET Core

December 04, 2021 (~15 Minute Read)
API DESIGN VERSIONING SEMANTIC POSTMAN ORGANIZE CODE ASP.NET CORE

Introduction

Before releasing an API, we should consider how to release future improvements and possible breaking changes. For example, it is most likely that something would have to change in our APIs, such as the business requirements, resources and their relationships, third-party APIs, etc.

In general, versioning is the process of assigning a unique version (based on a specific format) per software release. The most widely used versioning format is the Semantic versioning (also known as SemVer) which is defined as follows: X.Y.Z (Major.Minor.Patch).

  • Major: A version for incompatible API changes.
  • Minor: A version for adding backward compatible functionalities.
  • Patch: A version for making backward compatible bug fixes.

Providing versioning in Web APIs (i.e., services) is more complex in compassion to API libraries (e.g., NuGet libraries). That is because multiple implementations (and versions) of the Web API (including their dependencies, e.g., third-party systems, databases, etc.) should be maintained and be available to the consumers. The consumers can request a specific API version by defining the version in the URI, query-string, HTTP header, or media type.

As we can understand, serving and maintaining multiple implementations of the Minor and Patch versions are not practical for Web APIs because they are backward compatible (assuming we are using the Semantic versioning). Supporting and maintaining multiple implementations of the Major versions of our APIs is a way to control how old consumers coexist with new ones.

The advice of Roy Thomas Fielding (creator or the REST architectural style on how to approach versioning in Web APIs is “Don’t” (Fielding R.T., 2013). In a very interesting interview (Fielding R.T., 2014), he provides additional information about versioning and HATEOAS in Web APIs.

From my point of view, the main question to recognize the need for versioning (Major) in a Web API is if the old consumers can coexist with the new ones. In such a case, we can provide major versioning not to break or replace already deployed components. However, we should decide carefully how many versions we will serve and maintain and how we will deprecate them. So, we will need a versioning plan 🙂.

In in-house software, where we have control over the consumers and servers (on the code and release process), we might decide not to use major versioning in our API. That is not something terrible. For example, when our consumers are internal Web APIs that we can release together or a small downtime is acceptable.

However, even in in-house software, we should consider the Single Page Applications (SPAs) cases in which different released versions can coexist on the consumer browsers. Based on our project, we should decide if it’s possible to force a refresh on the user’s web page or let the users decide when to trigger it.

In this article, we will learn the most commonly used versioning mechanisms and how we can apply them in our Web APIs by using .NET Core. Furthermore, we will see two strategies to organize our controller actions and code files. Finally, we will use Postman to perform API requests for the examined versioning mechanisms.

Versioning Mechanisms

The following table shows several versioning mechanisms that we can use to provide versioning in our Web APIs. It is important to note that the decision of the versioning mechanism influences the use of client caching and HATEOAS.

Version Mechanisms Description Example Client Caching Friendly
New Route Versioning An easy to implement but difficult to maintain solution in both consumers (clients) and server code. From: https://api.mydomain.tld/products/
To: https://api.mydomain.tld/productsv2/
Yes
URI Versioning The most commonly used versioning, in which we can add a version (e.g., number) to the API base URL. https://api.mydomain.tld/v2/products/ Yes
Query String Versioning The version is provided by using a query string parameter, such as “api-version”. https://api.mydomain.tld/products/?api-version=2 Yes (in most cases)
Custom Header Versioning Α custom header to indicate the version (e.g., API-Version, Accept-Version, etc.). GET https://api.mydomain.tld/products
Accept-Version: 2
No
Media Type Versioning The Accept header is used to indicate the version. GET https://api.mydomain.tld/products
Accept: application/json;api-version=2.0
No

Apply Versioning at Existing ASP.NET Core Web API Project

ASP.NET Core provides all the tools that we need to apply all the mentioned versioning mechanisms. Let’s assume that we are would like to apply versioning to an existing project. So, the current API calls should work whether we specify a version or not.

Step 1: Install the Versioning Package Reference

We should install the Microsoft.AspNetCore.Mvc.Versioning package reference either by using the NuGet Package Manager in Visual Studio or using the .NET CLI (check more options here). To install it using the NuGet Package Manager (Figure 1), follow the steps:

  1. Right-click on your Web API project and then click on “Manage NuGet Packages…”.
  2. Search with the term “Microsoft.AspNetCore.Mvc.Versioning”.
  3. Click on the Install button.
  4. Click OK to the Preview Changes window and Accept the License.
How to install the ""Microsoft.AspNetCore.Mvc.Versioning" package.
Figure 1. - How to install the "Microsoft.AspNetCore.Mvc.Versioning" package.

Step 2: Configure the Versioning Services

To configure the API versioning properties of our project, such as return headers, version format, etc. I would recommend creating an extension method, such as the following. In this way, we would keep our Startup.cs (or Program.cs in .NET 6.0) file cleaner and readable.

public static class ConfigureApiVersioning
{
    /// <summary>

    /// Configure the API versioning properties of the project, such as return headers, version format, etc.

    /// </summary>

    /// <param name="services"></param>

    public static void AddApiVersioningConfigured(this IServiceCollection services)
    {
        services.AddApiVersioning(options =>
        {
            // ReportApiVersions will return the "api-supported-versions" and "api-deprecated-versions" headers.

            options.ReportApiVersions = true;

            // Set a default version when it's not provided,

            // e.g., for backward compatibility when applying versioning on existing APIs

            options.AssumeDefaultVersionWhenUnspecified = true;
            options.DefaultApiVersion = new ApiVersion(1, 0);

            // Combine (or not) API Versioning Mechanisms:

            options.ApiVersionReader = ApiVersionReader.Combine(
                // The Default versioning mechanism which reads the API version from the "api-version" Query String paramater.

                new QueryStringApiVersionReader("api-version"),
                // Use the following, if you would like to specify the version as a custom HTTP Header.

                new HeaderApiVersionReader("Accept-Version"),
                // Use the following, if you would like to specify the version as a Media Type Header.

                new MediaTypeApiVersionReader("api-version")
            );
        });

        // Here, we will add another service, e.g., to support versioning on our documentation.

    }
}

In this example extension method, we perform all the configurations regarding versioning based on our needs. So, in this example, we:

  • Enable the ReportApiVersions to let .NET return lists of our API’s supported and deprecated versions as HTTP headers (api-supported-versions and api-deprecated-versions, respectively). In this way, our API consumers can be informed by the API for any change in versions.
  • Set a default API version when the consumers do not provide it (e.g., v1.0). In our case, we are assuming that we are applying versioning to an existing project. Thus, our API should be backward compatible for the consumers that do not define the version.
  • Read the version from multiple sources, such as query string, custom HTTP header, and Media Type header. However, we could select only one or none of them. Moreover, reading the version from our API base URL (URI Versioning) is not configured here. Thus, we could remove this part of the code if we would like to support only the URI Versioning.

Step 3: Register the Versioning Services

In this step, we should register the API versioning services in the Startup.cs or Program.cs file depending on the used style (current or .NET 6.0). That’s an easy task because of our extension method. Just add the services.AddApiVersioningConfigured(); line in the ConfigureServices method, as shown below.

// Startup.cs file in the current style

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddApiVersioningConfigured();
}
// Program.cs file in .NET 6.0 new style


// Add services to the container.

builder.Services.AddControllers();
builder.Services.AddApiVersioningConfigured();

Step 4: Apply Versioning

Applying versioning either to an existing or new project is relatively the same. In both cases, we will have to define the API version(s) of the controller by using the ApiVersion attribute (e.g., [ApiVersion("1.0")]). Depending on our chosen versioning mechanism, the main difference is the route or routes that we would specify.

In the following code example, we are specifying two routes on the controller.

  • [Route("api/v{version:apiVersion}/[controller]")]: This route is specified to support URI Versioning on new or existing controllers, such as:
    • api/v1/weatherforecast
    • api/v1.0/weatherforecast
    • api/v2.0/weatherforecast
  • [Route("api/[controller]")]: This route could be used in an existing implementation, in which we would like to support versioning (e.g., Query string and HTTP header versioning) or just used for backward compatibility.
// List of available and deprecated Versions for this ApiController. [ApiVersion("1.0")]


// Specify the base (or current) API route to:

// - Keep the existing route serving a default version (backward compatible).

// - Support query string and HTTP header versioning.

[Route("api/[controller]")]

// Specify the route to support URI Versioning. URI example: api/v1/weatherforecast

[Route("api/v{version:apiVersion}/[controller]")]
public class WeatherForecastController : ControllerBase
{
    // Our controller's code...

}

Strategies to Organize Controllers and Actions

So, we have decided to support and maintain multiple implementations of the Major versions of our API. First, however, we could organize our controller actions and code files, either by the version or in the controller. You can find the following examples in GitHub.

Organize Controllers by Version

In this strategy, we group the files related to each version, such as Controllers, Data Transfer Objects (DTOs), Extensions, etc. For example, in Figure 2, we see how we could organize the files per version. We can observe that each controller uses the DTOs of its version, which can be entirely different (Figure 3).

As we can understand, the drawback of this strategy is that if only a small part of the project has Major changes (i.e., not backward compatible), it will result in a lot of duplicate code. So, we can use this strategy when we are not releasing Major changes very often.

Example of organizing controllers by version.
Figure 2. - Example of organizing controllers by version.
Example of different DTOs per version.
Figure 3. - Example of different DTOs per version.

Organize Versioning in the Controller

When we organize the versioning in the controller, all different versions of the same functionality (action) are in the same controller. In this case, we use the ApiVersion attribute multiple times in the controller to list the available and deprecated versions. Furthermore, the MapToApiVersion attribute is used per action to map the API version with the specific implementation. In Figure 4, we can see how we can organize the versioning in the controller.

As we can see, if an action doesn’t have breaking changes, we can map it to multiple versions. For the sake of the example, let’s assume that the GetById should return the WeatherForecast class. Thus, this strategy has the flexibility to perform Major changes only for some parts of the project. However, it may lead to complex code and, thus, difficult to maintain, e.g., when multiple versions exist, removing deprecated code, sharing the same DTOs, etc.

Example of organizing the versioning in the controller.
Figure 4. - Example of organizing the versioning in the controller.

API Requests with Versioning: Postman Examples

This section demonstrates how we can perform API requests for each versioning mechanism by using Postman. The presented code examples, which use the mentioned versioning mechanisms, can be found in GitHub. In addition, you can download the Postman collection for the given examples from here.

Supported and Deprecated Versions Headers

The .NET Platform
Figure 5. - Postman Example of supported and deprecated versions headers.

URI Versioning

The .NET Platform
Figure 6. - Postman Example of URI Versioning.

Query String Versioning

The .NET Platform
Figure 7. - Postman Example of Query String Versioning.

Custom Header Versioning

The .NET Platform
Figure 8. - Postman Example of Custom Header Versioning.

Media Type Versioning

The .NET Platform
Figure 9. - Postman Example of Media Type Versioning.

Summary

Versioning is the acceptance that improvements and breaking changes will occur in our project. For that reason, we are assigning a unique version per software release (usually based on the Semantic versioning format: Major.Minor.Patch).

When we release Major (breaking) changes in Web APIs, the current consumers (clients) could coexist with the new ones. In that case, we should decide carefully how many versions we will serve and maintain and how we will deprecate them. So, we will need a versioning plan 🙂.

The most commonly used versioning mechanisms use the URI, Query String, Custom Header, and Media Type. It is important to notice that the decision of the versioning mechanism influence the use of client caching and HATEOAS. Finally, we saw how the examined versioning mechanisms can be applied using .NET Core and how we can perform API requests using Postman.

Furthermore, we saw two strategies to organize our controller actions and code files, either by the version or in the controller. If we are not sure which strategy is appropriate for our project, we could start by organizing our controller actions and code in the controller (e.g., for V1). Then, we can decide when the “V2” requirements are available.

References

If you liked this article (or not), do not hesitate to leave comments, questions, suggestions, complaints, or just say Hi in the section below. Don't be a stranger 😉!

Dont't forget to follow my feed and be a .NET Nakama. Have a nice day 😁.