How To Support Multiple Versions of ASP.NET Core Web API

You are currently viewing How To Support Multiple Versions of ASP.NET Core Web API

Rolling out new features and enhancements without breaking the existing systems is one of the important aspects of mature and well-crafted Web Services and APIs. We as developers, also commonly need to provide additional features to selected customers and it is important to learn how to support multiple versions of Web APIs to keep everyone happy. In this tutorial, I will show you different techniques for implementing versioning in ASP.NET Core Web APIs. You will learn how to run multiple versions in parallel and how to use a specific version using the version information in query strings, request URIs, HTTP request headers, or media types.

Implement the First Version of ASP.NET Core Web API

I am a big fan of doing things right the first time so I will create a proper folder structure for each version of the API. Every model, service, or API controller will be created in a folder named V1 that will be available inside folders such as Models, Services, and Controllers. This will make it easier for everyone to work on a file related to a specific API version in the future. The solution explorer will look something like this after the first version is complete.

ASP.NET Core Web API Version 1 Solution Explorer View

Let’s create a new ASP.NET Core Web API project in Visual Studio 2019 and add Models and Services folders in the project. Inside both the Models and Services folders, create a folder named V1. Create the following Product class inside Models\V1 folder.

Product.cs

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Next, create the following IProductService and ProductService classes in Services\V1 folder and return some fake products from the service.

IProductService.cs

public interface IProductService
{
    public List<Product> GetProducts();
}

IProductService.cs

public class ProductService : IProductService
{
    public List<Product> GetProducts()
    {
        return new List<Product>()
        {
            new Product()
            {
                Id = 1,
                Name = "Wireless Mouse",
                Price = 29.99m 
            },
            new Product()
            {
                Id = 2,
                Name = "HP Headphone",
                Price = 79.99m 
            },
            new Product()
            {
                Id = 3,
                Name = "Sony Keyboard",
                Price = 119.99m 
            }
        };
    }
}

Next, create an ASP.NET Core Web API controller named ProductsController in Controllers\V1 folder and simply inject and call the IProductService in the controller to return the products from API.

ProductsController.cs

[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsController(IProductService productService)
    {
        _productService = productService;
    }

    [HttpGet]
    public IActionResult GetProducts()
    {
        return Ok(_productService.GetProducts());
    }
}

Register the Service in Startup.cs file as shown below.

Startup.cs

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

    services.AddScoped<Services.V1.IProductService, Services.V1.ProductService>();
}

Open the browser and try to access the first version of the API at api/products URL and we should be able to see the products returned from API in JSON format.

ASP.NET Core Web API Versioning  - Version 1

Our first API version is up and running and clients are happy but let’s say after some time we have a new requirement from one of the new clients that we also need to return orders information with every product. Of course, we can update the same V1 API and can return the orders information but we don’t want to break our existing client’s systems by introducing this new feature. It is much better option to let old clients use the same V1 API and implement a new V2 version for new clients.

READ ALSO:  A Step by Step Guide to In-Memory Caching in ASP.NET Core

Implement the Second Version of ASP.NET Core Web API

Create new folders with the name V2 in Models, Services, and Controllers. We will create new versions of our model, service, or API controller in the V2 folders we just created. The solution explorer will look something like this after the second version implementation.

ASP.NET Core Web API Version 2 Solution Explorer View

Create the following Order and Product classes inside Models\V2 folder. The new Product class will have one additional property Orders to support the new feature.

Order.cs

public class Order
{
    public int Id { get; set; }
    public string OrderNo { get; set; }
    public DateTime OrderDate { get; set; }
    public string Status { get; set; }
}

Product.cs

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }

    public List<Order> Orders { get; set; }
}

Next, create the following IProductService and ProductService classes in Services\V2 folder and return some fake products along with the information of their order from the service. The reason we are creating a separate interface V2 folder is that this new interface can have the same or different methods than what we have in the V1 service.

IProductService.cs

public interface IProductService
{
    public List<Product> GetProducts();
}

ProductService.cs

public class ProductService : IProductService
{
    public List<Product> GetProducts()
    {
        return new List<Product>()
        {
            new Product()
            {
                Id = 1,
                Name = "Wireless Mouse",
                Price = 29.99m, 
                Orders = new List<Order>()
                {
                    new Order()
                    {
                        Id = 1,
                        OrderNo = "12345",
                        OrderDate = DateTime.Today.AddDays(-2),
                        Status = "Pending" 
                    },
                    new Order()
                    {
                        Id = 2,
                        OrderNo = "67890",
                        OrderDate = DateTime.Today.AddDays(-5),
                        Status = "Completed" 
                    }
                }
            },
            new Product()
            {
                Id = 2,
                Name = "HP Headphone",
                Price = 79.99m, 
                Orders = new List<Order>()
                {
                    new Order()
                    {
                        Id = 3,
                        OrderNo = "13579",
                        OrderDate = DateTime.Today.AddDays(-7),
                        Status = "Completed" 
                    }
                }
            },
            new Product()
            {
                Id = 3,
                Name = "Sony Keyboard",
                Price = 119.99m 
            }
        };
    }
}

