Custom API version selector
In this blog post, I will demonstrate how to write a custom .NET CORE API version selector. While it's easy and libraries provided all the necessary information to do that, you should weigh the pros and cons before doing so. The implementation that I will share below is used in API which communicates with the web and two mobile apps (IOS, Android), while at first, this custom version selection seemed a great idea, after some time it looks more like a ticking time bomb.
Source code at GitHub
Implementation
Install Microsoft.AspNetCore.Mvc.Versioning
nuget package.
[HttpGet]
[ApiVersion("1.0")]
public WeatherResponse Get()
{
var rng = new Random();
var respones = new WeatherResponse
{
Forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
}),
ApiVersion = "1.0"
};
return respones;
}
[HttpGet]
[ApiVersion("2.0")]
public WeatherResponse Get_v2()
{
var rng = new Random();
var respones = new WeatherResponse
{
Forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
}),
ApiVersion = "2.0"
};
return respones;
}
services.AddApiVersioning(options =>
{
options.ReportApiVersions = true;
options.AssumeDefaultVersionWhenUnspecified = true;
//Important to version reader ALWAYS report version as missing
options.ApiVersionReader = new HeaderApiVersionReader("custom-version-selection-works-only-if-api-version-is-not-specified");
options.ApiVersionSelector = new HighestUpToSpecifiedIncludedVersionSelector(new ApiVersion(2, 0));
});
ApiVersion
has extension to parse version object from string, SelectVersion
method has all the information required to decide which method to select. What's left is for us to make decision and implement our custom version selector.
public class HighestUpToSpecifiedIncludedVersionSelector : IApiVersionSelector
{
private readonly ApiVersion _defaultVersion;
public HighestUpToSpecifiedIncludedVersionSelector(ApiVersion defaultVersion)
{
_defaultVersion = defaultVersion;
}
public ApiVersion SelectVersion(HttpRequest request, ApiVersionModel model)
{
if (model == null)
throw new ArgumentNullException(nameof(model));
var requestedVersion = ParseVersion(request) ?? _defaultVersion;
var selectedVersion = model.ImplementedApiVersions.Count switch
{
0 => _defaultVersion,
1 => model.ImplementedApiVersions[0].Status == null ? model.ImplementedApiVersions[0] : _defaultVersion,
_ => model.ImplementedApiVersions
.Where(v => v.Status == null && v <= requestedVersion)
.Max(v => v) ?? _defaultVersion
};
return selectedVersion;
}
private static ApiVersion ParseVersion(HttpRequest request)
{
if (!request.Headers.TryGetValue("api-version", out var version))
return null;
if (string.IsNullOrEmpty(version) || version.Count != 1)
return null;
return ApiVersion.TryParse(version[0], out var apiVersion) ? apiVersion : null;
}
}
Summary
We used this approach in mobile application. Would I use it again ? No. As of right now we don't make any breaking changes to the API, only small bug fixes. With this versioning style, if we already have v2.0 endpoint and mobile app is using it, we can't create new v2.0 endpoint for other controller, if we do, mobile app will start to use it immediately and will break.
It's good to know that you can change version selection, but it's best to fail if endpoint or version is not found.