Table of Contents
- Introduction
- Benefits of API Versioning
- Things to keep in mind while Versioning
- How to implement API Versioning in ASP.NET Core
- Modify API Controller
- How do we pass the API Version Information?
- Reading from More than one Sources
- How to integrate API Versions with Swagger UI
- Adding SwaggerUI
- Solution
- Conclusion
Introduction
Imagine a scenario where we need to make changes to our API in its structure or in its functionality, but at the same time ensure that the existing API clients shouldn’t face any issues.
We would want to upgrade our API, but ensure that it’s still backward compatible.
How’d we solve this problem? Answer – Versioning.
We can simply mark our existing APIs as the current (older version), while all the changes we intend to do on the APIs move to the next (or the latest) version.
Benefits of API Versioning
This solves two problems for us. Firstly, we no longer need to worry about backward compatibility because the current state of the APIs still exists (or coexists). Therefore, our clients won’t face any issues.
Secondly, we gain the capability to provide optional functionalities to our clients in the form of versions.
Clients can subscribe to a particular version for new or improved functionalities or features without having to disrupt the existing ones.
Things to keep in mind while Versioning
When we are building versioning capabilities into our APIs, we must ensure the following –
- Whether the system can correctly parse the requested version.
- Whether the requests are being routed to the correct endpoints based on their versions – ensuring that, for example, a v2 request goes to the v2 endpoint.
- Choosing the right way to pass the expected version information in the requests.
How to implement API Versioning in ASP.NET Core
In ASP.NET Core, versioning APIs is a straightforward and simple process, thanks to the support for API versioning offered by the API Versioning library for ASP.NET Core.
In this article, let’s look into the details of how we can set up API Versioning for our applications, explore the commonly employed versioning strategies, and address a few issues we may encounter along the way.
For a clearer understanding, let’s consider the example of constructing a Heroes API that oversees all the registered Heroes in a Hero Association.
Let’s say we planned to create a Heroes API, and we aimed for simplicity with minimal complexity. We designed a Hero entity with attributes as follows –
public class Hero
{
public int Id { get; set; }
public string Name { get; set; }
public string[] Powers { get; set; }
public bool HasCape { get; set; }
public DateTime Created { get; set; }
public bool IsAlive { get; set; }
public Category Category { get; set; }
}
public enum Category
{
Anime,
Comic,
History,
Mythology
}
We used this Entity in our endpoint as below.
[ApiController]
[Route("api/[controller]")]
public class HeroesController : ControllerBase
{
private readonly IHeroesRepository _data;
public HeroesController(IHeroesRepository data)
{
_data = data;
}
[HttpGet]
public IEnumerable<Hero> Get()
{
return _data.All();
}
[HttpGet, Route("{id}")]
public Hero Get(int id)
{
return _data.Single(x => x.Id == id);
}
[HttpPost]
public Hero Post(Hero model)
{
return _data.Create(item);
}
}
The setup appeared to be functioning well, and we pushed the APIs to be used by the clients.
As the data size increased, we wanted to introduce additional functionalities into this API, such as searching by name.
We also wanted to modify the way requests were handled because the APIs were directly providing data from the Hero entity, which contained some unnecessary data for the clients to access.
But if we were to make such structural changes to the API already in production, the changes might cause issues with the clients.
We don’t want the new functionalities we intended to introduce to be readily available to all our clients; it was a feature requested by only some of them.
To solve this, we implement the API versioning mechanism in 3 steps –
- Install Nuget Package
- Register API Versioning service
- Decorate API Controllers with Version numbers
Install Nuget Package
Install the API Versioning library by adding the corresponding NuGet package –
dotnet add package Microsoft.AspNetCore.Mvc.Versioning
Register API Versioning service
Add the API Versioning service to the IServiceCollection. This addition provides the essential capabilities for detecting and routing requests based on their version.
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IHeroesRepository, HeroesRepository>();
services.AddControllers();
services.AddApiVersioning(setup =>
{
setup.DefaultApiVersion = new ApiVersion(1, 0);
setup.AssumeDefaultVersionWhenUnspecified = true;
setup.ReportApiVersions = true;
});
}
We configure the following within the AddApiVersioning() service:
- We set the DefaultApiVersion to specify where the requests should fallback.
- The AssumeDefaultVersionWhenUnspecified allows the router to fallback to the default version (specified by the DefaultApiVersion setting) when it cannot determine the requested API version.
The ReportApiVersions setting includes the available API versions in all responses, using the ‘api-supported-versions’ header.
api-supported-versions: 1.0, 2.0
Modify API Controller
We decorate the API Controller with the version numbers that it uses when a request is made for that specific version.
namespace Heroes.Api.Controllers.V2
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Route("api/[controller]")] // for backward compatibility
public class HeroesController : ControllerBase
{
// heroes CRUD operations
}
}
The [ApiVersion] describes which version the controller maps to, with the version specified in the route (in this case) using the fragment “v{version:apiVersion.”
This fragment incorporates the version number defined in the ApiVersion into the route during matching.
In this case, both “/api/v1.0/heroes/1” and “/api/v1/heroes/1” map to this controller.
An API version consists of a major and a minor version. Sometimes, the minor version is omitted as a convention, and it’s up to the API developers to decide how to specify the version.
When we mark a controller with both [Route(“api/v{version:apiVersion}/[controller]”)] and [Route(“api/[controller]”)], we ensure support for requests from older route templates. This is necessary because it’s an upgrade from regular routing, and we must ensure compatibility with the old route paths.
This means that both the requests /api/v1/heroes/1 and /api/heroes/1 are directed to the controller mentioned above.
When we need to introduce the same controller with additional features or modifications, we simply create a new controller (or extend the current one if no modifications are needed) and decorate the new controller with the next version number.
After versioning, the controllers look something like this:
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using SwaggerHeroes.Core.Data.Entities;
using SwaggerHeroes.Core.Data.Repositories;
using SwaggerHeroes.Core.Data.Services;
namespace SwaggerHeroes.Api.Controllers.V1
{
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
// [Route("api/[controller]")] // for backward compatibility
public class HeroesController : ControllerBase
{
private readonly ILogger<HeroesController> _logger;
private readonly IHeroesRepository _data;
public HeroesController(ILogger<HeroesController> logger, IDataService data)
{
_logger = logger;
_data = data.Heroes;
}
[MapToApiVersion("1.0")]
[HttpGet, Route("alive")]
public string Alive()
{
return "Captain, 1.0 Here. I'm Alive and Kicking!";
}
[MapToApiVersion("1.0")]
[HttpGet]
public IEnumerable<Hero> Get()
{
return _data.All();
}
[MapToApiVersion("1.0")]
[HttpGet, Route("{id}")]
public Hero Get(int id)
{
return _data.Single(x => x.Id == id);
}
[MapToApiVersion("1.0")]
[HttpPost]
public Hero Post(Hero model)
{
var item = new Hero
{
Category = model.Category,
HasCape = model.HasCape,
IsAlive = model.IsAlive,
Name = model.Name,
Powers = model.Powers
};
return _data.Create(item);
}
[MapToApiVersion("1.0")]
[HttpGet, Route("searchbyname")]
public IEnumerable<Hero> SearchByName(string name = "")
{
return _data.Search(x => x.Name.Contains(name));
}
}
}
namespace SwaggerHeroes.Controllers.V2
{
[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class HeroesController : ControllerBase
{
private readonly ILogger<HeroesController> _logger;
private readonly IHeroesRepository _data;
public HeroesController(ILogger<HeroesController> logger, IDataService data)
{
_logger = logger;
_data = data.Heroes;
}
[MapToApiVersion("2.0")]
[HttpGet, Route("alive")]
public string Alive()
{
return "Captain, 2.0 Here. I'm Alive and Kicking!";
}
[MapToApiVersion("2.0")]
[HttpGet]
public IEnumerable<Hero> Get()
{
return _data.All();
}
[MapToApiVersion("2.0")]
[HttpPost]
public Hero Post(HeroItem model)
{
var item = new Hero
{
Category = model.Category,
HasCape = model.HasCape,
IsAlive = model.IsAlive,
Name = model.Name,
Powers = model.Powers
};
return _data.Create(item);
}
[MapToApiVersion("2.0")]
[HttpGet, Route("searchbyname")]
public IEnumerable<Hero> SearchByName(string name = "")
{
return _data.Search(x => x.Name.Contains(name));
}
[MapToApiVersion("2.0")]
[HttpGet, Route("categories")]
public IEnumerable<string> Categories()
{
return (string[])Enum.GetNames(typeof(Category));
}
}
}
And they’re actually working fine:
GET 'https://localhost:5001/api/v1/heroes/alive'
Captain, 1.0 Here. I'm Alive and Kicking!
GET 'https://localhost:5001/api/v2/heroes/alive'
Captain, 2.0 Here. I'm Alive and Kicking!
How do we pass the API Version Information?
There are various approaches to pass version information to the API when requesting data.
services.AddApiVersioning(setup =>
setup.ApiVersionReader = // the source for the version to be extracted
);
We specify the source from which the ApiVersion service looks up version information when configuring it as follows –
- via URL Segment
- via Query
- via Header
- via Content Type
via URL Segment
The default option is the URL Segment approach, where we include the version information within the URL path. The ASP.NET Core ApiVersion service looks up this version in the request path and determines which controller the incoming request should be mapped to.
Many popular API providers, such as Google and Facebook, use URL Segments to determine the API version to map. To explicitly specify a URL Segment, we set it as the ApiVersionReader as follows:
services.AddApiVersioning(setup =>
setup.ApiVersionReader = new UrlSegmentApiVersionReader());
via Query
In this approach, the version attaches to the URL as a query parameter. The advantage is that the URL remains clean, while the version can be adjusted through the query parameter.
The drawback, however, is that if we need to change the version number, we must update it on all URLs where we include the query parameters.
To enable query parameter-based versioning, we set an instance of the QueryStringApiVersionReader in the AddApiVersioning().
By default, the version is passed using the “api-version” query parameter in the query string, but we can customize it by specifying the parameter name in an overloaded constructor of the QueryStringApiVersionReader.
A good example of this approach is seen in AWS APIs, where we convey the version of the service we intend to invoke through the query parameter.
// /api/heroes/?api-version=2.0 -- default
services.AddApiVersioning(
options => options.ApiVersionReader = new QueryStringApiVersionReader());
// /api/heroes/?v=2.0
services.AddApiVersioning(
options => options.ApiVersionReader = new QueryStringApiVersionReader("v"));
via Header
The third approach involves passing version information through request headers. This keeps the URL entirely free of any versioning information and allows it to remain constant at all times, while we determine the version to be mapped using the request header.
We set an instance of HeaderApiVersionReader as the ApiVersionReader to consider headers for version information.
services.AddApiVersioning(
options => options.ApiVersionReader = new HeaderApiVersionReader("api-version"));
via Content Type
The fourth approach involves extending the media types used in our request headers to convey version information. To accomplish this, we assign ApiVersionReader to an instance of MediaTypeApiVersionReader().
By default, the version is transmitted as “v={versionNumber},” which can be customized in the same way as with QueryStringApiVersionReader() by specifying the desired key in the constructor.
// Content-Type: application/json;v=2.0 -- default
services.AddApiVersioning(
options => options.ApiVersionReader = new MediaTypeApiVersionReader());
// Content-Type: application/json;version=2.0
services.AddApiVersioning(
options => options.ApiVersionReader = new MediaTypeApiVersionReader("version"));
Reading from More than one Sources
At times, we may prefer to retrieve version information from various sources instead of adhering to a single common constraint consistently.
In such cases, we can pass multiple ApiVersionReader instances, as demonstrated below.
services.AddApiVersioning(setup =>
{
setup.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(),
new HeaderApiVersionReader("api-version"),
new QueryStringApiVersionReader("api-version"),
new MediaTypeApiVersionReader("version"));
});
How to integrate API Versions with Swagger UI
So far, we’ve explored how to adapt our conventional ASP.NET Core API resources to support versions using the Microsoft.AspNetCore.Mvc.Versioning
NuGet package. As a result of this transformation, we now have two API versions.
One is mapped to the path /api/v1/heroes
, which also corresponds to the conventional /api/heroes
API (to support older API routes). The other is a newer version, /api/v2/heroes
, which introduces additional functionalities alongside the existing endpoints, modified for the new version.
Let’s take a step further and create Swagger documentation for these API versions, addressing any issues we encounter along the way.
Adding SwaggerUI
Let’s start by installing the Swashbuckle.AspNetCore
NuGet package and then adding the necessary services and middleware into the Startup class.
namespace Heroes.Api
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IHeroesRepository, HeroesRepository>();
services.AddControllers();
services.AddApiVersioning(setup =>
{
setup.DefaultApiVersion = new ApiVersion(1, 0);
setup.AssumeDefaultVersionWhenUnspecified = true;
setup.ReportApiVersions = true;
});
services.AddSwaggerGen();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "V1");
});
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
When we attempt to access the Swagger UI to check if the APIs are displayed, we encounter an error message on the UI and an exception in the console.
An unhandled exception has occurred while executing the request.
Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorException: Conflicting method/path combination "GET api/v{version}/Heroes/alive" for actions - Heroes.Api.Controllers.V2.HeroesController.Alive (Heroes.Api),Heroes.Api.Controllers.V1.HeroesController.Alive (Heroes.Api). Actions require a unique method/path combination for Swagger/OpenAPI 3.0.
Solution
The error indicates that Swagger does not comprehend the API Versioning configuration we’ve implemented when parsing API metadata.
To resolve this issue, we’ll start by tagging the endpoints with version information for mapping. To accomplish this, we decorate each endpoint with the [MapToApiVersion(“”)] attribute, which requires a single string argument denoting the version to which this endpoint should be mapped. Our V2 HeroesController is then updated as follows.
namespace Heroes.Api.Controllers.V2
{
[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class HeroesController : ControllerBase
{
public HeroesController(ILogger logger, IHeroesRepository data)
{
// code ommitted for brevity
}
[MapToApiVersion("2.0")]
[HttpGet, Route("alive")]
public string Alive()
{
// code ommitted for brevity
}
[MapToApiVersion("2.0")]
[HttpGet]
public IEnumerable<Hero> Get()
{
// code ommitted for brevity
}
[MapToApiVersion("2.0")]
[HttpPost]
public Hero Post(HeroItem model)
{
// code ommitted for brevity
}
[MapToApiVersion("2.0")]
[HttpGet, Route("searchbyname")]
public IEnumerable<Hero> SearchByName(string name = "")
{
// code ommitted for brevity
}
[MapToApiVersion("2.0")]
[HttpGet, Route("categories")]
public IEnumerable<string> Categories()
{
// code ommitted for brevity
}
}
}
To obtain information about these versions and endpoints, we include the Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
NuGet package. This package processes version information on the endpoints to provide us with metadata.
dotnet add package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
We include the AddVersionedApiExplorer service in the Startup class and then use the ApiExplorer service in the Configure method to leverage the collected metadata in the SwaggerUI middleware.
namespace Heroes.Api
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IHeroesRepository, HeroesRepository>();
services.AddControllers();
services.AddApiVersioning(setup =>
{
setup.DefaultApiVersion = new ApiVersion(1, 0);
setup.AssumeDefaultVersionWhenUnspecified = true;
setup.ReportApiVersions = true;
});
services.AddVersionedApiExplorer(setup =>
{
setup.GroupNameFormat = "'v'VVV";
setup.SubstituteApiVersionInUrl = true;
});
services.AddSwaggerGen();
}
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,
IApiVersionDescriptionProvider provider
)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseSwagger();
app.UseSwaggerUI(options =>
{
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerEndpoint(
$"/swagger/{description.GroupName}/swagger.json",
description.GroupName.ToUpperInvariant()
);
}
});
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
This actually resolves the error, and now we can see two versions in the Swagger version dropdown, and V1 is functioning correctly.
However, when attempting to access V2, we encounter a ‘not found’ error, and the Swagger JSON is not generated for V2 APIs (/swagger/v2/swagger.json).
How do we resolve this issue?
The solution involves ensuring that Swagger generates documentation for all available versions provided by the ApiExplorer.
For a single version, we can create documentation info inside the SwaggerGen() service. But when dealing with multiple versions, hardcoding documentation info is not a good practice.
Instead, we configure the SwaggerGenOptions by providing a NamedOptions implementation to substitute it during runtime.
Why? Because to obtain information about the APIs, we need to use the ApiExplorer service, which we cannot inject into the ConfigureServices method (it’s not a good practice).
The ConfigureSwaggerOptions class is implemented as below.
namespace Heroes.Api
{
public class ConfigureSwaggerOptions : IConfigureNamedOptions
{
private readonly IApiVersionDescriptionProvider provider;
public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider)
{
this.provider = provider;
}
public void Configure(SwaggerGenOptions options)
{
// add swagger document for every API version discovered
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, CreateVersionInfo(description));
}
}
public void Configure(string name, SwaggerGenOptions options)
{
Configure(options);
}
private OpenApiInfo CreateVersionInfo(ApiVersionDescription description)
{
var info = new OpenApiInfo()
{
Title = "Heroes API",
Version = description.ApiVersion.ToString()
};
if (description.IsDeprecated)
{
info.Description += " This API version has been deprecated.";
}
return info;
}
}
}
We then register this in the Startup class.
services.ConfigureOptions<ConfigureSwaggerOptions>();
When we load Swagger now, we have both versions running, each with its information created.
Conclusion
API Versioning is a crucial aspect of designing extensible APIs that can be updated or enhanced with new features over time while maintaining backward compatibility.
ASP.NET Core provides a useful library for handling API Versioning, which simplifies the process. Swagger API documentation generally works seamlessly with ASP.NET Core APIs, but challenges can arise when creating versions of APIs with similar routes and methods.
With the assistance of the ApiExplorer NuGet package and a slight adjustment in how we generate the Info, we can set up this configuration without encountering any issues.
We can further enhance this setup by ensuring that proper default values are loaded for the API documentation through the use of IOperationFilter implementations.
However, for a quickstart setup, the provided solution suffices.
The code snippets in this article belong to a solution known as SwaggerHeroes. This solution serves as a straightforward boilerplate example, showcasing key takeaways like Swagger integration, API versioning, and Swagger UI for API versions.
Please do leave a Star if you find it useful – https://github.com/referbruv/SwaggerHeroes
Thank you! You saved my day.
Glad it helped you Evandro!