Next, create a new ASP.NET Core Web API controller named ProductsController in Controllers\V2 folder and return the products from API.

ProductsController.cs

[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsController(IProductService productService)
    {
        _productService = productService;
    }

    [HttpGet]
    public IActionResult GetProducts()
    {
        return Ok(_productService.GetProducts());
    }
}

Register the V2 Service in Startup.cs file in the same way we register the V1 service earlier.

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

    services.AddScoped<Services.V1.IProductService, Services.V1.ProductService>();
    services.AddScoped<Services.V2.IProductService, Services.V2.ProductService>();
}

We now have two versions of product API and when we will try to access the API in the browser we will get the following error. The error specifies that the request is matching with multiple endpoints and the framework failed to decide which GetProducts method to call. If we want to support multiple API versions, we have to solve this problem.

ASP.NET Core Web API Multiple Versions Error Request Match Multiple Endpoints

Support Multiple API Versions using Query Strings

To support multiple versions in Web APIs, we need to download a Microsoft library called Microsoft.AspNetCore.Mvc.Versioning and we can install it in our project using NuGet package manager. You can read the complete guide about this library here.

Once the library is installed and restored, we need to configure it in Startup.cs file by calling the AddApiVersioning method in the ConfigureServices method.

Startup.cs

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

    services.AddScoped<Services.V1.IProductService, Services.V1.ProductService>();
    services.AddScoped<Services.V2.IProductService, Services.V2.ProductService>();

    services.AddApiVersioning();
}

Next, we need to specify the versions with both V1 and V2 ProductsController classes. Open the ProductsController we added in the V1 folder and add the [ApiVersion(“1.0”)] attribute on top of the class.

V1\ProductsController.cs

[ApiVersion("1.0")]
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsController(IProductService productService)
    {
        _productService = productService;
    }

    [HttpGet]
    public IActionResult GetProducts()
    {
        return Ok(_productService.GetProducts());
    }
}

Similarly, open the ProductsController we added in the V2 folder and add the [ApiVersion(“2.0”)] attribute on top of the class.

READ ALSO:  Observer Design Pattern in ASP.NET Core

V2\ProductsController.cs

[ApiVersion("2.0")]
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsController(IProductService productService)
    {
        _productService = productService;
    }

    [HttpGet]
    public IActionResult GetProducts()
    {
        return Ok(_productService.GetProducts());
    }
}

That’s all we need to support versioning using this wonderful library provided by Microsoft. By default, it supports versioning from query strings so if we want to use a specific version of the API, we need to specify the version in the query string using the api-version parameter. For example, if we will pass the api-version=1.0 then we will see the response from the ProductsController we implemented in the V1 folder with [ApiVersion(“1.0”)] attribute.

ASP.NET Core Web API Version 1 - Specify Versions in Query String

If we will pass the api-version=2.0 then we will see the response from the ProductsController we implemented in the V2 folder with [ApiVersion(“2.0”)] attribute.

ASP.NET Core Web API Version 2 - Specify Versions in Query String

Specify the Default API Version for Clients

We are now supporting both old and new clients and clients can specify the version in the query string to use both versions of the API as per their requirements. What if any client doesn’t specify the version or forgets to specify the version? The clients will see the following error message in response. This is not what we want our clients to receive in the response.

ASP.NET Core Web API Versioning - API Version Not Specified in Query String

We need to specify either version 1.0 or version 2.0 as the default version so that the default version API response is sent to clients instead of the above error. The version library provides us many options to customize the API versioning as per our need. To get rid of the above error, we can set AssumeDefaultVersionWhenUnspecified to true. This option will be used to serve the request without a version. The assumed API version by default would be 1.0.

services.AddApiVersioning(option =>
{
    option.AssumeDefaultVersionWhenUnspecified = true; 
});

Now the version 1.0 API will send the response if we will not specify any version in the query string.

ASP.NET Core Web API Versioning - Set Version 1 As Default Version

If we want to specify version 2.0 as the default version, we need to set the DefaultApiVersion option as shown below.

