When developing APIs, you should keep one thing in mind: Change is inevitable. When your API has reached a point where you need to add more responsibilities, you should consider versioning your API. Hence you will need a versioning strategy.
// Startup.ConfigureServices using Microsoft.AspNetCore.Mvc.Versioning;
publicvoidConfigureServices(IServiceCollection services) { services.AddApiVersioning(config => { // You can specify the default version as 1.0. config.DefaultApiVersion = new ApiVersion(1, 0); // Set defaul version if you dont specify it. config.AssumeDefaultVersionWhenUnspecified = true; // Let the clients of the API know all supported versions. // The consumers could read the 'api-supported-versions' header. config.ReportApiVersions = true; config.ApiVersionReader = new HeaderApiVersionReader("x-api-version"); }); }
if you don’t set these two configurations
1 2
config.DefaultApiVersion = new ApiVersion(1, 0); config.AssumeDefaultVersionWhenUnspecified = true;
And don’t set any version for your Controller/Action, You will get the following error when you send a request.
1 2 3 4 5 6 7 8
{ "error": { "code": "ApiVersionUnspecified", "message": "An API version is required, but was not specified.", "innerError": null } }
API Version Reader
API Version Reader defines how an API version is read from the HTTP request. If not explicitly configured, the default setting is that our API version will be a query string parameter named v (example: ../users?v=2.0). Another, probably more popular option is to store the API version in the HTTP header. We have also the possibility of having an API version both in a query string as well as in an HTTP header.
Query string parameter
1 2
// /api/home?v=2.0 config.ApiVersionReader = new QueryStringApiVersionReader("v");
HTTP header
1 2
// x-api-version: 2.0 config.ApiVersionReader = new HeaderApiVersionReader("x-api-version");
config.ApiVersionReader = ApiVersionReader.Combine( new QueryStringApiVersionReader("v"), new HeaderApiVersionReader("x-api-version"), new MediaTypeApiVersionReader("version") );
The URL Path
1 2
// [Route("api/v{version:apiVersion}/[controller]")] config.ApiVersionReader = new UrlSegmentApiVersionReader();
// GET api/v2/values/5 // GET api/v2.0/values/5 [HttpGet("{id}")] [MapToApiVersion("2.0")] public ActionResult<string> Get(int id) { return"value"; }
// POST api/v1/values/5 // POST api/v1.0/values/5 // POST api/v1.1/values/5 // POST api/v2/values/5 // POST api/v2.0/values/5 [HttpPost("{id}")] public ActionResult<string> Post(int id) { return"value"; } }
Tip: Since no version number is specified to the actions in ValuesController, all the endpoints are assumed to have the default version of 1.0.
[ApiVersion(“1.0”, Deprecated = true)]
Annotating our controller with, for example, [ApiVersion(“1.0”)] attribute, means that this controller supports API version 1.0.
To deprecate some version in our API controller, we need to set Deprecated flag to true: [ApiVersion(“1.0”, Deprecated = true)].
In such cases a api-deprecated-versions header will be added to identify deprecated versions.
[ApiVersion(“1.1”)]
[ApiVersion(“2.0”)]
Controllers can support multiple API versions.
[Route(“api/v{version:apiVersion}/[controller]”)]
To implement URL path versioning, You can modify the Route attribute of the controllers to accept API versioning info in the path param.
[MapToApiVersion(“2.0”)]
From the several versions introduced for the controller, you can specify a specific version (eg. version 2.0) for the action. The action is only available with this version.
If you try to access it with other versions, you will encounter an UnsupportedApiVersion error.
1 2 3 4 5 6 7
{ "error": { "code": "UnsupportedApiVersion", "message": "The HTTP resource that matches the request URI 'http://localhost:5000/api/v1.0/values/4' does not support the API version '1.0'.", "innerError": null } }
[ApiVersionNeutral]
If we have a service that is version-neutral, we will mark that controller with [ApiVersionNeutral] attribute.
How to get the API version inside a controller?
1
var apiVersion = HttpContext.GetRequestedApiVersion();
And also
1
public IActionResult Get(int id, ApiVersion apiVersion ) {}
API Version Conventions
Besides attributing our controllers and methods, another way to configure service versioning is with API versioning conventions. There are several reasons why would we use API versioning conventions instead of attributes:
Centralized management and application of all service API versions
Apply API versions to services defined by controllers in external .NET assemblies
Dynamically apply API versions from external sources; for example, from the configuration
There is also an option to define custom conventions.There is a IControllerConvention interface for this purpose. Custom conventions are added to the convention builder through the API versioning options:
[HttpGet("{id}")] [MapToApiVersion("2.0")] public ActionResult<string> Get(int id) { return"value"; } }
In this example, we have two valid versions (1.1, 2.0) for all actions but the Get() has mapped to version 1.0 which is invalid for us. We want to prevent this method from loading.
To do this, you should write an ActionFilterAttribute as following
[HttpGet("{id}")] [MapToApiVersion("2.0")] public ActionResult<string> Get(int id) { return"value"; } }
Now, whenever you call the Get() action, the PreventUnavailableApiVersions checks the version you requested (1.0) with versions of your controller (1.1, 2.0), If the requested version is not in the list, it returns the same error as below.
1 2 3 4 5 6 7 8 9 10 11 12
// Status code: 400 Bad Request { "code": "UnavailableApiVersion", "message": "The HTTP resource that matches the request URI 'http://localhost:5000/api/v1/WeatherForecast?v=1.0' does not available by the API version '1.0'.", "availableVersions": [ "1.1", "2.0" ], "deprecatedVersions": [ "1.0" ] }
Deprecated versions
As a default behaviour deprecated versions are in valid list it means if you write like the below code
[HttpGet("{id}")] [MapToApiVersion("2.0")] public ActionResult<string> Get(int id) { return"value"; } }
You will see the result of action method without any unavailable error but you can config to remove deprecated versions from list of valid versions. You just need to write
1 2
// IsADeprecatedVersionValid: default is true [PreventUnavailableApiVersions(IsADeprecatedVersionValid = false)]
Now, the action filter consider the version 1.0 as an unavailable and you will get an UnavailableApiVersion response.
Configuration
PreventUnavailableApiVersions action filter supports header, query string and url segment mode so you can configure any of these just like what you did in ApiVersionReader.
1 2 3 4 5 6 7 8 9 10 11 12
// Default is 'v' // QueryStringApiVersionReader("ver") [PreventUnavailableApiVersions(QueryString = "ver")]
// Default is 'x-api-version' // HeaderApiVersionReader("api-version-header") [PreventUnavailableApiVersions(Header = "api-version-header")]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] publicclassUnavailableApiVersionsAttribute : ActionFilterAttribute { privatestringGetUrl(HttpRequest request) { return$"{request.Scheme}://{request.Host}{request.Path}{request.QueryString}"; } privatestringFixVersion(string version) { var v = version.Contains('.') ? version : $"{version}.0"; return v.Trim(); } privatereadonlystring _commaSeparatedVersions; publicUnavailableApiVersionsAttribute(string commaSeparatedVersions) { _commaSeparatedVersions = commaSeparatedVersions; } publicstring Header { get; set; } = "x-api-version"; publicstring QueryString { get; set; } = "v"; publicstring UrlSegment { get; set; } = "version"; publicbool IsADeprecatedVersionValid { get; set; } = true; publicoverridevoidOnActionExecuting(ActionExecutingContext context) { if (string.IsNullOrEmpty(_commaSeparatedVersions)) return; var props = context.ActionDescriptor.Properties; var url = GetUrl(context.HttpContext.Request); var headerVersion = context.HttpContext.Request.Headers.Count(x => string.Equals(x.Key, Header.Trim(), StringComparison.InvariantCultureIgnoreCase)); var routeVersion = context.RouteData.Values[UrlSegment.Trim()]; var queryVersion = context.HttpContext.Request.QueryString.Value.Trim(); var matchedQuery = queryVersion.Replace("?", "").Split('&').FirstOrDefault(x => x.StartsWith($"{QueryString}=")); var isSkippable = routeVersion == null && headerVersion == 0 && string.IsNullOrEmpty(matchedQuery); if (isSkippable) return; var version = ""; if (routeVersion != null) { version = routeVersion.ToString(); } if (headerVersion > 0) { version = context.HttpContext.Request.Headers["x-api-version"].ToString(); } if (!string.IsNullOrEmpty(matchedQuery)) { version = matchedQuery.Replace($"{QueryString}=", ""); } version = FixVersion(version); var unavailableVersions = _commaSeparatedVersions.Split(',').Select(x => FixVersion(x.Trim())); var isUnavailableVersion = unavailableVersions.Contains(version); foreach (var prop in props) { var apiVersionModel = prop.Value as ApiVersionModel; if (apiVersionModel != null) { if (apiVersionModel.IsApiVersionNeutral) return; var deprecated = apiVersionModel.DeprecatedApiVersions.Select(x => x.ToString()); var supported = IsADeprecatedVersionValid ? apiVersionModel.SupportedApiVersions.Select(x => x.ToString()).Concat(deprecated).Except(unavailableVersions) : apiVersionModel.SupportedApiVersions.Select(x => x.ToString()).Except(unavailableVersions); if(!isUnavailableVersion && !IsADeprecatedVersionValid) { isUnavailableVersion = deprecated.Contains(version); } if (isUnavailableVersion) { context.Result = new JsonResult(new UnavailableApiVersion { Code = "UnavailableApiVersion", AvailableVersions = supported, DeprecatedVersions = deprecated, Message = $"The HTTP resource that matches the request URI '{url}' does not available via the API version '{version}'." }); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; } } } } }
UnavailableApiVersions attribute get a comma-separated versions via constructor just on action methods. When you use this action filter you cannot access to the api via these forbidden versions.
///<summary> /// Configures the Swagger generation options. ///</summary> ///<remarks> /// This allows API versioning to define aSwagger document per API version after the ///<see cref="IApiVersionDescriptionProvider" />service has been resolved from the service container. ///</remarks> publicsealedclassConfigureSwaggerOptions :IConfigureOptions<SwaggerGenOptions> { privatereadonly IApiVersionDescriptionProvider _provider; ///<summary> /// Initializes a new instance of the <see cref="ConfigureSwaggerOptions" /> class. ///</summary> ///<param name="provider"> /// The <see cref="IApiVersionDescriptionProvider">provider</see> used to generate Swagger /// documents. ///</param> publicConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) => _provider = provider; ///<inheritdoc /> publicvoidConfigure(SwaggerGenOptions options) { // add a swagger document for each discovered API version // note: you might choose to skip or document deprecated API versions differently foreach (ApiVersionDescription description in _provider.ApiVersionDescriptions) { options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description)); } } privatestatic OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description) { var info = new OpenApiInfo { Title = "A Web API", Version = description.ApiVersion.ToString(), Description = "An API sample.", Contact = new OpenApiContact { Name = "hamedfathi", Email = "hamedfathi@outlook.com" }, TermsOfService = new Uri("https://hamedfathi.me"), License = new OpenApiLicense { Name = "MIT License", Url = new Uri("https://opensource.org/licenses/MIT") } }; if (description.IsDeprecated) { info.Description += " This API version has been deprecated."; } return info; } }
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>(); services.AddSwaggerGen(c => { var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); c.IncludeXmlComments(xmlPath); }); services.AddApiVersioning(config => { config.DefaultApiVersion = new ApiVersion(1, 0); config.AssumeDefaultVersionWhenUnspecified = true; config.ReportApiVersions = true; config.ApiVersionReader = new UrlSegmentApiVersionReader(); }); services.AddVersionedApiExplorer( options => { // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service // note: the specified format code will format the version as "'v'major[.minor][-status]" options.GroupNameFormat = "'v'VVV";
// note: this option is only necessary when versioning by url segment. the SubstitutionFormat // can also be used to control the format of the API version in route templates options.SubstituteApiVersionInUrl = true; });