Versioning an ASP.NET Core API
API versioning is a way to conform with the Postel's law. Jon Postel wrote this law in an early specification of TCP:
Be conservative in what you do, be liberal in what you accept from others
This means that you must be conservative in what you send, be liberal in what you accept. Once you have published a version of your API, you cannot change the format of the data it sends to the clients. Adding a new property in a JSON payload or pretty formatting the output may be a breaking change. If you want to change the output of your API, you need to use versioning.
#Multiple ways to version an API
There are multiple ways to version an API. Here're the most common ones:
Creating a new route
// v1 GET https://example.com/api/weatherforecast // v2 GET https://example.com/api/weatherforecast2
Adding the version in the query string
// v1 GET https://example.com/api/weatherforecast?api-version=1.0 // v2 GET https://example.com/api/weatherforecast?api-version=2.0
Adding the version in the header
// v1 GET https://example.com/api/weatherforecast X-API-VERSION: 1.0 // v2 GET https://example.com/api/weatherforecast X-API-VERSION: 1.0
Adding the version in the header
Accept
// v1 GET https://example.com/api/weatherforecast Accept: application/json;v=1.0 // v2 GET https://example.com/api/weatherforecast Accept: application/json;v=2.0
Using the request path to define the version
// v1 GET https://example.com/api/v1.0/weatherforecast // v2 GET https://example.com/api/v2.0/weatherforecast
#Versioning in ASP.NET Core
Microsoft has developed a ready to use NuGet package to support versioning. It supports most of the versioning schema defined in the previous section out of the box. It is extensible if you need a custom way to define the version.
Install the package
Microsoft.AspNetCore.Mvc.Versioning
:csproj (MSBuild project file)<ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="4.1.1" /> </ItemGroup>
Add the API versioning services:
C#public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddControllers(); // Documentation: https://github.com/microsoft/aspnet-api-versioning/wiki/API-Versioning-Options services.AddApiVersioning(options => { // Add the headers "api-supported-versions" and "api-deprecated-versions" // This is better for discoverability options.ReportApiVersions = true; // AssumeDefaultVersionWhenUnspecified should only be enabled when supporting legacy services that did not previously // support API versioning. Forcing existing clients to specify an explicit API version for an // existing service introduces a breaking change. Conceptually, clients in this situation are // bound to some API version of a service, but they don't know what it is and never explicit request it. options.AssumeDefaultVersionWhenUnspecified = true; options.DefaultApiVersion = new ApiVersion(2, 0); // Defines how an API version is read from the current HTTP request options.ApiVersionReader = ApiVersionReader.Combine( new QueryStringApiVersionReader("api-version"), new HeaderApiVersionReader("api-version")); }); }
Modify the controller to specify the version:
C#using Microsoft.AspNetCore.Mvc; namespace WebApplication1.Controllers { [ApiController] [Route("HelloWorld")] [ApiVersion("1.0", Deprecated = true)] public class HelloWorld1Controller : ControllerBase { [HttpGet] public string Get() => "v1.0"; } [ApiController] [Route("HelloWorld")] [ApiVersion("2.0")] public class HelloWorld2Controller : ControllerBase { [HttpGet] public string Get() => "v2.0"; } }
You can now query the url https://localhost:44316/helloworld?api-version=2.0
and check the result:
In the previous example, I use one controller per version. If the controller has multiple methods, you may not want to duplicate the whole controller. Instead, you can only add the new method and decorate it with [MapToApiVersion("")]
:
// 👇 Declare both versions
[ApiVersion("2.0")]
[ApiVersion("2.1")]
[ApiController, Route("HelloWorld")]
public class HelloWorld2Controller : ControllerBase
{
// Common to v2.0 and v2.1
// You can use HttpContext.GetRequestedApiVersion to get the matched version
[HttpPost]
public string Post() => "v" + HttpContext.GetRequestedApiVersion();
// 👇 Map to v2.0
[HttpGet, MapToApiVersion("2.0")]
public string Get() => "v2.0";
// 👇 Map to v2.1
[HttpGet, MapToApiVersion("2.1")]
public string Get2_1() => "v2.1";
}
In the previous example, the client can use the query string or a specific header to specify the API version. If you want to use the path, such as https://example.com/api/v2.0/helloworld
, you need to change the route:
// Will match "/v1.0/HelloWorld" and "/HelloWorld?api-version=1.0"
[ApiController]
[Route("HelloWorld")] // Support query string / header versioning
[Route("v{version:apiVersion}/HelloWorld")] // Support path versioning
[ApiVersion("1.0")]
public class HelloWorld1Controller : ControllerBase
{
public string Get() => "v1.0";
}
#Integration with OpenAPI Specification (Swagger)
As you have multiple versions of the API, you should have multiple versions of the swagger file. The code is copied from https://github.com/microsoft/aspnet-api-versioning/tree/master/samples/aspnetcore/SwaggerSample
Add the NuGet packages
Swashbuckle.AspNetCore
andMicrosoft.AspNetCore.Mvc.Versioning.ApiExplorer
:csproj (MSBuild project file)<ItemGroup> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.4.1" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="4.1.1" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="4.1.1" /> </ItemGroup>
Edit the
startup.cs
file to configure Swagger
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(2, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = ApiVersionReader.Combine(
new QueryStringApiVersionReader("api-version"),
new HeaderApiVersionReader("api-version"));
});
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;
});
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
services.AddSwaggerGen(options => options.OperationFilter<SwaggerDefaultValues>());
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseSwagger();
app.UseSwaggerUI(
options =>
{
// build a swagger endpoint for each discovered API version
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
}
});
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
public class SwaggerDefaultValues : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var apiDescription = context.ApiDescription;
operation.Deprecated |= apiDescription.IsDeprecated();
if (operation.Parameters == null)
return;
// REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/412
// REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/413
foreach (var parameter in operation.Parameters)
{
var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name);
if (parameter.Description == null)
{
parameter.Description = description.ModelMetadata?.Description;
}
if (parameter.Schema.Default == null && description.DefaultValue != null)
{
parameter.Schema.Default = new OpenApiString(description.DefaultValue.ToString());
}
parameter.Required |= description.IsRequired;
}
}
}
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IApiVersionDescriptionProvider _provider;
public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) => _provider = provider;
public void Configure(SwaggerGenOptions options)
{
// add a swagger document for each discovered API version
// note: you might choose to skip or document deprecated API versions differently
foreach (var description in _provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
}
}
private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
{
var info = new OpenApiInfo()
{
Title = "Sample API",
Version = description.ApiVersion.ToString(),
};
if (description.IsDeprecated)
{
info.Description += " This API version has been deprecated.";
}
return info;
}
}
You can then get the generated swagger files at:
https://example.com/swagger/v1/swagger.json
https://example.com/swagger/v2/swagger.json
https://example.com/swagger/v2.1/swagger.json
#Additional resources
- How to Version Your Service
- Microsoft REST API Guidelines - Versioning
- Robustness principle
- ASP.NET API Versioning - GitHub repository
Do you have a question or a suggestion about this post? Contact me!