services.AddApiVersioning(option =>
{
    option.AssumeDefaultVersionWhenUnspecified = true;
    option.DefaultApiVersion = new ApiVersion(2, 0);
}

Now the clients will see the version 2.0 API response by default if they will not specify any version in the query string.

ASP.NET Core Web API Versioning - Set Version 2 As Default Version

Support Multiple API Versions using URL Path

Passing version information in query strings is quite a painful task when we have long URLs or we also have other query strings parameters in the URL. In this situation, we can configure versioning in such a way that the version information can become part of the URL path. Once we have this configuration in place, we can call APIs by passing the version information in the URL as follows:

  • api/1.0/products
  • api/2.0/products

To configure versioning using the URL path, we need to update the [Route] attributes of our controllers. Let’s open the V1 ProductsController and update the [Route] parameter as shown below:

V1\ProductsController.cs

[ApiVersion("1.0")]
[Route("api/{version:apiVersion}/[controller]")]
[ApiController]
public class ProductsController : ControllerBase

We also need to update the [Route] attribute of V2 ProductsController in a similar manner.

V2\ProductsController.cs

[ApiVersion("2.0")]
[Route("api/{version:apiVersion}/[controller]")]
[ApiController]
public class ProductsController : ControllerBase

That’s all you need to configure URL path versioning. We can now specify version 1.0 or 2.0 in the URL path and the URL looks less ugly.

ASP.NET Core Web API Version 1 - Specify Versions in Request URL

Support Multiple API Versions using HTTP Headers

In both the above approaches, we have to modify the API URL to support versioning and we have to document the URL information in API docs for all the clients. If we want our API URLs to stay clean and we don’t want clients to modify them for a specific version, then we can pass version information in the request HTTP Header. To configure the support of HTTP header versioning, we need to configure the ApiVersionReader option in the Startup.cs file. The HeaderApiVersionReader represents a service API version reader that reads the value from HTTP headers.

services.AddApiVersioning(option =>
{
    option.AssumeDefaultVersionWhenUnspecified = true;
    option.DefaultApiVersion = new ApiVersion(1, 0);
    option.ApiVersionReader = new HeaderApiVersionReader("api-version");
});

Once the above configuration is in place, we can pass api-version header in the API request as shown below.

READ ALSO:  Working with Packages and Libraries in ASP.NET Core
ASP.NET Core Web API Version 1 - Specify Versions in HTTP Headers

If we want to call version 2.0 API, we can pass the value 2.0 in the header.

ASP.NET Core Web API Version 2 - Specify Versions in HTTP Headers

Please note that if you will specify the version in both query string and HTTP header then the HTTP header version will override the query string version as you can see in the following example where version 1.0 API response is returned and the version 2.0 we specify in the query string is ignored.

ASP.NET Core Web API Versioning - HTTP Header Version Override Query String Version

REST APIs support content negotiation, which is the mechanism used for serving different resources at the same URI, so that the user agent can specify which resource is most suited for the user (for example, which language of a document, which image format, or which content encoding). We can pass custom information such as version in the parameters used in media types for content negotiation and we can configure our APIs to read this version information from the media type parameters.

To configure the versioning support by media type parameters, we once again need to configure the ApiVersionReader option in the Startup.cs file but this time we need to use MediaTypeApiVersionReader. The MediaTypeApiVersionReader represents a service API version reader that reads the value from a media type HTTP header in the request.

services.AddApiVersioning(option =>
{
    option.AssumeDefaultVersionWhenUnspecified = true;
    option.DefaultApiVersion = new ApiVersion(1, 0);
    option.ApiVersionReader = new MediaTypeApiVersionReader(); 
});

Once the above configuration is in place, we can pass version 1.0 in Accept header parameters as shown below.

ASP.NET Core Web API Version 1 - Specify Versions in Media Type Accept Header

If we want to call version 2.0 API, we can pass the value 2.0 in the Accept header.

ASP.NET Core Web API Version 2 - Specify Versions in Media Type Accept Header

Display Supported API Versions to Clients

Sometimes, it’s useful to let the clients know about the supported API versions and we can do this easily using the ReportApiVersions setting.

services.AddApiVersioning(option =>
{
    option.AssumeDefaultVersionWhenUnspecified = true;
    option.DefaultApiVersion = new ApiVersion(1, 0);
    option.ReportApiVersions = true; 
});

When the value of ReportApiVersions is set to true, the APIs return supported versions information in the response header as shown below.

ASP.NET Core Web API Versioning - Display Supported Version in HTTP Response Header

Display Deprecated API Versions to Clients

When we implement and support multiple versions of the API, some old versions eventually deprecate over time. We need to let our clients know which versions are deprecated and will not be available in the future. To mark one or more API versions have been deprecated, we simply need to decorate our controller with the Deprecated property.

[ApiVersion("1.0", Deprecated = true)]
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase

If we will run our version 1.0 API, we will see the api-deprecated-versions: 1.0 information in the response header.

ASP.NET Core Web API Versioning  - Specify Old API Versions Deprecated

Summary

Implementing and supporting multiple versions of the APIs help in rolling out the enhanced or custom features in a more effective way. Multiple developers can work on different versions of the API and it’s easy to track changes. The Microsoft.AspNetCore.Mvc.Versioning library we covered in this post is a feature-rich library and it has many more features than what we covered in this tutorial. If you are interested to learn more about this library or you have some specific versioning requirements then I will recommend you to read more about this library here.

This Post Has One Comment

  1. Jason

    Absolutely brilliant tutorial. Thank you. So glad I subscribed to Ezzylearning newsletters. This one is really helpful.

Leave a Reply