Introduction
Versioning is an important aspect of any mature web service. Microsoft has published REST API guidelines that require that all compliant services must support explicit versioning. This ensures that clients can rely on services to be stable over time, while still enabling service changes and new features. The goal of the ASP.NET API Versioning project is to adhere to the Microsoft REST Guidelines for versioning using the ASP.NET technology stack out-of-the-box, but there are numerous extensions and customizations that allow you to version your APIs however you like. Detailed information about the recommended guidance can be found in the Microsoft REST Guidelines.
Features
.NET
Abstractions
The core abstractions provide a common set of interfaces and types for API versioning across all supported platforms. These capabilities can be used to version your data models or using version metadata outside of ASP.NET.
Client
The client-side extensions make it simple to create API version-aware HTTP clients.
ASP.NET Core
Minimal API
Everything you need to add service API versioning to your ASP.NET Core applications and Minimal APIs. The API Explorer and OpenAPI extensions provided everything you need to document your services.
MVC (Core)
Expands upon the service API versioning for ASP.NET Core and adds support for controller classes. The API Explorer and OpenAPI extensions provided everything you need to document your services.
gRPC
Expands upon the service API versioning for ASP.NET Core and adds support for gRPC services. The API Explorer and OpenAPI extensions provided everything you need to document your services.
OData
Expands upon the service API versioning for ASP.NET Core and adds OData-specific features for your OData v4.0 applications and OData controllers, including support for versioned Entity Data Models (EDMs). The API Explorer and OpenAPI extensions provided everything you need to document your services.
ASP.NET (Classic)
Web API
Everything you need to add service API versioning to your Web API applications and controller classes. The API Explorer extensions provided everything you need to document your services.
OData
Expands upon the service API versioning for Web API and adds OData-specific features for your OData v4.0 applications and OData controllers, including support for versioned Entity Data Models (EDMs). The API Explorer extensions provided everything you need to document your services.
Contributing
ASP.NET API Versioning is free and open source. You can find the source code on GitHub and issues and feature requests can be posted on the GitHub issue tracker. ASP.NET API Versioning relies on the community to fix bugs and add features: if you’d like to contribute, please read the CONTRIBUTING guide and consider opening a pull request.
License
This project is licensed under the MIT license.
Getting Started
The simplest way to get started is to install the library.
dotnet add package Asp.Versioning.Http
Example
The following example sets up the ubiquitous “Hello World” service with two versions of the same endpoint. The version
parameter resolves to the request API version and echoes it back to illustrate different endpoints were reached.
using Asp.Versioning;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
builder.Services.AddApiVersioning();
var app = builder.Build();
var helloworld = app.NewVersionedApi().MapGroup("/helloworld");
var v1 = helloworld.MapGroup("/").HasApiVersion(1.0);
var v2 = helloworld.MapGroup("/").HasApiVersion(2.0);
// GET /helloworld?api-version=1.0
v1.MapGet("/", (ApiVersion version) => $"Hello World! (v{version})");
// GET /helloworld?api-version=2.0
v2.MapGet("/", (ApiVersion version) => $"Hello World! (v{version})");
app.Run();
To run the example, use:
dotnet run
and then navigate to the endpoint or use:
curl https://localhost:5001/helloworld?api-version=1.0
curl https://localhost:5001/helloworld?api-version21.0
New Services
When a service author creates new services that consider API versioning upfront, then the configuration and setup is very straightforward. The following examples provide a quick start setup for the respective platforms with default configurations.
API versions can be expressed with .NET attributes or by configured conventions. These examples all use .NET attributes. If you’re interested in using conventions instead, please review the API version conventions topic.
Minimal API
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddProblemDetails();
builder.Services.AddApiVersioning();
var app = builder.Build();
var people = app.NewVersionedApi();
people.MapGet( "/people", () => new[] { new Person() } ).HasApiVersion( 1.0 );
app.Run();
MVC (Core)
[ApiVersion( 1.0 )]
[ApiController]
[Route( "[controller]" )]
public class PeopleController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok( new[] { new Person() } );
}
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddControllers();
builder.Services.AddProblemDetails();
builder.Services.AddApiVersioning().AddMvc();
var app = builder.Build();
app.MapControllers();
app.Run();
OData
[ApiVersion( 1.0 )]
public class PeopleController : ODataController
{
[EnableQuery]
public IActionResult Get() => Ok( new[] { new Person() } );
}
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddControllers().AddOData();
builder.Services.AddProblemDetails();
builder.Services.AddApiVersioning().AddOData(
options =>
{
options.ModelBuilder.DefaultModelConfiguration = ( builder, apiVersion, routePrefix ) =>
{
builder.EntitySet<Person>( "People" );
};
options.AddRouteComponents();
} );
var app = builder.Build();
app.MapControllers();
app.Run();
Existing Services
While it’s great to plan for an API versioning story for your services upfront, it’s all too common to need API versioning after your services are in production. The ASP.NET versioning libraries provide features to help you retrofit existing services and integrate formal API versioning without breaking your existing clients.
Unless a service is API version-neutral, existing services have some logical, yet undefined, API version that is not formally declared by the service or known to a client. In order to prevent existing clients from breaking, they must be able to make requests to the original URL without specifying any API version information.
When API versioning is applied, all of the existing services now have an explicit API version on the service side. The
initial, default API version is 1.0, but that can be configured to be a different API version. All existing controller
definitions that do not have explicit API version definitions will now be implicitly bound to the default API version.
Once a controller has any API version attribution or conventions, it will never be implicitly matched. This enables
service authors to permanently sunset API versions over time. Controllers that have an implicit API version can be
confusing to service authors; especially, in a team environment. It is recommended that you explicitly apply API
versions to all of your existing services when you introduce formal API versioning.
Minimal API
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddProblemDetails();
// allow a client to call you without specifying an api version
// since we haven't configured it otherwise, the assumed api version will be 1.0
builder.Services.AddApiVersioning( options => options.AssumeDefaultVersionWhenUnspecified = true );
var app = builder.Build();
var people = app.NewVersionedApi();
var v1 = people.MapGroup( "/people" ).HasApiVersion( 1.0 );
var v2 = people.MapGroup( "/people" ).HasApiVersion( 2.0 );
v1.MapGet( "/", () => new[] { new Person() } );
v2.MapGet( "/", () => new[] { new Person() } );
app.Run();
MVC (Core)
[ApiVersion( 1.0 )] // ← this attribute isn't required, but it's easier to understand
[ApiController]
[Route( "[controller]" )]
public class PeopleController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok( new[] { new Person() } );
}
[ApiVersion( 2.0 )]
[ApiController]
[Route( "People" )]
public class People2Controller : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok( new[] { new Person() } );
}
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddControllers();
builder.Services.AddProblemDetails();
// allow a client to call you without specifying an api version
// since we haven't configured it otherwise, the assumed api version will be 1.0
builder.Services.AddApiVersioning( options => options.AssumeDefaultVersionWhenUnspecified = true )
.AddMvc();
var app = builder.Build();
app.MapController();
app.Run();
OData
[ApiVersion( 1.0 )] // ← this attribute isn't required, but it's easier to understand
public class PeopleController : ODataController
{
// GET ~/people
// GET ~/people?api-version=1.0
[EnableQuery]
public IActionResult Get() => Ok( new[] { new Person() } );
}
[ApiVersion( 2.0 )]
[ControllerName( "People" )]
public class People2Controller : ODataController
{
// GET ~/people?api-version=2.0
[EnableQuery]
public IActionResult Get() => Ok( new[] { new Person() } );
}
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddControllers().AddOData();
builder.Services.AddProblemDetails();
// allow a client to call you without specifying an api version
// since we haven't configured it otherwise, the assumed api version will be 1.0
builder.Services
.AddApiVersioning( options => options.AssumeDefaultVersionWhenUnspecified = true )
.AddOData( options =>
{
options.ModelBuilder.DefaultModelConfiguration = ( builder, apiVersion, routePrefix ) =>
{
builder.EntitySet<Person>( "People" );
};
options.AddRouteComponents();
} );
var app = builder.Build();
app.MapController();
app.Run();
Migration From Previous Versions
This topic serves as the guide for migrating from version <= 5.x.x to version >= 6.0.0. The majority of this
information has been outlined in previous discussions.
Note
If you’d like more information on the background context, you can read the Hello Project “Asp” announcement.
For the most part, you can expect the required changes to be a new package identifier and different namespaces. It is entirely possible that you may update those and find the rest of the code to be identical. The mileage will vary depending on your level of customization, but you can expect the changes to be trivial in most cases.
Package Identifiers
The original Microsoft.* packages are now deprecated and will only undergo servicing:
| Package | Version | TFM |
|---|---|---|
| Microsoft.AspNetCore.Mvc.Versioning | <= 5.x.x | netcoreapp3.1, net5.0 |
| Microsoft.AspNetCore.Mvc.ApiExplorer | <= 5.x.x | netcoreapp3.1, net5.0 |
| Microsoft.AspNetCore.OData | <= 5.x.x | netcoreapp3.1, net5.0 |
| Microsoft.AspNetCore.OData.ApiExplorer | <= 5.x.x | netcoreapp3.1, net5.0 |
All new features and platform support will use the Asp.Versioning.* prefix:
| Package | Version | TFM |
|---|---|---|
| Asp.Versioning.Abstractions | 6.0.0+ | net6.0+, netstandard1.0, netstandard2.0 |
| Asp.Versioning.Http1 | 6.0.0+ | net6.0+ |
| Asp.Versioning.Mvc2 | 6.0.0+ | net6.0+ |
| Asp.Versioning.Mvc.ApiExplorer3 | 6.0.0+ | net6.0+ |
| Asp.Versioning.OData | 6.0.0+ | net6.0+ |
| Asp.Versioning.OData.ApiExplorer | 6.0.0+ | net6.0+ |
[1] Base library that supports Minimal APIs
[2] MVC Core with controller support
[3] Supports exploration of Minimal APIs and controllers
Namespaces
As the project is no longer part of Microsoft, all namespaces have become Asp.Versioning.*. It didn’t make sense to
keep using Microsoft.* when things don’t line up. Furthermore, what namespace should all new code live under?
Continuing to use the Microsoft namespace seemed wrong. An interesting benefit, however, is that using
Api.Versioning.* allows for more consistency across the ASP.NET Web API and Core implementations. The existing
differences in library namespaces for shared code often led to conditional compiler directives. For ease of use,
extension methods will continue to live in the namespace they correspond to.
API Version
The format and default implementation has not changed, but parsing has been broken apart. The new IApiVersionParser
service has been introduced to support this capability. ApiVersion.Parse and ApiVersion.TryParse have been removed,
but are replaced by ApiVersionParser.Default, which will provide a default implementation.
ApiVersion.GroupVersion in .NET 6.0 and beyond is now represented as DateOnly. DateOnly accurately represents how
a group or date version was always meant to be, but couldn’t be represented without introducing its own type due to the
design of DateTime. The .NET Standard and .NET Framework representations will continue to use DateTime.
API Version Reader
IApiVersionReader.Read now returns IReadOnlyList<string> instead of string?. There are a few reasons for this
change. First, the Null Mistake is removed as an empty list is completely acceptable. Second, it was entirely possible
for a particular reader implementation to return more than one value. Consider that ?api-version=1.0&api-version=2.0
would return both 1.0 and 2.0. In previous versions, the implementation would instead throw
AmbiguousApiVersionException that would have to be handled. That behavior becomes problematic for the server to
correctly report the response to the client. Reading multiple API version values in and of itself isn’t exceptional,
it’s just an invalid client request. ApiVersionReader.Combine also enables combining different types of readers
through composition. Readers for different parts of a request are even more likely to return different values.
Refactoring to return a list makes it very simple to return all of the raw API versions provided without any exceptions
and regardless of where they were read from.
API Version Reporting
IReportApiVersions.Report now accepts the entire HTTP response as opposed to just the headers. Accepting only the
headers was an over-normalization that wasn’t really necessary. Additional information was also necessary to support
sunset policies. The Report overload that accepts Lazy<ApiVersionModel> has been removed as it’s no longer used
or necessary.
API Version Model Extensions
Extension methods related to retrieving an ApiVersionModel have been supplanted by the new extension property
ApiVersionMetadata. The previous GetApiVersionModel() extension method, for example, was a shortcut for
GetApiVersionModel(ApiVersionMapping.Explicit). A new type - ApiVersionMetadata - has been introduced that unifies
the metadata implementation across ASP.NET platforms.
The following is the mapping between the old and new extension methods or properties:
GetApiVersionModel(ApiVersionMapping) → ApiVersionMetadataGetApiVersionModel() → ApiVersionMetadata.Map(ApiVersionMapping.Explicit)MappingTo(ApiVersion) → ApiVersionMetadata.MappingTo(ApiVersion)IsMappedTo(ApiVersion) → ApiVersionMetadata.IsMappedTo(ApiVersion)
Error Responses
The IErrorResponseProvider service had been the hook to provide custom error responses. Problem Details (RFC 7807)
had only just been ratified when this project started and they were not part of ASP.NET yet. ASP.NET Core eventually
added first-class support for Problem Details and IErrorResponseProvider had an adapter implementation for alignment
in previous versions. Now that Problem Details are the de factor method for error reporting, it no longer makes sense to
retain IErrorResponseProvider and it has been removed.
The error responses bodies provided by IErrorResponseProvider complied with the
Microsoft REST Guidelines error response format, which is itself the error response format used by the OData protocol
(see OData JSON Format §21.1). If you need to retain that format, the Error Response backward compatibility topic
discusses how to enable it.
ProblemDetails.Type could logically be used to model the established error Code; however, the value is supposed to
be a URI. For backward compatibility, the existing error codes will be emitted as the Code extension in Problem
Details. The Error Responses topic provides details for each well-known problem that may be returned in responses.
API Behaviors
In versions >= 2.1.0 && < 6.0.0, the ApiVersioningOptions provided the property UseApiBehavior. This setting was a
bridge to the API Behaviors feature introduced in ASP.NET Core 2.1. In earlier versions of ASP.NET Core, there was not a
clear way to disambiguate between a UI and API controller. Adding API Behaviors via [ApiController] to a controller or
assembly provided a way to solve that problem. API Versioning subsequently added two new services that align to it:
IApiControllerFilter- filters out non-API controllersIApiControllerSpecification- determines whether a controller is for an API
The default filter is an aggregation over all specifications. The default specifications look for API Behaviors and OData routing.
In the 2.1.x time frame, this was a behavioral breaking change. To facilitate a smoother transition, the
UseApiBehavior option was introduced with a value of false, which maintained the existing behavior. Starting in
3.0, the value defaulted to true, which only considers controllers with API Behaviors applied. Starting in 6.0,
the property has been completely removed as it is no longer necessary.
IApiControllerFilter and any of the IApiControllerSpecification services can be modified through dependency
injection. To align with the legacy behavior of UseApiBehavior = false, you can use the NoControllerFilter
implementation:
builder.Services.AddTransient<IApiControllerFilter, NoControllerFilter>();
builder.Services.AddApiVersioning().AddMvc();
Routing Behaviors
The legacy, convention-based routing with IActionSelector has been dropped. Limitations in the original ASP.NET Core
routing design caused a number of issues and inconsistencies, which were resolved when Endpoint Routing was introduced;
especially 405 or 415 responses. The primary reason it continued to be supported was waiting for OData to support
Endpoint Routing, which it does as of 8.0.
The routing logic has been updated to properly return a response for 404, 405, 406, and 415. Due to necessary
API Versioning fixes and the way routing works in ASP.NET Core, it is no longer possible to always report 400 when an
API version could be matched, but doesn’t. In some of these cases it is also not possible to add ProblemDetails;
especially prior to .NET 7 because ASP.NET Core did not provide a hook for it.
What happens when an API version could match, but doesn’t has always been a bit of a gray area. The general consensus
seems to be that developers don’t care because it’s a client error or they expect it to be 404. These default rule
will continue to return 400 when versioning by query string or header, but that can now be changed via
ApiVersioningOptions.UnsupportedApiVersionStatusCode. Versioning by URL segment will always return 404. Versioning
by media type will always return 406 or 415.
The UseApiVersioning() middleware in ASP.NET Core has been removed. It never did anything except setup the
IApiVersioningFeature in the current request, which doesn’t require middleware.
Configuration
Support for Minimal APIs and OData in ASP.NET Core required some changes to how services are configured in an
application. The new IApiVersioningBuilder interface provides a way to hang all API Versioning related extensions off
of. This approach also helps address extension method naming conflicts and scenarios where you might forget to register
another set of required services. If you referenced and enabled everything supported by API Versioning, then your
configuration might look like:
var builder = WebApplication.CreateBuilder( args );
var services = builder.Services;
services.AddApiVersioning() // Core services with support for Minimal APIs
.AddMvc() // MVC Core with controllers (not full MVC)
.AddApiExplorer() // API version-aware API Explorer extensions
.AddOData() // API versioning extensions for OData
.AddODataApiExplorer(); // API version-aware API Explorer extensions for OData
Changes
- As noted above,
ApiVersioningOptions.UseApiBehaviorshas been removed ApiVersioningOptions.Conventionshas been moved toMvcApiVersioningOptions.Conventionsas API Versioning no longer requires MVC Core- To configure conventions, use
.AddMvc(options => options.Conventions = ?)via theIApiVersioningBuilderextension method
- To configure conventions, use
ApiVersioningOptions.ControllerNameConventionhas been removed as an explicit option, but can be changed via dependency injection- To configure a different naming convention, use
builder.Services.AddSingleton<IControllerNameConvention, OriginalControllerNameConvention>()
- To configure a different naming convention, use
Version Format
Services are versioned using a version group (e.g. date) or major and minor version scheme with an optional status. The version format has the following syntax:
letter = "A" | "B" | "C" | "D" | "E" | "F" | "G"
| "H" | "I" | "J" | "K" | "L" | "M" | "N"
| "O" | "P" | "Q" | "R" | "S" | "T" | "U"
| "V" | "W" | "X" | "Y" | "Z" | "a" | "b"
| "c" | "d" | "e" | "f" | "g" | "h" | "i"
| "j" | "k" | "l" | "m" | "n" | "o" | "p"
| "q" | "r" | "s" | "t" | "u" | "v" | "w"
| "x" | "y" | "z" ;
positive = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
digit = "0" | positive ;
day = ( [ "0" ] positive ) | ( "1" | "2" ) digit | ( "3" ( "0" | "1" ) ) ;
month = ( [ "0" ] positive ) | ( "1" ( "0" | "1" | "2" ) ) ;
year = 4 * digit ;
group = year "-" month "-" day ;
version = { digit } [ "." { digit } ] ;
status = letter [ { letter | digit | "." } { letter | digit } ] ;
api-version = ( group | version ) [ "-" status ] ;
The version status allows you to provide a condition to a version such as alpha, beta, rc, and so on. While the status is optional, either the version group or the major and minor versions must be specified.
Versioned Request
By default, clients must explicitly request the version of a service via the api-version query string parameter or URL path segment per the Microsoft REST Guidelines for versioning. It is possible to customize this behavior for legacy and other non-compliant services, which will be covered in the Advanced Versioning topic.
Note
When versioning by URL segment, the
vprefix is neither required nor part of the API version.
Versioned Request Examples
The following outlines examples of various service version formats:
- /api/foo?api-version=1.0
- /api/foo?api-version=2.0-alpha
- /api/foo?api-version=2015-05-01.3.0
- /api/v1/foo
- /api/v2.0-alpha/foo
- /api/v2015-05-01.3.0/foo
Custom
The ApiVersion class implements IFormattable and uses the ApiVersionFormatProvider for formatting by default. The
following table outlines the supported format specifiers.
| Format Specifier | Description | Examples |
|---|---|---|
| F | Full API version as [group version][.major[.minor]][-status] | 2017-05-01.1-RC -> 2017-05-01.1-RC |
| FF | Full API version with optional minor version as [group version][.major[.minor,0]][-status] | 2017-05-01.1-RC -> 2017-05-01.1.0-RC |
| G | Group version as yyyy-MM-dd | 2017-05-01.1-RC -> 2017-05-01 |
| GG | Group version as yyyy-MM-dd with status | 2017-05-01.1-RC -> 2017-05-01-RC |
| y | Group version year from 0 to 99 | 2001-05-01.1-RC -> 1 |
| yy | Group version year from 00 to 99 | 2001-05-01.1-RC -> 01 |
| yyy | Group version year with a minimum of three digits | 2017-05-01.1-RC -> 017 |
| yyyy | Group version year as a four-digit number | 2017-05-01.1-RC -> 2017 |
| M | Group version month from 1 through 12 | 2001-05-01.1-RC -> 5 |
| MM | Group version month from 01 through 12 | 2001-05-01.1-RC -> 05 |
| MMM | Group version abbreviated name of the month | 2001-06-01.1-RC -> Jun |
| MMMM | Group version full name of the month | 2001-06-01.1-RC -> June |
| d | Group version day of the month, from 1 through 31 | 2001-05-01.1-RC -> 1 |
| dd | Group version day of the month, from 01 through 31 | 2001-05-01.1-RC -> 01 |
| ddd | Group version abbreviated name of the day of the week | 2001-05-01.1-RC -> Mon |
| dddd | Group version full name of the day of the week | 2001-05-01.1-RC -> Monday |
| v | Minor version | 2001-05-01.1-RC -> 1 1.1 -> 1 |
| V | Major version | 1.0-RC -> 1 2.0 -> 2 |
| VV | Major and minor version | 1-RC -> 1 1.1-RC -> 1.1 1.1 -> 1.1 |
| VVV | Major, optional minor version, and status | 1-RC -> 1-RC 1.1 -> 1.1 |
| VVVV | Major, minor version, and status | 1-RC -> 1.0-RC 1.1 -> 1.1 1 -> 1.0 |
| S | Status | 1.0-Beta -> Beta |
| p | Padded minor version with default of two digits | 1.1 -> 01 1 -> 00 |
| p[n] | Padded minor version with N digits | p2: 1.1 -> 01 p3: 1.1 -> 001 |
| P | Padded major version with default of two digits | 2.1 -> 02 2 -> 02 |
| P[n] | Padded major version with N digits | P2: 2.1 -> 02 P3: 2.1 -> 002 |
| PP | Padded major and minor version with a default of two digits | 2.1 -> 02.01 2 -> 02.00 |
| PPP | Padded major, optional minor version, and status with a default of two digits | 1-RC -> 01-RC 1.1-RC -> 01.01-RC |
| PPPP | Padded major, minor version, and status with a default of two digits | 1-RC -> 01.00-RC 1.1-RC -> 01.01-RC |
Custom Examples
var apiVersion = new ApiVersion( 1, 0 );
Console.WriteLine( "Welcome to version " + apiVersion.ToString( "V" ) );
apiVersion = new ApiVersion( 1, 1, "Beta" );
var message = string.Format( "Welcome to version {0:VV}{0:' ('S')'}", apiVersion );
Console.WriteLine( message );
apiVersion = new ApiVersion( 2, 0 );
message = string.Format( "Welcome to version {0:VV}{0:' ('S')'}", apiVersion );
Console.WriteLine( message );
// Output: Welcome to version 1
// Output: Welcome to version 1.1 (Beta)
// Output: Welcome to version 2.0
Version Discovery
Requiring an explicit service version helps ensure existing clients don’t break, but we also need a way to advertise which service versions are currently supported and which versions are deprecated.
To facilitate this need, services should respond with the api-supported-versions and api-deprecated-versions, which
are multi-value HTTP headers that indicate the supported and deprecated API versions, respectively. A deprecated version
is still implemented, but is expected to be permanently removed in six months or more. When a version is no longer
supported, it should stop being advertised. Additional information can be provided via versioning policies.
Reporting API versions is disabled by default. Service authors can enable this behavior for all services by setting the
ApiVersioningOptions.ReportApiVersions to true or scoped to individual services by applying the [ReportApiVersions]
attribute or the ReportApiVersions() convention.
Service authors might also choose to implement the OPTIONS method so that clients and tooling can interrogate which
API versions their service supports.
Minimal API
using static Microsoft.AspNetCore.Http.HttpMethods;
// OPTIONS ~/api/myservice?api-version=[1.0|2.0|3.0]
app.MapMethods("/api/myservice", [Options], ( HttpContext context ) =>
{
context.Response.Headers.Allow = new( [Get, Post, Options] );
return Results.Ok();
});
HTTP/2 200
allow: GET, POST, OPTIONS
api-supported-versions: 1.0, 2.0, 3.0
MVC (Core)
using static Microsoft.AspNetCore.Http.HttpMethods;
// OPTIONS ~/api/myservice?api-version=[1.0|2.0|3.0]
[HttpOptions]
public IActionResult Options()
{
Response.Headers.Allow = new( [Get, Post, Options] );
return Ok();
}
HTTP/2 200
allow: GET, POST, OPTIONS
api-supported-versions: 1.0, 2.0, 3.0
Version Policies
Version discovery supports advertising which API versions are supported and deprecated via the
api-supported-versions and api-deprecated-versions respectively. A key limitation of this support is that it does
not indicate when an API version will be deprecated, sunset, nor what the stated policy is.
Version policies introduce support for RFC 9745 (Deprecation) and RFC 8594 (Sunset). These will allow an API version
to indicate when it will be deprecated via the deprecation header as well as when it will disappear for good via the
sunset header. These headers do not necessarily apply to all API versions; they will only apply to the API version
that was requested. The deprecation and sunset policies can include additional information such as a web page or OpenAPI
document. These additional links will conform to RFC 8288 (Web Linking).
These capabilities are useful, not only for instrumented clients, but also for tooling. As an example, an API might
support an OPTIONS request to retrieve this information for tooling:
OPTIONS /weather?api-version=1.0 HTTP/2
host: localhost
HTTP/2 200
allow: GET, POST, OPTIONS
api-supported-versions: 1.0, 2.0, 3.0
api-deprecated-versions: 0.9
deprecation: @1640995200
sunset: Thu, 01 Apr 2022 00:00:00 GMT
link: <https://docs.api.com/policies.html?api-version=1.0>; rel="deprecation"; title="API Policy"; type="text/html"
link: <https://docs.api.com/policies.html?api-version=1.0>; rel="sunset"; title="API Policy"; type="text/html"
link: </openapi/v1.json>; rel="openapi"; title="OpenAPI"; type="application/json"
This indicates to a client that the requested API version 1.0 was deprecated on January 1, 2022 and will sunset on
April 1, 2022. It also provides links to public documentation that outlines the API versioning policies as well as where
to locate the OpenAPI document.
Policies do not have to have a date. The following scenarios are supported:
- Define a policy by API name and version
- Define a policy by API name for any version
- Define a policy by API version for any API
- A sunset policy may have a date
- A sunset policy can have zero or more links
Supporting a policy with links alone enables advertising a stated policy when you don’t know when an API version might
actually be deprecated or sunset, which will be common for the current version of an API. If a policy is defined, it
will be emitted through the existing IReportApiVersions service. This service is automatically utilized whenever
ApiVersioningOptions.ReportApiVersions is set to true, ReportApiVersionsAttribute is applied, or the
ReportApiVersions() convention is applied.
Configuration
The configuration is performed the same way across all platforms via:
AddApiVersioning( options =>
{
// version 1.0 deprecates 1/1/2022 with a public policy page
options.Policies.Deprecate( 1.0 )
.Effective( 2022, 1, 1 )
.Link( "https://docs.api.com/policies/deprecation.html" )
.Title( "Version Deprecation Policy" )
.Type( "text/html" );
// version 1.0 sunsets 4/1/2022 with a public policy page
options.Policies.Sunset( 1.0 )
.Effective( 2022, 4, 1 )
.Link( "https://docs.api.com/policies/sunset.html" )
.Title( "Version Sunset Policy" )
.Type( "text/html" );
// public policy page for version 2.0 without a sunset date
options.Policies.Sunset( 2.0 )
.Link( "https://docs.api.com/policies/sunset.html" )
.Title( "Version Sunset Policy" )
.Type( "text/html" )
})
Note
It should be noted that although links confirm to RFC 8288, all configurable links are meant to be specific to API versioning policies. The provided configuration APIs, therefore, only expose a subset of what is configurable and always use a relation type of
rel="deprecation"orrel="sunset". The default implementation can be replaced or extended or you can use theLinkHeaderValuedirectly in your own code, which exposes the complete feature set.
API Explorer Integration
The API Explorer extensions will attach the appropriate DeprecationPolicy or SunsetPolicy to a
ApiVersionDescription and ApiDescription. The policy for a ApiVersionDescription will be for an entire API version,
while the policy for an ApiDescription could be for a specific API, version, or combination of both.
The provided information can be used in any number of different ways, but would most likely be used in conjunction with OpenAPI. There is currently no direct support for a deprecation or sunset policy in OpenAPI, but it can be exposed via an OpenAPI extension or directly in the API documentation.
The Asp.Versioning.OpenApi package will document these policies in OpenAPI when they are present.
How to Version Your Service
REST services are implemented in ASP.NET as an endpoint. To version your service, you simply need to decorate your endpoints with the appropriate API version information. The method of decoration will vary depending on whether you are using controllers or Minimal APIs as well as whether you want to use attributes or conventions.
How It Works
The way that you create and define routes remains unchanged. The key difference is that routes may now overlap depending on whether you are using convention-based routing, attribute-based routing, or both. In the case of attribute routing, multiple controllers will define the same route. The default services in each flavor of ASP.NET assumes a one-to-one mapping between routes and endpoints and, therefore, considers duplicate routes to be ambiguous. The API versioning services replace the default implementations and allow endpoints to also be disambiguated by API version. Although multiple routes may match a request, they are expected to be distinguishable by API version. If the routes cannot be disambiguated, this is likely a developer mistake and the behavior is the same as the default implementation.
Naming and Collation
While it might seem more intuitive that similar route templates are collated together, that is simply not the case.
Consider that order/{id} and order/{id:int} are different, but semantically identical. API Versioning makes no
attempt understand this difference. Although it is possible to have an API with a single endpoint, most APIs consist of
a collection of endpoints; for example the Orders API. What if we saw the route template order/{id}/items? Is this
part of the Orders API or some other API? For this reason, API Versioning collates on the logical name of an API and
not individual route templates. For more information see: Controller Conventions.
Routing Methods
The following table outlines the various supported routing methods:
| Routing Method | Supported |
|---|---|
| Attribute-based routing | Yes |
| Convention-based routing | Yes |
| Attribute and convention-based routing (mixed) | Yes |
Minimal API
Minimal APIs do not use controllers nor any of these conventions or attributes. The intrinsic grouping capabilities define collation without having to infer anything. It is, however, possible to add a logical API name to the group if you want to:
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddProblemDetails();
builder.Services.AddApiVersioning();
var app = builder.Build();
var people = app.NewVersionedApi( "People" ); // ← provides optional, logical name
people.MapGet( "/people", () => new[] { new Person() } ).HasApiVersion( 1.0 );
app.Run();
Versioning Methods
Several API versioning methods are supported out-of-the-box:
- By Query String (default)
- By Media Type
- By Header
- By URL Segment
Multiple methods of API versioning can be supported simultaneously. Use the ApiVersionReader.Combine method to compose
two or more IApiVersionReader instances together. You can also implement your own method of extracting the requested
API version using a custom IApiVersionReader.
Defining a Service Version
There are four out-of-the-box supported approaches for versioning a service:
- By query string parameter
- By media type parameter
- By HTTP header
- By URL path segment
The default method is to use a query string parameter named api-version. You can also combine API versioning approaches together or define your own custom method of API versioning.
Query String Versioning
The initial version of a controller may not have any API version attribution and will implicitly become the configured
default API version. The default configuration uses the value 1.0.
Minimal API
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddProblemDetails();
builder.Services.AddApiVersioning();
var app = builder.Build();
var hello = app.NewVersionedApi();
var v1 = hello.MapGroup( "/helloworld" ).HasApiVersion( 1.0 );
var v2 = hello.MapGroup( "/helloworld" ).HasApiVersion( 2.0 );
v1.MapGet( "/", () => "Hello world!" );
v2.MapGet( "/", () => "Hello world!" );
app.Run();
MVC (Core)
[ApiController]
[Route( "api/[controller]" )]
public class HelloWorldController : ControllerBase
{
[HttpGet]
public string Get() => "Hello world!";
}
OData
public class PeopleController : ODataController
{
[HttpGet]
public IHttpActionResult Get( ODataQueryOptions<Person> options ) =>
Ok( new[]{ new Person() } );
}
Next Version
To create the next version of the controller, you can choose to create a new controller with the same route but
decorate it as API version 2.0. For example:
MVC (Core)
[ApiVersion( 2.0 )]
[ApiController]
[Route( "api/helloworld" )]
public class HelloWorld2Controller : ControllerBase
{
[HttpGet]
public string Get() => "Hello world!";
}
OData
[ApiVersion( 2.0 )]
[ControllerName( "People" )]
public class People2Controller : ODataController
{
[HttpGet]
public IHttpActionResult Get( ODataQueryOptions<Person> options ) =>
Ok( new[]{ new Person() } );
}
The effect of this attribution is that the following requests match different controller implementations:
| Request URL | Matched Controller |
|---|---|
| /api/helloworld?api-version=1.0 | HelloWorldController |
| /api/helloworld?api-version=2.0 | HelloWorld2Controller |
| /api/People?api-version=1.0 | PeopleController |
| /api/People?api-version=2.0 | People2Controller |
It’s important to note that only an undecorated controller will be inferred as the configured, default API version. Once a controller has any API version attribution, it will never be considered as the default API version again unless the API version attribute includes the default API version. This allows you permanently remove API versions over time.
Media Type Versioning
Content negotiation is the defined method in REST for reasoning about the content expectations between a client and server. The parameters used in media types for content negotiation can contain custom input that can be used to drive API versioning.
Let’s assume the following controllers are defined:
Minimal API
var hello = app.NewVersionedApi();
var v1 = hello.MapGroup( "/helloworld" ).HasApiVersion( 1.0 );
var v2 = hello.MapGroup( "/helloworld" ).HasApiVersion( 2.0 );
v1.MapGet( "/", () => "Hello world!" );
v2.MapGet( "/", () => "Hello world!" );
v2.MapPost( "/", (string text) => text );
MVC (Core)
namespace Services.V1
{
[ApiVersion( 1.0 )]
[ApiController]
[Route( "api/[controller]" )]
public class HelloWorldController : ControllerBase
{
[HttpGet]
public string Get() => "Hello world!";
}
}
namespace Services.V2
{
[ApiVersion( 2.0 )]
[ApiController]
[Route( "api/[controller]" )]
public class HelloWorldController : ControllerBase
{
[HttpGet]
public string Get() => "Hello world!";
[HttpPost]
public string Post( string text ) => text;
}
}
Configuration
The configuration will then change the default API version reader as follows:
.AddApiVersioning( options => options.ApiVersionReader = new MediaTypeApiVersionReader() );
The parameterless constructor uses the media type parameter name v, but you can specify any name you like. The default
behavior will require that clients always specify an API version, so service authors will likely want their
configuration to be:
.AddApiVersioning(
options =>
{
options.ApiVersionReader = new MediaTypeApiVersionReader();
options.AssumeDefaultVersionWhenUnspecified = true;
options.ApiVersionSelector = new CurrentImplementationApiVersionSelector( options );
} );
This will allow clients to request a specific API version by media type, but if they don’t specify anything, they will receive the current implementation (e.g. API version). For example:
GET api/helloworld HTTP/2
host: localhost
Figure 1: returns the result from API version 2.0 because it’s the current version
GET api/helloworld HTTP/2
host: localhost
accept: text/plain;v=1.0
Figure 2: returns the result from API version 1.0
POST api/helloworld HTTP/2
host: localhost
content-type: text/plain;v=2.0
content-length: 12
Hello there!
Figure 3: explicitly posts the content to API version 2.0, even though it would be implicitly matched
Multiple Media Types
The MediaTypeApiVersionReader matches the configured media type parameter of any incoming request. This might be
undesirable if you support multiple media types or there is ambiguity in matching a media type.
Consider the following request:
GET api/helloworld HTTP/2
host: localhost
accept: application/json;v=1.0;q=0.8,application/signed-exchange;v=b3;q=0.9
In this scenario, a client has specified multiple media types and they both have the media type parameter v. The
MediaTypeApiVersionReader will honor quality (e.g. q) when specified. If multiple media types have the same quality,
the first one is selected. In this example application/signed-exchange is selected because it has the highest quality.
When the v parameter is parsed, the value is b3 is not a valid API version and will return HTTP status code 406
(Not Acceptable).
The MediaTypeApiVersionReaderBuilder provides a number of additional capabilities to build media type matching rules
that enable to you configure how you would like things to match. You can specify and combine any of the following
behaviors:
- Define multiple media type parameters
- Mutually include specific media types
- Mutually exclude specific media types
- Match media types by template
- Match media types by pattern
- Disambiguate between multiple API versions
To configure that only JSON be matched, you might use a configuration similar to the following:
.AddApiVersioning(
options =>
{
var builder = new MediaTypeApiVersionReaderBuilder();
options.ApiVersionReader = builder.Parameter( "v" )
.Include( "application/json" )
.Build();
options.AssumeDefaultVersionWhenUnspecified = true;
options.ApiVersionSelector = new CurrentImplementationApiVersionSelector( options );
} );
An important difference between MediaTypeApiVersionReaderBuilder and MediaTypeApiVersionReader is that
MediaTypeApiVersionReader expects there to be exactly one API version and selects the first one with the highest
quality. The MediaTypeApiVersionReaderBuilder, on the other hand, makes no such assumption and returns all matched
API versions in descending order of quality. You can use the SelectFirstOrDefault or SelectLastOrDefault extension
methods to have the MediaTypeApiVersionReaderBuilder choose the first or last API version respectively. If neither of
these approaches meet your requirements, you can provide you own callback to determine how to disambiguate multiple
choices via MediaTypeApiVersionReaderBuilder.Select.
Custom Media Types
Defining new, custom media types (ex: application/vnd.my.company.1+json) to drive API versioning is another variant of
this approach that is compliant with the constraints of REST. There is no specific IApiVersionReader meant to address
this scenario, however, the MediaTypeApiVersionReaderBuilder provides two approaches that can be used.
Templates
The most natural approach is to a use a template to match an API version in the media type. The specified template uses the same syntax and matching as a route template. For example,
.AddApiVersioning(
options =>
{
var builder = new MediaTypeApiVersionReaderBuilder();
options.ApiVersionReader = builder.Template( "application/vnd.my.company.{version}+json" )
.Build();
} );
This allows matching the API version the same way as if it were in a URL segment. All of the same format and parsing rules apply. In most cases, this is sufficient; however, the template expects exactly one parameter and that will be assumed to the API version parameter. If there are multiple route parameters, for whatever reason, the expected name must be provided as the second, optional parameter:
Template( "application/vnd.{tenant}.{version}+json", "version" );
Patterns
If a template will not suffice, then a regular expression pattern can be used.
.AddApiVersioning(
options =>
{
var builder = new MediaTypeApiVersionReaderBuilder();
options.ApiVersionReader = builder.Match( @"-v(\d+(\.\d+)?)\+" ).Build();
} );
MediaTypeApiVersionReaderBuilder.Match will only consider the first match. The match may optionally use grouping,
but only the first regular expression group will be considered. If a requested media type does not match the pattern,
then it is ignored.
It is assumed that your pattern matching requirements will fall under the date (e.g. group) or numeric version formats; however, if you have something more complex, the following pattern will match all forms of a valid API version:
^(\d{4}-\d{2}-\d{2})?\.?(\d{0,9})\.?(\d{0,9})\.?-?(.*)$
API Versioning no longer uses regular expressions to parse API versions; however, if you need to know how this can be used from previous implementations, you can review the old code.
Additional Considerations
While using a template or pattern can be used to match and extract an API version from an incoming request, it does not currently provide any additional support that may be need to implement a full solution. These should be known issues and exist even without API Versioning. You should simply beware that API Versioning isn’t providing any additional features beyond matching the API version from the media type in the incoming request.
The specific issues include:
- Mapping
IInputFormatterto the custom media typeIOutputFormatterto the custom media type
- OpenAPI
- Listing all of the consumes media types
- Listing all of the produces media types
Header Versioning
While media type negotiation is the defined method in REST for reasoning about the content expectations between a client and server, any arbitrary HTTP header can also be used to drive API versioning.
Let’s assume the following controllers are defined:
Minimal API
var hello = app.NewVersionedApi();
hello.MapGet( "/helloworld", () => "Hello world!" ).HasApiVersion( 1.0 );
MVC (Core)
namespace Services.V1
{
[ApiVersion( 1.0 )]
[ApiController]
[Route( "api/[controller]" )]
public class HelloWorldController : ControllerBase
{
[HttpGet]
public string Get() => "Hello world!";
}
}
namespace Services.V2
{
[ApiVersion( 2.0 )]
[ApiController]
[Route( "api/[controller]" )]
public class HelloWorldController : ControllerBase
{
[HttpGet]
public string Get() => "Hello world!";
[HttpPost]
public string Post( string text ) => text;
}
}
Configuration
The configuration will then change the default API version reader as follows:
.AddApiVersioning( options => options.ApiVersionReader = new HeaderApiVersionReader( "x-ms-version" ) );
This will allow clients to request a specific API version by the custom HTTP header x-ms-version. For example:
GET api/helloworld HTTP/2
host: localhost
x-ms-version: 1.0
HTTP/2 200
host: localhost
content-type: text/plain
content-length: 12
Hello world!
URL Path Versioning
An alternate, but common, method of API versioning is to use a URL path segment. This approach does not allow implicitly
matching the initial, default API version of a service; therefore, all API versions must be explicitly declared. In
addition, the API version value specified for the URL segment must still conform to the version format. The v prefix
is not part of the API version, but may be included in route templates if you so desire.
Important
It is not possible to have a default API version for a URL path segment. This means that setting
ApiVersioningOptions.AssumedDefaultVersionWhenUnspecifiedis unlikely to have any affect when you use this method of versioning. For more information and possible solutions to address this scenario, refer to the known limitations.
Minimal API
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddProblemDetails();
builder.Services.AddApiVersioning();
var app = builder.Build();
var people = app.NewVersionedApi();
var v1 = people.MapGroup( "/people/v{version:apiVersion}" ).HasApiVersion( 1.0 );
v1.MapGet( "/", () => new[] { new Person() } );
app.Run();
MVC (Core)
[ApiVersion( 1.0 )]
[ApiController]
[Route( "api/v{version:apiVersion}/[controller]" )]
public class HelloWorldController : ControllerBase
{
[HttpGet]
public string Get() => "Hello world!";
}
[ApiVersion( 2.0 )]
[ApiVersion( 3.0 )]
[ApiController]
[Route( "api/v{version:apiVersion}/helloworld" )]
public class HelloWorld2Controller : ControllerBase
{
[HttpGet]
public string Get() => "Hello world v2!";
[HttpGet, MapToApiVersion( 3.0 )]
public string GetV3() => "Hello world v3!";
}
OData
[ApiVersion( 1.0 )]
public class PeopleController : ODataController
{
[EnableQuery]
public IQueryable<Person> Get() => new[]{ new Person() }.AsQueryable();
}
[ApiVersion( 2.0 )]
[ApiVersion( 3.0 )]
[ControllerName( "People" )]
public class People2Controller : ODataController
{
[EnableQuery]
[ODataRoute]
public IQueryable<Person> Get() => new[]{ new Person() }.AsQueryable();
[EnableQuery]
[ODataRoute, MapToApiVersion( 3.0 )]
public IQueryable<Person> GetV3() => new[]{ new Person() }.AsQueryable();
}
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddControllers().AddOData();
builder.Services.AddProblemDetails();
builder.Services.AddApiVersioning().AddOData(
options =>
{
options.ModelBuilder.DefaultModelConfiguration = ( builder, apiVersion, routePrefix ) =>
{
builder.EntitySet<Person>( "People" );
};
options.AddRouteComponents( "api/v{version:apiVersion}" );
} );
var app = builder.Build();
app.MapControllers();
app.Run();
The effect of the API version attribution is that the following requests match different controller implementations:
| Request URL | Matched Controller | Matched Action |
|---|---|---|
| /api/v1/helloworld | HelloWorldController | Get |
| /api/v2/helloworld | HelloWorld2Controller | Get |
| /api/v3/helloworld | HelloWorld2Controller | GetV3 |
| /api/v1/People | PeopleController | Get |
| /api/v2/People | People2Controller | Get |
| /api/v3/People | People2Controller | GetV3 |
Version Interleaving
API versions do not have to be split across different controller classes. A service author might choose to have a controller implement multiple API versions simultaneously. Controller actions can subsequently be mapped to specific API versions. This approach is useful for small version differences but should be used sparingly to prevent developer confusion and complicate code maintenance. For example:
Minimal API
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddProblemDetails();
builder.Services.AddApiVersioning();
var app = builder.Build();
var hello = app.NewVersionedApi();
var v1 = hello.MapGroup( "/helloworld" ).HasApiVersion( 1.0 );
var v2_v3 = hello.MapGroup( "/helloworld" )
.HasApiVersion( 2.0 )
.HasApiVersion( 3.0 );
v1.MapGet( "/", () => "Hello world v1.0!" );
v2_v3.MapGet( "/", () => "Hello world v2.0!" ).MapToApiVersion( 2.0 );
v2_v3.MapGet( "/", () => "Hello world v3.0!" ).MapToApiVersion( 3.0 );
app.Run();
MVC (Core)
[ApiVersion( 1.0 )]
[ApiController]
[Route( "api/[controller]" )]
public class HelloWorldController : ControllerBase
{
[HttpGet]
public string Get() => "Hello world v1.0!";
}
[ApiVersion( 2.0 )]
[ApiVersion( 3.0 )]
[ApiController]
[Route( "api/helloworld" )]
public class HelloWorld2Controller : ControllerBase
{
[HttpGet]
public string Get() => "Hello world v2.0!";
[HttpGet, MapToApiVersion( 3.0 )]
public string GetV3() => "Hello world v3.0!";
}
OData
[ApiVersion( 1.0 )]
public class PeopleController : ODataController
{
public IActionResult Get( ODataQueryOptions<Person> options ) =>
Ok( new[]{ new Person() } );
}
[ApiVersion( 2.0 )]
[ApiVersion( 3.0 )]
[ControllerName( "People" )]
public class People2Controller : ODataController
{
public IActionResult Get( ODataQueryOptions<Person> options ) =>
Ok( new[]{ new Person() } );
[MapToApiVersion( 3.0 )]
public IActionResult GetV3( ODataQueryOptions<Person> options ) =>
Ok( new[]{ new Person() } );
}
Although not illustrated in these examples, it’s important to note that different versions of a service action might have different return values. The effect of the API versioning attribution is that the following requests match different controller and action implementations:
| Request URL | Matched Controller | Matched Action |
|---|---|---|
| /api/helloworld?api-version=1.0 | HelloWorldController | Get |
| /api/helloworld?api-version=2.0 | HelloWorld2Controller | Get |
| /api/helloworld?api-version=3.0 | HelloWorld2Controller | GetV3 |
| /api/People?api-version=1.0 | PeopleController | Get |
| /api/People?api-version=2.0 | People2Controller | Get |
| /api/People?api-version=3.0 | People2Controller | GetV3 |
It should be reiterated that the defined API version, even for an action, never directly influences routing. When the action matched for a route is ambiguous, the selection process will look for an explicit API version that matches the requested API version. If an explicit match is not found, then the action will be implicitly matched. If two actions are ambiguous by route and API version, then this is a developer mistake and the default behavior is unchanged.
Version-Neutral
All services should be explicitly versioned. In rare cases, however, you may have a service that is version-neutral. A common scenario is a health check service that behaves in the exact same way, regardless of API version. This might also apply to a legacy service that doesn’t support API versioning. To effectively opt out individual services from API versioning, a service must indicate that it is version-neutral.
Technically, it’s not a supported scenario to completely opt out of API versioning. A version-neutral service has the following characteristics:
- Accepts any valid API version
- Accepts no API version at all (e.g. unspecified)
This is an important distinction and why the term version-neutral is used. A version-neutral service accepts any and all versions, including none. This behavior can be used to define a service that accepts all API versions or service that simply does not care about specific API versions.
It is not possible to have some versions of a controller that are API version-neutral and other versions of the same controller require an explicit API version. If the route of an API version-neutral service matches any other service, it will result in an ambiguous match (e.g. server error).
Minimal API
var hello = app.NewVersionedApi();
hello.MapGet( "/api/health/ping", () => Results.Ok() ).IsApiVersionNeutral();
MVC (Core)
[ApiVersionNeutral]
[ApiController]
[Route( "api/[controller]/[action]" )]
public class HealthController : ControllerBase
{
[HttpGet]
public IActionResult Ping() => Ok();
}
A version-neutral controller using the query string method will not require that a client specify an API version. A version-neutral controller using the URL path method will match any well-formed API version in the URL path segment.
Minimal API
var hello = app.NewVersionedApi();
hello.MapGet( "/api/v{version:apiVersion}/health/ping", () => Results.Ok() ).IsApiVersionNeutral();
MVC (Core)
[ApiVersionNeutral]
[ApiController]
[Route( "api/v{version:apiVersion}/[controller]/[action]" )]
public class HealthController : ControllerBase
{
[HttpGet]
public IActionResult Ping() => Ok();
}
Requested API Version
All of the service API version information is accessible via extension methods and properties. Beginning in version
3.0, Model Binding is also supported. These features allow you to determine which API version was requested by a
client as well as determine which versions are supported and deprecated. The API versions provided are automatically
aggregated across all service implementations.
The most common usage is the current, client requested API version:
Minimal API
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddProblemDetails();
builder.Services.AddApiVersioning().EnableApiVersionBinding();
var app = builder.Build();
var api = app.NewVersionedApi();
api.MapGet( "/", ( ApiVersion version ) => Results.Ok() )
.HasApiVersion( 1.0 )
.HasApiVersion( 2.0 );
app.Run();
MVC (Core)
[ApiVersion( 1.0 )]
[ApiVersion( 2.0 )]
[ApiController]
public class Controller : ControllerBase
{
public IActionResult Get()
{
var apiVersion = HttpContext.RequestedApiVersion;
return Ok();
}
// supported in 3.0+
public IActionResult Get( int id, ApiVersion apiVersion ) => Ok();
}
Existing Services
It’s a fairly common scenario that services are released to production and, at some point in the future, it becomes evident that service versioning is needed. The question now becomes, “How do I add API versioning without breaking existing clients?”
Before API versioning was applied to your service, clients were already bound to some version of the service; they just don’t know which version. A client in this situation doesn’t have any flexibility to go backward. If the service changes, hopefully that carries forward without breaking any clients. When you’re ready to introduce formal API versioning semantics into your service, then any previously unversioned services snap to a single, default API version.
Enable Backward Compatibility
The default API versioning semantics require that all clients explicitly request an API version for a service. This would break backward compatibility with existing clients, so we need a way to address this. The API versioning options provide a way to change the default behaviors that will enable supporting services that don’t explicitly declare API versions.
The bare minimum requirement to enable backward compatibility is to assume the default API version when a client does not explicitly request an API version. This will allow a client to continue making requests to existing services without providing API version information. Your existing controller implementations that back these services do not require any attribution or configuration to enable this behavior. Clients wishing to upgrade to new versions of a service must begin explicitly specifying an API version.
services.AddApiVersioning( options => options.AssumeDefaultVersionWhenUnspecified = true );
The assumed API version is 1.0 by default. From a client’s perspective, the default API version is inconsequential. As
a service author, however, you may want to choose a different default API version so that it aligns with your overall
API versioning scheme and instrumentation requirements.
services.AddApiVersioning(
options =>
{
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion( new DateOnly( 2016, 7, 1 ) );
} );
If these basic configuration settings are still insufficient for your needs, then you will need to use or create an API version selector and register it in the API versioning options.
Deprecating Versions
When a service supports multiple API versions, some versions will eventually be deprecated over time. To advertise that one or more API versions have been deprecated, simply decorate your controller with the deprecated API versions. A deprecated API version does not mean the API version is not supported. A deprecated API version means that the version will become unsupported after six months or more.
The following examples illustrate how to specify deprecated API versions depending on which service API versioning approach you selected.
This example demonstrates API versioning using all non-URL segment methods.
Minimal API
var api = app.NewVersionedApi();
var hello = api.MapGroup( "/api/helloworld" )
.HasDeprecatedApiVersion( 1.0 )
.HasApiVersion( 2.0 );
hello.MapGet( "/", () => "Hello world!" );
hello.MapGet( "/", () => "Hello world v2.0!" ).MapToApiVersion( 2.0 );
Mvc (Core)
[ApiController]
[ApiVersion( 2.0 )]
[ApiVersion( 1.0, Deprecated = true )]
[Route( "api/[controller]" )]
public class HelloWorldController : ControllerBase
{
[HttpGet]
public string Get() => "Hello world!"
[HttpGet, MapToApiVersion( 2.0 )]
public string GetV2() => "Hello world v2.0!";
}
This example demonstrates API versioning using the URL segment method.
Minimal API
var api = app.NewVersionedApi();
var hello = api.MapGroup( "/api/v{version:apiVersion}/helloworld" )
.HasDeprecatedApiVersion( 1.0 )
.HasApiVersion( 2.0 );
hello.MapGet( "/", () => "Hello world!" );
hello.MapGet( "/", () => "Hello world v2.0!" ).MapToApiVersion( 2.0 );
MVC (Core)
[ApiController]
[ApiVersion( 2.0 )]
[ApiVersion( 1.0, Deprecated = true )]
[Route( "api/v{version:apiVersion}/[controller]" )]
public class HelloWorldController : ControllerBase
{
[HttpGet]
public string Get() => "Hello world!"
[HttpGet, MapToApiVersion( 2.0 )]
public string GetV2() => "Hello world v2.0!";
}
Removing a Service
To permanently sunset a service, simply remove that controller or API version from your implementation. The route will
no longer be matched. When one or more specific API versions cannot be matched, clients will receive HTTP status code
400 (Bad Request). If no candidate routes match at all, clients will receive HTTP status code 404 (Not Found).
Version Advertisement
Splitting implemented service API versions across hosted applications or endpoints is a fairly common scenario. There are several reasons why you might choose to split hosted endpoints, such as different run-time versions or traffic load balancing.
When service API versions are split across deployments, two issues arise:
- The correct service API version cannot be selected across deployments.
- The set of implemented service API versions cannot be aggregated across deployments.
Service Gateway
The first issue can be remedied by a using a service gateway. The gateway becomes responsible for obfuscating which endpoints host which API versions. The exact method in which gateways implement this functionality is at the discretion of service authors.
Future consideration is being investigated to support YARP.
Service API Version Advertisement
Since there is no direct way to know or interrogate the available API version information at runtime in a performant manner when services are deployed separately, an alternate approach is required. This concept is referred to as service API version advertisement. Each service will advertise the supported and deprecated API versions it knows about.
A service can advertise its supported and deprecated API versions using the AdvertiseApiVersionsAttribute. This
attribute functions almost identically to the ApiVersionAttribute, except that it is never considered for controller
resolution and cannot be applied to an action. The advertised and implemented API versions are always aggregated
together.
The following is an example of a service with API version 2.0 hosted at another endpoint that knows that API version
1.0 is a supported version somewhere else:
[ApiVersion( 2.0 )]
[AdvertiseApiVersions( 1.0 )]
[ApiController]
[Route( "api/[controller]" )]
public class HelloWorld2Controller : ControllerBase
{
[HttpGet]
public string Get() => "Hello world v2.0!" );
}
[ApiVersion( 2.0 )]
[AdvertiseApiVersions( 1.0 )]
[ApiController]
[Route( "api/v{version:apiVersion}/helloworld" )]
public class HelloWorld2Controller : ControllerBase
{
[HttpGet]
public string Get() => "Hello world v2.0!" );
}
This service implementation will now advertise that API version 1.0 and 2.0 are supported through the
api-supported-versions HTTP header even though it has no knowledge about where API version 1.0 is. In a similar
fashion, a service can also advertise deprecated API versions. Note that the ApiVersioningOptions.ReportApiVersions
must be enabled for the HTTP headers to be returned in responses.
The only drawback to this approach is that each implementation needs to be updated with the supported and deprecated API
versions when new API versions are released. One possible solution to this limitation is to create an
IApiVersionProvider attribute that reads the advertised API versions from a configuration source such as a file or
database. If this is still undesirable, then there is still the option of using HTTP header injection by the host server
or another mechanism to send the supported and deprecated API version information.
Mixing Minimal APIs with Controllers
Mixing existing controller-based APIs with Minimal APIs is a supported scenario, but the collation of API versions is broken by default. This is simply because there is no intrinsic way to group controllers and Minimal APIs together. However, by advertising API versions across implementations with the same name, the correct collation is possible.
[ApiController]
[ApiVersion( 1.0 )]
[AdvertiseApiVersions( 2.0 )]
[Route( "api/[controller]" )]
public class HelloWorld2Controller : ControllerBase
{
[HttpGet]
public string Get() => "Hello world v1.0!" );
}
Figure 1: the controller-based API in 1.0
var hello = app.NewVersionedApi();
hello.MapGet( "/api/helloworld", () => "Hello world v2.0!" )
.HasApiVersion( 2.0 )
.AdvertisesApiVersion( 1.0 );
Figure 1: a minimal API in 2.0
When ApiVersioningOptions.ReportApiVersions is enabled the controller and Minimal API implementations will both return
api-supported-versions: 1.0, 2.0.
Controller Naming Conventions
There are a few implicit conventions to be aware of.
Always Versioned
Once you opt into API versioning, every API controller has an API version. This is true even if the controller does not have an explicit attribute or configured convention. When otherwise unspecified, the version applied to a controller derives from ApiVersioningOptions.DefaultApiVersion.
Naming
ASP.NET provides a built-in convention for controller names that use the form <Name>Controller where Controller will
be trimmed off when exactly that text. API Versioning slightly expands this convention. It will honor the convention of
<Name>[#]Controller. This allows you to have two controller types in the same namespace for different API versions,
but for the same resource; for example, ValuesController and Values2Controller will both have the name Values.
Naming is important for grouping controllers together.
Unfortunately, this can cause an issue for service API versioning if you want to split the implementation across different types. If the defining type is in a different .NET namespace, then there is no issue; however, if they are in the same namespace there would be a name collision. For example:
namespace My.Services.V1
{
[ApiVersion( 1.0 )]
[Route( "[controller]" )]
public class HelloWorldController : ControllerBase
{
[HttpGet]
public string Get() => "Hello world v1.0!";
}
}
namespace My.Services.V2
{
[ApiVersion( 2.0 )]
[Route( "[controller]" )]
public class HelloWorldController : ControllerBase
{
[HttpGet]
public string Get() => "Hello world v2.0!";
}
}
Controllers separated by .NET namespace
namespace My.Services.Controllers
{
[ApiVersion( 1.0 )]
[Route( "[controller]" )]
public class HelloWorldController : ControllerBase
{
[HttpGet]
public string Get() => "Hello world v1.0!";
}
[ApiVersion( 2.0 )]
[Route( "helloworld" )]
public class HelloWorld2Controller : ControllerBase
{
[HttpGet]
public string Get() => "Hello world v2.0!";
}
}
Controllers with different names in the same .NET namespace
To address name collisions and provide control over how collation happens, API Versioning provides the following service:
public interface IControllerNameConvention
{
string NormalizeName( string controllerName );
string GroupName( string controllerName );
}
NormalizeName controls how or whether a controller name is normalized. GroupName provides the name used to group
and collate on, which may not necessarily be the same as the normalized name. ControllerNameConvention provides
three implementations out-of-the-box.
Default
ControllerNameConvention.Default provides the default configuration which extends the original convention to have the
form: <Name>[#]Controller. This means that if you already have a HelloWorldController, you can now have a
HelloWorld2Controller and HelloWorld3Controller. Each type name removes the Controller suffix as well as any
trailing numbers. All of these controllers would end up named and grouped HelloWorld.
Original
ControllerNameConvention.Original provides an alternate configuration that retains the original naming convention.
Consider that you have a type named S3Controller. In this scenario, you do not want the 3 to be stripped away.
If you have multiple versions of a such a controller, you would need your own implementation that understands this
behavior or separate the types into different .NET namespaces.
Grouped
ControllerNameConvention.Grouped is a hybrid configuration the combines the Default and Original conventions.
For the purposes of the name, the original convention is used. For the purposes of grouping, the default convention is
used. A controller type of S3Controller would have the name S3, but the group name S. The group name is only used
for collation and is never displayed anywhere, so this behavior is acceptable.
Attribute
If you do not want to rely on a convention, you can explicitly provide a name using the ControllerNameAttribute. The
name provided will be used verbatim for the [controller] token, the controller name, and for grouping. This attribute
is particularly useful with OData because the name of the controller must also exactly match the name of the associated
entity set.
[ApiVersion( 2.0 )]
[ControllerName( "HelloWorld" )]
[Route( "[controller]" )]
public class HelloWorld2Controller : ControllerBase
{
[HttpGet]
public string Get() => "Hello world v2.0!";
}
API Controllers
A controller is just a controller in ASP.NET Core; there is no distinction between a UI Controller and an API
Controller. Some applications mix UI controllers and API controllers together. This will result in all controllers
requiring an API version, which is undesirable for UI controllers. The advent of the ApiControllerAttribute made it
possible to disambiguate the two types of controllers.
API Versioning 3.0 introduced two new interfaces:
interface IApiControllerFilter
{
IList<ControllerModel> Apply( IList<ControllerModel> controllers );
}
interface IApiControllerSpecification
{
bool IsSatisifedBy( ControllerModel controller );
}
The IApiControllerFilter filters which controllers should be considered API controllers. The default implementation
typically does not need to be replaced. The IApiControllerSpecification defines a specification as to whether a
particular controller is an API controller.
There are two built-in specifications:
ApiBehaviorSpecification- matches controllers decorated by[ApiController]ODataControllerSpecification- matches controllers decorated by[ODataRouting]
An API controller will be considered any controller that matches at least one specification. If a built-in specification does not meet your specific needs, you can create your own:
// considers controllers inheriting from Controller to be a UI controller
public class NonUIControllerSpecification : IApiControllerSpecification
{
private readonly Type UIControllerType = typeof( Controller ).GetTypeInfo();
public bool IsSatisfiedBy( ControllerModel controller ) =>
!UIControllerType.IsAssignableFrom( controller.ControllerType )
}
Register your specification in the services configuration:
services.TryAddEnumerable(
ServiceDescriptor.Transient<IApiControllerSpecification, NonUIControllerSpecification>() );
Versioned Models
When an API is versioned, it is often necessary to version the models that are used in the API. This is especially true when shared models are used in request and response messages.
Asp.Versioning.Abstractions provides the [VisibleInApiVersion] attribute to indicate which API versions a model is
visible in. When no attribute is applied, the model is visible in all APIs. The abstractions library does not have any
dependency on ASP.NET and carries no additional dependencies. Any intended use case is referencing abstractions in
libraries that provide version-specific metadata.
Consider the following model:
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
[VisibleInApiVersion( "2.0" )]
public string MiddleName { get; set; }
public string LastName { get; set; }
[VisibleInApiVersion( "2.0" )]
public string Email { get; set; }
[VisibleInApiVersion( "3.0" )]
public string Phone { get; set; }
}
The Person model indicates that:
Id,FirstName, andLastNameare visible in all API versionsMiddleNameandEmailare only visible starting in API version2.0Phoneis only visible starting in API version3.0
Each value passed to the [VisibleInApiVersion] attribute is a range expression representing a rule set. The rule is
parsed into a range that determines if the annotated member applies to an API version. Annotations never define any
API versions.
In rare cases, you might need a split range. The [VisibleInApiVersion] attribute supports multiple entries; for example:
public class ExperimentalSettings
{
[VisibleInApiVersion( "[,2.0)", "(2.0,]" )]
public bool IsEnabled { get; set; }
}
These rules would express that IsEnabled is included in every API version except 2.0.
Notation
The interval notation for version ranges is as follows:
| Notation | Applied Rule | Description |
|---|---|---|
| 1.0 | x ≥ 1.0 | Minimum version, inclusive |
| [1.0,) | x ≥ 1.0 | Minimum version, inclusive |
| (1.0,) | x > 1.0 | Minimum version, exclusive |
| [1.0] | x == 1.0 | Exact version match |
| (,1.0] | x ≤ 1.0 | Maximum version, inclusive |
| (,1.0) | x < 1.0 | Maximum version, exclusive |
| [1.0,2.0] | 1.0 ≤ x ≤ 2.0 | Exact range, inclusive |
| (1.0,2.0) | 1.0 < x < 2.0 | Exact range, exclusive |
| [1.0,2.0) | 1.0 ≤ x < 2.0 | Mixed inclusive minimum and exclusive maximum version |
| (1.0) | invalid | invalid |
Validation
Important
This feature is currently only available for JSON content.
When a versioned API receives a request the deserialization process will enforce that a client did not over-post
more data than is allowed for the requested API version, even if the backing model defines the corresponding property.
If a client attempts to post data that is not visible in the requested API version, the request will be rejected with
HTTP status code 400 (Bad Request). The response body will indicate which properties were not visible in the
requested API version. This is the same behavior as if the property did not exist on the model at all.
API Explorer
The API Explorer will look for and respect annotations; specifically, IAnnotation<T, ApiVersionRange>. The explored
API descriptions will only include models and properties that are visible in the API version being explored.
The OpenAPI extensions will leverage this information to generate version-specific OpenAPI documents with constrained model properties. A client will not be able to tell whether you used a single model behind the scenes or many. From their perspective, each model will appear to be unique with its own affinity the API version that defined it.
Configuring Your Application
Although different variations of ASP.NET have distinct application initialization methods, careful consideration was taken to make the API versioning configuration as similar as possible across all applications models.
Two methods of configuration are supported. All examples will use the new top-level statements method, but the older
Startup.cs method is still supported.
Top-Level Statements
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddControllers();
builder.Services.AddProblemDetails();
builder.Services.AddApiVersioning()
.AddMvc(); // ← brings in MVC Core; unnecessary for Minimal APIs
// remaining setup omitted for brevity
Startup
The configuration for ASP.NET Core applications typically occur in the ConfigureServices method of the Startup.cs
file. To enable API versioning support with the default options, use the following configuration:
public void ConfigureServices( IServiceCollection services )
{
services.AddControllers();
services.AddProblemDetails();
services.AddApiVersioning()
.AddMvc(); // ← brings in MVC Core; unnecessary for Minimal APIs
// remaining setup omitted for brevity
}
API Versioning Options
The API Versioning options allows you to configure, customize, and extend the default behaviors when you add API versioning to your application.
ApiVersioningOptions has the following configuration settings:
- ApiVersionReader
- ApiVersionSelector
- DefaultApiVersion
- AssumeDefaultVersionWhenUnspecified
- ReportApiVersions
- Policies
- Conventions
- RouteConstraintName
- UnsupportedApiVersionStatusCode
Assume Default Version When Unspecified
This option enables support for clients to make requests with implicit API versioning. This option is disabled by
default, which means that all clients must send requests with an explicit API version. Services will respond to client
requests that do not specify an API version with either HTTP status code 400 (Bad Request) or HTTP status code 404
(Not Found), depending whether the requested route exists.
This option 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.
When this option is enabled, clients will be able to make a request without specifying a specific API version. The API version of the service that is selected will be based on the configured IApiVersionSelector.
Default API Version
This option defines what the default ApiVersion will be for a service without explicit API version information. This
is useful for services that use implicit API versioning in their initial release. This value can also be used for
services that may be defined in external assemblies that are not decorated with any API version information. The
configured, default value is 1.0.
AddApiVersioning( options => options.DefaultApiVersion = new ApiVersion( 2.0 ) );
Report API Versions
This option enables sending the api-supported-versions and api-deprecated-versions HTTP header in responses. When
this option is enabled, it will add the ReportApiVersionsAttribute as a global action filter to the application
configuration. If there are any deprecation or sunset policies defined, they will also be included in the
response headers. This option is disabled by default.
AddApiVersioning( options => options.ReportApiVersions = true );
Conventions
This option allows you to construct API version conventions for each of your services as opposed to using .NET attributes. You can also choose to additionally use .NET attributes and the union of both sets of defined API version information will be applied. The default convention builders can be extended and/or replaced in this option. For more information on using conventions see the API version conventions topic.
Route Constraint Name
This option allows you to change the name of the API version route constraint. The default name is "apiVersion".
Policies
This option allows you to define API versioning policies. This is primarily used to define deprecation and sunset policies about when an API. Related links, such as to a public policy web page, can also be reported that may be useful to clients for more information about your API policies.
Unsupported API Version Status Code
This option allows you to configure the HTTP status code used when an unsupported API version is requested. The default
value is 400 (Bad Request).
While any HTTP status code can be used, the following are the most sensible:
| Status Code | Meaning | Description |
|---|---|---|
| 400 | Bad Request | The API doesn’t support this version |
| 404 | Not Found | The API doesn’t exist |
| 501 | Not Implemented | The API isn’t implemented |
Remarks
Regardless of the configured option, when versioning by:
- URL segment,
404is always returned - media type,
406or415is always returned
API Version Reader
The IApiVersionReader interface defines the behavior of how an API version is read in its raw, unparsed form from the
current HTTP request. There are multiple methods for reading an API version provided out-of-the-box or you can implement
your own. The default, configured API version reader is a composed instance QueryStringApiVersionReader and
UrlSegmentApiVersionReader.
Query String
The QueryStringApiVersionReader reads the requested API version from the requested query string. The default query
string parameter name is api-version. The constructor for this class accepts the name of a query string parameter
so that an alternate query string parameter can be used.
// svc?api-version=2.0
AddApiVersioning( options => options.ApiVersionReader = new QueryStringApiVersionReader() );
// svc?v=2.0
AddApiVersioning( options => options.ApiVersionReader = new QueryStringApiVersionReader( "v" ) );
Media Type
The MediaTypeApiVersionReader reads the requested API version from a HTTP media type request header. The supported
headers are Content-Type and Accept. If both headers are present, then Content-Type is preferred. If the
Accept header specifies qualities, then the API version associated with the highest quality is selected. This
behavior is independent of media type negotiation. The default media type parameter is "v", but you may specify an
alternate name. This method of API versioning does not conform to the Microsoft REST Guidelines; however, it is
generally accepted as a fully REST-compliant method of versioning.
// Content-Type: application/json;v=2.0
AddApiVersioning( options => options.ApiVersionReader = new MediaTypeApiVersionReader() );
// Content-Type: application/json;version=2.0
AddApiVersioning( options => options.ApiVersionReader = new MediaTypeApiVersionReader( "version" ) );
The MediaTypeApiVersionReaderBuilder is also available with additional features that allow:
- Define multiple media type parameters
- Mutually include specific media types
- Mutually exclude specific media types
- Match media types by template
- Match media types by pattern
- Disambiguate between multiple API versions
// Accept: application/json;v=2.0
AddApiVersioning(
options =>
{
var builder = new MediaTypeApiVersionReaderBuilder();
options.ApiVersionReader = builder.Parameter( "v" )
.Include( "application/json" )
.Build();
} );
// Accept: application/vnd.my.company.v1+json
AddApiVersioning(
options =>
{
var builder = new MediaTypeApiVersionReaderBuilder();
options.ApiVersionReader = builder.Template( "application/vnd.my.company.v{version}+json" )
.Build();
} );
Header
The HeaderApiVersionReader reads the requested API version from a HTTP request header. There is no default or standard
HTTP header. You must define which HTTP header name or names contain the API version information. This method of API
versioning does not conform to the Microsoft REST Guidelines.
AddApiVersioning( options => options.ApiVersionReader = new HeaderApiVersionReader( "api-version" ) );
URL Path Segment
The UrlSegmentApiVersionReader reads the requested API version from a URL path segment. Extraction of the value is
dependent upon the ApiVersionRouteConstraint which is matched by the ApiVersioningOptions.RouteConstraintName
property.
AddApiVersioning( options => options.ApiVersionReader = new UrlSegmentApiVersionReader() );
Warning
This method of API versioning violates the REST Uniform Interface constraint and is the slowest of all versioning methods because the requested value cannot always easily be extracted from the URL path segment. If you’re creating a new API, consider using query string or media type versioning instead.
Composition
Multiple IApiVersionReader implementations can be combined using composition instead of inheritance. For convenience,
you can use ApiVersionReader.Combine to compose multiple API version reading styles.
AddApiVersioning(
options => options.ApiVersionReader = ApiVersionReader.Combine(
new QueryStringApiVersionReader(),
new HeaderApiVersionReader() { HeaderNames = { "x-ms-api-version" } } ) );
API Version Conventions
API version conventions allow you to specify API version information for your services without having to use .NET attributes. There are a number of reasons why you might choose this option. The most common reasons are:
- 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 configuration
Instead of applying [ApiVersion] to the controller, we can instead choose to define a convention in the
API versioning options.
services.AddApiVersioning()
.AddMvc( options =>
{
options.Conventions.Controller<MyController>().HasApiVersion( 1.0 );
} );
All of the semantics that can be expressed with .NET attributes can be defined using conventions. Consider what version
2.0 of the previous controller with interleaved API versions might look like:
[ApiController]
[Route( "[controller]" )]
public class MyController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
[HttpGet]
public IActionResult GetV2() => Ok();
[HttpGet( "{id:int}" )]
public IActionResult GetV2( int id ) => Ok();
}
The API version conventions might then be defined as:
options.Conventions.Controller<MyController>()
.HasDeprecatedApiVersion( 1.0 )
.HasApiVersion( 2.0 )
.Action( c => c.GetV2() ).MapToApiVersion( 2.0 )
.Action( c => c.GetV2( default ) ).MapToApiVersion( 2.0 );
If you use API version conventions and .NET attributes, then the constructed ApiVersionModel for the corresponding
controller will be an aggregated union of the two sets of information.
Custom
You can also define custom conventions via the IControllerConvention interface and add them to the builder:
public interface IControllerConvention
{
bool Apply( IControllerConventionBuilder controller, ControllerModel controllerModel );
}
Custom conventions are added to the convention builder through the API versioning options:
options.Conventions.Add( new MyCustomConvention() );
Namespace
This built-in convention allows you to version your controllers by the .NET namespace they reside in when applied.
options.Conventions.Add( new VersionByNamespaceConvention() );
The defined namespace name must conform to the API version format so that it can be parsed. The language-neutral syntax is:
letter = "A" | "B" | "C" | "D" | "E" | "F" | "G"
| "H" | "I" | "J" | "K" | "L" | "M" | "N"
| "O" | "P" | "Q" | "R" | "S" | "T" | "U"
| "V" | "W" | "X" | "Y" | "Z" | "a" | "b"
| "c" | "d" | "e" | "f" | "g" | "h" | "i"
| "j" | "k" | "l" | "m" | "n" | "o" | "p"
| "q" | "r" | "s" | "t" | "u" | "v" | "w"
| "x" | "y" | "z" ;
prefix = "v" | "V" ;
positive = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
digit = "0" | positive ;
day = ( [ "0" ] positive ) | ( "1" | "2" ) digit | ( "3" ( "0" | "1" ) ) ;
month = ( [ "0" ] positive ) | ( "1" ( "0" | "1" | "2" ) ) ;
year = 4 * digit ;
api-version = prefix ( ( year "_" month "_" day ) | ( digit [ "_" digit ] ) ) [ "_" { letter } ] ;
The . character is considered a namespace delimiter in many programming languages. This character must be changed to
_ so that newly added files have the correct format. In addition, most languages do not allow the name of a namespace
to start with a number. Since a leading character is required, the first character must be v or V. There is no
requirement as to where the API version must appear in the namespace.
By default, API versions derived from a namespace will be considered supported. If the controller is decorated with the
ObsoleteAttribute, then the API version inferred from the containing namespace will be considered deprecated.
Examples
Contoso.Api.v1.Controllers→ 1.0Contoso.Api.v1_1.Controllers→ 1.1Contoso.Api.v0_9_Beta.Controllers→ 0.9-BetaContoso.Api.v20180401.Controllers→ 2018-04-01Contoso.Api.v2018_04_01.Controllers→ 2018-04-01Contoso.Api.v2018_04_01_Beta.Controllers→ 2018-04-01-BetaContoso.Api.v2018_04_01_1_0_Beta.Controllers→ 2018-04-01.1.0-Beta
Contoso
└ Api
├─ v1
│ └ Controllers
├─ v2
│ └ Controllers
└─ v2_5
└ Controllers
Figure 1: Sample folder layout with numeric API versions
Contoso
└ Api
├─ v2018_07_01
│ └ Controllers
├─ v2018_08_01
│ └ Controllers
└─ v2018_09_01
└ Controllers
Figure 2: Sample folder layout with date API versions
API Version Selector
The IApiVersionSelector interface defines the behavior of how an API version is selected for a given request context.
This service is typically only used when a client has not requested an explicit API version and the
AssumeDefaultVersionWhenUnspecified option is enabled. The role of the API version selector is to select the
appropriate API version given the current request and a model of available API versions.
Note
Although the
IApiVersionSelectorcan be used for other scenarios, it is currently only utilized when no API version is requested by a client and the server allows this behavior. The selector provides the rules that selects the most appropriate API version according to the server. There is no built-in capability to ignore an API version explicitly requested by a client.
There are four API version selectors provided out-of-the-box or you can implement your own. The default, configured API
version selector is DefaultApiVersionSelector.
Default
The DefaultApiVersionSelector always selects the configured DefaultApiVersion, regardless of the request or
available API version information.
Constant
The ConstantApiVersionSelector always selects a user-defined API version, regardless of the request or available API
version information.
AddApiVersioning(
options => options.ApiVersionSelector =
new ConstantApiVersionSelector(
new ApiVersion( new( 2016, 7, 1 ) ) );
Current
The CurrentImplementationApiVersionSelector selects the maximum API version available which does not have a version
status. If no match is found, it falls back to the configured DefaultApiVersion. An an example, if the versions 1.0,
2.0, and 3.0-alpha are available, then 2.0 will be selected because it’s the highest, implemented or released API
version.
AddApiVersioning(
options => options.ApiVersionSelector =
new CurrentImplementationApiVersionSelector( options ) );
Lowest
The LowestImplementedApiVersionSelector selects the minimum API version available which does not have a version
status. If no match is found, it falls back to the configured DefaultApiVersion. As an example, if the versions
0.9-beta, 1.0, 2.0, and 3.0-alpha are available, then 1.0 will be selected because it’s the lowest,
implemented or released API version. Your services must be decorated with one or more API versions for the selector to
work effectively or it will always select the configured DefaultApiVersion.
AddApiVersioning(
options => options.ApiVersionSelector =
new LowestImplementedApiVersionSelector( options ) );
API Versioning with OData
Service API versioning using OData is similar to the normal configuration with a few slight variations. Each implemented OData controller has an associated entity set and each entity set is defined in an Entity Data Model (EDM). Once we introduce API versioning, each versioned OData controller now needs an EDM per API version. To satisfy this requirement, we’ll use the new VersionedODataModelBuilder, build a collection of EDMs for each API version, and then map a set of routes for them.
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddControllers().AddOData();
builder.Services.AddProblemDetails();
builder.Services.AddApiVersioning()
.AddOData( options => options.AddRouteComponents( "api" ) );
var app = builder.Build();
app.MapControllers();
app.Run();
It is possible to imperatively use:
.AddOData( options => options.ModelConfigurations.Add( new PersonModelConfiguration() ) )
however, it is typically unnecessary because this will automatically happen via dependency injection.
Important
Calling
AddControllers().AddOData( options => options.AddRouteComponents( ... ) )will be completely ignored by API Versioning. Due to the OData design, it is impossible to extend or customize this behavior. Instead, you need to useAddApiVersioning().AddOData( options => options.AddRouteComponents( ... ) ). The standardAddODataconfiguration can still be used to configure global query option settings.
Model Configurations
A model configuration enables OData service authors to apply model setups that are specific to a service API version.
The VersionedODataModelBuilder will call Apply for each discovered API version with the current ODataModelBuilder.
public interface IModelConfiguration
{
void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix );
}
The implementation of a model configuration can provide all variations of a model or they can be spit across multiple implementations. The applied model does not have to be same across API versions.
Consider the following model:
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
}
Let us assume that the OData service for this model has three versions: 1.0, 2.0, and 3.0. In API version 1.0,
a person had the properties Id, FirstName, and LastName. In API version 2.0 we introduced the Email property.
In API version 3.0 we introduced the Phone property. If we implement the entire model configuration in a single
class, it might look like:
public class PersonModelConfiguration : IModelConfiguration
{
private void ConfigureV1( ODataModelBuilder builder ) =>
ConfigureCurrent( builder ).Ignore( p => p.Email ).Ignore( p => p.Phone );
private void ConfigureV2( ODataModelBuilder builder ) =>
ConfigureCurrent( builder ).Ignore( p => p.Phone );
private EntityTypeConfiguration<Person> ConfigureCurrent( ODataModelBuilder builder )
{
var person = builder.EntitySet<Person>( "People" ).EntityType;
person.HasKey( p => p.Id );
return person;
}
public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix )
{
switch ( apiVersion.MajorVersion )
{
case 1:
ConfigureV1( builder );
break;
case 2:
ConfigureV2( builder );
break;
default:
ConfigureCurrent( builder );
break;
}
}
}
Even through we have a single Person class, the EDM associated with the service API version will render the model
according the requested API version.
~/people(1)?api-version=1.0
{
"id": 1,
"firstName": "John",
"lastName": "Doe"
}
~/people(1)?api-version=2.0
{
"id": 1,
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@somewhere.com"
}
~/people(1)?api-version=3.0
{
"id": 1,
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@somewhere.com",
"phone": "555-555-5555"
}
Dependency Injection
Dependency injection (DI) is a first-class concept in ASP.NET Core. This intrinsic capability enables API versioning to
automatically register all discovered implementations of IModelConfiguration. API versioning also registers a
single, but replaceable mapping for VersionedODataModelBuilder. This enables you to declare
VersionedODataModelBuilder as a dependent parameter wherever you would like ASP.NET Core to inject the configured
instance. The injected instance will always have all of the discovered IModelConfiguration instances, but you can
continue to modify the builder until you are ready to create all of the EDMs via
VersionedODataModelBuilder.GetEdmModels().
Model Substitution
The Entity Data Model (EDM) does not have a one-to-one correlation with the corresponding .NET type. As a result, it’s common and quite plausible that a single .NET type for a model will be used in different EDMs. This is already supported by defining model configurations.
The challenge is representing this same model in the OData API Explorer. API Explorer consumers, such as OpenAPI/Swagger document generators, rely on using Reflection to enumerate the members of a model. These consumers have no intrinsic understanding of an EDM and do not know that the response type may be a subset of the discovered .NET type. To address this, the OData API Explorer supports Model Substitution.
Model substitution takes effect whenever a .NET type does not exactly match the definition of the corresponding EDM type. When this occurs, the API Explorer will generate a new .NET type that is a subset of the original type, but exactly matches the definition of the EDM type. When consumers use Reflection on the substituted type, it will only be a subset of the original .NET type. A similar scenario occurs for OData actions because the action parameters are modeled as a dictionary of key/value pairs. A substitution type will be generated which matches the definition of the OData action parameters.
There is no configuration or additional setup required to enable Model Substitution. As the OData API Explorer would otherwise report incorrect response types, this feature is automatically enabled and cannot be disabled out-of-the-box.
Model substitution supports the following features:
- Entity Types
- Complex Types
- Structured Type Properties
- Self-Referencing
- Parent-Child collections
- Attributes (ex: Model Bound Attributes, Data Annotations, etc)
- Defined on the original .NET type (ex: class or structure)
- Defined on the original .NET type property
- Action parameters
IEnumerable<T>response typesSingleResult<T>response typesODataValue<T>response typesDelta<T>parameters
The OData API Explorer generates substitution types using the IModelTypeBuilder.
public interface IModelTypeBuilder
{
Type NewStructuredType(
IEdmStructuredType structuredType,
Type clrType,
ApiVersion apiVersion,
IEdmModel edmModel );
Type NewActionParameters(
IServiceProvider services,
IEdmAction action,
ApiVersion apiVersion,
string controllerName );
}
Partial OData
The DefaultModelTypeBuilder does not enable support for ad hoc models using only part of the OData stack. This is
the default behavior because without an EDM and the OData response writers, no filtering of model members is performed.
This mostly likely means that you have a different model per API version, which would negate the usefulness of model
substitution.
If you have a way to filter you models to match what you have configured in an ad hoc EDM, you can re-enable model
substitution by re-registering IModelTypeBuilder with new DefaultModelTypeBuilder(includeAdHocModels: true).
Versioned Model Builder
The VersionedODataModelBuilder is a builder of builders, which enables creating an Entity Data Model (EDM) for each
service API version.
public class VersionedODataModelBuilder
{
public Func<ODataModelBuilder> ModelBuilderFactory { get; set; }
public Action<ODataModelBuilder, ApiVersion, string> DefaultModelConfiguration { get; set; }
public IList<IModelConfiguration> ModelConfigurations { get; }
public Action<ODataModelBuilder, IEdmModel> OnModelCreated { get; set; }
public IEnumerable<IEdmModel> GetEdmModels();
public virtual IEnumerable<IEdmModel> GetEdmModels(string routePrefix);
}
Model Builder Factory
The ModelBuilderFactory property defines a factory function used to initialize a new ODataModelBuilder for each
service API version. The default value creates a new instance of the ODataConventionModelBuilder. You can update
this property to substitute your own ODataModelBuilder or provide a custom initialization setup.
var modelBuilder = new VersionedODataModelBuilder( configuration )
{
ModelBuilderFactory = () => new ODataConventionModelBuilder().EnableLowerCamelCase()
};
Note
Using camel-casing for JSON documents is very common. Beginning 3.0,
EnableLowerCamelCase()is automatically called.
Model Configurations
The ModelConfigurations property is a collection of IModelConfiguration objects which define the
configuration of one or more models to be applied for each API version. Although it’s not required, it’s recommended
that you create one IModelConfiguration per entity model.
var modelBuilder = new VersionedODataModelBuilder( configuration )
{
ModelConfigurations =
{
new PersonModelConfiguration()
}
};
Note
IModelConfigurationinstances are automatically discovered through Dependency Injection when you declareIEnumerable<IModelConfiguration>orVersionedODataModelBuilderas a dependent parameter. TheModelConfigurationsproperty can be modified after injection, if required.
Default Model Configuration
The DefaultModelConfiguration property defines a callback that can be used to apply a default model configuration.
Specifying a callback is useful if you have a configuration that applies to all models or if you want to have a single,
inline model configuration.
var modelBuilder = new VersionedODataModelBuilder( configuration )
{
DefaultModelConfiguration = ( builder, apiVersion, routePrefix )
{
// TODO: default configuration for all models
}
};
On Model Created
The OnModelCreated property is a callback that serves the same purpose as
ODataConventionModelBuilder.OnModelCreated. This callback can be used to perform any additional setup or configuration
required after each EDM model is created.
Get EDM Models
The GetEdmModels method behavior is similar to the ODataModelBuilder.GetEdmModel method. This method performs the
following actions:
- Discover and enumerate each service API version
- For each service API version:
- Create an
ODataModelBuildervia the ModelBuilderFactory - Invoke IModelConfiguration.Apply for each item defined in
ModelConfigurations, including theDefaultModelConfiguration, with the current model builder and API version - Invoke
ODataModelBuilder.GetEdmModelto generate the current EDM model - Apply the
ApiVersionAnnotationwith the current API version to the generated EDM model - Invoke
OnModelCreatedwith the current model builder and generated EDM model, if defined
- Create an
Versioned Controllers
Creating an OData controller that supports API versioning isn’t much different from creating a regular OData controller.
The following controller depicts a service that support API version 1.0 and 2.0.
[ApiVersion( 1.0 )]
[ApiVersion( 2.0 )]
public class PeopleController : ODataController
{
// GET ~/people?api-version=[1.0|2.0]
public IQueryable<Person> Get() => new[] { new Person() }.AsQueryable();
// GET ~/people/1?api-version=[1.0|2.0]
public SingleResult<Person> Get( int key ) => SingleResult.Create( new Person() );
// PATCH ~/people/1?api-version=2.0
[MapToApiVersion( 2.0 )]
public UpdatedODataResult<Person> Patch( int key, Delta<Person> delta )
{
if ( !ModelState.IsValid )
{
return BadRequest( ModelState );
}
var person = new Person();
delta.Patch( person );
return Updated( person );
}
}
The PATCH method is only supported in API version 2.0 of the service. To be truly OData compliant, this service
should define an action mapped to API version 1.0 that always returns HTTP status code 501 (Not Implemented) instead
of falling back to HTTP status code 400 (Bad Request) or 404 (Not Found).
If you reviewed the Person model and configuration example for the IModelConfiguration, you’ll know what we
configured a single Person model with different properties available in different API versions. The default OData
model validation does some automatic heavy lifting for us using the defined EDM model. In addition to the other normal
validation you might have from Data Annotations, the current EDM model will provide further validation. For example,
even though the Person class has a Phone property, it was not defined until API version 3.0. If you try to send
a PATCH request like this:
PATCH /people/1?api-version=2.0 HTTP/2
content-type: application/json
content-length: 27
{ "phone": "555-555-5555" }
the built-in OData model validation will fail. The response will end up being HTTP status code 400 (Bad Request) with
an error message that indicates the phone property does not exist. In version 2.0 of the service, that is true and
the correct behavior.
Split Implementation
Service authors can choose to split service API versions across multiple controller types. In fact, for all but the simplest of version variations, this is the recommended approach. You may, however, notice something extra and a little unusual about the attribution for this controller.
Under the hood, the OData implementation still uses convention-based routing. When we split services across multiple
controller types, the new service implementation cannot have the same name. The only exception to this rule is if you
create version-specific namespaces for each version of the service. If the name of the controller cannot be the same as
the original controller type and we’re stuck with convention-based routing, how to do indicate what the name of the
controller should be? Enter the ControllerNameAttribute.
The ControllerNameAttribute allows you to specify an arbitrary name for a controller. In the strictest sense, this is
not convention-based; however, short of using different namespaces, there isn’t a way to define the correct name of the
controller. Without the ControllerNameAttribute, this controller would be named People2, which won’t match any
routes or, more specifically, any defined entity set. In OData, the controller route is paired with the corresponding
entity set name. The API version services honor this attribute and will use the controller name defined by the attribute
over the default convention name when present.
[ApiVersion( 3.0 )]
[ControllerName( "People" )]
public class People2Controller : ODataController
{
// GET ~/people?api-version=3.0
public IQueryable<Person> Get() => new[] { new Person() }.AsQueryable();
// GET ~/people/1?api-version=3.0
public SingleResult<Person> Get( int key ) => SingleResult.Create( new Person() );
// PATCH ~/people/1?api-version=3.0
public UpdatedODataResult<Person> Patch( int key, Delta<Person> delta )
{
if ( !ModelState.IsValid )
{
return BadRequest( ModelState );
}
var person = new Person();
delta.Patch( person );
return Updated( person );
}
}
Versioned Metadata
In order to support API versioning, the default MetadataController is replaced with a VersionedMetadataController
implementation. The main difference between the two is that the VersionedMetadataController will return service
document and entity data model (EDM) information for each defined API version.
[ReportApiVersions]
public class VersionedMetadataController : MetadataController
{
// omitted for brevity
}
Clients can now build proxies that have an affinity to a specific API version.
~/$metadata~/$metadata?api-version=1.0~/$metadata?api-version=2.0~/$metadata?api-version=3.0
If a client does not specify an API version, the assumed value will be the configured default API version. When a client is ready to adopt a new version of the service, they can update their tooling to point to the appropriate API version of the metadata endpoint and generate a new proxy based on the version-specific EDMX.
Tooling Support
The VersionedMetadataController also supports the HTTP OPTIONS method. This allows tools to query the service
document (~/) or $metadata endpoints and provide a client with choices as to which API version they would like to
create an OData client for.
For example, a tool can query the metadata endpoint:
OPTIONS /$metadata HTTP/2
host: my.api.com
which will produce a response that looks like:
HTTP/2 200
allow: GET, OPTIONS
odata-version: 4.0
api-supported-versions: 1.0, 2.0, 3.0
api-deprecated-versions: 0.9
deprecation: @1640995200
sunset: Thu, 01 Apr 2022 00:00:00 GMT
link: <https://docs.api.com/policies.html>; rel="deprecation"; title="API Policy"; type="text/html"
link: <https://docs.api.com/policies.html>; rel="sunset"; title="API Policy"; type="text/html"
link: </openapi/v1.json>; rel="openapi"; title="OpenAPI"; type="application/json"
A tool can choose to use this information is several ways. Any supported or deprecated API version is allowable. User
interface tools should filter out deprecated API versions by default, but it could alternatively provide warning if a
deprecated version is selected or will sunset in the near future. Tools that do not afford user interaction will
likely select the highest supported API version. Tools should also consider that the api-supported-versions and
api-deprecated-versions HTTP headers can be reported multiple times as defined in RFC 2616 §4.2.
An OData service which does not support API versioning should return with HTTP 501 (Not Implemented) as defined in
OData: Protocol §9.3.1 of the OData v4.0 specification. However, given that API versioning behaviors of an OData
service are not explicitly defined in the OData protocol, a client may also respond with HTTP 405 (Method Not
Allowed). Tools should graceful fallback to the standard metadata query operations when API versioning information is
unavailable.
Batching
OData batch operations are meant to execute the same way that other requests do; however, there may be some minor, but crucial differences required in the setup configuration depending on your target platform.
OData batch operations are facilitated by the OData batching middleware in ASP.NET Core. The built-in OData middleware
only allows a single ODataBatchHandler and cannot be extended. In theory, there should only be a single
ODataBatchHandler for the entire application, but there is no guarantee that is what a developer has done or wants.
API Versioning, therefore, provides alternate OData batch middleware that allows a version-specific ODataBatchHandler
if that is what you have configured. Additionally, API Versioning always registers a default ODataBatchHandler in
AddRouteComponents so you don’t have to, which is something OData does not do by default.
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddControllers().AddOData();
builder.Services.AddProblemDetails();
builder.Services.AddApiVersioning()
.AddOData( options => options.AddRouteComponents( "api" ) );
var app = builder.Build();
app.UseVersionedODataBatching();
app.UseRouting()
app.UseEndpoints( endpoints => endpoints.MapControllers() );
app.Run();
API Versioning with gRPC
Service API versioning using gRPC is nearly identical to the standard configuration with only a few modifications. When a gRPC service is registered, it indicates which API versions it serves. As with all other versioned services, the routes for gRPC services can overlap and they will be disambiguated by the requested API version.
syntax = "proto3";
import "google/api/annotations.proto";
package greet;
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
service Greeter {
// GET /greet/{name}?api-version=1.0
rpc SayHello (HelloRequest) returns (HelloReply) {
option (google.api.http) = {
get: "/greet/{name}"
response_body: "message"
};
}
}
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddApiVersioning().AddGrpc();
var app = builder.Build();
var greeter = app.NewVersionedApi();
greeter.MapGrpcService<GreeterService>().HasApiVersion( 1.0 );
app.Run();
Request Parameters
In most cases, adding API versioning to your gRPC services is orthogonal to how you define your service. gRPC sits atop HTTP. API versioning relies on the Uniform Interface REST constraint, which does not require knowing anything about your service. It doesn’t even know that gRPC is in use. This allows versioning by query string, HTTP header, or media type to work without any direct changes to your service. gRPC only supports mapping query string and route parameters in the path to messages defined by your service.
Query String
When you version by query string, you do not need to define any additional message fields, but you can. Fields that
do not appear as route parameters, the request body, nor response body are considered to be query parameters. The
default query parameter is "api-version", but this can be any name you like as long as the API versioning
configuration and message field name align.
syntax = "proto3";
import "google/api/annotations.proto";
package greet;
message HelloRequest {
// optional if you want to retrieve the api-version query parameter in requests
string api_version = 1 [json_name = "api-version"];
string name = 2;
}
message HelloReply {
string message = 1;
}
service Greeter {
// GET /greet/{name}?api-version=1.0
rpc SayHello (HelloRequest) returns (HelloReply) {
option (google.api.http) = {
get: "/greet/{name}"
response_body: "message"
};
}
}
If you need or want to retrieve the requested API version, but you do not want to include it as a message field, you can get it directly from the incoming request:
public class GreeterService : Greeter.GreeterBase
{
public override Task<HelloReply> SayHello( HelloRequest request, ServerCallContext context )
{
var apiVersion = context.GetHttpContext().ApiVersioningFeature.RawRequestedApiVersion;
return Task.FromResult( new HelloReply { Message = $"Hello {request.Name} (v{apiVersion})" } );
}
}
Note
RawRequestedApiVersionis the API version as it appear over the wire, whereasRequestedApiVersionwill be the parsedApiVersiontype.
URL Path Segment
An alternate method of API versioning is using a URL path segment. This method of versioning contradicts the Uniform Interface REST constraint and, therefore, comes with several limitations and additional configuration.
syntax = "proto3";
import "google/api/annotations.proto";
package greet;
message HelloRequest {
// required because it is a route parameter
string api_version = 1;
string name = 2;
}
message HelloReply {
string message = 1;
}
service Greeter {
// GET /v1/greet/{name}
rpc SayHello (HelloRequest) returns (HelloReply) {
option (google.api.http) = {
get: "/{api_version}/greet/{name}"
response_body: "message"
};
}
}
When you version by a URL path segment, you need a route parameter to serve as the placeholder for the API version. If
you do not do this, then /v1/greet/{name} cannot be mapped to other API versions. Mapping v1 → 3.0 is nonsensical.
gRPC route parameters do not have constraints and do not behave the same as ASP.NET Core route parameters and their
constraints. API versioning has to do additional work to determine if the incoming request matches and is thus slower
to execute. The matched route parameter will also hold the entire URL path segment, not just the API version. Recall
that the literal 'v' is not part of the API version. The {api_version} route parameter is, therefore, useful for
routing, but not useful in the message. This method of versioning requires you to get the incoming API version from the
server call context instead.
public class GreeterService : Greeter.GreeterBase
{
public override Task<HelloReply> SayHello( HelloRequest request, ServerCallContext context )
{
// required because `request.ApiVersion` will hold 'v1', but the requested version is '1.0'
var apiVersion = context.GetHttpContext().ApiVersioningFeature.RawRequestedApiVersion;
return Task.FromResult( new HelloReply { Message = $"Hello {request.Name} (v{apiVersion})" } );
}
}
Versioned Message Fields
Protocol Buffer messages are designed to support backward compatibility. JSON schemas, on the other hand, can be strict or not provide extension points. API-to-model affinity is one of the reasons to version a service in the first place. gRPC messages do not have a built-in way to express this when they are transcoded.
API Versioning provides a set of custom annotations that enables decorating which message fields are visible in which
API versions. The asp.api.version annotation indicates an API version range which indicates which API versions the
field should be visible in. The specified value must follow the [interval notation].
While the examples illustrate numbers, any API version is valid; for example:
[2026-07-01,2027-01-01) → 2026-07-01 ≤ x < 2027-01-01
In rare cases, you might need a split range. The asp.api.version annotation supports multiple entries; for example:
[(asp.api.version) = "[,2.0)", (asp.api.version) = "(2.0,]"]
This would indicate a field that is included in every API version except 2.0.
Annotations
The following sample gRPC service demonstrates how fields can be annotated to indicate which API versions they appear in. When no annotation is applied, the field appears in all API versions.
syntax = "proto3";
import "asp/api/annotations.proto";
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";
package people;
message Person {
int32 id = 1;
string first_name = 2;
string middle_name = 4 [(asp.api.version) = "2.0"];
string last_name = 3;
string email = 5 [(asp.api.version) = "2.0"];
string phone = 6 [(asp.api.version) = "3.0"];
}
message PersonRequest {
int32 id = 1;
Person person = 2;
}
message PeopleReply {
Person person = 1;
repeated Person people = 2;
}
service People {
// GET /people?api-version=[1.0,2.0,3.0]
rpc GetPeople (google.protobuf.Empty) returns (PeopleReply) {
option (google.api.http) = {
get: "/people"
response_body: "people"
};
}
// GET /people/{id}?api-version=[1.0,2.0,3.0]
rpc GetPerson (PeopleRequest) returns (PeopleReply) {
option (google.api.http) = {
get: "/people/{id}"
response_body: "person"
};
}
// POST /people?api-version=[1.0,2.0,3.0]
rpc AddPerson (PersonRequest) returns (PeopleReply) {
option (google.api.http) = {
post: "/people"
body: "person"
response_body: "person"
};
}
}
Validation
A transcoded message will appear to clients as though the field does not exist. This is pure obfuscation. The field does exist, but is omitted. There is nothing to prevent a client from sending a field that exists, but does not apply to an API version. A service author can choose to ignore the field according to the requested API version or the service can return an error if the client sends fields that are unexpected.
Error Responses
There are several built-in error responses. The body of each error response complies with [RFC 7807: Problem Details].
Note
In earlier versions, the error responses bodies complied with the [Microsoft REST Guidelines error response format], which is itself the error response format used by the OData protocol (see [OData JSON Format §21.1]). There wasn’t a broad standard at that time, which made any common error response format sensible.
Each problem detail also contains a code extension to retain a level of backward compatibility for clients that may
have relied on that value. If you need to retain the old functionality, refer to
backward compatibility below.
Unspecified
All versioned services require that an API version be specified. When a client makes a request without providing an API
version, then the server will respond with a bad request. This behavior is typically not exhibited when the API is
version-neutral or the AssumeDefaultVersionWhenUnspecified option is configured to true.
| Title | Unspecified API version |
| Type | https://docs.api-versioning.org/problems#unspecified |
| Status | 400 |
| Detail | An API version is required, but was not specified |
| Code | ApiVersionUnspecified |
Unsupported
When a client requested API version does not match any of the available controllers or their actions, then the server
will respond with a problem. If the ReportApiVersions option is true, then the supported versions will be returned
to the client in the api-supported-versions HTTP header.
| Title | Unsupported API version |
| Type | https://docs.api-versioning.org/problems#unsupported |
| Status | 4001 2 |
| Detail | The specified API version is not supported |
| Code | UnsupportedApiVersion |
1: Defined by
ApiVersioningOptions.UnsupportedApiVersionStatusCode
2: The value is always404when versioning by URL segment
Invalid
When a client makes a request with an API version, but the value is malformed or cannot be parsed, then the server will respond with a bad request. This typically occurs where the value contains incomplete version components or the date-only form is invalid (ex: 2016-02-30).
| Title | Invalid API version |
| Type | https://docs.api-versioning.org/problems#invalid |
| Status | 400 |
| Detail | An API version was specified, but it is invalid |
| Code | InvalidApiVersion |
Ambiguous
When a client requests a specific API version, the specified API version must be unambiguous to the server. A client is allowed to specify an API version more than once, but if the values are not identical, then the server will respond with a bad request.
| Title | Ambiguous API version |
| Type | https://docs.api-versioning.org/problems#ambiguous |
| Status | 400 |
| Detail | An API version was specified multiple times with different values |
| Code | AmbiguousApiVersion |
Examples
GET /resource?api-version=1.0 HTTP/1.1
host: localhost
api-version: 1.0
Figure 1: Multiple, unambiguous API versions requested
GET /resource?api-version=1.0 HTTP/1.1
host: localhost
api-version: 2.0
Figure 2: Ambiguous API versions requested between in query string and headers
GET /resource?api-version=1.0&api-version=2.0 HTTP/1.1
host: localhost
Figure 3: Ambiguous API versions requested in the query string
GET /resource HTTP/1.1
host: localhost
api-version: 1.0
api-version: 2.0
Figure 4: Ambiguous API versions requested in the headers
Customization
Error responses can be customized or extended in a variety of ways. You must opt into using problem details via:
services.AddProblemDetails();
Note
Applies to .NET 7+
If problem details are not added, clients will receive an error response which only has the HTTP status code. You might choose this approach if you don’t want a response body or your error responses do not comply with RFC 7807.
To modify the way a problem is written to clients, you can implement and register a your own IProblemDetailsWriter
implementation. Each registered implementation is injected into the IProblemDetailsService. The first matching writer
is used to write the response body. For more information see the ASP.NET Core Problem Details documentation.
The IProblemDetailsService was not added to support ProblemDetails in Minimal APIs and MVC Core until .NET 7. In API
Versioning 6.x, the IProblemDetailsFactory interface was used to bridge this gap. Contrary to the opt-in behavior of
AddProblemDetails(), a default implementation of IProblemDetailsFactory is automatically registered for Minimal
APIs. If MVC Core is added, then a decorated adapter is automatically provided over ProblemDetailsFactory. You have
the choice of replacing the entire IProblemDetailsFactory service or the MVC Core specific ProblemDetailsFactory.
The IProblemDetailsFactory interface was completely removed in .NET 7+ because it is no longer used in any way.
Important
Applies to .NET 6
Backward Compatibility
While it is possible to customize error responses and retain the previous Error Object format, there is considerable work required to enable this behavior and may block adoption of new library versions. Additional extensions have been added to retain backward compatibility or continue to use Error Objects if you so desire.
Using Error Object responses is as simple as registering the ErrorObjectWriter to emit them. The critical part of
each setup is the order in which the writer is registered. If the writer is not registered in the correct order, it will
not be selected. Each configuration must occur before AddApiVersioning().
The default implementation of the ErrorObjectWriter only writes Error Objects for API versioning related
errors. The default ASP.NET Core behavior provided by AddProblemDetails() is used for writing other types of errors.
If you want to use Error Objects for other error responses, you can extend ErrorObjectWriter and override which
types of Problem Details it should match - perhaps all of them.
Note
Applies to 8.1.0+
Minimal API
AddErrorObjects() adds the default behavior; however, you can register a custom ErrorObjectWriter via
AddErrorObject<TWriter>(). Both methods allow a custom Action<JsonOptions> setup and will configure the default
behavior if not otherwise specified.
builder.Services.AddProblemDetails().AddErrorObjects();
builder.Services.ApiVersioning();
MVC (Core)
builder.Services.AddControllers();
builder.Services.AddErrorObjects().AddProblemDetails();
builder.Services.ApiVersioning().AddMvc();
Note
Applies to 7.1.0+
Minimal API
builder.Services.AddProblemDetails();
builder.Services.TryAddEnumerable( ServiceDescriptor.Singleton<IProblemDetailsWriter, ErrorObjectWriter>() );
builder.Services.ApiVersioning();
MVC (Core)
builder.Services.AddControllers();
builder.Services.TryAddEnumerable( ServiceDescriptor.Singleton<IProblemDetailsWriter, ErrorObjectWriter>() );
builder.Services.AddProblemDetails();
builder.Services.ApiVersioning().AddMvc();
Note
Applies to .NET 6 and 6.5.0+
Since IProblemDetailsService did not exist in .NET 6, you must instead replace IProblemDetailsFactory with the
ErrorObjectFactory service. The configuration process and order are the same regardless of whether you are using
Minimal APIs or controllers. The replaced service should occur before AddApiVersioning().
builder.Services.AddSingleton<IProblemDetailsFactory, ErrorObjectFactory>();
builder.Services.ApiVersioning();
API Documentation
Adding documentation is often the final, pivotal step in making your versioned services available to clients and fosters their utilization. While there are many approaches to documenting your services, OpenAPI (formerly Swagger) has quickly become the de facto method for describing REST services.
The ASP.NET API versioning project provides several new API explorer implementations that make it easy to add versioning into your OpenAPI configurations. Each of these API explorers do all of the heavy lifting to discover and collate your REST services by API version. They do not directly rely on nor use any external OpenAPI libraries so that you can use them for other scenarios as well.
Any OpenAPI generator such as Microsoft, Swashbuckle, or NSwag that leverage the API Explorer can be used.
Minimal API or MVC (Core)
Note
Applies to ASP.NET Core 10.0+. For earlier versions, see the previous examples with Swashbuckle.
Everything you need to add versioned documentation to your Minimal and controller-based APIs using the API Explorer extensions, OpenAPI extensions, and Scalar.
var builder = WebApplication.CreateBuilder( args );
// only required if you're using controllers
builder.Services.AddControllers();
builder.Services.AddProblemDetails();
builder.Services.AddApiVersioning()
.AddMvc() // ← bring in MVC (Core); not required for Minimal APIs
.AddApiExplorer(
// (optional) format the version as "'v'major[.minor][-status]"
options => options.GroupNameFormat = "'v'VVV" )
.AddOpenApi(
// (optional) apply Scalar-specific transformers
options => options.Document.AddScalarTransformers()
);
var app = builder.Build();
// configure OpenAPI and Scalar to use a document per version
app.MapOpenApi().WithDocumentPerVersion();
app.MapScalarApiReference(
options =>
{
var descriptions = app.DescribeApiVersions();
for ( var i = 0; i < descriptions.Count; i++ )
{
var description = descriptions[i];
var isDefault = i == descriptions.Count - 1;
options.AddDocument( description.GroupName, description.GroupName, isDefault: isDefault );
}
} );
// only required if you're using controllers
app.MapControllers();
app.Run();
Review the following example projects for additional setup and configuration options:
gRPC
Everything you need to add versioned documentation to your gRPC APIs using the [API Explorer extensions], OpenAPI extensions, and Scalar.
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddApiVersioning()
.AddGrpc()
.AddApiExplorer(
// (optional) format the version as "'v'major[.minor][-status]"
options => options.GroupNameFormat = "'v'VVV" )
.AddGrpcApiExplorer()
.AddOpenApi(
// (optional) apply Scalar-specific transformers
options => options.Document.AddScalarTransformers()
);
var app = builder.Build();
var greeter = app.NewVersionedApi( "Greeter" );
greeter.MapGrpcService<GreeterService>()
.HasApiVersion( 1.0 )
.HasApiVersion( 2.0 )
.HasApiVersion( 3.0 );
// configure OpenAPI and Scalar to use a document per version
app.MapOpenApi().WithDocumentPerVersion();
app.MapScalarApiReference(
options =>
{
var descriptions = app.DescribeApiVersions();
for ( var i = 0; i < descriptions.Count; i++ )
{
var description = descriptions[i];
var isDefault = i == descriptions.Count - 1;
options.AddDocument( description.GroupName, description.GroupName, isDefault: isDefault );
}
} );
app.Run();
Review the following example projects for additional setup and configuration options:
OData
Everything you need to add versioned documentation to your OData controllers using the API Explorer extensions, OpenAPI extensions, and Scalar.
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddControllers().AddOData();
builder.Services.AddProblemDetails();
builder.Services.AddApiVersioning()
.AddOData( options => options.AddRouteComponents() )
.AddODataApiExplorer(
// (optional) format the version as "'v'major[.minor][-status]"
options => options.GroupNameFormat = "'v'VVV" )
.AddOpenApi(
// (optional) apply Scalar-specific transformers
options => options.Document.AddScalarTransformers()
);
var app = builder.Build();
// configure OpenAPI and Scalar to use a document per version
app.MapOpenApi().WithDocumentPerVersion();
app.MapScalarApiReference(
options =>
{
var descriptions = app.DescribeApiVersions();
for ( var i = 0; i < descriptions.Count; i++ )
{
var description = descriptions[i];
var isDefault = i == descriptions.Count - 1;
options.AddDocument( description.GroupName, description.GroupName, isDefault: isDefault );
}
} );
app.MapControllers();
app.Run();
Review the following example projects for additional setup and configuration options:
API Explorer Options
The API Explorer options allows you to configure, customize, and extend the default behaviors when you add API exploration support. The configuration options are specified by providing a callback to the appropriate extension method:
The ApiExplorerOptions have the following configuration settings:
- GroupNameFormat
- SubstituteApiVersionInUrl
- SubstitutionFormat
- DefaultApiVersion
- DefaultApiVersionParameterDescription
- AssumeDefaultVersionWhenUnspecified
- ApiVersionParameterSource
- AddApiVersionParametersWhenVersionNeutral
- RouteConstraintName
- FormatGroupName
Format Group Name
This option allows you to define an optional FormatGroupNameCallback, which will provide the current group name and
formatted API version. By default, the formatted API version is used as the group name and is the most logical choice.
A developer, however, may specify their own group name in a variety of ways such as
[ApiExplorerSettings(GroupName = "Custom")]. When a developer explicitly sets a group name, that name is honored.
If, and only if, a developer sets both a custom group name and defines a FormatGroupName callback, the method
will be invoked to produce a combination of both.
Consider the following controller:
[ApiVersion( 1.0 )]
[ApiExplorerSettings( GroupName = "Sales" )]
[Route( "[controller]" )]
public class OrderController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
A callback can be defined to control how the combination of the API version and group name will be formatted.
builder.Services.AddApiVersioning()
.AddMvc()
.AddApiExplorer(
options =>
{
// the default is ToString(), but we want "'v'major[.minor][-status]"
options.GroupNameFormat = "'v'VVV";
// if we have both parts, decided how to format the group
// from the example: "Sales - v1"
options.FormatGroupName = (group, version) => $"{group} - {version}";
} );
Use Qualified Names
The OData API Explorer is responsible for building URLs that refer to your entity sets, functions, and actions. This
property determines whether the constructed URLs use qualified names. The default value is false. The
ODataUriResolver instance configured for your application must be configured to match the generated URLs
(ex: UnqualifiedCallAndEnumPrefixFreeResolver).
Query Options
This option allows you to configure OData query options. The configuration for query options can be expressed purely by convention, through the use of supported OData query attribute, or both. The default behavior will always apply conventions from OData query attributes without additional configuration. For more information see the OData query options topic.
Metadata Options
This option allows you to determine whether the OData metadata ($metadata) and service document (/) are explored as
available endpoints. The available options are: None, ServiceDocument, Metadata, or All. The default value is
None.
Ad Hoc Model Builder
This property returns an VersionedODataModelBuilder that can be used for building ad hoc Entity Data Models (EDMs)
that are used when defining the query options for APIs that do not use the full OData stack. Some OData query
options can only be set via Model Bound settings. This builder constructs an ad hoc EDM that will contain those
settings solely for the purposes of API exploration and without opting into any other OData-specific features. For more
information see the OData query options topic.
Related Entity Id Parameter Description
This option enables you to specify the description for OData related entity links. The default value is
"The identifier of the related entity." OData related entity links appear in $ref requests. This description is
used to describe dynamic parameters such as the $id query parameter.
gRPC Options
The API Explorer support for gRPC has a few options that allow you to customize the behavior. The options are minimal because the API Explorer support can technically operate without API Versioning. Most of the configuration will be performed against the versioned API Explorer, but there are a few options that are specific to gRPC.
The GrpcApiExplorerOptions have the following configuration settings:
Route Parameter
Represents route parameter information required for API exploration.
Name
When versioning by URL path segment, there is not a clear way to identify which segment represents the API version.
Rather than use an explicit annotation, the route parameter is matched by name. The default value of the Name
property is "api_version"; however, you can use whatever name you choose. The name will be matched in a
case-sensitive manner.
Prefix Literal
gRPC supports route parameters in route templates, but a parameter must match an entire segment. It cannot match part
of a segment in the same manner as an ASP.NET route constraint and an API version does not include literal characters
such as 'v'. As a result, the character is not included in the route template.
The PrefixLiteral property adds the expected literal in the route template when it is built for the API Explorer. As
an example, the gRPC route template "api/{api-version}/example" will be generated as "api/v{api-version}/example"
and produce the expected behavior in the API Explorer.
OData Options
The ODataApiExplorerOptions extends the above options with the following additional settings:
- UseQualifiedNames
- QueryOptions
- RelatedEntityIdParameterDescription
- MetadataOptions
- AdHocModelBuilder
Use Qualified Names
The OData API Explorer is responsible for building URLs that refer to your entity sets, functions, and actions. This
property determines whether the constructed URLs use qualified names. The default value is false. The
ODataUriResolver instance configured for your application must be configured to match the generated URLs
(ex: UnqualifiedCallAndEnumPrefixFreeResolver).
Query Options
This option allows you to configure OData query options. The configuration for query options can be expressed purely by convention, through the use of supported OData query attribute, or both. The default behavior will always apply conventions from OData query attributes without additional configuration. For more information see the OData query options topic.
Metadata Options
This option allows you to determine whether the OData metadata ($metadata) and service document (/) are explored as
available endpoints. The available options are: None, ServiceDocument, Metadata, or All. The default value is
None.
Ad Hoc Model Builder
This property returns an VersionedODataModelBuilder that can be used for building ad hoc Entity Data Models (EDMs)
that are used when defining the query options for APIs that do not use the full OData stack. Some OData query
options can only be set via Model Bound settings. This builder constructs an ad hoc EDM that will contain those
settings solely for the purposes of API exploration and without opting into any other OData-specific features. For more
information see the OData query options topic.
Related Entity Id Parameter Description
This option enables you to specify the description for OData related entity links. The default value is
"The identifier of the related entity." OData related entity links appear in $ref requests. This description is
used to describe dynamic parameters such as the $id query parameter.
Query Options
OData query option conventions allow you to specify information for your OData services without having to rely solely on .NET attributes. There are a number of reasons why you might uses these conventions. The most common reasons are:
- Centralized management and application of all OData query options
- Define OData query options that cannot be expressed with any OData query attributes
- Apply OData query options to services defined by controllers in external .NET assemblies
The parameter names generated are based on the name of the OData query option and the configuration of the
ODataUriResolver. OData supports query options without the system $ prefix. This is enabled or disabled by the
ODataUriResolver.EnableNoDollarQueryOptions property.
Attribute Model
The attribute model relies on Model Bound settings attributes and the EnableQueryAttribute. The
EnableQueryAttribute indicates API-specific options that might be too restrictive or not applicable to specific
models. Consider the following model and controller definitions.
using System;
using Microsoft.AspNet.OData.Query;
using static Microsoft.AspNet.OData.Query.SelectExpandType;
[Select]
[Select( "effectiveDate", SelectType = Disabled )]
public class Order
{
public int Id { get; set; }
public DateTime CreatedDate { get; set; } = DateTime.Now;
public DateTime EffectiveDate { get; set; } = DateTime.Now;
public string Customer { get; set; }
public string Description { get; set; }
}
using Asp.Versioning;
using Asp.Versioning.OData;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Results;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using static Microsoft.AspNetCore.Http.StatusCodes;
using static Microsoft.AspNetCore.OData.Query.AllowedQueryOptions;
using static System.DateTime;
[ApiVersion( 1.0 )]
public class OrdersController : ODataController
{
[Produces( "application/json" )]
[ProducesResponseType( typeof( ODataValue<IEnumerable<Order>> ), Status200OK )]
[EnableQuery( MaxTop = 100, AllowedQueryOptions = Select | Top | Skip | Count )]
public IQueryable<Order> Get()
{
var orders = new[]
{
new Order(){ Id = 1, Customer = "John Doe" },
new Order(){ Id = 2, Customer = "John Doe" },
new Order(){ Id = 3, Customer = "Jane Doe", EffectiveDate = UtcNow.AddDays(7d) }
};
return orders.AsQueryable();
}
[Produces( "application/json" )]
[ProducesResponseType( typeof( Order ), Status200OK )]
[ProducesResponseType( Status404NotFound )]
[EnableQuery( AllowedQueryOptions = Select )]
public SingleResult<Order> Get( int key )
{
var orders = new[] { new Order(){ Id = key, Customer = "John Doe" } };
return SingleResult.Create( orders.AsQueryable() );
}
}
Convention Model
The convention model relies on Model Bound settings via the fluent API of the ODataModelBuilderand the
EnableQueryAttribute. The EnableQueryAttribute indicates API-specific options that might be too restrictive or
nonapplicable to specific models. Consider the following model and controller definitions.
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
}
public class PersonModelConfiguration : IModelConfiguration
{
public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix )
{
var person = builder.EntitySet<Person>( "People" ).EntityType;
person.HasKey( p => p.Id );
// configure model bound conventions
person.Select().OrderBy( "firstName", "lastName" );
if ( apiVersion < ApiVersions.V3 )
{
person.Ignore( p => p.Phone );
}
if ( apiVersion <= ApiVersions.V1 )
{
person.Ignore( p => p.Email );
}
if ( apiVersion > ApiVersions.V1 )
{
var function = person.Collection.Function( "NewHires" );
function.Parameter<DateTime>( "Since" );
function.ReturnsFromEntitySet<Person>( "People" );
}
if ( apiVersion > ApiVersions.V2 )
{
person.Action( "Promote" ).Parameter<string>( "title" );
}
}
}
using Asp.Versioning;
using Asp.Versioning.OData;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Results;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using static Microsoft.AspNetCore.Http.StatusCodes;
using static Microsoft.AspNetCore.OData.Query.AllowedQueryOptions;
using static System.DateTime;
public class PeopleController : ODataController
{
[Produces( "application/json" )]
[ProducesResponseType( typeof( ODataValue<IEnumerable<Person>> ), Status200OK )]
public IActionResult Get( ODataQueryOptions<Person> options )
{
var validationSettings = new ODataValidationSettings()
{
AllowedQueryOptions = Select | OrderBy | Top | Skip | Count,
AllowedOrderByProperties = { "firstName", "lastName" },
AllowedArithmeticOperators = AllowedArithmeticOperators.None,
AllowedFunctions = AllowedFunctions.None,
AllowedLogicalOperators = AllowedLogicalOperators.None,
MaxOrderByNodeCount = 2,
MaxTop = 100,
};
try
{
options.Validate( validationSettings );
}
catch ( ODataException )
{
return BadRequest();
}
var people = new[]
{
new Person()
{
Id = 1,
FirstName = "John",
LastName = "Doe",
Email = "john.doe@somewhere.com",
Phone = "555-987-1234",
},
new Person()
{
Id = 2,
FirstName = "Bob",
LastName = "Smith",
Email = "bob.smith@somewhere.com",
Phone = "555-654-4321",
},
new Person()
{
Id = 3,
FirstName = "Jane",
LastName = "Doe",
Email = "jane.doe@somewhere.com",
Phone = "555-789-3456",
}
};
return Ok( options.ApplyTo( people.AsQueryable() ) );
}
[Produces( "application/json" )]
[ProducesResponseType( typeof( Person ), Status200OK )]
[ProducesResponseType( Status404NotFound )]
public IActionResult Get( int key, ODataQueryOptions<Person> options )
{
var people = new[]
{
new Person()
{
Id = key,
FirstName = "John",
LastName = "Doe",
Email = "john.doe@somewhere.com",
Phone = "555-987-1234",
}
};
var person = options.ApplyTo( people.AsQueryable() ).SingleOrDefault();
if ( person == null )
{
return NotFound();
}
return Ok( person );
}
}
Conventions
If you only define OData query options imperatively using ODataQuerySettings and ODataValidationSettings, then
there are no attributes or Entity Data Model (EDM) data annotations to explore the query options from. In this scenario,
you can use the conventions in the API Explorer extensions to document any query option setting that can be defined by
ODataQuerySettings or ODataValidationSettings.
.AddODataApiExplorer( options =>
{
var queryOptions = options.QueryOptions;
queryOptions.Controller<V2.PeopleController>()
.Action( c => c.Get( default( ODataQueryOptions<Person> ) ) )
.Allow( Skip | Count )
.AllowTop( 100 );
queryOptions.Controller<V3.PeopleController>()
.Action( c => c.Get( default( ODataQueryOptions<Person> ) ) )
.Allow( Skip | Count )
.AllowTop( 100 );
} );
The OData API Explorer will discover and add the following parameters for an entity set query:
| Name | Parameter Type | Data Type | Description |
|---|---|---|---|
$select | query | string | Limits the properties returned in the result. |
$orderby | query | string | Specifies the order in which results are returned. The allowed properties are: firstName, lastName. |
$top | query | integer | Limits the number of items returned from a collection. The maximum value is 100. |
$skip | query | integer | Excludes the specified number of items of the queried collection from the result. |
Parameters
While each OData query option has a default provided description, the description can be changed by providing a custom
description. Descriptions are generated by the IODataQueryOptionDescriptionProvider:
public interface IODataQueryOptionDescriptionProvider
{
string Describe(
AllowedQueryOptions queryOption,
ODataQueryOptionDescriptionContext context );
}
Note
Although
AllowedQueryOptionsis a bitwise enumeration, only a single query option value is ever passed
You can change the default description by implementing your own IODataQueryOptionDescriptionProvider or extending the
built-in DefaultODataQueryOptionDescriptionProvider. The implementation is updated in the OData API Explorer options using:
AddODataApiExplorer( options => options.QueryOptions.DescriptionProvider = new MyQueryOptionDescriptor() );
Custom Conventions
You can also define custom conventions via the IODataQueryOptionsConvention interface and add them to the builder:
public interface IODataQueryOptionsConvention
{
void ApplyTo( ApiDescription apiDescription );
}
AddODataApiExplorer( options => options.QueryOptions.Add( new MyODataQueryOptionsConvention() ) );
Partial OData
OData supports query capabilities without using the full OData stack. Consider the following controller, which is not an OData controller, but uses OData query options:
[ApiVersion( 1.0 )]
[ApiController]
[Route( "[controller]" )]
public class BooksController : ControllerBase
{
[HttpGet]
[Produces( "application/json" )]
[ProducesResponseType( typeof( IEnumerable<Book> ), 200 )]
public IActionResult Get( ODataQueryOptions<Book> options ) =>
Ok( options.ApplyTo( books.AsQueryable() ) );
}
[ApiVersion( 1.0 )]
[ApiController]
[Route( "[controller]" )]
public class BooksController : ControllerBase
{
[HttpGet]
[Produces( "application/json" )]
[ProducesResponseType( typeof( IEnumerable<Book> ), 200 )]
public IActionResult Get( ODataQueryOptions<Book> options ) =>
Ok( options.ApplyTo( books.AsQueryable() ) );
}
When OData query capabilities are used this way, query options can be discovered via EnableQueryAttribute or via the
API Explorer extensions. Unfortunately, these are both ultimately limited to what can be expressed via
ODataQuerySettings and ODataValidationSettings, which does not cover the gambit of all possible OData query options;
for example, the allowable $filter properties. These other properties can be configured via Model Bound settings,
but without using the full OData stack there is no Entity Data Model (EDM) to retrieve these annotations from.
To address this limitation, OData query options can now also be explored using an ad hoc EDM. This EDM only exists for the purposes of query option exploration. Using an ad hoc EDM does not opt into other OData feature and only exists during exploration. Applying Model Bound settings to an ad hoc model is almost identical to the normal method. If you want to use attributes, just apply them to your model.
[Filter( "author", "published" )]
public class Book
{
public string Id { get; set; }
public string Author { get; set; }
public string Title { get; set; }
public int Published { get; set; }
}
Every action that appears to be OData-like will automatically be discovered and its model explored. Discovered models are registered as a complex type by default. If you prefer to use entities or need additional control over the applied settings, you can use conventions as well.
AddODataApiExplorer(
options =>
{
options.AdHocModelBuilder.DefaultModelConfiguration = (builder, version, prefix) =>
{
builder.ComplexType<Book>().Filter( "author", "published" );
};
}
)
The AdHocModelBuilder is part of the ODataApiExplorerOptions as opposed to ODataApiVersioningOptions. If you
have numerous models and would like to break the settings into different configurations, you can still use
IModelConfiguration. IModelConfiguration instances are automatically discovered and injected the same way as they
are when using the full OData stack.
public class BookConfiguration : IModelConfiguration
{
public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string? routePrefix )
{
builder.EntitySet<Book>( "Books" ).EntityType.Filter( "author", "published" );
}
}
Model configuration for an ad hoc model; the routePrefix will always be null.
There is no distinction between an IModelConfiguration that is used for ad hoc EDM exploration versus normal model
registration. It is unlikely that you would be mixing the full and partial OData stack. If you are mixing use cases,
then you can tell the difference between models from the provided API version. There should be no scenario where a
model is registered two different ways for the same API version.
OpenAPI Options
The OpenAPI options allows you to configure, customize, and extend the default behaviors when you add OpenAPI support. The configuration options are specified by providing a callback to the appropriate extension method:
The VersionedOpenApiOptions has the following configuration settings:
Description
The description provides the ApiVersionDescription for the current OpenAPI document being generated. This information
includes the API version, its group name, and whether it is deprecated.
Document
This provides access to the current OpenApiOptions. These are the same options you would configure when you document
an unversioned API. Your configuration can be the same for all versions or vary version by version.
Document Description
The document description provides the configuration settings for the current OpenAPI document being generated. The
description for an OpenAPI document is defined as text, but most user interfaces allow Markdown to be specified.
These additional settings control the text and format to be included in the description attribute.
The OpenApiDocumentDescriptionOptions provide the following configuration settings:
Hide Policy Links
This setting controls whether deprecation or sunset policy links are displayed. The default value is false, which
means any defined policies links are displayed as bulleted list of hyperlinks. Policy links rendered for user interface
purposes must have the link type text/html.
Policies may define other types of links, but these are not shown in the user interface. These links are rendered in
OpenAPI documents in the x-api-versioning document extension.
Deprecation
If an API has an applicable deprecation policy, this setting defines the callback function used to generate the message text based on the provided policy. The default setting returns a generic message in English indicating that the API is deprecated if no date is specified or a date-specific message in English when the API became, or will become, deprecated.
Sunset
If an API has an applicable sunset policy, this setting defines the callback function used to generate the message text based on the provided policy. If the policy does not have a date, then no message is generated; otherwise, a date-specific message in English is generated indicating when the API became, or will become, sunset. A sunset policy is for an API that is sunset and will no longer exist. The effective sunset policy date should always be in the future as there should be no information about the API after it is sunset.
Scalar Integration
Scalar has quickly become one of the more common, modern OpenAPI user interfaces and it easily integrates with API Versioning. The only thing you need to do is tell Scalar about the documents your application will generate.
Remember to add the necessary references to one or both of the following:
- Versioned OpenAPI Extensions for ASP.NET Core
- API Explorer Extensions for ASP.NET Core
- API Explorer Extensions for ASP.NET Core with gRPC
- API Explorer Extensions for ASP.NET Core with OData
- Scalar for ASP.NET Core
- Scalar for ASP.NET Core with Microsoft OpenAPI extensions
Minimal APIs
builder.Services.AddApiVersioning()
.AddApiExplorer()
.AddOpenApi( options => options.Document.AddScalarTransformers() );
Controllers
builder.Services.AddApiVersioning()
.AddMvc()
.AddApiExplorer()
.AddOpenApi( options => options.Document.AddScalarTransformers() );
gRPC
builder.Services.AddApiVersioning()
.AddApiExplorer()
.AddGrpc()
.AddGrpcApiExplorer()
.AddOpenApi( options => options.Document.AddScalarTransformers() );
OData
builder.Services.AddApiVersioning()
.AddOData()
.AddODataApiExplorer()
.AddOpenApi( options => options.Document.AddScalarTransformers() );
Once you have that configured, you need only generate an OpenAPI document per version and let Scalar know which generated documents it should expect.
app.MapOpenApi().WithDocumentPerVersion();
app.MapScalarApiReference(
options =>
{
var descriptions = app.DescribeApiVersions();
for ( var i = 0; i < descriptions.Count; i++ )
{
var description = descriptions[i];
var isDefault = i == descriptions.Count - 1;
options.AddDocument( description.GroupName, description.GroupName, isDefault: isDefault );
}
} );
Examples
There are end-to-end examples using API versioning, OpenAPI, and Scalar:
- Minimal APIs, API Versioning and Scalar
- MVC (Core), API Versioning and Scalar
- gRPC, API Versioning and Scalar
- OData, API Versioning, and Scalar
- Partial OData, API Versioning, and Scalar
Swashbuckle Integration
Although the API explorers for API versioning provide all of the necessary information, there is select information
that OpenAPI (formerly Swagger) and Swashbuckle will not wire up for you. This includes iterating through all the
available API versions so that they don’t have to be imperatively declared and changed one at a time. Fortunately,
bridging this gap is really easy to achieve using Swashbuckle’s extensibility model. The following are simple
IOperationFilter implementations that leverage the metadata provided by the corresponding API explorer to fill in
these gaps.
Remember to add the necessary references to one or both of the following:
public class SwaggerDefaultValues : IOperationFilter
{
public void Apply( OpenApiOperation operation, OperationFilterContext context )
{
var apiDescription = context.ApiDescription;
operation.Deprecated |= apiDescription.IsDeprecated();
foreach ( var responseType in context.ApiDescription.SupportedResponseTypes )
{
var responseKey = responseType.IsDefaultResponse
? "default"
: responseType.StatusCode.ToString();
var response = operation.Responses[responseKey];
foreach ( var contentType in response.Content.Keys )
{
if ( !responseType.ApiResponseFormats.Any( x => x.MediaType == contentType ) )
{
response.Content.Remove( contentType );
}
}
}
if ( operation.Parameters == null )
{
return;
}
foreach ( var parameter in operation.Parameters )
{
var description = apiDescription.ParameterDescriptions
.First( p => p.Name == parameter.Name );
parameter.Description ??= description.ModelMetadata?.Description;
if ( parameter.Schema.Default == null && description.DefaultValue != null )
{
var json = JsonSerializer.Serialize(
description.DefaultValue,
description.ModelMetadata.ModelType );
parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson( json );
}
parameter.Required |= description.IsRequired;
}
}
}
We also need a way to tell Swashbuckle about the API versions in the application:
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IApiVersionDescriptionProvider provider;
public ConfigureSwaggerOptions( IApiVersionDescriptionProvider provider ) => this.provider = provider;
public void Configure( SwaggerGenOptions options )
{
foreach ( var description in provider.ApiVersionDescriptions )
{
options.SwaggerDoc(
description.GroupName,
new OpenApiInfo()
{
Title = "Example API",
Description = "An example API",
Version = description.ApiVersion.ToString(),
} );
}
}
}
Now we can put it all together:
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddControllers();
builder.Services.AddApiVersioning().AddMvc().AddApiExplorer();
builder.Services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
builder.Services.AddSwaggerGen( options => options.OperationFilter<SwaggerDefaultValues>() );
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI(
options =>
{
foreach ( var description in app.DescribeApiVersions() )
{
options.SwaggerEndpoint(
$"/swagger/{description.GroupName}/swagger.json",
description.GroupName );
}
} );
app.MapControllers();
app.Run();
Examples
There are end-to-end examples using API versioning and Swashbuckle:
- Minimal APIs, API Versioning and Swashbuckle
- MVC (Core), API Versioning and Swashbuckle
- OData, API Versioning, and Swashbuckle
- Partial OData, API Versioning, and Swashbuckle
Attributes
In addition to the API versioning options, there are few other customization and extension points. Attributes are the
primary mechanism used to decorate the API version metadata with a specific controller type, but the attributes used
can be any IApiVersionProvider.
public interface IApiVersionProvider
{
ApiVersionProviderOptions Options { get; }
IReadOnlyList<ApiVersion> Versions { get; }
}
There are several API version provider attributes defined out-of-the-box:
ApiVersionsBaseAttributeApiVersionAttributeMapToApiVersionAttributeAdvertiseApiVersionsAttribute
These attributes are themselves extensible. For example, you might choose to have your own attributes that are unambiguously a specific version:
[AttributeUsage( AttributeUsage.Class, AllowMultiple = true, Inherited = false )]
public sealed class V1Attribute : ApiVersionAttribute
{
public V1Attribute() : base( new ApiVersion( new( 2016, 7, 1 ) ) ) { }
}
[V1]
[ApiController]
[Route( "api/[controller]" )]
public class HelloWorldController : ControllerBase
{
[HttpGet]
public string Get() => "Hello world!";
}
This approach can help centralize API version management and avoid developer typographical errors when implementing a set of services that all use the same API version.
Version Format
It is possible to extend or change the provided API version format, but that capability comes with several rules:
- You must extend
ApiVersion - You must override:
GetHashCodeCompareToToString(string,IFormatProvider)
- You must implement
IApiVersionParser- It may be possible to extend
ApiVersionParserdepending on your requirements
- It may be possible to extend
You will likely need to extend ApiVersionFormatProvider or implement a custom IFormatProvider. Although not
strictly required, you may want to implement operator overloads for your custom type to retain functional parity with
ApiVersion. The custom parser will need to be passed to components that accept IApiVersionParser and/or replace the
default implementation registered for dependency injection.
You should consider the impact that a custom API version may have on clients. Your custom format and parsing logic may need to be distributed to them for to use.
Versioned Clients
The Asp.Versioning.Http.Client package brings client-side extensions that make your HttpClient instances
API version-aware.
API Version Writer
The reciprocal to IApiVersionReader is IApiVersionWriter. As the name implies, the IApiVersionWriter is
responsible for writing the configured API version into outgoing requests. The default configured writer is the
QueryStringApiVersionWriter using the query parameter name "api-version".
Adding API versions to your HttpClient instances can easily be configured using the IHttpClientFactory dependency
injection extensions.
var services = new ServiceCollection();
services.AddHttpClient(
"MyApi",
client => client.BaseAddress = new Uri( "https://my.api.com") )
.AddApiVersion( 1.0 );
var provider = services.BuildServiceProvider();
var factory = provider.GetRequiredService<IHttpClientFactory>();
var client = factory.CreateClient( "MyApi" );
// GET https://my.api.com/data?api-version=1.0
var response = await client.GetAsync( "data" );
You can add or replace the default IApiVersionWriter with:
var services = new ServiceCollection();
services.AddSingleton<IApiVersionWriter>( new UrlSegmentApiVersionWriter( "{ver}" ) );
services.AddHttpClient(
"MyApi",
client => client.BaseAddress = new Uri( "https://my.api.com/v{ver}") )
.AddApiVersion( 1 );
var provider = services.BuildServiceProvider();
var factory = provider.GetRequiredService<IHttpClientFactory>();
var client = factory.CreateClient( "MyApi" );
// GET https://my.api.com/v1/data
var response = await client.GetAsync( "data" );
The following implementations are provided out-of-the-box:
QueryStringApiVersionWriterHeaderApiVersionWriterMediaTypeApiVersionWriterUrlSegmentApiVersionWriter
Specifying multiple API versions is typically unnecessary; however, if this is a capability you need or want, multiple writers can be composed together:
var writer = ApiVersionWriter.Combine(
new QueryApiVersionWriter( "api-version" ),
new HeaderApiVersionWriter( "x-ms-api-version" ) );
Your application might have multiple clients that communicate to services which use different API versioning methods. To accommodate these differences, you can specify a specific writer per client.
var services = new ServiceCollection();
services.AddHttpClient(
"SomeApi",
client => client.BaseAddress = new Uri( "https://some.api.com/") )
.AddApiVersion( 1.0, new QueryApiVersionWriter() );
services.AddHttpClient(
"OtherApi",
client => client.BaseAddress = new Uri( "https://other.api.com/v{ver}/") )
.AddApiVersion( 2, new UrlSegmentApiVersionWriter( "{ver}" ) );
If you’re not using dependency injection or the IHttpClientFactory, you can still configure writers by explicitly
configuring the ApiVersionHandler:
using var client = new HttpClient(
new ApiVersionHandler(
new QueryApiVersionWriter(),
new ApiVersion( 1, 0 ) )
{
InnerHandler = new HttpClientHandler(),
} );
Notifications
API clients always have a few common questions:
- “How do I know when an API version is deprecated?”
- “How do I know when an API version will be sunset?”
- “How do I know when a new API version is available?”
These questions can now be answered via:
public interface IApiNotification
{
Task OnApiDeprecatedAsync( ApiNotificationContext context, CancellationToken cancellationToken );
Task OnNewApiAvailableAsync( ApiNotificationContext context, CancellationToken cancellationToken );
}
Where the notification information provided is:
public class ApiNotificationContext
{
public HttpResponseMessage Response { get; }
public ApiVersion ApiVersion { get; }
public SunsetPolicy SunsetPolicy { get; }
}
If the API reports its versions, then the ApiVersionHandler will detect when these events occur and invoke the
appropriate notification. The ApiVersionHandler will look for the api-supported-versions and
api-deprecated-versions HTTP headers by default, but alternate headers may be configured. If a deprecation or sunset
policy is specified by the API, then the deprecation date will be read from the deprecation HTTP header and the sunset
date will be read from the sunset HTTP header. Any link HTTP headers where the relation type is
rel="deprecation" or rel="sunset" will also be read.
No notifications or actions occur by default. The most logical action to perform when a notification occurs is to log
it. The ApiVersionHandlerLogger<T> implements an IApiNotification that is paired with an ILogger<T> that will:
- Log a warning message when an API reports that the version requested is deprecated.
- Log an informational message when an API reports that a newer version than the one requested is available.
Logged messages can be connected to alerts to notify developers when these events occur in an automated fashion.
If you configuration uses dependency injection and ILogger<ApiVersionHandler> is a resolvable service,
ApiVersionHandlerLogger<ApiVersionHandler> will be used as the default IApiNotification implementation unless
configured otherwise.
API Information
Using API information provided in responses is useful, but not always provided for every request. Furthermore, if you’re onboarding to an API, how do you know which API versions are available or deprecated? How do you know the policies around these APIs? Detailed information might be provided by OpenAPI, but how do you know where the OpenAPI documents are?
The most logical way for an API to expose this information is to provide an OPTIONS method, which may be
version-specific or version-neutral, that returns all of the available API information. This information is useful for
automation and client tooling.
The GetApiInformationAsync extension method for the HttpClient provides a prescribed implementation to make the
appropriate OPTIONS request and parse its response into:
using var client = new HttpClient()
{
BaseAddress = new Uri( "https://my.api.com" ),
};
var info = await client.GeApiInformationAsync( "/?api-version=1.0" );
Request API information
OPTIONS /?api-version=1.0 HTTP/2
host: my.api.com
HTTP request sent
HTTP/2 200
api-supported-versions: 2.0
api-deprecated-versions: 1.0
deprecation: @1688169600
sunset: Mon, 01 Jan 2024 00:00:00 GMT
link: <https://api.docs.com/policies/deprecation.html>; rel="deprecation"; type="text/html"
link: <https://api.docs.com/policies/sunset.html>; rel="sunset"; type="text/html"
link: <openapi/v1.json>; rel="openapi"; type="application/json"; api-version="1.0"
HTTP response received
public class ApiInformation
{
public IReadOnlyList<ApiVersion> SupportedApiVersions { get; }
public IReadOnlyList<ApiVersion> DeprecatedApiVersions { get; }
public SunsetPolicy SunsetPolicy { get; }
public IReadOnlyDictionary<ApiVersion, Uri> OpenApiDocumentUrls { get; }
}
Parsed API information
Third-Party
The following are external, third-party extensions that showcase extensibility.
Custom Version Ranges
https://github.com/purplebricks/PB.ITOps.AspNetCore.Versioning
Diagnostic Code Analysis for ASP.NET API Versioning
.NET compiler platform analyzers inspect application code for code quality and style issues using ASP.NET API Versioning.
| ID | Category | Description | |
|---|---|---|---|
| AV0001 | Usage | Invalid API version | |
| AV0002 | Usage | Invalid API version range | |
| AV0003 | Usage | Invalid API version status | |
| AV0004 | Usage | Invalid API version number | |
| AV0005 | Usage | Invalid API version year | |
| AV0006 | Usage | Invalid API version month | |
| AV0007 | Usage | Invalid API version day | |
| AV0008 | Usage | Invalid API version date | |
| AV0009 | Usage | Invalid API version format specifier | |
| AV0010 | Usage | Unexpected API version format | |
| AV0011 | Style | Remove unnecessary default API version | |
| AV0012 | Usage | Invalid default API version | |
| AV0013 | Usage | Missing AddMvc | |
| AV0014 | Usage | Missing API behavior | |
| AV0015 | Performance | Use a specific API version reader | |
| AV0016 | Usage | Do not assume default API version | |
| AV0017 | Usage | Remove unnecessary default value | |
| AV0018 | Usage | All endpoints are version-neutral | |
| AV0019 | Usage | Versioned and version-neutral | |
| AV0020 | Style | Remove unnecessary API explorer | |
| AV0021 | Usage | Use the versioned API explorer | |
| AV0022 | Usage | Missing AddOData | |
| AV0023 | Usage | Route components are ignored | |
| AV0024 | Usage | Remove unnecessary API explorer option | |
| AV0025 | Documentation | Missing OpenAPI document description | |
| AV0026 | Usage | Remove unnecessary group name format | |
| AV0027 | Usage | Use DescribeApiVersions | |
| AV0028 | Usage | Sunset policy takes effect before deprecation | |
| AV0029 | Usage | Remove unnecessary OpenAPI services | |
| AV0030 | Usage | Missing WithDocumentPerVersion | |
| AV0031 | Usage | Missing API explorer |
Reporting
Most rules report as you type, but some report only when the project is built.
A rule that judges a single expression decides as soon as that expression is written. AV0017, for example, sees an assignment and has everything it needs. A rule that compares one call against another cannot decide until every file has been read, because the call it is looking for may be in a file that is not open. AV0028 cannot report a sunset until it has seen every deprecation, and AV0027 reports because a call is missing, which is only known once there is nothing left to read.
The rules that report only on build are AV0013, AV0015, AV0016, AV0018, AV0019, AV0020, AV0021, AV0022, AV0023, AV0024, AV0026, AV0027, AV0028, AV0029, AV0030, and AV0031. The rest report live in the editor.
These rules also report live in an editor configured to analyze the whole solution rather than only the documents that are open:
- Visual Studio: Tools → Options → Text Editor → C# → Advanced → Run background code analysis for → Entire solution
- Rider: enable Solution-Wide Analysis
- Visual Studio Code:
"dotnet.backgroundAnalysis.analyzerDiagnosticsScope": "fullSolution"
Suppression
A single rule is configured the same way as any other analyzer, by severity in an .editorconfig file:
[*.cs]
dotnet_diagnostic.AV0028.severity = none
All of the rules are turned off at once with a property, which removes the analyzers instead of silencing each rule:
<PropertyGroup>
<EnableApiVersioningAnalyzers>false</EnableApiVersioningAnalyzers>
</PropertyGroup>
Set it in Directory.Build.props to apply it to every project in a solution. The rules are enabled unless the
property is false.
Important
ExcludeAssets="analyzers"on a package reference does not turn the rules off. The packages that ship the analyzers are also reached through the dependencies of other packages, and NuGet combines the assets from every path that reaches a package, so an exclusion on one path is undone by another that has none. Use the property above instead.
AV0001: Invalid API version
| Value | |
|---|---|
| Rule ID | AV0001 |
| Category | Usage |
| Fix is | Breaking |
Cause
An API version expressed as literal text is invalid.
Rule Description
Some call sites allow specifying an API version as a string. If the format of the API version is invalid, it is not uncovered until runtime when the text is parsed.
Consider the following code:
[ApiController]
[ApiVersion("abc")]
[Route("[controller]")]
public class ExampleController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
The text "abc" is not a valid API version. This would not detected until runtime.
How to Fix Violations
Update the API version to be well-formed. In addition, consider using one of the typed value forms that allow specifying literal numerics to avoid common mistakes.
[ApiController]
[ApiVersion("1.0")]
[Route("[controller]")]
public class ExampleController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
When to Suppress Warnings
It is never safe to suppress this rule because a runtime exception will be thrown when the text is parsed.
AV0002: Invalid API version range
| Value | |
|---|---|
| Rule ID | AV0002 |
| Category | Usage |
| Fix is | Breaking |
Cause
The specific API version range is invalid.
Rule Description
An API version range must express a valid interval notation.
Consider the following code:
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
[VisibleInApiVersion( ")2.0,]" )]
public string MiddleName { get; set; }
public string LastName { get; set; }
}
The text ")2.0,]" is not a valid API version range. This would not detected until runtime.
How to Fix Violations
Update the API version range to be well-formed.
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
[VisibleInApiVersion( "(2.0,]" )]
public string MiddleName { get; set; }
public string LastName { get; set; }
}
When to Suppress Warnings
It is never safe to suppress this rule because a runtime exception will be thrown when the text is parsed.
AV0003: Invalid API version status
| Value | |
|---|---|
| Rule ID | AV0003 |
| Category | Usage |
| Fix is | Breaking |
Cause
The specific API version status is invalid.
Rule Description
An API version status must start with a letter and may contain letters, digits, and periods. The status not end with a period.
Consider the following code:
[ApiController]
[ApiVersion(2.0, "preview-1")]
[Route("[controller]")]
public class ExampleController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
The text "preview-1" is not a valid API version status. This would not detected until runtime.
How to Fix Violations
Update the API version status to be well-formed.
[ApiController]
[ApiVersion(2.0, "preview.1")]
[Route("[controller]")]
public class ExampleController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
When to Suppress Warnings
It is never safe to suppress this rule because a runtime exception will be thrown when the text is parsed.
AV0004: Invalid API version number
| Value | |
|---|---|
| Rule ID | AV0004 |
| Category | Usage |
| Fix is | Breaking |
Cause
An API version specified a negative number.
Rule Description
Specifying an API version as a number removes a class of issues, but the number can still be specified as a negative value.
Consider the following code:
[ApiController]
[ApiVersion(-2.0)]
[Route("[controller]")]
public class ExampleController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
The text -2.0 is not a valid API version. This would not detected until runtime.
How to Fix Violations
An API version should always be greater than or equal to 0.1. Update the API version to be well-formed.
[ApiController]
[ApiVersion(2.0)]
[Route("[controller]")]
public class ExampleController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
When to Suppress Warnings
It is never safe to suppress this rule because a runtime exception will be thrown when the attribute is initialized.
AV0005: Invalid API version year
| Value | |
|---|---|
| Rule ID | AV0005 |
| Category | Usage |
| Fix is | Breaking |
Cause
An API version expressed an invalid year.
Rule Description
When an API version is expressed as a date, the specified year must be valid.
Consider the following code:
[ApiController]
[ApiVersion(10_000, 1, 1)]
[Route("[controller]")]
public class ExampleController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
The year 10_000 is not a valid API version. The year must be between 1 and 9999. This would not detected until
runtime.
How to Fix Violations
Update the API version to be well-formed.
[ApiController]
[ApiVersion(2026, 1, 1)]
[Route("[controller]")]
public class ExampleController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
When to Suppress Warnings
It is never safe to suppress this rule because a runtime exception will be thrown when the attribute is initialized.
AV0006: Invalid API version month
| Value | |
|---|---|
| Rule ID | AV0006 |
| Category | Usage |
| Fix is | Breaking |
Cause
An API version expressed an invalid month.
Rule Description
When an API version is expressed as a date, the specified month must be valid.
Consider the following code:
[ApiController]
[ApiVersion(2026, 13, 1)]
[Route("[controller]")]
public class ExampleController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
The month 13 is not a valid API version. The month must be between 1 and 12. This would not detected until runtime.
How to Fix Violations
Update the API version to be well-formed.
[ApiController]
[ApiVersion(2026, 1, 1)]
[Route("[controller]")]
public class ExampleController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
When to Suppress Warnings
It is never safe to suppress this rule because a runtime exception will be thrown when the attribute is initialized.
AV0007: Invalid API version day
| Value | |
|---|---|
| Rule ID | AV0007 |
| Category | Usage |
| Fix is | Breaking |
Cause
An API version expressed an invalid day.
Rule Description
When an API version is expressed as a date, the specified day must be valid.
Consider the following code:
[ApiController]
[ApiVersion(2026, 1, 32)]
[Route("[controller]")]
public class ExampleController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
The day 32 is not a valid API version. The day must be between 1 and 31. This would not detected until runtime.
How to Fix Violations
Update the API version to be well-formed.
[ApiController]
[ApiVersion(2026, 1, 1)]
[Route("[controller]")]
public class ExampleController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
When to Suppress Warnings
It is never safe to suppress this rule because a runtime exception will be thrown when the attribute is initialized.
AV0008: Invalid API version date
| Value | |
|---|---|
| Rule ID | AV0008 |
| Category | Usage |
| Fix is | Breaking |
Cause
An API version expressed an invalid date.
Rule Description
When an API version is expressed as a date, the specified date must be valid.
Consider the following code:
[ApiController]
[ApiVersion(2026, 2, 29)]
[Route("[controller]")]
public class ExampleController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
The date 2026-02-29 is not a valid API version because 2026 is not a leap year. This would not detected until
runtime.
How to Fix Violations
Update the API version to be well-formed.
[ApiController]
[ApiVersion(2026, 2, 28)]
[Route("[controller]")]
public class ExampleController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
When to Suppress Warnings
It is never safe to suppress this rule because a runtime exception will be thrown when the attribute is initialized.
AV0009: Invalid API version format specifier
| Value | |
|---|---|
| Rule ID | AV0009 |
| Category | Usage |
| Fix is | Breaking |
Cause
An API version format specifier is invalid.
Rule Description
When an API version is formatted, it must use a valid format specifier.
Consider the following code:
Console.WriteLine(new ApiVersion(2026, 1, 1).ToString("'unterminated"));
The format specifier 'unterminated is not a valid API version format. This would not be detected until runtime.
How to Fix Violations
Update the API version format.
Console.WriteLine(new ApiVersion(2026, 1, 1).ToString("'v'VV"));
When to Suppress Warnings
It is never safe to suppress this rule because a runtime exception will be thrown when the value is formatted.
AV0010: Unexpected API version format
| Value | |
|---|---|
| Rule ID | AV0010 |
| Category | Usage |
| Fix is | Non-breaking |
Cause
An API version format specifier is unexpected.
Rule Description
When an API version is formatted, it must use a valid format specifier.
Consider the following code:
Console.WriteLine(new ApiVersion(2026, 1, 1).ToString("VVVVV"));
The format specifier V is only meaningful up to 4 times. Repeating it 5 times does not produce the expected result;
the extra specifier is silently reinterpreted rather than reported. This would not be detected until runtime.
How to Fix Violations
Update the API version format so that the specifier is not repeated beyond its maximum.
Console.WriteLine(new ApiVersion(2026, 1, 1).ToString("VVVV"));
When to Suppress Warnings
It is never safe to suppress this rule. Incorrect or unexpected output will be generated.
AV0011: Remove unnecessary default API version
| Value | |
|---|---|
| Rule ID | AV0011 |
| Category | Style |
| Fix is | Non-breaking |
Cause
The default API version is assigned the value it already has.
Rule Description
The default API version is 1.0 unless it is configured otherwise. Assigning that same version restates what the
options were already given.
Consider the following code:
builder.Services.AddApiVersioning(
options =>
{
options.DefaultApiVersion = ApiVersion.Default;
} );
ApiVersion.Default is 1.0, which is what the options start with. Writing new ApiVersion( 1, 0 ) or
new ApiVersion( 1.0 ) states the same version a different way and is equally unnecessary.
How to Fix Violations
Remove the assignment.
builder.Services.AddApiVersioning();
When to Suppress Warnings
It is safe to suppress this rule if you prefer the default API version to be stated explicitly so that a future change to it is deliberate rather than inherited.
AV0012: Invalid default API version
| Value | |
|---|---|
| Rule ID | AV0012 |
| Category | Usage |
| Fix is | Breaking |
Cause
The default API version is version-neutral.
Rule Description
The default API version is the version applied to a request that did not ask for one. Version-neutral is not a version; it is the absence of one. A request cannot be resolved to it, so it can never serve as a default.
Consider the following code:
builder.Services.AddApiVersioning(
options =>
{
options.DefaultApiVersion = ApiVersion.Neutral;
} );
ApiVersion.Neutral cannot be the default for either the API versioning options or the API explorer options.
How to Fix Violations
Assign a real API version, or remove the assignment and let the default of 1.0 stand.
builder.Services.AddApiVersioning(
options =>
{
options.DefaultApiVersion = new ApiVersion( 2.0 );
} );
If the intent was for endpoints to be reachable without naming a version, declare those endpoints version-neutral instead of making the default neutral.
When to Suppress Warnings
It is never safe to suppress this rule. No request can ever resolve to a version-neutral default.
AV0013: Missing AddMvc
| Value | |
|---|---|
| Rule ID | AV0013 |
| Category | Usage |
| Fix is | Breaking |
Cause
An application uses MVC controllers and API versioning, but never opted into versioning MVC.
Rule Description
API versioning covers minimal APIs on its own. Controllers are discovered and routed by MVC, which requires an explicit opt in so that the versioning metadata declared on a controller is applied to the actions it defines. Without it, the attributes are still compiled but nothing ever reads them.
Consider the following code:
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddControllers();
builder.Services.AddApiVersioning();
AddControllers() opts into MVC and AddApiVersioning() opts into API versioning, but neither versions the other.
Every controller in the application is routed as if it declared no API version at all.
How to Fix Violations
Call AddMvc() on the builder returned by AddApiVersioning().
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddControllers();
builder.Services.AddApiVersioning().AddMvc();
AddMvcCore() opts into MVC the same way AddControllers() does and requires the same call.
When to Suppress Warnings
It is safe to suppress this rule only if the controllers in the application are deliberately left unversioned; for
example, when API versioning was added for Minimal APIs and the controllers serve something else. Note that
AddApiVersioning().AddMvc() changes how requests are routed to controllers, so applying the fix to an existing service
is a breaking change for clients that do not send a version.
AV0014: Missing API behavior
| Value | |
|---|---|
| Rule ID | AV0014 |
| Category | Usage |
| Fix is | Breaking |
Cause
A controller that serves an API has not opted into API behavior.
Rule Description
A controller derived from ControllerBase may serve an API or something else entirely. [ApiController] is what
resolves that ambiguity. It also turns on the conventions an API is expected to follow, such as automatic model
validation and problem details for error responses, which is how a versioning error is reported in the shape a client
can read.
A controller derived from Controller is assumed to serve a user interface rather than an API and is never reported.
Consider the following code:
[ApiVersion( 2.0 )]
[Route( "[controller]" )]
public class ExampleController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
The controller declares an API version but never states that it is an API.
How to Fix Violations
Add [ApiController] to the controller.
[ApiController]
[ApiVersion( 2.0 )]
[Route( "[controller]" )]
public class ExampleController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
The attribute can also be applied to the assembly, in which case it covers every controller and nothing is reported:
[assembly: ApiController]
When to Suppress Warnings
It is safe to suppress this rule if the controller derives from ControllerBase but does not serve an API. Applying
[ApiController] changes model binding and error responses, so adding it to an existing service is a breaking change
for clients that depend on the current behavior.
AV0015: Use a specific API version reader
| Value | |
|---|---|
| Rule ID | AV0015 |
| Category | Performance |
| Fix is | Breaking |
Cause
An API reads its version one way, but the reader was left to accept more than one.
Rule Description
Without an explicit reader, an API version is looked for in both the query string and the URL segment. Every route in the application is examined to decide which of the two is actually used. When they all agree, the other reader is asked for a version on every request and never finds one.
Consider the following code:
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddApiVersioning();
var app = builder.Build();
app.MapGet( "/v{version:apiVersion}/order", () => Results.Ok() ).HasApiVersion( 1.0 );
app.MapGet( "/v{version:apiVersion}/customer", () => Results.Ok() ).HasApiVersion( 1.0 );
app.Run();
Every route carries the API version constraint, so the version is only ever read from the URL segment. The query string is searched on every request for a value that is never there.
Any mixture of the two styles, or a route that cannot be followed back to its origin, leaves the default in place and is not reported; narrowing the reader would then break a form the application relies on.
How to Fix Violations
Configure the reader the application actually uses.
builder.Services.AddApiVersioning(
options =>
{
options.ApiVersionReader = new UrlSegmentApiVersionReader();
} );
An application whose routes never carry the constraint reads its version from the query string instead:
builder.Services.AddApiVersioning(
options =>
{
options.ApiVersionReader = new QueryStringApiVersionReader();
} );
When to Suppress Warnings
It is safe to suppress this rule if the additional reader is intended; for example, when the routes visible to the compiler are only part of the application, or when a client is knowingly allowed to name a version either way. Configuring a specific reader stops the other form from being accepted, so applying the fix to an existing service is a breaking change for any client using it.
AV0016: Do not assume default API version
| Value | |
|---|---|
| Rule ID | AV0016 |
| Category | Usage |
| Fix is | Non-breaking |
Cause
A default API version is assumed where none can ever apply.
Rule Description
A default API version is only applied to an endpoint that carries no versioning metadata at all. The setting exists to grandfather the clients of a service that was not versioned before. Declaring any version, even a neutral one, takes an endpoint out of that arrangement, as does a route that can only be reached by naming a version in the URL. Once every endpoint is in one of those states, the setting does nothing.
Consider the following code:
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddApiVersioning(
options =>
{
options.AssumeDefaultVersionWhenUnspecified = true;
} );
var app = builder.Build();
app.MapGet( "/order", () => Results.Ok() ).HasApiVersion( 1.0 );
app.MapGet( "/customer", () => Results.Ok() ).HasApiVersion( 1.0 );
app.Run();
Every endpoint declares its own version, so there is nothing left for the default to be applied to.
Reading the version from the media type is the exception and is never reported. A client asking for
application/json has named no version and never will, whereas every version after the first is asked for as something
like application/json; v=2.0. Assuming a default is what keeps the original clients working, however the endpoints are
declared.
How to Fix Violations
Remove the assignment.
builder.Services.AddApiVersioning();
When to Suppress Warnings
It is safe to suppress this rule if endpoints that rely on the default are declared outside the compilation, such as in a referenced library. See existing services for when assuming a default is the right arrangement.
AV0017: Remove unnecessary default value
| Value | |
|---|---|
| Rule ID | AV0017 |
| Category | Usage |
| Fix is | Non-breaking |
Cause
An option is assigned the value it already has.
Rule Description
Most options already hold a usable value before any configuration runs. Assigning that same value restates the default without changing anything.
Consider the following code:
builder.Services.AddApiVersioning(
options =>
{
options.ReportApiVersions = false;
options.RouteConstraintName = "apiVersion";
} );
Both assignments state what the options already hold.
The default of a property is matched by the type declaring it rather than by name alone, because the same name can carry a different default on a different set of options. The default API version is reported by AV0011 instead, because it can be spelled more than one way, and a value the API explorer takes from the API versioning options is reported by AV0024.
How to Fix Violations
Remove the assignment.
builder.Services.AddApiVersioning();
When to Suppress Warnings
It is safe to suppress this rule if you prefer options to be stated explicitly so that a future change to a default is deliberate rather than inherited.
AV0018: All endpoints are version-neutral
| Value | |
|---|---|
| Rule ID | AV0018 |
| Category | Usage |
| Fix is | Breaking |
Cause
Every endpoint in the application is version-neutral, so no API version is ever defined.
Rule Description
A version-neutral endpoint belongs to every API version that has been defined. Requests still route when nothing else is defined, which is why this can go unnoticed, but the API explorer describes an endpoint once per explicitly defined version. With none defined, it describes nothing at all and the generated documentation is empty.
Consider the following code:
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddApiVersioning().AddApiExplorer();
var app = builder.Build();
app.MapGet( "/order", () => Results.Ok() ).IsApiVersionNeutral();
app.MapGet( "/customer", () => Results.Ok() ).IsApiVersionNeutral();
app.Run();
Neither endpoint defines an API version, so there is no version for the neutral endpoints to belong to.
An endpoint that declares nothing at all is a separate problem and is not reported here.
How to Fix Violations
Declare an explicit API version on at least one endpoint.
var app = builder.Build();
app.MapGet( "/order", () => Results.Ok() ).HasApiVersion( 1.0 );
app.MapGet( "/customer", () => Results.Ok() ).IsApiVersionNeutral();
app.Run();
When to Suppress Warnings
It is safe to suppress this rule if the endpoints that define the API versions are declared outside the compilation, such as in a referenced library. See version neutrality for what neutrality means and when it applies.
AV0019: An API cannot be versioned and version-neutral at the same time
| Value | |
|---|---|
| Rule ID | AV0019 |
| Category | Usage |
| Fix is | Breaking |
Cause
An API is declared both versioned and version-neutral.
Rule Description
Versioning metadata is inherited from a controller or an endpoint group as a convenience, and an action may state something more explicit in its place. Neutrality is the exception. It applies to the whole API, and an action cannot meaningfully claim a version of an API that has none.
Consider the following code:
[ApiController]
[ApiVersionNeutral]
[Route( "[controller]" )]
public class ExampleController : ControllerBase
{
[HttpGet]
[ApiVersion( 2.0 )]
public IActionResult Get() => Ok();
}
The controller states that the API has no versions while the action claims one of them.
The same conflict occurs with minimal APIs when a version is declared under a neutral group, or when both are declared together at the same level:
var orders = app.MapGroup( "/order" ).IsApiVersionNeutral();
orders.MapGet( "/", () => Results.Ok() ).HasApiVersion( 2.0 );
Controllers are collated by logical name, so a neutral declaration on one controller can silence versions declared on another that collates alongside it:
[ApiController]
[ApiVersionNeutral]
[Route( "example" )]
public class Example2Controller : ControllerBase { }
[ApiController]
[ApiVersion( 3.0 )]
[Route( "example" )]
public class Example3Controller : ControllerBase { }
How to Fix Violations
Decide whether the API is versioned or neutral and declare only that.
[ApiController]
[ApiVersion( 2.0 )]
[Route( "[controller]" )]
public class ExampleController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
When to Suppress Warnings
It is never safe to suppress this rule. The two declarations contradict each other and one of them will not be honored.
AV0020: Remove unnecessary API explorer
| Value | |
|---|---|
| Rule ID | AV0020 |
| Category | Style |
| Fix is | Non-breaking |
Cause
The endpoints API explorer is added alongside the versioned API explorer, which already adds it.
Rule Description
AddApiExplorer() adds the endpoints API explorer itself, as do the OData and OpenAPI variants on their way to their
own. Calling AddEndpointsApiExplorer() next to any of them repeats a registration that has already been made.
Consider the following code:
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddApiVersioning().AddApiExplorer();
AddApiExplorer() covers the call above it.
How to Fix Violations
Remove the call to AddEndpointsApiExplorer().
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddApiVersioning().AddApiExplorer();
When to Suppress Warnings
It is safe to suppress this rule. The extra call is redundant rather than wrong.
AV0021: Use the versioned API explorer
| Value | |
|---|---|
| Rule ID | AV0021 |
| Category | Usage |
| Fix is | Non-breaking |
Cause
An application versions its APIs but describes them with an API explorer that is unaware of API versions.
Rule Description
AddEndpointsApiExplorer() describes endpoints without their versions. Once API versioning is in use, the generated
documentation shows a single, version-less view of an API that actually has more than one version.
Consider the following code:
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddApiVersioning();
API versioning is configured, but nothing was told to describe the versions.
How to Fix Violations
Replace the call with the versioned API explorer, which adds the endpoints API explorer itself.
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddApiVersioning().AddApiExplorer();
An application using OData, gRPC, and/or OpenAPI calls the corresponding variant instead:
builder.Services.AddApiVersioning()
.AddOData()
.AddODataApiExplorer()
.AddOpenApi();
builder.Services.AddApiVersioning()
.AddGrpc()
.AddGrpcApiExplorer()
.AddOpenApi();
See API explorer options for what the versioned explorer can be configured to describe.
When to Suppress Warnings
It is safe to suppress this rule if the documentation is deliberately generated without API versions.
AV0022: Missing AddOData
| Value | |
|---|---|
| Rule ID | AV0022 |
| Category | Usage |
| Fix is | Breaking |
Cause
An application uses OData and API versioning, but never opted into versioning OData.
Rule Description
OData routes by its own conventions rather than by the routes API versioning otherwise observes, so versioning an OData API takes an explicit opt in. Without it, the versioning metadata declared on an OData controller is never applied.
Consider the following code:
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddControllers().AddOData();
builder.Services.AddApiVersioning();
The AddOData() above belongs to OData itself and opts into OData. It does not version it.
How to Fix Violations
Call AddOData() on the builder returned by AddApiVersioning().
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddControllers().AddOData();
builder.Services.AddApiVersioning().AddOData(
options => options.AddRouteComponents( "api" ) );
AddODataApiExplorer() registers the versioned OData services it needs on its own, which is a supported way to describe
a versioned OData API without taking on the rest of them.
When to Suppress Warnings
It is safe to suppress this rule only if the OData APIs in the application are deliberately left unversioned. Versioning OData changes how its routes are resolved, so applying the fix to an existing service is a breaking change for clients that do not send a version.
AV0023: Route components are ignored
| Value | |
|---|---|
| Rule ID | AV0023 |
| Category | Usage |
| Fix is | Breaking |
Cause
OData route components are added to the options that versioned OData replaces.
Rule Description
Versioned OData resolves the options for the API version of the current request. The options configured for OData itself are not part of that resolution. Route components added without saying which API version they belong to are left behind when the options are resolved, and a prefix stated in both places collides once they are.
Consider the following code:
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddControllers().AddOData(
options => options.AddRouteComponents( "api", model ) );
builder.Services.AddApiVersioning().AddOData();
The route components are added to ODataOptions rather than to the versioned options, so they are never applied.
How to Fix Violations
Add the route components through the options given to the versioned AddOData(), which applies them per API version.
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddControllers().AddOData();
builder.Services.AddApiVersioning().AddOData(
options => options.AddRouteComponents( "api" ) );
See OData options for how route components are applied per API version.
When to Suppress Warnings
It is never safe to suppress this rule. The route components are either ignored or collide with the versioned ones.
AV0024: Remove unnecessary API explorer option
| Value | |
|---|---|
| Rule ID | AV0024 |
| Category | Usage |
| Fix is | Non-breaking |
Cause
An API explorer option restates a value it already inherits from the API versioning options.
Rule Description
The API explorer takes the options it shares with API versioning before its own configuration runs. Stating one of those shared values again only repeats what it was already given.
The shared values are AssumeDefaultVersionWhenUnspecified, DefaultApiVersion, RouteConstraintName,
ApiVersionSelector, and ApiVersionParameterSource, which the API explorer takes from ApiVersionReader.
Consider the following code:
builder.Services.AddApiVersioning(
options =>
{
options.DefaultApiVersion = new ApiVersion( 2.0 );
} )
.AddApiExplorer(
options =>
{
options.DefaultApiVersion = new ApiVersion( 2.0 );
} );
The API explorer was already given 2.0 from the API versioning options.
A value that differs from the one configured for API versioning is a deliberate departure and is not reported.
How to Fix Violations
Remove the assignment and let the value be inherited.
builder.Services.AddApiVersioning(
options =>
{
options.DefaultApiVersion = new ApiVersion( 2.0 );
} )
.AddApiExplorer();
When to Suppress Warnings
It is safe to suppress this rule if you prefer the API explorer options to be stated in full so that a later change to the API versioning options does not silently change what is described.
AV0025: Missing OpenAPI document description
| Value | |
|---|---|
| Rule ID | AV0025 |
| Category | Documentation |
| Fix is | Non-breaking |
Cause
An OpenAPI document is generated without the description that documents it.
Rule Description
What an OpenAPI document says about itself is taken from the assembly it is generated for. The title is supplied by the project whether it was asked for or not, but the description is only there if it was stated.
Consider the following code:
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddApiVersioning().AddApiExplorer().AddOpenApi();
Nothing in the project describes what the generated document is for, so the description of every document is left
empty.
The description is taken from the assembly the application was started from, so this is only reported for a project that produces an application. A library configuring OpenAPI on an application’s behalf has nothing to give and is not reported.
How to Fix Violations
Set Description in the project file.
<PropertyGroup>
<Description>Order management APIs.</Description>
</PropertyGroup>
The attribute the project generates from that property can also be written by hand:
[assembly: AssemblyDescription( "Order management APIs." )]
When to Suppress Warnings
It is safe to suppress this rule if the generated documents are not published, or if the description is supplied when the document is transformed rather than by the assembly.
AV0026: Remove unnecessary group name format
| Value | |
|---|---|
| Rule ID | AV0026 |
| Category | Usage |
| Fix is | Non-breaking |
Cause
A group name format is configured for an application where no API has a group name.
Rule Description
FormatGroupName is only reached for an API that has a group name. An API without one is described by its API version
alone, so the callback is never invoked and the format has no effect.
Consider the following code:
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddApiVersioning().AddApiExplorer(
options =>
{
options.FormatGroupName = ( group, version ) => $"{group}-{version}";
} );
var app = builder.Build();
app.MapGet( "/order", () => Results.Ok() ).HasApiVersion( 1.0 );
app.Run();
No API in the application states a group name, so nothing is ever formatted.
How to Fix Violations
Either remove the format, or give the APIs it is meant for a group name.
var app = builder.Build();
app.MapGet( "/order", () => Results.Ok() ).HasApiVersion( 1.0 ).WithGroupName( "orders" );
app.Run();
A controller states its group name with [ApiExplorerSettings]:
[ApiController]
[ApiVersion( 1.0 )]
[ApiExplorerSettings( GroupName = "orders" )]
[Route( "[controller]" )]
public class OrderController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
}
When to Suppress Warnings
It is safe to suppress this rule if the APIs carrying group names are declared outside the compilation, such as in a referenced library.
AV0027: Use DescribeApiVersions
| Value | |
|---|---|
| Rule ID | AV0027 |
| Category | Usage |
| Fix is | Non-breaking |
Cause
API version descriptions are resolved from the services before every API has been mapped.
Rule Description
An IApiVersionDescriptionProvider resolved from the services describes the APIs that were known when the services were
built. Minimal APIs are mapped onto the application afterward, so they are not among them. Describing the versions from
the application itself waits until every API has been mapped, which is why there was nothing to choose between before
minimal APIs existed.
Consider the following code:
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddApiVersioning().AddApiExplorer();
var app = builder.Build();
app.MapGet( "/order", () => Results.Ok() ).HasApiVersion( 1.0 );
var descriptions = app.Services.GetRequiredService<IApiVersionDescriptionProvider>().ApiVersionDescriptions;
app.Run();
The provider was resolved from the services, so the endpoint mapped above it is not described.
How to Fix Violations
Describe the versions from the application, which waits until every API has been mapped.
var app = builder.Build();
app.MapGet( "/order", () => Results.Ok() ).HasApiVersion( 1.0 );
var descriptions = app.DescribeApiVersions();
app.Run();
When to Suppress Warnings
It is safe to suppress this rule if the descriptions are deliberately limited to the APIs known when the services were built.
AV0028: Sunset policy takes effect before deprecation
| Value | |
|---|---|
| Rule ID | AV0028 |
| Category | Usage |
| Fix is | Non-breaking |
Cause
An API is sunset before it is deprecated.
Rule Description
Deprecation announces that an API is going away and sunset is when it does, so the two are only in order when deprecation comes first. Taking effect on the same day is allowed.
Consider the following code:
builder.Services.AddApiVersioning(
options =>
{
options.Policies.Deprecate( 0.9 ).Effective( 2024, 6, 1 );
options.Policies.Sunset( 0.9 ).Effective( 2024, 1, 1 );
} );
The API is retired five months before its clients are told it is going away.
Only policies that some API reaches together are compared and only when both state a date that can be read as written. A date that comes from somewhere else is left alone because what it will be is not decided here.
How to Fix Violations
Move the sunset date on or after the deprecation date.
builder.Services.AddApiVersioning(
options =>
{
options.Policies.Deprecate( 0.9 ).Effective( 2024, 1, 1 );
options.Policies.Sunset( 0.9 ).Effective( 2024, 6, 1 );
} );
See version policies for how the dates are advertised to clients.
When to Suppress Warnings
It is safe to suppress this rule if the ordering is deliberate; for example, when an API is being retired without the usual notice and the deprecation is recorded after the fact.
AV0029: Remove unnecessary OpenAPI services
| Value | |
|---|---|
| Rule ID | AV0029 |
| Category | Usage |
| Fix is | Non-breaking |
Cause
The OpenAPI services are registered alongside the versioned ones that replace them.
Rule Description
AddApiVersioning().AddOpenApi() registers services of its own in place of the ones OpenAPI registers for itself, which
describe a single document that knows nothing about API versions. Calling AddOpenApi() on the service collection as
well registers services that are then replaced.
Consider the following code:
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddOpenApi();
builder.Services.AddApiVersioning().AddOpenApi();
The AddOpenApi() above belongs to OpenAPI itself and is superseded by the versioned one. Any of AddApiExplorer(),
AddODataApiExplorer(), AddGrpcApiExplorer(), or AddOpenApi() on the API versioning builder has the same effect.
How to Fix Violations
Remove the call to AddOpenApi() on the service collection.
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddApiVersioning().AddOpenApi();
See OpenAPI options for how the versioned documents are configured.
When to Suppress Warnings
It is safe to suppress this rule. The call is redundant rather than wrong.
AV0030: Missing WithDocumentPerVersion
| Value | |
|---|---|
| Rule ID | AV0030 |
| Category | Usage |
| Fix is | Non-breaking |
Cause
The endpoint serving OpenAPI documents was not told to serve one per API version.
Rule Description
The endpoint serving the documents resolves them from the services of the request it is answering, which is only where the versioned documents are to be found once the endpoint has been told to look there. Without that, the endpoint serves the single, version-less document it would have served before API versioning was configured.
Consider the following code:
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddApiVersioning().AddApiExplorer().AddOpenApi();
var app = builder.Build();
app.MapOpenApi();
app.Run();
The versioned documents are generated but never served.
How to Fix Violations
Continue the expression that mapped the endpoint with WithDocumentPerVersion().
var app = builder.Build();
app.MapOpenApi().WithDocumentPerVersion();
app.Run();
When to Suppress Warnings
It is safe to suppress this rule if a single document describing every API version is intended.
AV0031: Missing API explorer
| Value | |
|---|---|
| Rule ID | AV0031 |
| Category | Usage |
| Fix is | Non-breaking |
Cause
An OpenAPI document is generated without the API explorer that describes the APIs it is generated for.
Rule Description
An OpenAPI document is generated from what the API explorer discovered, and what it discovers depends on how the APIs were built. OData and gRPC are each described by an explorer of their own, which nothing else registers on their behalf.
Consider the following code:
var builder = WebApplication.CreateBuilder( args );
builder.Services.AddApiVersioning().AddOpenApi();
Nothing describes the APIs, so the generated documents are empty.
An application that versions OData or gRPC needs the matching explorer as well:
builder.Services.AddApiVersioning().AddOData().AddOpenApi();
builder.Services.AddApiVersioning().AddGrpc().AddOpenApi();
An API built any other way is described by the explorer the rest of them build on, so a specialized explorer on its own satisfies the rule for the APIs it specializes in.
How to Fix Violations
Add the API explorer that matches how the APIs were built.
builder.Services.AddApiVersioning().AddApiExplorer().AddOpenApi();
builder.Services.AddApiVersioning().AddOData().AddODataApiExplorer().AddOpenApi();
builder.Services.AddApiVersioning().AddGrpc().AddGrpcApiExplorer().AddOpenApi();
When to Suppress Warnings
It is safe to suppress this rule if the API explorer is registered outside the compilation, such as by a referenced library that configures the services on the application’s behalf.
Known Limitations
URL Path Segment
API versioning does not fundamentally change how routing works in ASP.NET. When you elect to support API versioning via a URL path segment, the API version is part of the path considered in routing. There is currently no built-in method to match a route where the API version URL path segment has not be specified.
The recommended method to enable this scenario is to use Double Route Registration by providing multiple routes for the corresponding controller actions as follows:
[ApiVersion( 1.0 )]
[ApiController]
[Route( "api/[controller]" )]
[Route( "api/v{version:apiVersion}/[controller]" )]
public class ValuesController : ControllerBase
{
// ~/api/values
// ~/api/v1/values
[HttpGet]
public IHttpActionResult Get() => Ok();
}
[ApiVersion( 2.0 )]
[ApiController]
[Route( "api/v{version:apiVersion}/values" )]
public class Values2Controller : ControllerBase
{
// ~/api/v2/values
[HttpGet]
public IHttpActionResult Get() => Ok();
}
Alternative
You can use middleware or other customizations to simplify your implementation. The Gist provides one such
implementation that allows URL versioning to external clients, but allows simplified URL mapping internally. For
example, api/v1/values becomes api/values internally, captures the 1.0 API version, and sets the requested API
version via the IApiVersioningFeature.
FAQ
What is the difference between the DefaultApiVersion and ApiVersionSelector options?
There are subtle differences between these two options. Typically, you only need to configure one or the other, but not both.
The DefaultApiVersion has the following uses:
- The API version defined for a controller that does not have any explicit attribution or conventions
- The fallback API version used when no other API version can be resolved
It’s important to understand that once you opt into API versioning, every controller has an API version, even if you do not apply an explicit definition via attributes or conventions. This behavior can also be thought of as the initial API Version.
The DefaultApiVersion value is 1.0, but that may not be your starting API version. For example, you might use the
date-only API versioning scheme. This configuration option prevents the value from being hard-coded and makes it easy
to change the API version for your initial set of services.
The ApiVersionSelector option has a familiar, but different purpose. Any implementation of the IApiVersionSelector
is used to select the best API version given the current HTTP request and API version model. The provided API version
model will already be aggregated across all known service versions.
While this component could be used for a number of different purposes, it is currently only used to select the API
version that should be used when a client does not provide an API version. This option is thus only used when the
AssumeDefaultVersionWhenUnspecified option is also true. The default, configured value for this option is a
instance of the DefaultApiVersionSelector, which always returns the value of DefaultApiVersion. Most of the built-in
IApiVersionSelector implementations accept the ApiVersioningOptions in their constructors so that they can use the
DefaultApiVersion as the final fallback value.
It’s recommended that the IApiVersionSelector implementation you use provides stable, deterministic results. This is particularly important for existing clients that may not be aware that you have introduced API versioning. Contrary to this guidance, a number of service authors have requested granular control over how the API versions should be selected. As an example, a service author might want to allow a client to never specify an API version and use an internal client-to-version mapping that is maintained on the server after the first client connects. How this is implemented in an IApiVersionSelector is up to the service author, but it likely requires information from the current HTTP request and the available API versions for a service.
Examples
Complete, runnable sample projects live in the examples folder of the repository.
- Web API
- Includes Minimal APIs
- Includes controllers with MVC (Core)
- Includes gRPC
- Includes OpenAPI
- OData
- Includes controllers with MVC (Core) and OData
- Includes OpenAPI
New Services
When a service author creates new services that consider API versioning upfront, then the configuration and setup is very straightforward. The following examples provide a quick start setup for the respective platforms with default configurations.
API versions can be expressed with .NET attributes or by configured conventions. These examples all use .NET attributes. If you’re interested in using conventions instead, please review the API version conventions topic.
Web API
public static class WebApiConfig
{
public static void Configuration( HttpConfiguration configuration )
{
configuration.AddApiVersioning();
// remaining configuration omitted for brevity
}
}
[ApiVersion( 1.0 )]
[RoutePrefix( "People" )]
public class PeopleController : ApiController
{
[Route]
public IHttpActionResult Get() => Ok( new[] { new Person() } );
}
OData
public static class WebApiConfig
{
public static void Configuration( HttpConfiguration configuration )
{
configuration.AddApiVersioning();
var modelBuilder = new VersionedODataModelBuilder( configuration )
{
DefaultModelConfiguration = ( builder, apiVersion, routePrefix ) =>
{
builder.EntitySet<Person>( "People" );
}
};
configuration.MapVersionedODataRoutes( "odata", null, modelBuilder );
// remaining configuration omitted for brevity
}
}
[ApiVersion( 1.0 )]
[ODataRoutePrefix( "People" )]
public class PeopleController : ODataController
{
[EnableQuery]
[ODataRoute]
public IHttpActionResult Get() => Ok( new[] { new Person() } );
}
Existing Services
While it’s great to plan for an API versioning story for your services upfront, it’s all too common to need API versioning after your services are in production. The ASP.NET versioning libraries provide features to help you retrofit existing services and integrate formal API versioning without breaking your existing clients.
Unless a service is API version-neutral, existing services have some logical, yet undefined, API version that is not formally declared by the service or known to a client. In order to prevent existing clients from breaking, they must be able to make requests to the original URL without specifying any API version information.
When API versioning is applied, all of the existing services now have an explicit API version on the service side. The
initial, default API version is 1.0, but that can be configured to be a different API version. All existing controller
definitions that do not have explicit API version definitions will now be implicitly bound to the default API version.
Once a controller has any API version attribution or conventions, it will never be implicitly matched. This enables
service authors to permanently sunset API versions over time. Controllers that have an implicit API version can be
confusing to service authors; especially, in a team environment. It is recommended that you explicitly apply API
versions to all of your existing services when you introduce formal API versioning.
Web API
public static class WebApiConfig
{
public static void Configuration( HttpConfiguration configuration )
{
// allow a client to call you without specifying an api version
// since we haven't configured it otherwise, the assumed api version will be 1.0
configuration.AddApiVersioning( options => options.AssumeDefaultVersionWhenUnspecified = true );
// remaining configuration omitted for brevity
}
}
[ApiVersion( 1.0 )] // ← this attribute isn't required, but it's easier to understand
[RoutePrefix( "People" )]
public class PeopleController : ApiController
{
// GET ~/people
// GET ~/people?api-version=1.0
[Route]
public IHttpActionResult Get() => Ok( new[] { new Person() } );
}
[ApiVersion( 2.0 )]
[RoutePrefix( "People" )]
public class People2Controller : ApiController
{
// GET ~/people?api-version=2.0
[Route]
public IHttpActionResult Get() => Ok( new[] { new Person() } );
}
OData
public static class WebApiConfig
{
public static void Configuration( HttpConfiguration configuration )
{
// allow a client to call you without specifying an api version
// since we haven't configured it otherwise, the assumed api version will be 1.0
configuration.AddApiVersioning( options => options.AssumeDefaultVersionWhenUnspecified = true );
var modelBuilder = new VersionedODataModelBuilder( configuration )
{
DefaultModelConfiguration = ( builder, apiVersion, routePrefix ) =>
{
builder.EntitySet<Person>( "People" );
}
};
configuration.MapVersionedODataRoutes( "odata", null, modelBuilder );
// remaining configuration omitted for brevity
}
}
[ApiVersion( 1.0 )] // ← this attribute isn't required, but it's easier to understand
[ODataRoutePrefix( "People" )]
public class PeopleController : ODataController
{
// GET ~/people
// GET ~/people?api-version=1.0
[EnableQuery]
[ODataRoute]
public IHttpActionResult Get() => Ok( new[] { new Person() } );
}
[ApiVersion( 2.0 )]
[ControllerName( "People" )]
[ODataRoutePrefix( "People" )]
public class People2Controller : ODataController
{
// GET ~/people?api-version=2.0
[EnableQuery]
[ODataRoute]
public IHttpActionResult Get() => Ok( new[] { new Person() } );
}
Migration From Previous Versions
This topic serves as the guide for migrating from version <= 5.x.x to version >= 6.0.0. The majority of this
information has been outlined in previous discussions.
Note
If you’d like more information on the background context, you can read the Hello Project “Asp” announcement.
For the most part, you can expect the required changes to be a new package identifier and different namespaces. It is entirely possible that you may update those and find the rest of the code to be identical. The mileage will vary depending on your level of customization, but you can expect the changes to be trivial in most cases.
Package Identifiers
The original Microsoft.* packages are now deprecated and will only undergo servicing:
| Package | Version | TFM |
|---|---|---|
| Microsoft.AspNet.WebApi.Versioning | <= 5.x.x | net45 |
| Microsoft.AspNet.WebApi.Versioning.ApiExplorer | <= 5.x.x | net45 |
| Microsoft.AspNet.OData.Versioning | <= 5.x.x | net45 |
| Microsoft.AspNet.OData.Versioning.ApiExplorer | <= 5.x.x | net45 |
All new features and platform support will use the Asp.Versioning.* prefix:
| Package | Version | TFM |
|---|---|---|
| Asp.Versioning.Abstractions | 6.0.0+ | net6.0+, netstandard1.0, netstandard2.0 |
| Asp.Versioning.WebApi | 6.0.0+ | net45, net472 |
| Asp.Versioning.WebApi.ApiExplorer | 6.0.0+ | net45, net472 |
| Asp.Versioning.WebApi.OData | 6.0.0+ | net45, net472 |
| Asp.Versioning.WebApi.OData.ApiExplorer | 6.0.0+ | net45, net472 |
Namespaces
As the project is no longer part of Microsoft, all namespaces have become Asp.Versioning.*. It didn’t make sense to
keep using Microsoft.* when things don’t line up. Furthermore, what namespace should all new code live under?
Continuing to use the Microsoft namespace seemed wrong. An interesting benefit, however, is that using
Api.Versioning.* allows for more consistency across the ASP.NET Web API and Core implementations. The existing
differences in library namespaces for shared code often led to conditional compiler directives. For ease of use,
extension methods will continue to live in the namespace they correspond to.
API Version
The format and default implementation has not changed, but parsing has been broken apart. The new IApiVersionParser
service has been introduced to support this capability. ApiVersion.Parse and ApiVersion.TryParse have been removed,
but are replaced by ApiVersionParser.Default, which will provide a default implementation.
ApiVersion.GroupVersion in .NET 6.0 and beyond is now represented as DateOnly. DateOnly accurately represents how
a group or date version was always meant to be, but couldn’t be represented without introducing its own type due to the
design of DateTime. The .NET Standard and .NET Framework representations will continue to use DateTime.
API Version Reader
IApiVersionReader.Read now returns IReadOnlyList<string> instead of string?. There are a few reasons for this
change. First, the Null Mistake is removed as an empty list is completely acceptable. Second, it was entirely possible
for a particular reader implementation to return more than one value. Consider that ?api-version=1.0&api-version=2.0
would return both 1.0 and 2.0. In previous versions, the implementation would instead throw
AmbiguousApiVersionException that would have to be handled. That behavior becomes problematic for the server to
correctly report the response to the client. Reading multiple API version values in and of itself isn’t exceptional,
it’s just an invalid client request. ApiVersionReader.Combine also enables combining different types of readers
through composition. Readers for different parts of a request are even more likely to return different values.
Refactoring to return a list makes it very simple to return all of the raw API versions provided without any exceptions
and regardless of where they were read from.
API Version Reporting
IReportApiVersions.Report now accepts the entire HTTP response as opposed to just the headers. Accepting only the
headers was an over-normalization that wasn’t really necessary. Additional information was also necessary to support
sunset policies. The Report overload that accepts Lazy<ApiVersionModel> has been removed as it’s no longer used
or necessary.
API Version Model Extensions
Extension methods related to retrieving an ApiVersionModel have been supplanted by the new extension property
ApiVersionMetadata. The previous GetApiVersionModel() extension method, for example, was a shortcut for
GetApiVersionModel(ApiVersionMapping.Explicit). A new type - ApiVersionMetadata - has been introduced that unifies
the metadata implementation across ASP.NET platforms.
The following is the mapping between the old and new extension methods or properties:
GetApiVersionModel(ApiVersionMapping) → ApiVersionMetadataGetApiVersionModel() → ApiVersionMetadata.Map(ApiVersionMapping.Explicit)MappingTo(ApiVersion) → ApiVersionMetadata.MappingTo(ApiVersion)IsMappedTo(ApiVersion) → ApiVersionMetadata.IsMappedTo(ApiVersion)
Error Responses
The IErrorResponseProvider service had been the hook to provide custom error responses. Problem Details (RFC 7807)
had only just been ratified when this project started and they were not part of ASP.NET yet. ASP.NET Core eventually
added first-class support for Problem Details and IErrorResponseProvider had an adapter implementation for alignment
in previous versions. Now that Problem Details are the de factor method for error reporting, it no longer makes sense to
retain IErrorResponseProvider and it has been removed.
The error responses bodies provided by IErrorResponseProvider complied with the
Microsoft REST Guidelines error response format, which is itself the error response format used by the OData protocol
(see OData JSON Format §21.1). If you need to retain that format, the Error Response backward compatibility topic
discusses how to enable it.
ProblemDetails.Type could logically be used to model the established error Code; however, the value is supposed to
be a URI. For backward compatibility, the existing error codes will be emitted as the Code extension in Problem
Details. The Error Responses topic provides details for each well-known problem that may be returned in responses.
Defining a Service Version
There are four out-of-the-box supported approaches for versioning a service:
- By query string parameter
- By media type parameter
- By HTTP header
- By URL path segment
The default method is to use a query string parameter named api-version. You can also combine API versioning approaches together or define your own custom method of API versioning.
Version Discovery
Requiring an explicit service version helps ensure existing clients don’t break, but we also need a way to advertise which service versions are currently supported and which versions are deprecated.
To facilitate this need, services should respond with the api-supported-versions and api-deprecated-versions, which
are multi-value HTTP headers that indicate the supported and deprecated API versions, respectively. A deprecated version
is still implemented, but is expected to be permanently removed in six months or more. When a version is no longer
supported, it should stop being advertised. Additional information can be provided via versioning policies.
Reporting API versions is disabled by default. Service authors can enable this behavior for all services by setting the
ApiVersioningOptions.ReportApiVersions to true or scoped to individual services by applying the [ReportApiVersions]
attribute or the ReportApiVersions() convention.
Service authors might also choose to implement the OPTIONS method so that clients and tooling can interrogate which
API versions their service supports.
Web API
// OPTIONS ~/api/myservice?api-version=[1.0|2.0|3.0]
[HttpOptions]
public IHttpActionResult Options()
{
var response = new HttpResponseMessage( HttpStatusCode.OK );
response.Content = new StringContent( string.Empty );
response.Content.Headers.Add( "Allow", new[] { "GET", "POST", "OPTIONS" } );
response.Content.Headers.ContentType = null;
return ResponseMessage( response );
}
HTTP/1.1 200 OK
allow: GET, POST, OPTIONS
api-supported-versions: 1.0, 2.0, 3.0
Version Policies
Version discovery supports advertising which API versions are supported and deprecated via the
api-supported-versions and api-deprecated-versions respectively. A key limitation of this support is that it does
not indicate when an API version will be deprecated, sunset, nor what the stated policy is.
Version policies introduce support for RFC 9745 (Deprecation) and RFC 8594 (Sunset). These will allow an API version
to indicate when it will be deprecated via the deprecation header as well as when it will disappear for good via the
sunset header. These headers do not necessarily apply to all API versions; they will only apply to the API version
that was requested. The deprecation and sunset policies can include additional information such as a web page or OpenAPI
document. These additional links will conform to RFC 8288 (Web Linking).
These capabilities are useful, not only for instrumented clients, but also for tooling. As an example, an API might
support an OPTIONS request to retrieve this information for tooling:
OPTIONS /weather?api-version=1.0 HTTP/2
host: localhost
HTTP/2 200
allow: GET, POST, OPTIONS
api-supported-versions: 1.0, 2.0, 3.0
api-deprecated-versions: 0.9
deprecation: @1640995200
sunset: Thu, 01 Apr 2022 00:00:00 GMT
link: <https://docs.api.com/policies.html?api-version=1.0>; rel="deprecation"; title="API Policy"; type="text/html"
link: <https://docs.api.com/policies.html?api-version=1.0>; rel="sunset"; title="API Policy"; type="text/html"
link: </openapi/v1.json>; rel="openapi"; title="OpenAPI"; type="application/json"
This indicates to a client that the requested API version 1.0 was deprecated on January 1, 2022 and will sunset on
April 1, 2022. It also provides links to public documentation that outlines the API versioning policies as well as where
to locate the OpenAPI document.
Policies do not have to have a date. The following scenarios are supported:
- Define a policy by API name and version
- Define a policy by API name for any version
- Define a policy by API version for any API
- A sunset policy may have a date
- A sunset policy can have zero or more links
Supporting a policy with links alone enables advertising a stated policy when you don’t know when an API version might
actually be deprecated or sunset, which will be common for the current version of an API. If a policy is defined, it
will be emitted through the existing IReportApiVersions service. This service is automatically utilized whenever
ApiVersioningOptions.ReportApiVersions is set to true, ReportApiVersionsAttribute is applied, or the
ReportApiVersions() convention is applied.
Configuration
The configuration is performed the same way across all platforms via:
AddApiVersioning( options =>
{
// version 1.0 deprecates 1/1/2022 with a public policy page
options.Policies.Deprecate( 1.0 )
.Effective( 2022, 1, 1 )
.Link( "https://docs.api.com/policies/deprecation.html" )
.Title( "Version Deprecation Policy" )
.Type( "text/html" );
// version 1.0 sunsets 4/1/2022 with a public policy page
options.Policies.Sunset( 1.0 )
.Effective( 2022, 4, 1 )
.Link( "https://docs.api.com/policies/sunset.html" )
.Title( "Version Sunset Policy" )
.Type( "text/html" );
// public policy page for version 2.0 without a sunset date
options.Policies.Sunset( 2.0 )
.Link( "https://docs.api.com/policies/sunset.html" )
.Title( "Version Sunset Policy" )
.Type( "text/html" )
})
Note
It should be noted that although links confirm to RFC 8288, all configurable links are meant to be specific to API versioning policies. The provided configuration APIs, therefore, only expose a subset of what is configurable and always use a relation type of
rel="deprecation"orrel="sunset". The default implementation can be replaced or extended or you can use theLinkHeaderValuedirectly in your own code, which exposes the complete feature set.
API Explorer Integration
The API Explorer extensions will attach the appropriate DeprecationPolicy or SunsetPolicy to a
ApiVersionDescription and ApiDescription. The policy for a ApiVersionDescription will be for an entire API version,
while the policy for an ApiDescription could be for a specific API, version, or combination of both.
The provided information can be used in any number of different ways, but would most likely be used in conjunction with OpenAPI. There is currently no direct support for a deprecation or sunset policy in OpenAPI, but it can be exposed via an OpenAPI extension or directly in the API documentation.
How to Version Your Service
REST services are implemented in ASP.NET as an endpoint. To version your service, you simply need to decorate your endpoints with the appropriate API version information. The method of decoration will vary depending on whether you are using controllers or Minimal APIs as well as whether you want to use attributes or conventions.
How It Works
The way that you create and define routes remains unchanged. The key difference is that routes may now overlap depending on whether you are using convention-based routing, attribute-based routing, or both. In the case of attribute routing, multiple controllers will define the same route. The default services in each flavor of ASP.NET assumes a one-to-one mapping between routes and endpoints and, therefore, considers duplicate routes to be ambiguous. The API versioning services replace the default implementations and allow endpoints to also be disambiguated by API version. Although multiple routes may match a request, they are expected to be distinguishable by API version. If the routes cannot be disambiguated, this is likely a developer mistake and the behavior is the same as the default implementation.
Naming and Collation
While it might seem more intuitive that similar route templates are collated together, that is simply not the case.
Consider that order/{id} and order/{id:int} are different, but semantically identical. API Versioning makes no
attempt understand this difference. Although it is possible to have an API with a single endpoint, most APIs consist of
a collection of endpoints; for example the Orders API. What if we saw the route template order/{id}/items? Is this
part of the Orders API or some other API? For this reason, API Versioning collates on the logical name of an API and
not individual route templates. For more information see: Controller Conventions.
Routing Methods
The following table outlines the various supported routing methods:
| Routing Method | Supported |
|---|---|
| Attribute-based routing | Yes |
| Convention-based routing | Yes |
| Attribute and convention-based routing (mixed) | Yes |
Important
Due to limitations in the routing infrastructure in ASP.NET Web API, API versioning is not guaranteed to work for controllers that define both attribute and convention-based routes for the same route.
Versioning Methods
Several API versioning methods are supported out-of-the-box:
- By Query String (default)
- By Media Type
- By Header
- By URL Segment
Multiple methods of API versioning can be supported simultaneously. Use the ApiVersionReader.Combine method to compose
two or more IApiVersionReader instances together. You can also implement your own method of extracting the requested
API version using a custom IApiVersionReader.
Defining a Service Version
There are four out-of-the-box supported approaches for versioning a service:
- By query string parameter
- By media type parameter
- By HTTP header
- By URL path segment
The default method is to use a query string parameter named api-version. You can also combine API versioning approaches together or define your own custom method of API versioning.
Query String Versioning
The initial version of a controller may not have any API version attribution and will implicitly become the configured
default API version. The default configuration uses the value 1.0.
Web API
[RoutePrefix( "api/helloworld" )]
public class HelloWorldController : ApiController
{
[Route]
public string Get() => "Hello world!";
}
OData
[ODataRoutePrefix( "People" )]
public class PeopleController : ODataController
{
[ODataRoute]
public IHttpActionResult Get( ODataQueryOptions<Person> options ) =>
Ok( new[]{ new Person() } );
}
Next Version
To create the next version of the controller, you can choose to create a new controller with the same route but
decorate it as API version 2.0. For example:
Web API
[ApiVersion( 2.0 )]
[RoutePrefix( "api/helloworld" )]
public class HelloWorldController : ApiController
{
[Route]
public string Get() => "Hello world!";
}
OData
[ApiVersion( 2.0 )]
[ControllerName( "People" )]
[ODataRoutePrefix( "People" )]
public class People2Controller : ODataController
{
[ODataRoute]
public IHttpActionResult Get( ODataQueryOptions<Person> options ) =>
Ok( new[]{ new Person() } );
}
The effect of this attribution is that the following requests match different controller implementations:
| Request URL | Matched Controller |
|---|---|
| /api/helloworld?api-version=1.0 | HelloWorldController |
| /api/helloworld?api-version=2.0 | HelloWorld2Controller |
| /api/People?api-version=1.0 | PeopleController |
| /api/People?api-version=2.0 | People2Controller |
It’s important to note that only an undecorated controller will be inferred as the configured, default API version. Once a controller has any API version attribution, it will never be considered as the default API version again unless the API version attribute includes the default API version. This allows you permanently remove API versions over time.
Media Type Versioning
Content negotiation is the defined method in REST for reasoning about the content expectations between a client and server. The parameters used in media types for content negotiation can contain custom input that can be used to drive API versioning.
Let’s assume the following controllers are defined:
Web API
namespace Services.V1
{
[ApiVersion( 1.0 )]
[RoutePrefix( "api/helloworld" )]
public class HelloWorldController : ApiController
{
[Route]
public string Get() => "Hello world!";
}
}
namespace Services.V2
{
[ApiVersion( 2.0 )]
[RoutePrefix( "api/helloworld" )]
public class HelloWorldController : ApiController
{
[Route]
public string Get() => "Hello world!";
[Route]
public string Post( string text ) => text;
}
}
Configuration
The configuration will then change the default API version reader as follows:
.AddApiVersioning( options => options.ApiVersionReader = new MediaTypeApiVersionReader() );
The parameterless constructor uses the media type parameter name v, but you can specify any name you like. The default
behavior will require that clients always specify an API version, so service authors will likely want their
configuration to be:
.AddApiVersioning(
options =>
{
options.ApiVersionReader = new MediaTypeApiVersionReader();
options.AssumeDefaultVersionWhenUnspecified = true;
options.ApiVersionSelector = new CurrentImplementationApiVersionSelector( options );
} );
This will allow clients to request a specific API version by media type, but if they don’t specify anything, they will receive the current implementation (e.g. API version). For example:
GET api/helloworld HTTP/2
host: localhost
Figure 1: returns the result from API version 2.0 because it’s the current version
GET api/helloworld HTTP/2
host: localhost
accept: text/plain;v=1.0
Figure 2: returns the result from API version 1.0
POST api/helloworld HTTP/2
host: localhost
content-type: text/plain;v=2.0
content-length: 12
Hello there!
Figure 3: explicitly posts the content to API version 2.0, even though it would be implicitly matched
Multiple Media Types
The MediaTypeApiVersionReader matches the configured media type parameter of any incoming request. This might be
undesirable if you support multiple media types or there is ambiguity in matching a media type.
Consider the following request:
GET api/helloworld HTTP/2
host: localhost
accept: application/json;v=1.0;q=0.8,application/signed-exchange;v=b3;q=0.9
In this scenario, a client has specified multiple media types and they both have the media type parameter v. The
MediaTypeApiVersionReader will honor quality (e.g. q) when specified. If multiple media types have the same quality,
the first one is selected. In this example application/signed-exchange is selected because it has the highest quality.
When the v parameter is parsed, the value is b3 is not a valid API version and will return HTTP status code 406
(Not Acceptable).
The MediaTypeApiVersionReaderBuilder provides a number of additional capabilities to build media type matching rules
that enable to you configure how you would like things to match. You can specify and combine any of the following
behaviors:
- Define multiple media type parameters
- Mutually include specific media types
- Mutually exclude specific media types
- Match media types by template
- Match media types by pattern
- Disambiguate between multiple API versions
To configure that only JSON be matched, you might use a configuration similar to the following:
.AddApiVersioning(
options =>
{
var builder = new MediaTypeApiVersionReaderBuilder();
options.ApiVersionReader = builder.Parameter( "v" )
.Include( "application/json" )
.Build();
options.AssumeDefaultVersionWhenUnspecified = true;
options.ApiVersionSelector = new CurrentImplementationApiVersionSelector( options );
} );
An important difference between MediaTypeApiVersionReaderBuilder and MediaTypeApiVersionReader is that
MediaTypeApiVersionReader expects there to be exactly one API version and selects the first one with the highest
quality. The MediaTypeApiVersionReaderBuilder, on the other hand, makes no such assumption and returns all matched
API versions in descending order of quality. You can use the SelectFirstOrDefault or SelectLastOrDefault extension
methods to have the MediaTypeApiVersionReaderBuilder choose the first or last API version respectively. If neither of
these approaches meet your requirements, you can provide you own callback to determine how to disambiguate multiple
choices via MediaTypeApiVersionReaderBuilder.Select.
Custom Media Types
Defining new, custom media types (ex: application/vnd.my.company.1+json) to drive API versioning is another variant of
this approach that is compliant with the constraints of REST. There is no specific IApiVersionReader meant to address
this scenario, however, the MediaTypeApiVersionReaderBuilder provides two approaches that can be used.
Templates
The most natural approach is to a use a template to match an API version in the media type. The specified template uses the same syntax and matching as a route template. For example,
.AddApiVersioning(
options =>
{
var builder = new MediaTypeApiVersionReaderBuilder();
options.ApiVersionReader = builder.Template( "application/vnd.my.company.{version}+json" )
.Build();
} );
This allows matching the API version the same way as if it were in a URL segment. All of the same format and parsing rules apply. In most cases, this is sufficient; however, the template expects exactly one parameter and that will be assumed to the API version parameter. If there are multiple route parameters, for whatever reason, the expected name must be provided as the second, optional parameter:
Template( "application/vnd.{tenant}.{version}+json", "version" );
Patterns
If a template will not suffice, then a regular expression pattern can be used.
.AddApiVersioning(
options =>
{
var builder = new MediaTypeApiVersionReaderBuilder();
options.ApiVersionReader = builder.Match( @"-v(\d+(\.\d+)?)\+" ).Build();
} );
MediaTypeApiVersionReaderBuilder.Match will only consider the first match. The match may optionally use grouping,
but only the first regular expression group will be considered. If a requested media type does not match the pattern,
then it is ignored.
It is assumed that your pattern matching requirements will fall under the date (e.g. group) or numeric version formats; however, if you have something more complex, the following pattern will match all forms of a valid API version:
^(\d{4}-\d{2}-\d{2})?\.?(\d{0,9})\.?(\d{0,9})\.?-?(.*)$
API Versioning no longer uses regular expressions to parse API versions; however, if you need to know how this can be used from previous implementations, you can review the old code.
Additional Considerations
While using a template or pattern can be used to match and extract an API version from an incoming request, it does not currently provide any additional support that may be need to implement a full solution. These should be known issues and exist even without API Versioning. You should simply beware that API Versioning isn’t providing any additional features beyond matching the API version from the media type in the incoming request.
The specific issues include:
- Mapping
MediaTypeFormatterto the custom media typeMediaTypeFormatterto the custom media type
- OpenAPI
- Listing all of the consumes media types
- Listing all of the produces media types
Header Versioning
While media type negotiation is the defined method in REST for reasoning about the content expectations between a client and server, any arbitrary HTTP header can also be used to drive API versioning.
Let’s assume the following controllers are defined:
Web API
namespace Services.V1
{
[ApiVersion( 1.0 )]
[RoutePrefix( "api/helloworld" )]
public class HelloWorldController : ApiController
{
[Route]
public string Get() => "Hello world!";
}
}
namespace Services.V2
{
[ApiVersion( 2.0 )]
[RoutePrefix( "api/helloworld" )]
public class HelloWorldController : ApiController
{
[Route]
public string Get() => "Hello world!";
[Route]
public string Post( string text ) => text;
}
}
Configuration
The configuration will then change the default API version reader as follows:
.AddApiVersioning( options => options.ApiVersionReader = new HeaderApiVersionReader( "x-ms-version" ) );
This will allow clients to request a specific API version by the custom HTTP header x-ms-version. For example:
GET api/helloworld HTTP/2
host: localhost
x-ms-version: 1.0
HTTP/2 200
host: localhost
content-type: text/plain
content-length: 12
Hello world!
URL Path Versioning
An alternate, but common, method of API versioning is to use a URL path segment. This approach does not allow implicitly
matching the initial, default API version of a service; therefore, all API versions must be explicitly declared. In
addition, the API version value specified for the URL segment must still conform to the version format. The v prefix
is not part of the API version, but may be included in route templates if you so desire.
Important
It is not possible to have a default API version for a URL path segment. This means that setting
ApiVersioningOptions.AssumedDefaultVersionWhenUnspecifiedis unlikely to have any affect when you use this method of versioning. For more information and possible solutions to address this scenario, refer to the known limitations.
Web API
public static class WebApiConfig
{
public static void Configuration( HttpConfiguration configuration )
{
var constraintResolver = new DefaultInlineConstraintResolver()
{
ConstraintMap =
{
["apiVersion"] = typeof( ApiVersionRouteConstraint )
}
};
configuration.MapHttpAttributeRoutes( constraintResolver );
configuration.AddApiVersioning();
}
}
[ApiVersion( 1.0 )]
[Route( "api/v{version:apiVersion}/helloworld" )]
public class HelloWorldController : ApiController
{
public string Get() => "Hello world!";
}
[ApiVersion( 2.0 )]
[ApiVersion( 3.0 )]
[Route( "api/v{version:apiVersion}/helloworld" )]
public class HelloWorld2Controller : ApiController
{
public string Get() => "Hello world v2!";
[MapToApiVersion( 3.0 )]
public string GetV3() => "Hello world v3!";
}
OData
Since the OData implementation uses convention-based routes under the hood, the ApiVersionRouteConstraint is
automatically added to all versioned OData routes when needed. The name of the constraint used in prefixes of OData
routes must be apiVersion and cannot be changed.
public static class WebApiConfig
{
public static void Configuration( HttpConfiguration configuration )
{
var modelBuilder = new VersionedODataModelBuilder( configuration )
{
ModelConfigurations =
{
new PersonModelConfiguration()
}
};
configuration.AddApiVersioning();
configuration.MapVersionedODataRoutes( "odata-bypath", "api/v{apiVersion}", modelBuilder );
}
}
[ApiVersion( 1.0 )]
[ODataRoutePrefix( "People" )]
public class PeopleController : ODataController
{
[EnableQuery]
[ODataRoute]
public IQueryable<Person> Get() => new[]{ new Person() }.AsQueryable();
}
[ApiVersion( 2.0 )]
[ApiVersion( 3.0 )]
[ControllerName( "People" )]
[ODataRoutePrefix( "People" )]
public class People2Controller : ODataController
{
[EnableQuery]
[ODataRoute]
public IQueryable<Person> Get() => new[]{ new Person() }.AsQueryable();
[EnableQuery]
[ODataRoute, MapToApiVersion( 3.0 )]
public IQueryable<Person> GetV3() => new[]{ new Person() }.AsQueryable();
}
The effect of the API version attribution is that the following requests match different controller implementations:
| Request URL | Matched Controller | Matched Action |
|---|---|---|
| /api/v1/helloworld | HelloWorldController | Get |
| /api/v2/helloworld | HelloWorld2Controller | Get |
| /api/v3/helloworld | HelloWorld2Controller | GetV3 |
| /api/v1/People | PeopleController | Get |
| /api/v2/People | People2Controller | Get |
| /api/v3/People | People2Controller | GetV3 |
Version Interleaving
API versions do not have to be split across different controller classes. A service author might choose to have a controller implement multiple API versions simultaneously. Controller actions can subsequently be mapped to specific API versions. This approach is useful for small version differences but should be used sparingly to prevent developer confusion and complicate code maintenance. For example:
Web API
[ApiVersion( 1.0 )]
[RoutePrefix( "api/helloworld" )]
public class HelloWorldController : ApiController
{
[Route]
public string Get() => "Hello world v1.0!";
}
[ApiVersion( 2.0 )]
[ApiVersion( 3.0 )]
[RoutePrefix( "api/helloworld" )]
public class HelloWorld2Controller : ApiController
{
[Route]
public string Get() => "Hello world v2.0!";
[Route, MapToApiVersion( 3.0 )]
public string GetV3() => "Hello world v3.0!";
}
OData
[ApiVersion( 1.0 )]
[ODataRoutePrefix( "People" )]
public class PeopleController : ODataController
{
[ODataRoute]
public IHttpActionResult Get( ODataQueryOptions<Person> options ) =>
Ok( new[]{ new Person() } );
}
[ApiVersion( 2.0 )]
[ApiVersion( 3.0 )]
[ControllerName( "People" )]
[ODataRoutePrefix( "People" )]
public class People2Controller : ODataController
{
[ODataRoute]
public IHttpActionResult Get( ODataQueryOptions<Person> options ) =>
Ok( new[]{ new Person() } );
[ODataRoute, MapToApiVersion( 3.0 )]
public IHttpActionResult GetV3( ODataQueryOptions<Person> options ) =>
Ok( new[]{ new Person() } );
}
Although not illustrated in these examples, it’s important to note that different versions of a service action might have different return values. The effect of the API versioning attribution is that the following requests match different controller and action implementations:
| Request URL | Matched Controller | Matched Action |
|---|---|---|
| /api/helloworld?api-version=1.0 | HelloWorldController | Get |
| /api/helloworld?api-version=2.0 | HelloWorld2Controller | Get |
| /api/helloworld?api-version=3.0 | HelloWorld2Controller | GetV3 |
| /api/People?api-version=1.0 | PeopleController | Get |
| /api/People?api-version=2.0 | People2Controller | Get |
| /api/People?api-version=3.0 | People2Controller | GetV3 |
It should be reiterated that the defined API version, even for an action, never directly influences routing. When the action matched for a route is ambiguous, the selection process will look for an explicit API version that matches the requested API version. If an explicit match is not found, then the action will be implicitly matched. If two actions are ambiguous by route and API version, then this is a developer mistake and the default behavior is unchanged.
Version-Neutral
All services should be explicitly versioned. In rare cases, however, you may have a service that is version-neutral. A common scenario is a health check service that behaves in the exact same way, regardless of API version. This might also apply to a legacy service that doesn’t support API versioning. To effectively opt out individual services from API versioning, a service must indicate that it is version-neutral.
Technically, it’s not a supported scenario to completely opt out of API versioning. A version-neutral service has the following characteristics:
- Accepts any valid API version
- Accepts no API version at all (e.g. unspecified)
This is an important distinction and why the term version-neutral is used. A version-neutral service accepts any and all versions, including none. This behavior can be used to define a service that accepts all API versions or service that simply does not care about specific API versions.
It is not possible to have some versions of a controller that are API version-neutral and other versions of the same controller require an explicit API version. If the route of an API version-neutral service matches any other service, it will result in an ambiguous match (e.g. server error).
Web API
[ApiVersionNeutral]
[RoutePrefix( "api/health" )]
public class HealthController : ApiController
{
[HttpGet]
[Route( "ping" )]
public IHttpActionResult Ping() => Ok();
}
A version-neutral controller using the query string method will not require that a client specify an API version. A version-neutral controller using the URL path method will match any well-formed API version in the URL path segment.
Web API
[ApiVersionNeutral]
[RoutePrefix( "api/v{version:apiVersion}/health" )]
public class HealthController : ApiController
{
[HttpGet]
[Route( "ping" )]
public IHttpActionResult Ping() => Ok();
}
Requested API Version
All of the service API version information is accessible via extension methods and properties. Beginning in version
3.0, Model Binding is also supported. These features allow you to determine which API version was requested by a
client as well as determine which versions are supported and deprecated. The API versions provided are automatically
aggregated across all service implementations.
The most common usage is the current, client requested API version:
Web API
[ApiVersion( 1.0 )]
[ApiVersion( 2.0 )]
public class MyController : ApiController
{
public IHttpActionResult Get()
{
var apiVersion = Request.RequestedApiVersion;
return Ok();
}
// supported in 3.0+
public IHttpActionResult Get( int id, ApiVersion apiVersion ) => Ok();
}
Existing Services
It’s a fairly common scenario that services are released to production and, at some point in the future, it becomes evident that service versioning is needed. The question now becomes, “How do I add API versioning without breaking existing clients?”
Before API versioning was applied to your service, clients were already bound to some version of the service; they just don’t know which version. A client in this situation doesn’t have any flexibility to go backward. If the service changes, hopefully that carries forward without breaking any clients. When you’re ready to introduce formal API versioning semantics into your service, then any previously unversioned services snap to a single, default API version.
Enable Backward Compatibility
The default API versioning semantics require that all clients explicitly request an API version for a service. This would break backward compatibility with existing clients, so we need a way to address this. The API versioning options provide a way to change the default behaviors that will enable supporting services that don’t explicitly declare API versions.
The bare minimum requirement to enable backward compatibility is to assume the default API version when a client does not explicitly request an API version. This will allow a client to continue making requests to existing services without providing API version information. Your existing controller implementations that back these services do not require any attribution or configuration to enable this behavior. Clients wishing to upgrade to new versions of a service must begin explicitly specifying an API version.
config.AddApiVersioning( options => options.AssumeDefaultVersionWhenUnspecified = true );
The assumed API version is 1.0 by default. From a client’s perspective, the default API version is inconsequential. As
a service author, however, you may want to choose a different default API version so that it aligns with your overall
API versioning scheme and instrumentation requirements.
configuration.AddApiVersioning(
options =>
{
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion( new DateTime( 2016, 7, 1 ) );
} );
If these basic configuration settings are still insufficient for your needs, then you will need to use or create an API version selector and register it in the API versioning options.
Deprecating Versions
When a service supports multiple API versions, some versions will eventually be deprecated over time. To advertise that one or more API versions have been deprecated, simply decorate your controller with the deprecated API versions. A deprecated API version does not mean the API version is not supported. A deprecated API version means that the version will become unsupported after six months or more.
The following examples illustrate how to specify deprecated API versions depending on which service API versioning approach you selected.
This example demonstrates API versioning using all non-URL segment methods.
[ApiVersion( 2.0 )]
[ApiVersion( 1.0, Deprecated = true )]
[RoutePrefix( "api/helloworld" )]
public class HelloWorldController : ApiController
{
[Route]
public string Get() => "Hello world!"
[Route, MapToApiVersion( 2.0 )]
public string GetV2() => "Hello world v2.0!";
}
This example demonstrates API versioning using the URL segment method.
[ApiVersion( 2.0 )]
[ApiVersion( 1.0, Deprecated = true )]
[RoutePrefix( "api/v{version:apiVersion}/helloworld" )]
public class HelloWorldController : ApiController
{
[Route]
public string Get() => "Hello world!"
[Route, MapToApiVersion( 2.0 )]
public string GetV2() => "Hello world v2.0!";
}
Removing a Service
To permanently sunset a service, simply remove that controller or API version from your implementation. The route will
no longer be matched. When one or more specific API versions cannot be matched, clients will receive HTTP status code
400 (Bad Request). If no candidate routes match at all, clients will receive HTTP status code 404 (Not Found).
Version Advertisement
Splitting implemented service API versions across hosted applications or endpoints is a fairly common scenario. There are several reasons why you might choose to split hosted endpoints, such as different run-time versions or traffic load balancing.
When service API versions are split across deployments, two issues arise:
- The correct service API version cannot be selected across deployments.
- The set of implemented service API versions cannot be aggregated across deployments.
Service Gateway
The first issue can be remedied by a using a service gateway. The gateway becomes responsible for obfuscating which endpoints host which API versions. The exact method in which gateways implement this functionality is at the discretion of service authors.
Future consideration is being investigated to support YARP.
Service API Version Advertisement
Since there is no direct way to know or interrogate the available API version information at runtime in a performant manner when services are deployed separately, an alternate approach is required. This concept is referred to as service API version advertisement. Each service will advertise the supported and deprecated API versions it knows about.
A service can advertise its supported and deprecated API versions using the AdvertiseApiVersionsAttribute. This
attribute functions almost identically to the ApiVersionAttribute, except that it is never considered for controller
resolution and cannot be applied to an action. The advertised and implemented API versions are always aggregated
together.
The following is an example of a service with API version 2.0 hosted at another endpoint that knows that API version
1.0 is a supported version somewhere else:
[ApiVersion( 2.0 )]
[AdvertiseApiVersions( 1.0 )]
[Route( "api/helloworld" )]
public class HelloWorld2Controller : ApiController
{
[HttpGet]
public string Get() => "Hello world v2.0!" );
}
[ApiVersion( 2.0 )]
[AdvertiseApiVersions( 1.0 )]
[Route( "api/v{version:apiVersion}/helloworld" )]
public class HelloWorld2Controller : ControllerBase
{
[HttpGet]
public string Get() => "Hello world v2.0!" );
}
This service implementation will now advertise that API version 1.0 and 2.0 are supported through the
api-supported-versions HTTP header even though it has no knowledge about where API version 1.0 is. In a similar
fashion, a service can also advertise deprecated API versions. Note that the ApiVersioningOptions.ReportApiVersions
must be enabled for the HTTP headers to be returned in responses.
The only drawback to this approach is that each implementation needs to be updated with the supported and deprecated API
versions when new API versions are released. One possible solution to this limitation is to create an
IApiVersionProvider attribute that reads the advertised API versions from a configuration source such as a file or
database. If this is still undesirable, then there is still the option of using HTTP header injection by the host server
or another mechanism to send the supported and deprecated API version information.
Controller Naming Conventions
There are a few implicit conventions to be aware of.
Always Versioned
Once you opt into API versioning, every API controller has an API version. This is true even if the controller does not have an explicit attribute or configured convention. When otherwise unspecified, the version applied to a controller derives from ApiVersioningOptions.DefaultApiVersion.
Naming
ASP.NET provides a built-in convention for controller names that use the form <Name>Controller where Controller will
be trimmed off when exactly that text. API Versioning slightly expands this convention. It will honor the convention of
<Name>[#]Controller. This allows you to have two controller types in the same namespace for different API versions,
but for the same resource; for example, ValuesController and Values2Controller will both have the name Values.
Naming is important for grouping controllers together.
Unfortunately, this can cause an issue for service API versioning if you want to split the implementation across different types. If the defining type is in a different .NET namespace, then there is no issue; however, if they are in the same namespace there would be a name collision. For example:
namespace My.Services.V1
{
[ApiVersion( 1.0 )]
[RoutePrefix( "helloworld" )]
public class HelloWorldController : ApiController
{
[Route]
public string Get() => "Hello world v1.0!";
}
}
namespace My.Services.V2
{
[ApiVersion( 2.0 )]
[RoutePrefix( "helloworld" )]
public class HelloWorldController : ApiController
{
[Route]
public string Get() => "Hello world v2.0!";
}
}
Controllers separated by .NET namespace
namespace My.Services.Controllers
{
[ApiVersion( 1.0 )]
[RoutePrefix( "helloworld" )]
public class HelloWorldController : ApiController
{
[Route]
public string Get() => "Hello world v1.0!";
}
[ApiVersion( 2.0 )]
[RoutePrefix( "helloworld" )]
public class HelloWorld2Controller : ApiController
{
[Route]
public string Get() => "Hello world v2.0!";
}
}
Controllers with different names in the same .NET namespace
To address name collisions and provide control over how collation happens, API Versioning provides the following service:
public interface IControllerNameConvention
{
string NormalizeName( string controllerName );
string GroupName( string controllerName );
}
NormalizeName controls how or whether a controller name is normalized. GroupName provides the name used to group
and collate on, which may not necessarily be the same as the normalized name. ControllerNameConvention provides
three implementations out-of-the-box.
Default
ControllerNameConvention.Default provides the default configuration which extends the original convention to have the
form: <Name>[#]Controller. This means that if you already have a HelloWorldController, you can now have a
HelloWorld2Controller and HelloWorld3Controller. Each type name removes the Controller suffix as well as any
trailing numbers. All of these controllers would end up named and grouped HelloWorld.
Original
ControllerNameConvention.Original provides an alternate configuration that retains the original naming convention.
Consider that you have a type named S3Controller. In this scenario, you do not want the 3 to be stripped away.
If you have multiple versions of a such a controller, you would need your own implementation that understands this
behavior or separate the types into different .NET namespaces.
Grouped
ControllerNameConvention.Grouped is a hybrid configuration the combines the Default and Original conventions.
For the purposes of the name, the original convention is used. For the purposes of grouping, the default convention is
used. A controller type of S3Controller would have the name S3, but the group name S. The group name is only used
for collation and is never displayed anywhere, so this behavior is acceptable.
Attribute
If you do not want to rely on a convention, you can explicitly provide a name using the ControllerNameAttribute. This
attribute is particularly useful with OData because the name of the controller must also exactly match the name of the
associated entity set.
[ApiVersion( 2.0 )]
[RoutePrefix( "helloworld" )]
[ControllerName( "HelloWorld" )]
public class HelloWorld2Controller : ControllerBase
{
[Route]
public string Get() => "Hello world v2.0!";
}
Configuring Your Application
Although different variations of ASP.NET have distinct application initialization methods, careful consideration was taken to make the API versioning configuration as similar as possible across all applications models.
The configuration for ASP.NET Web API applications typically occurs in the Register method of the WebApiConfig.cs
file. To enable API versioning support with the default options, use the following configuration:
public static void Register( HttpConfiguration configuration )
{
configuration.AddApiVersioning();
// remaining web api setup omitted for brevity
}
If you intend to use the URL segment versioning method, then you also need to register the appropriate route constraint:
public static void Register( HttpConfiguration configuration )
{
var constraintResolver = new DefaultInlineConstraintResolver()
{
ConstraintMap =
{
["apiVersion"] = typeof( ApiVersionRouteConstraint ),
},
};
configuration.MapHttpAttributeRoutes( constraintResolver );
configuration.AddApiVersioning();
// remaining setup omitted for brevity
}
Custom route constraints can only be configured through the MapHttpAttributeRoutes method. This method is only
expected to be called once in an application. Since API versioning may be added to an existing application, you must
explicitly add the route constraint to ensure the current configuration does not break.
This is also the same basic setup for OData applications, except that you do not need to add any route constraints or map attribute routes. OData uses its own route constraints and convention-based routing. For more information, see the topic on API versioning with OData.
API Versioning Options
The API Versioning options allows you to configure, customize, and extend the default behaviors when you add API versioning to your application.
ApiVersioningOptions has the following configuration settings:
- ApiVersionReader
- ApiVersionSelector
- DefaultApiVersion
- AssumeDefaultVersionWhenUnspecified
- ReportApiVersions
- Policies
- Conventions
- RouteConstraintName
- UnsupportedApiVersionStatusCode
Assume Default Version When Unspecified
This option enables support for clients to make requests with implicit API versioning. This option is disabled by
default, which means that all clients must send requests with an explicit API version. Services will respond to client
requests that do not specify an API version with either HTTP status code 400 (Bad Request) or HTTP status code 404
(Not Found), depending whether the requested route exists.
This option 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.
When this option is enabled, clients will be able to make a request without specifying a specific API version. The API version of the service that is selected will be based on the configured IApiVersionSelector.
Default API Version
This option defines what the default ApiVersion will be for a service without explicit API version information. This
is useful for services that use implicit API versioning in their initial release. This value can also be used for
services that may be defined in external assemblies that are not decorated with any API version information. The
configured, default value is 1.0.
AddApiVersioning( options => options.DefaultApiVersion = new ApiVersion( 2.0 ) );
Report API Versions
This option enables sending the api-supported-versions and api-deprecated-versions HTTP header in responses. When
this option is enabled, it will add the ReportApiVersionsAttribute as a global action filter to the application
configuration. If there are any deprecation or sunset policies defined, they will also be included in the
response headers. This option is disabled by default.
AddApiVersioning( options => options.ReportApiVersions = true );
Conventions
This option allows you to construct API version conventions for each of your services as opposed to using .NET attributes. You can also choose to additionally use .NET attributes and the union of both sets of defined API version information will be applied. The default convention builders can be extended and/or replaced in this option. For more information on using conventions see the API version conventions topic.
Route Constraint Name
This option allows you to change the name of the API version route constraint. The default name is "apiVersion".
Policies
This option allows you to define API versioning policies. This is primarily used to define deprecation and sunset policies about when an API. Related links, such as to a public policy web page, can also be reported that may be useful to clients for more information about your API policies.
Unsupported API Version Status Code
This option allows you to configure the HTTP status code used when an unsupported API version is requested. The default
value is 400 (Bad Request).
While any HTTP status code can be used, the following are the most sensible:
| Status Code | Meaning | Description |
|---|---|---|
| 400 | Bad Request | The API doesn’t support this version |
| 404 | Not Found | The API doesn’t exist |
| 501 | Not Implemented | The API isn’t implemented |
Remarks
Regardless of the configured option, when versioning by:
- URL segment,
404is always returned - media type,
406or415is always returned
API Version Reader
The IApiVersionReader interface defines the behavior of how an API version is read in its raw, unparsed form from the
current HTTP request. There are multiple methods for reading an API version provided out-of-the-box or you can implement
your own. The default, configured API version reader is a composed instance QueryStringApiVersionReader and
UrlSegmentApiVersionReader.
Query String
The QueryStringApiVersionReader reads the requested API version from the requested query string. The default query
string parameter name is api-version. The constructor for this class accepts the name of a query string parameter
so that an alternate query string parameter can be used.
// svc?api-version=2.0
AddApiVersioning( options => options.ApiVersionReader = new QueryStringApiVersionReader() );
// svc?v=2.0
AddApiVersioning( options => options.ApiVersionReader = new QueryStringApiVersionReader( "v" ) );
Media Type
The MediaTypeApiVersionReader reads the requested API version from a HTTP media type request header. The supported
headers are Content-Type and Accept. If both headers are present, then Content-Type is preferred. If the
Accept header specifies qualities, then the API version associated with the highest quality is selected. This
behavior is independent of media type negotiation. The default media type parameter is "v", but you may specify an
alternate name. This method of API versioning does not conform to the Microsoft REST Guidelines; however, it is
generally accepted as a fully REST-compliant method of versioning.
// Content-Type: application/json;v=2.0
AddApiVersioning( options => options.ApiVersionReader = new MediaTypeApiVersionReader() );
// Content-Type: application/json;version=2.0
AddApiVersioning( options => options.ApiVersionReader = new MediaTypeApiVersionReader( "version" ) );
The MediaTypeApiVersionReaderBuilder is also available with additional features that allow:
- Define multiple media type parameters
- Mutually include specific media types
- Mutually exclude specific media types
- Match media types by template
- Match media types by pattern
- Disambiguate between multiple API versions
// Accept: application/json;v=2.0
AddApiVersioning(
options =>
{
var builder = new MediaTypeApiVersionReaderBuilder();
options.ApiVersionReader = builder.Parameter( "v" )
.Include( "application/json" )
.Build();
} );
// Accept: application/vnd.my.company.v1+json
AddApiVersioning(
options =>
{
var builder = new MediaTypeApiVersionReaderBuilder();
options.ApiVersionReader = builder.Template( "application/vnd.my.company.v{version}+json" )
.Build();
} );
Header
The HeaderApiVersionReader reads the requested API version from a HTTP request header. There is no default or standard
HTTP header. You must define which HTTP header name or names contain the API version information. This method of API
versioning does not conform to the Microsoft REST Guidelines.
AddApiVersioning( options => options.ApiVersionReader = new HeaderApiVersionReader( "api-version" ) );
URL Path Segment
The UrlSegmentApiVersionReader reads the requested API version from a URL path segment. Extraction of the value is
dependent upon the ApiVersionRouteConstraint which is matched by the ApiVersioningOptions.RouteConstraintName
property.
AddApiVersioning( options => options.ApiVersionReader = new UrlSegmentApiVersionReader() );
Warning
This method of API versioning violates the REST Uniform Interface constraint and is the slowest of all versioning methods because the requested value cannot always easily be extracted from the URL path segment. If you’re creating a new API, consider using query string or media type versioning instead.
Composition
Multiple IApiVersionReader implementations can be combined using composition instead of inheritance. For convenience,
you can use ApiVersionReader.Combine to compose multiple API version reading styles.
AddApiVersioning(
options => options.ApiVersionReader = ApiVersionReader.Combine(
new QueryStringApiVersionReader(),
new HeaderApiVersionReader() { HeaderNames = { "x-ms-api-version" } } ) );
API Version Conventions
API version conventions allow you to specify API version information for your services without having to use .NET attributes. There are a number of reasons why you might choose this option. The most common reasons are:
- 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 configuration
Instead of applying [ApiVersion] to the controller, we can instead choose to define a convention in the
API versioning options.
configuration.AddApiVersioning( options =>
{
options.Conventions.Controller<MyController>().HasApiVersion( 1.0 );
} );
All of the semantics that can be expressed with .NET attributes can be defined using conventions. Consider what version
2.0 of the previous controller with interleaved API versions might look like:
[RoutePrefix( "my" )]
public class MyController : ApiController
{
[Route]
public IHttpActionResult Get() => Ok();
[Route]
public IHttpActionResult GetV2() => Ok();
[Route( "{id:int}" )]
public IHttpActionResult GetV2( int id ) => Ok();
}
The API version conventions might then be defined as:
options.Conventions.Controller<MyController>()
.HasDeprecatedApiVersion( 1.0 )
.HasApiVersion( 2.0 )
.Action( c => c.GetV2() ).MapToApiVersion( 2.0 )
.Action( c => c.GetV2( default ) ).MapToApiVersion( 2.0 );
If you use API version conventions and .NET attributes, then the constructed ApiVersionModel for the corresponding
controller will be an aggregated union of the two sets of information.
Custom
You can also define custom conventions via the IControllerConvention interface and add them to the builder:
public interface IControllerConvention
{
bool Apply( IControllerConventionBuilder controller,
HttpControllerDescriptor controllerDescriptor );
}
Custom conventions are added to the convention builder through the API versioning options:
options.Conventions.Add( new MyCustomConvention() );
Namespace
This built-in convention allows you to version your controllers by the .NET namespace they reside in when applied.
options.Conventions.Add( new VersionByNamespaceConvention() );
The defined namespace name must conform to the API version format so that it can be parsed. The language-neutral syntax is:
letter = "A" | "B" | "C" | "D" | "E" | "F" | "G"
| "H" | "I" | "J" | "K" | "L" | "M" | "N"
| "O" | "P" | "Q" | "R" | "S" | "T" | "U"
| "V" | "W" | "X" | "Y" | "Z" | "a" | "b"
| "c" | "d" | "e" | "f" | "g" | "h" | "i"
| "j" | "k" | "l" | "m" | "n" | "o" | "p"
| "q" | "r" | "s" | "t" | "u" | "v" | "w"
| "x" | "y" | "z" ;
prefix = "v" | "V" ;
positive = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
digit = "0" | positive ;
day = ( [ "0" ] positive ) | ( "1" | "2" ) digit | ( "3" ( "0" | "1" ) ) ;
month = ( [ "0" ] positive ) | ( "1" ( "0" | "1" | "2" ) ) ;
year = 4 * digit ;
api-version = prefix ( ( year "_" month "_" day ) | ( digit [ "_" digit ] ) ) [ "_" { letter } ] ;
The . character is considered a namespace delimiter in many programming languages. This character must be changed to
_ so that newly added files have the correct format. In addition, most languages do not allow the name of a namespace
to start with a number. Since a leading character is required, the first character must be v or V. There is no
requirement as to where the API version must appear in the namespace.
By default, API versions derived from a namespace will be considered supported. If the controller is decorated with the
ObsoleteAttribute, then the API version inferred from the containing namespace will be considered deprecated.
Examples
Contoso.Api.v1.Controllers→ 1.0Contoso.Api.v1_1.Controllers→ 1.1Contoso.Api.v0_9_Beta.Controllers→ 0.9-BetaContoso.Api.v20180401.Controllers→ 2018-04-01Contoso.Api.v2018_04_01.Controllers→ 2018-04-01Contoso.Api.v2018_04_01_Beta.Controllers→ 2018-04-01-BetaContoso.Api.v2018_04_01_1_0_Beta.Controllers→ 2018-04-01.1.0-Beta
Contoso
└ Api
├─ v1
│ └ Controllers
├─ v2
│ └ Controllers
└─ v2_5
└ Controllers
Figure 1: Sample folder layout with numeric API versions
Contoso
└ Api
├─ v2018_07_01
│ └ Controllers
├─ v2018_08_01
│ └ Controllers
└─ v2018_09_01
└ Controllers
Figure 2: Sample folder layout with date API versions
API Version Selector
The IApiVersionSelector interface defines the behavior of how an API version is selected for a given request context.
This service is typically only used when a client has not requested an explicit API version and the
AssumeDefaultVersionWhenUnspecified option is enabled. The role of the API version selector is to select the
appropriate API version given the current request and a model of available API versions.
Note
Although the
IApiVersionSelectorcan be used for other scenarios, it is currently only utilized when no API version is requested by a client and the server allows this behavior. The selector provides the rules that selects the most appropriate API version according to the server. There is no built-in capability to ignore an API version explicitly requested by a client.
There are four API version selectors provided out-of-the-box or you can implement your own. The default, configured API
version selector is DefaultApiVersionSelector.
Default
The DefaultApiVersionSelector always selects the configured DefaultApiVersion, regardless of the request or
available API version information.
Constant
The ConstantApiVersionSelector always selects a user-defined API version, regardless of the request or available API
version information.
AddApiVersioning(
options => options.ApiVersionSelector =
new ConstantApiVersionSelector(
new ApiVersion( new( 2016, 7, 1 ) ) );
Current
The CurrentImplementationApiVersionSelector selects the maximum API version available which does not have a version
status. If no match is found, it falls back to the configured DefaultApiVersion. An an example, if the versions 1.0,
2.0, and 3.0-alpha are available, then 2.0 will be selected because it’s the highest, implemented or released API
version.
AddApiVersioning(
options => options.ApiVersionSelector =
new CurrentImplementationApiVersionSelector( options ) );
Lowest
The LowestImplementedApiVersionSelector selects the minimum API version available which does not have a version
status. If no match is found, it falls back to the configured DefaultApiVersion. As an example, if the versions
0.9-beta, 1.0, 2.0, and 3.0-alpha are available, then 1.0 will be selected because it’s the lowest,
implemented or released API version. Your services must be decorated with one or more API versions for the selector to
work effectively or it will always select the configured DefaultApiVersion.
AddApiVersioning(
options => options.ApiVersionSelector =
new LowestImplementedApiVersionSelector( options ) );
API Versioning with OData
Service API versioning using OData is similar to the normal configuration with a few slight variations. Each implemented OData controller has an associated entity set and each entity set is defined in an Entity Data Model (EDM). Once we introduce API versioning, each versioned OData controller now needs an EDM per API version. To satisfy this requirement, we’ll use the new VersionedODataModelBuilder, build a collection of EDMs for each API version, and then map a set of routes for them.
public class Startup
{
public void Configuration( IAppBuilder appBuilder )
{
var configuration = new HttpConfiguration();
var httpServer = new HttpServer( configuration );
configuration.AddApiVersioning();
var modelBuilder = new VersionedODataModelBuilder( configuration )
{
ModelConfigurations =
{
new PersonModelConfiguration()
}
};
configuration.MapVersionedODataRoute( "odata", "api", modelBuilder );
appBuilder.UseWebApi( httpServer );
}
}
Model Configurations
A model configuration enables OData service authors to apply model setups that are specific to a service API version.
The VersionedODataModelBuilder will call Apply for each discovered API version with the current ODataModelBuilder.
public interface IModelConfiguration
{
void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix );
}
The implementation of a model configuration can provide all variations of a model or they can be spit across multiple implementations. The applied model does not have to be same across API versions.
Consider the following model:
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
}
Let us assume that the OData service for this model has three versions: 1.0, 2.0, and 3.0. In API version 1.0,
a person had the properties Id, FirstName, and LastName. In API version 2.0 we introduced the Email property.
In API version 3.0 we introduced the Phone property. If we implement the entire model configuration in a single
class, it might look like:
public class PersonModelConfiguration : IModelConfiguration
{
private void ConfigureV1( ODataModelBuilder builder ) =>
ConfigureCurrent( builder ).Ignore( p => p.Email ).Ignore( p => p.Phone );
private void ConfigureV2( ODataModelBuilder builder ) =>
ConfigureCurrent( builder ).Ignore( p => p.Phone );
private EntityTypeConfiguration<Person> ConfigureCurrent( ODataModelBuilder builder )
{
var person = builder.EntitySet<Person>( "People" ).EntityType;
person.HasKey( p => p.Id );
return person;
}
public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix )
{
switch ( apiVersion.MajorVersion )
{
case 1:
ConfigureV1( builder );
break;
case 2:
ConfigureV2( builder );
break;
default:
ConfigureCurrent( builder );
break;
}
}
}
Even through we have a single Person class, the EDM associated with the service API version will render the model
according the requested API version.
~/people(1)?api-version=1.0
{
"id": 1,
"firstName": "John",
"lastName": "Doe"
}
~/people(1)?api-version=2.0
{
"id": 1,
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@somewhere.com"
}
~/people(1)?api-version=3.0
{
"id": 1,
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@somewhere.com",
"phone": "555-555-5555"
}
Model Substitution
The Entity Data Model (EDM) does not have a one-to-one correlation with the corresponding .NET type. As a result, it’s common and quite plausible that a single .NET type for a model will be used in different EDMs. This is already supported by defining model configurations.
The challenge is representing this same model in the OData API Explorer. API Explorer consumers, such as OpenAPI/Swagger document generators, rely on using Reflection to enumerate the members of a model. These consumers have no intrinsic understanding of an EDM and do not know that the response type may be a subset of the discovered .NET type. To address this, the OData API Explorer supports Model Substitution.
Model substitution takes effect whenever a .NET type does not exactly match the definition of the corresponding EDM type. When this occurs, the API Explorer will generate a new .NET type that is a subset of the original type, but exactly matches the definition of the EDM type. When consumers use Reflection on the substituted type, it will only be a subset of the original .NET type. A similar scenario occurs for OData actions because the action parameters are modeled as a dictionary of key/value pairs. A substitution type will be generated which matches the definition of the OData action parameters.
There is no configuration or additional setup required to enable Model Substitution. As the OData API Explorer would otherwise report incorrect response types, this feature is automatically enabled and cannot be disabled out-of-the-box.
Model substitution supports the following features:
- Entity Types
- Complex Types
- Structured Type Properties
- Self-Referencing
- Parent-Child collections
- Attributes (ex: Model Bound Attributes, Data Annotations, etc)
- Defined on the original .NET type (ex: class or structure)
- Defined on the original .NET type property
- Action parameters
IEnumerable<T>response typesSingleResult<T>response typesODataValue<T>response typesDelta<T>parameters
The OData API Explorer generates substitution types using the IModelTypeBuilder.
public interface IModelTypeBuilder
{
Type NewStructuredType(
IEdmStructuredType structuredType,
Type clrType,
ApiVersion apiVersion,
IEdmModel edmModel );
Type NewActionParameters(
IServiceProvider services,
IEdmAction action,
ApiVersion apiVersion,
string controllerName );
}
Partial OData
The DefaultModelTypeBuilder does not enable support for ad hoc models using only part of the OData stack. This is
the default behavior because without an EDM and the OData response writers, no filtering of model members is performed.
This mostly likely means that you have a different model per API version, which would negate the usefulness of model
substitution.
If you have a way to filter you models to match what you have configured in an ad hoc EDM, you can re-enable model
substitution by re-registering IModelTypeBuilder with new DefaultModelTypeBuilder(includeAdHocModels: true).
Versioned Model Builder
The VersionedODataModelBuilder is a builder of builders, which enables creating an Entity Data Model (EDM) for each
service API version.
public class VersionedODataModelBuilder
{
public Func<ODataModelBuilder> ModelBuilderFactory { get; set; }
public Action<ODataModelBuilder, ApiVersion, string> DefaultModelConfiguration { get; set; }
public IList<IModelConfiguration> ModelConfigurations { get; }
public Action<ODataModelBuilder, IEdmModel> OnModelCreated { get; set; }
public IEnumerable<IEdmModel> GetEdmModels();
public virtual IEnumerable<IEdmModel> GetEdmModels(string routePrefix);
}
Model Builder Factory
The ModelBuilderFactory property defines a factory function used to initialize a new ODataModelBuilder for each
service API version. The default value creates a new instance of the ODataConventionModelBuilder. You can update
this property to substitute your own ODataModelBuilder or provide a custom initialization setup.
var modelBuilder = new VersionedODataModelBuilder( configuration )
{
ModelBuilderFactory = () => new ODataConventionModelBuilder().EnableLowerCamelCase()
};
Note
Using camel-casing for JSON documents is very common. Beginning 3.0,
EnableLowerCamelCase()is automatically called.
Model Configurations
The ModelConfigurations property is a collection of IModelConfiguration objects which define the
configuration of one or more models to be applied for each API version. Although it’s not required, it’s recommended
that you create one IModelConfiguration per entity model.
var modelBuilder = new VersionedODataModelBuilder( configuration )
{
ModelConfigurations =
{
new PersonModelConfiguration()
}
};
Default Model Configuration
The DefaultModelConfiguration property defines a callback that can be used to apply a default model configuration.
Specifying a callback is useful if you have a configuration that applies to all models or if you want to have a single,
inline model configuration.
var modelBuilder = new VersionedODataModelBuilder( configuration )
{
DefaultModelConfiguration = ( builder, apiVersion, routePrefix )
{
// TODO: default configuration for all models
}
};
On Model Created
The OnModelCreated property is a callback that serves the same purpose as
ODataConventionModelBuilder.OnModelCreated. This callback can be used to perform any additional setup or configuration
required after each EDM model is created.
Get EDM Models
The GetEdmModels method behavior is similar to the ODataModelBuilder.GetEdmModel method. This method performs the
following actions:
- Discover and enumerate each service API version
- For each service API version:
- Create an
ODataModelBuildervia the ModelBuilderFactory - Invoke IModelConfiguration.Apply for each item defined in
ModelConfigurations, including theDefaultModelConfiguration, with the current model builder and API version - Invoke
ODataModelBuilder.GetEdmModelto generate the current EDM model - Apply the
ApiVersionAnnotationwith the current API version to the generated EDM model - Invoke
OnModelCreatedwith the current model builder and generated EDM model, if defined
- Create an
Versioned Controllers
Creating an OData controller that supports API versioning isn’t much different from creating a regular OData controller.
The following controller depicts a service that support API version 1.0 and 2.0.
[ApiVersion( 1.0 )]
[ApiVersion( 2.0 )]
public class PeopleController : ODataController
{
// GET ~/people?api-version=[1.0|2.0]
public IQueryable<Person> Get() => new[] { new Person() }.AsQueryable();
// GET ~/people/1?api-version=[1.0|2.0]
public SingleResult<Person> Get( int key ) => SingleResult.Create( new Person() );
// PATCH ~/people/1?api-version=2.0
[MapToApiVersion( 2.0 )]
public UpdatedODataResult<Person> Patch( int key, Delta<Person> delta )
{
if ( !ModelState.IsValid )
{
return BadRequest( ModelState );
}
var person = new Person();
delta.Patch( person );
return Updated( person );
}
}
The PATCH method is only supported in API version 2.0 of the service. To be truly OData compliant, this service
should define an action mapped to API version 1.0 that always returns HTTP status code 501 (Not Implemented) instead
of falling back to HTTP status code 400 (Bad Request) or 404 (Not Found).
If you reviewed the Person model and configuration example for the IModelConfiguration, you’ll know what we
configured a single Person model with different properties available in different API versions. The default OData
model validation does some automatic heavy lifting for us using the defined EDM model. In addition to the other normal
validation you might have from Data Annotations, the current EDM model will provide further validation. For example,
even though the Person class has a Phone property, it was not defined until API version 3.0. If you try to send
a PATCH request like this:
PATCH /people/1?api-version=2.0 HTTP/2
content-type: application/json
content-length: 27
{ "phone": "555-555-5555" }
the built-in OData model validation will fail. The response will end up being HTTP status code 400 (Bad Request) with
an error message that indicates the phone property does not exist. In version 2.0 of the service, that is true and
the correct behavior.
Split Implementation
Service authors can choose to split service API versions across multiple controller types. In fact, for all but the simplest of version variations, this is the recommended approach. You may, however, notice something extra and a little unusual about the attribution for this controller.
Under the hood, the OData implementation still uses convention-based routing. When we split services across multiple
controller types, the new service implementation cannot have the same name. The only exception to this rule is if you
create version-specific namespaces for each version of the service. If the name of the controller cannot be the same as
the original controller type and we’re stuck with convention-based routing, how to do indicate what the name of the
controller should be? Enter the ControllerNameAttribute.
The ControllerNameAttribute allows you to specify an arbitrary name for a controller. In the strictest sense, this is
not convention-based; however, short of using different namespaces, there isn’t a way to define the correct name of the
controller. Without the ControllerNameAttribute, this controller would be named People2, which won’t match any
routes or, more specifically, any defined entity set. In OData, the controller route is paired with the corresponding
entity set name. The API version services honor this attribute and will use the controller name defined by the attribute
over the default convention name when present.
[ApiVersion( 3.0 )]
[ControllerName( "People" )]
public class People2Controller : ODataController
{
// GET ~/people?api-version=3.0
public IQueryable<Person> Get() => new[] { new Person() }.AsQueryable();
// GET ~/people/1?api-version=3.0
public SingleResult<Person> Get( int key ) => SingleResult.Create( new Person() );
// PATCH ~/people/1?api-version=3.0
public UpdatedODataResult<Person> Patch( int key, Delta<Person> delta )
{
if ( !ModelState.IsValid )
{
return BadRequest( ModelState );
}
var person = new Person();
delta.Patch( person );
return Updated( person );
}
}
Versioned Metadata
In order to support API versioning, the default MetadataController is replaced with a VersionedMetadataController
implementation. The main difference between the two is that the VersionedMetadataController will return service
document and entity data model (EDM) information for each defined API version.
[ReportApiVersions]
public class VersionedMetadataController : MetadataController
{
// omitted for brevity
}
Clients can now build proxies that have an affinity to a specific API version.
~/$metadata~/$metadata?api-version=1.0~/$metadata?api-version=2.0~/$metadata?api-version=3.0
If a client does not specify an API version, the assumed value will be the configured default API version. When a client is ready to adopt a new version of the service, they can update their tooling to point to the appropriate API version of the metadata endpoint and generate a new proxy based on the version-specific EDMX.
Tooling Support
The VersionedMetadataController also supports the HTTP OPTIONS method. This allows tools to query the service
document (~/) or $metadata endpoints and provide a client with choices as to which API version they would like to
create an OData client for.
For example, a tool can query the metadata endpoint:
OPTIONS /$metadata HTTP/2
host: my.api.com
which will produce a response that looks like:
HTTP/2 200
allow: GET, OPTIONS
odata-version: 4.0
api-supported-versions: 1.0, 2.0, 3.0
api-deprecated-versions: 0.9
deprecation: @1640995200
sunset: Thu, 01 Apr 2022 00:00:00 GMT
link: <https://docs.api.com/policies.html>; rel="deprecation"; title="API Policy"; type="text/html"
link: <https://docs.api.com/policies.html>; rel="sunset"; title="API Policy"; type="text/html"
link: </openapi/v1.json>; rel="openapi"; title="OpenAPI"; type="application/json"
A tool can choose to use this information is several ways. Any supported or deprecated API version is allowable. User
interface tools should filter out deprecated API versions by default, but it could alternatively provide warning if a
deprecated version is selected or will sunset in the near future. Tools that do not afford user interaction will
likely select the highest supported API version. Tools should also consider that the api-supported-versions and
api-deprecated-versions HTTP headers can be reported multiple times as defined in RFC 2616 §4.2.
An OData service which does not support API versioning should return with HTTP 501 (Not Implemented) as defined in
OData: Protocol §9.3.1 of the OData v4.0 specification. However, given that API versioning behaviors of an OData
service are not explicitly defined in the OData protocol, a client may also respond with HTTP 405 (Method Not
Allowed). Tools should graceful fallback to the standard metadata query operations when API versioning information is
unavailable.
Protocol Transitions
One of the primary reasons to version a service is to facilitate changes in behavior and/or data exchange with the service. In the scope of OData, this can mean transitioning new versions of a service to use the OData protocol or it can mean existing OData services that are transitioning away from the OData protocol. The API versioning support for OData enables both of these scenarios.
Consider the following partial controller implementations:
[ApiVersion( 1.0 )]
public class OrdersController : ApiController
{
public IHttpActionResult Get() => Ok();
}
[ApiVersion( 2.0 )]
[ControllerName( "Orders" )]
[ODataRoutePrefix( "Orders" )]
public class Orders2Controller : ODataController
{
[ODataRoute]
public IHttpActionResult Get() => Ok();
}
[ApiVersion( 3.0 )]
[ControllerName( "Orders" )]
public class Orders3Controller : ApiController
{
public IHttpActionResult Get() => Ok();
}
This set of controllers produce the following semantics for the Orders service:
- Version 1.0 of the service uses basic REST semantics and convention-based routing
- Version 2.0 of the service switches to the OData protocol and convention-based routing
- Version 3.0 of the service switches back to basic REST semantics and convention-based routing
Important
Due to routing limitations in ASP.NET Web API, all versioned routes for a service must be either convention-based or attribute-based. Since OData relies on convention-based routing, all routes for a controller with the same name must also be convention-based in order for API versioning to function properly.
The configuration required to support this type of scenario will be:
config.AddApiVersioning();
var modelBuilder = new VersionedODataModelBuilder( config )
{
ModelConfigurations = { new OrderModelConfiguration() }
};
config.MapVersionedODataRoutes( "odata", "api", modelBuilder );
config.Routes.MapHttpRoute( "orders", "api/{controller}/{id}", new { id = Optional } );
You can see a complete end-to-end implementation of this scenario in the advanced OData Web API sample.
Error Responses
There are several built-in error responses. The body of each error response complies with [RFC 7807: Problem Details].
Note
In earlier versions, the error responses bodies complied with the [Microsoft REST Guidelines error response format], which is itself the error response format used by the OData protocol (see [OData JSON Format §21.1]). There wasn’t a broad standard at that time, which made any common error response format sensible.
Each problem detail also contains a code extension to retain a level of backward compatibility for clients that may
have relied on that value. If you need to retain the old functionality, refer to
backward compatibility below.
Unspecified
All versioned services require that an API version be specified. When a client makes a request without providing an API
version, then the server will respond with a bad request. This behavior is typically not exhibited when the API is
version-neutral or the AssumeDefaultVersionWhenUnspecified option is configured to true.
| Title | Unspecified API version |
| Type | https://docs.api-versioning.org/problems#unspecified |
| Status | 400 |
| Detail | An API version is required, but was not specified |
| Code | ApiVersionUnspecified |
Unsupported
When a client requested API version does not match any of the available controllers or their actions, then the server
will respond with a problem. If the ReportApiVersions option is true, then the supported versions will be returned
to the client in the api-supported-versions HTTP header.
| Title | Unsupported API version |
| Type | https://docs.api-versioning.org/problems#unsupported |
| Status | 4001 2 |
| Detail | The specified API version is not supported |
| Code | UnsupportedApiVersion |
1: Defined by
ApiVersioningOptions.UnsupportedApiVersionStatusCode
2: The value is always404when versioning by URL segment
Invalid
When a client makes a request with an API version, but the value is malformed or cannot be parsed, then the server will respond with a bad request. This typically occurs where the value contains incomplete version components or the date-only form is invalid (ex: 2016-02-30).
| Title | Invalid API version |
| Type | https://docs.api-versioning.org/problems#invalid |
| Status | 400 |
| Detail | An API version was specified, but it is invalid |
| Code | InvalidApiVersion |
Ambiguous
When a client requests a specific API version, the specified API version must be unambiguous to the server. A client is allowed to specify an API version more than once, but if the values are not identical, then the server will respond with a bad request.
| Title | Ambiguous API version |
| Type | https://docs.api-versioning.org/problems#ambiguous |
| Status | 400 |
| Detail | An API version was specified multiple times with different values |
| Code | AmbiguousApiVersion |
Examples
GET /resource?api-version=1.0 HTTP/1.1
host: localhost
api-version: 1.0
Figure 1: Multiple, unambiguous API versions requested
GET /resource?api-version=1.0 HTTP/1.1
host: localhost
api-version: 2.0
Figure 2: Ambiguous API versions requested between in query string and headers
GET /resource?api-version=1.0&api-version=2.0 HTTP/1.1
host: localhost
Figure 3: Ambiguous API versions requested in the query string
GET /resource HTTP/1.1
host: localhost
api-version: 1.0
api-version: 2.0
Figure 4: Ambiguous API versions requested in the headers
Customization
Error responses can be customized or extended in a variety of ways. RFC 7807 was ratified after active development on
ASP.NET Web API ceased. There are no out-of-the-box services provided. API Versioning provides a backport of the
ProblemDetails type as well as the IProblemDetailsFactory. The default implementation can be replaced by
implementing IProblemDetailsFactory and exposing it as a resolvable service via HttpConfiguration.DependencyResolver.
Backward Compatibility
While it is possible to customize error responses and retain the previous Error Object format, there is considerable work required to enable this behavior and may block adoption of new library versions. Additional extensions have been added to retain backward compatibility or continue to use Error Objects if you so desire.
ASP.NET Web API does not provide an out-of-the-box dependency injection container; however, the following extension method will wire up the necessary changes without having to add one of your own.
configuration.ConvertProblemDetailsToErrorObject();
Note
Applies to 7.1.0+
API Documentation
Adding documentation is often the final, pivotal step in making your versioned services available to clients and fosters their utilization. While there are many approaches to documenting your services, OpenAPI (formerly Swagger) has quickly become the de facto method for describing REST services.
The ASP.NET API versioning project provides several new API explorer implementations that make it easy to add versioning into your OpenAPI configurations. Each of these API explorers do all of the heavy lifting to discover and collate your REST services by API version. They do not directly rely on nor use any external OpenAPI libraries so that you can use them for other scenarios as well.
Any OpenAPI generator such as Swashbuckle, or NSwag that leverage the API Explorer can be used.
Web API
Everything you need to add versioned documentation to your API controllers using API Explorer extensions with [Swashbuckle][openapi-swashbuckle-old].
config.AddApiVersioning();
// (optional) format the version as "'v'major[.minor][-status]"
var apiExplorer = config.AddVersionedApiExplorer( o => o.GroupNameFormat = "'v'VVV" );
config.EnableSwagger(
"{apiVersion}/swagger",
swagger =>
{
swagger.MultipleApiVersions(
( apiDescription, version ) => apiDescription.GetGroupName() == version,
info =>
{
foreach ( var group in apiExplorer.ApiDescriptions )
{
info.Version( group.Name, $"Example API {group.ApiVersion}" );
}
} );
} )
.EnableSwaggerUi( swagger => swagger.EnableDiscoveryUrlSelector() );
Review the example project for additional setup and configuration options.
OData
Everything you need to add versioned documentation to your OData controllers using the OData API Explorer extensions with Swashbuckle.
configuration.AddApiVersioning();
var modelBuilder = new VersionedODataModelBuilder( configuration )
{
ModelConfigurations = { new MyModelConfiguration() }
};
configuration.MapVersionedODataRoutes( "odata", "api", modelBuilder );
// (optional) format the version as "'v'major[.minor][-status]"
var apiExplorer = configuration.AddODataApiExplorer( o => o.GroupNameFormat = "'v'VVV" );
configuration.EnableSwagger(
"{apiVersion}/swagger",
swagger =>
{
swagger.MultipleApiVersions(
( apiDescription, version ) => apiDescription.GetGroupName() == version,
info =>
{
foreach ( var group in apiExplorer.ApiDescriptions )
{
info.Version( group.Name, $"Example API {group.ApiVersion}" );
}
} );
} )
.EnableSwaggerUi( swagger => swagger.EnableDiscoveryUrlSelector() );
Review the following example projects for additional setup and configuration options:
Note
This API explorer does not directly tie into Swashbuckle with OData because that project also prescribes how API versioning is performed, which is incompatible with this project.
API Explorer Options
The API Explorer options allows you to configure, customize, and extend the default behaviors when you add API exploration support. The configuration options are specified by providing a callback to the appropriate extension method:
The ApiExplorerOptions have the following configuration settings:
- GroupNameFormat
- SubstituteApiVersionInUrl
- SubstitutionFormat
- DefaultApiVersion
- DefaultApiVersionParameterDescription
- AssumeDefaultVersionWhenUnspecified
- ApiVersionParameterSource
- AddApiVersionParametersWhenVersionNeutral
- RouteConstraintName
Use Qualified Names
The OData API Explorer is responsible for building URLs that refer to your entity sets, functions, and actions. This
property determines whether the constructed URLs use qualified names. The default value is false. The
ODataUriResolver instance configured for your application must be configured to match the generated URLs
(ex: UnqualifiedCallAndEnumPrefixFreeResolver).
Query Options
This option allows you to configure OData query options. The configuration for query options can be expressed purely by convention, through the use of supported OData query attribute, or both. The default behavior will always apply conventions from OData query attributes without additional configuration. For more information see the OData query options topic.
Metadata Options
This option allows you to determine whether the OData metadata ($metadata) and service document (/) are explored as
available endpoints. The available options are: None, ServiceDocument, Metadata, or All. The default value is
None.
Ad Hoc Model Builder
This property returns an VersionedODataModelBuilder that can be used for building ad hoc Entity Data Models (EDMs)
that are used when defining the query options for APIs that do not use the full OData stack. Some OData query
options can only be set via Model Bound settings. This builder constructs an ad hoc EDM that will contain those
settings solely for the purposes of API exploration and without opting into any other OData-specific features. For more
information see the OData query options topic.
Related Entity Id Parameter Description
This option enables you to specify the description for OData related entity links. The default value is
"The identifier of the related entity." OData related entity links appear in $ref requests. This description is
used to describe dynamic parameters such as the $id query parameter.
OData Options
The ODataApiExplorerOptions extends the above options with the following additional settings:
- UseQualifiedNames
- QueryOptions
- RelatedEntityIdParameterDescription
- MetadataOptions
- AdHocModelBuilder
- UseApiExplorerSettings1
Use API Explorer Settings
OData controllers are not explored by default. The API explorer for OData services does not initially honor this
setting so that OData APIs will be discovered. You might decide, however, to use the API explorer settings to explicitly
define which OData services should be explored. You must set this property to a value of true in order for the API
explorer to respect API explorer settings.
Use Qualified Names
The OData API Explorer is responsible for building URLs that refer to your entity sets, functions, and actions. This
property determines whether the constructed URLs use qualified names. The default value is false. The
ODataUriResolver instance configured for your application must be configured to match the generated URLs
(ex: UnqualifiedCallAndEnumPrefixFreeResolver).
Query Options
This option allows you to configure OData query options. The configuration for query options can be expressed purely by convention, through the use of supported OData query attribute, or both. The default behavior will always apply conventions from OData query attributes without additional configuration. For more information see the OData query options topic.
Metadata Options
This option allows you to determine whether the OData metadata ($metadata) and service document (/) are explored as
available endpoints. The available options are: None, ServiceDocument, Metadata, or All. The default value is
None.
Ad Hoc Model Builder
This property returns an VersionedODataModelBuilder that can be used for building ad hoc Entity Data Models (EDMs)
that are used when defining the query options for APIs that do not use the full OData stack. Some OData query
options can only be set via Model Bound settings. This builder constructs an ad hoc EDM that will contain those
settings solely for the purposes of API exploration and without opting into any other OData-specific features. For more
information see the OData query options topic.
Related Entity Id Parameter Description
This option enables you to specify the description for OData related entity links. The default value is
"The identifier of the related entity." OData related entity links appear in $ref requests. This description is
used to describe dynamic parameters such as the $id query parameter.
Query Options
OData query option conventions allow you to specify information for your OData services without having to rely solely on .NET attributes. There are a number of reasons why you might uses these conventions. The most common reasons are:
- Centralized management and application of all OData query options
- Define OData query options that cannot be expressed with any OData query attributes
- Apply OData query options to services defined by controllers in external .NET assemblies
The parameter names generated are based on the name of the OData query option and the configuration of the
ODataUriResolver. OData supports query options without the system $ prefix. This is enabled or disabled by the
ODataUriResolver.EnableNoDollarQueryOptions property.
Attribute Model
The attribute model relies on Model Bound settings attributes and the EnableQueryAttribute. The
EnableQueryAttribute indicates API-specific options that might be too restrictive or not applicable to specific
models. Consider the following model and controller definitions.
using System;
using Microsoft.AspNet.OData.Query;
using static Microsoft.AspNet.OData.Query.SelectExpandType;
[Select]
[Select( "effectiveDate", SelectType = Disabled )]
public class Order
{
public int Id { get; set; }
public DateTime CreatedDate { get; set; } = DateTime.Now;
public DateTime EffectiveDate { get; set; } = DateTime.Now;
public string Customer { get; set; }
public string Description { get; set; }
}
using Asp.Versioning;
using Asp.Versioning.OData;
using Microsoft.AspNet.OData;
using Microsoft.AspNet.OData.Routing;
using Microsoft.Web.Http;
using System.Web.Http;
using System.Web.Http.Description;
using static Microsoft.AspNet.OData.Query.AllowedQueryOptions;
using static System.Net.HttpStatusCode;
using static System.DateTime;
[ApiVersion( 1.0 )]
[ODataRoutePrefix( "Orders" )]
public class OrdersController : ODataController
{
[ODataRoute]
[Produces( "application/json" )]
[ProducesResponseType( typeof( ODataValue<IEnumerable<Order>> ), Status200OK )]
[EnableQuery( MaxTop = 100, AllowedQueryOptions = Select | Top | Skip | Count )]
public IQueryable<Order> Get()
{
var orders = new[]
{
new Order(){ Id = 1, Customer = "John Doe" },
new Order(){ Id = 2, Customer = "John Doe" },
new Order(){ Id = 3, Customer = "Jane Doe", EffectiveDate = UtcNow.AddDays( 7d ) }
};
return orders.AsQueryable();
}
[ODataRoute( "{key}" )]
[Produces( "application/json" )]
[ProducesResponseType( typeof( Order ), Status200OK )]
[ProducesResponseType( Status404NotFound )]
[EnableQuery( AllowedQueryOptions = Select )]
public SingleResult<Order> Get( int key )
{
var orders = new[] { new Order(){ Id = key, Customer = "John Doe" } };
return SingleResult.Create( orders.AsQueryable() );
}
}
Convention Model
The convention model relies on Model Bound settings via the fluent API of the ODataModelBuilderand the
EnableQueryAttribute. The EnableQueryAttribute indicates API-specific options that might be too restrictive or
nonapplicable to specific models. Consider the following model and controller definitions.
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
}
public class PersonModelConfiguration : IModelConfiguration
{
public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix )
{
var person = builder.EntitySet<Person>( "People" ).EntityType;
person.HasKey( p => p.Id );
// configure model bound conventions
person.Select().OrderBy( "firstName", "lastName" );
if ( apiVersion < ApiVersions.V3 )
{
person.Ignore( p => p.Phone );
}
if ( apiVersion <= ApiVersions.V1 )
{
person.Ignore( p => p.Email );
}
if ( apiVersion > ApiVersions.V1 )
{
var function = person.Collection.Function( "NewHires" );
function.Parameter<DateTime>( "Since" );
function.ReturnsFromEntitySet<Person>( "People" );
}
if ( apiVersion > ApiVersions.V2 )
{
person.Action( "Promote" ).Parameter<string>( "title" );
}
}
}
using Asp.Versioning;
using Asp.Versioning.OData;
using Microsoft.AspNet.OData;
using Microsoft.AspNet.OData.Routing;
using Microsoft.Web.Http;
using System.Web.Http;
using System.Web.Http.Description;
using static Microsoft.AspNet.OData.Query.AllowedQueryOptions;
using static System.Net.HttpStatusCode;
using static System.DateTime;
public class PeopleController : ODataController
{
[HttpGet]
[ResponseType( typeof( ODataValue<IEnumerable<Person>> ) )]
public IHttpActionResult Get( ODataQueryOptions<Person> options )
{
var validationSettings = new ODataValidationSettings()
{
AllowedQueryOptions = Select | OrderBy | Top | Skip | Count,
AllowedOrderByProperties = { "firstName", "lastName" },
AllowedArithmeticOperators = AllowedArithmeticOperators.None,
AllowedFunctions = AllowedFunctions.None,
AllowedLogicalOperators = AllowedLogicalOperators.None,
MaxOrderByNodeCount = 2,
MaxTop = 100,
};
try
{
options.Validate( validationSettings );
}
catch ( ODataException )
{
return BadRequest();
}
var people = new[]
{
new Person()
{
Id = 1,
FirstName = "John",
LastName = "Doe",
Email = "john.doe@somewhere.com",
Phone = "555-987-1234",
},
new Person()
{
Id = 2,
FirstName = "Bob",
LastName = "Smith",
Email = "bob.smith@somewhere.com",
Phone = "555-654-4321",
},
new Person()
{
Id = 3,
FirstName = "Jane",
LastName = "Doe",
Email = "jane.doe@somewhere.com",
Phone = "555-789-3456",
}
};
return this.Success( options.ApplyTo( people.AsQueryable() ) );
}
[HttpGet]
[ResponseType( typeof( Person ) )]
public IHttpActionResult Get( int key, ODataQueryOptions<Person> options )
{
var people = new[]
{
new Person()
{
Id = key,
FirstName = "John",
LastName = "Doe",
Email = "john.doe@somewhere.com",
Phone = "555-987-1234",
}
};
var query = options.ApplyTo( people.AsQueryable();
return this.SuccessOrNotFound( query ).SingleOrDefault() );
}
}
Conventions
If you only define OData query options imperatively using ODataQuerySettings and ODataValidationSettings, then
there are no attributes or Entity Data Model (EDM) data annotations to explore the query options from. In this scenario,
you can use the conventions in the API Explorer extensions to document any query option setting that can be defined by
ODataQuerySettings or ODataValidationSettings.
.AddODataApiExplorer( options =>
{
var queryOptions = options.QueryOptions;
queryOptions.Controller<V2.PeopleController>()
.Action( c => c.Get( default( ODataQueryOptions<Person> ) ) )
.Allow( Skip | Count )
.AllowTop( 100 );
queryOptions.Controller<V3.PeopleController>()
.Action( c => c.Get( default( ODataQueryOptions<Person> ) ) )
.Allow( Skip | Count )
.AllowTop( 100 );
} );
The OData API Explorer will discover and add the following parameters for an entity set query:
| Name | Parameter Type | Data Type | Description |
|---|---|---|---|
$select | query | string | Limits the properties returned in the result. |
$orderby | query | string | Specifies the order in which results are returned. The allowed properties are: firstName, lastName. |
$top | query | integer | Limits the number of items returned from a collection. The maximum value is 100. |
$skip | query | integer | Excludes the specified number of items of the queried collection from the result. |
Parameters
While each OData query option has a default provided description, the description can be changed by providing a custom
description. Descriptions are generated by the IODataQueryOptionDescriptionProvider:
public interface IODataQueryOptionDescriptionProvider
{
string Describe(
AllowedQueryOptions queryOption,
ODataQueryOptionDescriptionContext context );
}
Note
Although
AllowedQueryOptionsis a bitwise enumeration, only a single query option value is ever passed
You can change the default description by implementing your own IODataQueryOptionDescriptionProvider or extending the
built-in DefaultODataQueryOptionDescriptionProvider. The implementation is updated in the OData API Explorer options using:
AddODataApiExplorer( options => options.QueryOptions.DescriptionProvider = new MyQueryOptionDescriptor() );
Custom Conventions
You can also define custom conventions via the IODataQueryOptionsConvention interface and add them to the builder:
public interface IODataQueryOptionsConvention
{
void ApplyTo( ApiDescription apiDescription );
}
AddODataApiExplorer( options => options.QueryOptions.Add( new MyODataQueryOptionsConvention() ) );
Partial OData
OData supports query capabilities without using the full OData stack. Consider the following controller, which is not an OData controller, but uses OData query options:
[ApiVersion( 1.0 )]
[ApiController]
[Route( "[controller]" )]
public class BooksController : ControllerBase
{
[HttpGet]
[Produces( "application/json" )]
[ProducesResponseType( typeof( IEnumerable<Book> ), 200 )]
public IActionResult Get( ODataQueryOptions<Book> options ) =>
Ok( options.ApplyTo( books.AsQueryable() ) );
}
[ApiVersion( 1.0 )]
[ApiController]
[Route( "[controller]" )]
public class BooksController : ControllerBase
{
[HttpGet]
[Produces( "application/json" )]
[ProducesResponseType( typeof( IEnumerable<Book> ), 200 )]
public IActionResult Get( ODataQueryOptions<Book> options ) =>
Ok( options.ApplyTo( books.AsQueryable() ) );
}
When OData query capabilities are used this way, query options can be discovered via EnableQueryAttribute or via the
API Explorer extensions. Unfortunately, these are both ultimately limited to what can be expressed via
ODataQuerySettings and ODataValidationSettings, which does not cover the gambit of all possible OData query options;
for example, the allowable $filter properties. These other properties can be configured via Model Bound settings,
but without using the full OData stack there is no Entity Data Model (EDM) to retrieve these annotations from.
To address this limitation, OData query options can now also be explored using an ad hoc EDM. This EDM only exists for the purposes of query option exploration. Using an ad hoc EDM does not opt into other OData feature and only exists during exploration. Applying Model Bound settings to an ad hoc model is almost identical to the normal method. If you want to use attributes, just apply them to your model.
[Filter( "author", "published" )]
public class Book
{
public string Id { get; set; }
public string Author { get; set; }
public string Title { get; set; }
public int Published { get; set; }
}
Every action that appears to be OData-like will automatically be discovered and its model explored. Discovered models are registered as a complex type by default. If you prefer to use entities or need additional control over the applied settings, you can use conventions as well.
AddODataApiExplorer(
options =>
{
options.AdHocModelBuilder.DefaultModelConfiguration = (builder, version, prefix) =>
{
builder.ComplexType<Book>().Filter( "author", "published" );
};
}
)
The AdHocModelBuilder is part of the ODataApiExplorerOptions as opposed to ODataApiVersioningOptions. If you
have numerous models and would like to break the settings into different configurations, you can still use
IModelConfiguration. IModelConfiguration instances are automatically discovered and injected the same way as they
are when using the full OData stack.
public class BookConfiguration : IModelConfiguration
{
public void Apply( ODataModelBuilder builder, ApiVersion apiVersion, string? routePrefix )
{
builder.EntitySet<Book>( "Books" ).EntityType.Filter( "author", "published" );
}
}
Model configuration for an ad hoc model; the routePrefix will always be null.
There is no distinction between an IModelConfiguration that is used for ad hoc EDM exploration versus normal model
registration. It is unlikely that you would be mixing the full and partial OData stack. If you are mixing use cases,
then you can tell the difference between models from the provided API version. There should be no scenario where a
model is registered two different ways for the same API version.
Swashbuckle Integration
Although the API explorers for API versioning provide all of the necessary information, there is select information
that OpenAPI (formerly Swagger) and Swashbuckle will not wire up for you. This includes iterating through all the
available API versions so that they don’t have to be imperatively declared and changed one at a time. Fortunately,
bridging this gap is really easy to achieve using Swashbuckle’s extensibility model. The following are simple
IOperationFilter implementations that leverage the metadata provided by the corresponding API explorer to fill in
these gaps.
Remember to add the necessary references to one or both of the following:
public class SwaggerDefaultValues : IOperationFilter
{
public void Apply(
Operation operation,
SchemaRegistry schemaRegistry,
ApiDescription apiDescription )
{
operation.deprecated |= apiDescription.IsDeprecated();
if ( operation.parameters == null )
{
return;
}
foreach ( var parameter in operation.parameters )
{
var description = apiDescription.ParameterDescriptions
.First( p => p.Name == parameter.name );
parameter.description ??= description.Documentation;
parameter.@default ??= description.ParameterDescriptor?.DefaultValue;
}
}
}
Use MultipleApiVersions to iterate over each ApiDescription and collate them by their corresponding group. The
default group name for each ApiDescription is the formatted API version that is associated with it.
configuration.EnableSwagger(
"{apiVersion}/swagger",
swagger =>
{
swagger.MultipleApiVersions(
( apiDescription, version ) => apiDescription.GetGroupName() == version,
info =>
{
foreach ( var group in apiExplorer.ApiDescriptions )
{
info.Version( group.Name, $"Example API {group.ApiVersion}" )
.Description( "An example API" );
}
} );
swagger.OperationFilter<SwaggerDefaultValues>();
} )
.EnableSwaggerUi( swagger => swagger.EnableDiscoveryUrlSelector() );
Examples
There are end-to-end examples using API versioning and Swashbuckle:
- API Versioning and Swashbuckle
- OData, API Versioning, and Swashbuckle
- Partial OData, API Versioning, and Swashbuckle
Attributes
In addition to the API versioning options, there are few other customization and extension points. Attributes are the
primary mechanism used to decorate the API version metadata with a specific controller type, but the attributes used
can be any IApiVersionProvider.
public interface IApiVersionProvider
{
ApiVersionProviderOptions Options { get; }
IReadOnlyList<ApiVersion> Versions { get; }
}
There are several API version provider attributes defined out-of-the-box:
ApiVersionsBaseAttributeApiVersionAttributeMapToApiVersionAttributeAdvertiseApiVersionsAttribute
These attributes are themselves extensible. For example, you might choose to have your own attributes that are unambiguously a specific version:
[AttributeUsage( AttributeUsage.Class, AllowMultiple = true, Inherited = false )]
public sealed class V1Attribute : ApiVersionAttribute
{
public V1Attribute() : base( new ApiVersion( new( 2016, 7, 1 ) ) ) { }
}
[V1]
[RoutePrefix( "api/helloworld" )]
public class HelloWorldController : ApiController
{
[Route]
public string Get() => "Hello world!";
}
This approach can help centralize API version management and avoid developer typographical errors when implementing a set of services that all use the same API version.
Version Format
It is possible to extend or change the provided API version format, but that capability comes with several rules:
- You must extend
ApiVersion - You must override:
GetHashCodeCompareToToString(string,IFormatProvider)
- You must implement
IApiVersionParser- It may be possible to extend
ApiVersionParserdepending on your requirements
- It may be possible to extend
You will likely need to extend ApiVersionFormatProvider or implement a custom IFormatProvider. Although not
strictly required, you may want to implement operator overloads for your custom type to retain functional parity with
ApiVersion. The custom parser will need to be passed to components that accept IApiVersionParser and/or replace the
default implementation registered for dependency injection.
You should consider the impact that a custom API version may have on clients. Your custom format and parsing logic may need to be distributed to them for to use.
Versioned Clients
The Asp.Versioning.Http.Client package brings client-side extensions that make your HttpClient instances
API version-aware.
API Version Writer
The reciprocal to IApiVersionReader is IApiVersionWriter. As the name implies, the IApiVersionWriter is
responsible for writing the configured API version into outgoing requests. The default configured writer is the
QueryStringApiVersionWriter using the query parameter name "api-version".
Adding API versions to your HttpClient instances can easily be configured using the IHttpClientFactory dependency
injection extensions.
var services = new ServiceCollection();
services.AddHttpClient(
"MyApi",
client => client.BaseAddress = new Uri( "https://my.api.com") )
.AddApiVersion( 1.0 );
var provider = services.BuildServiceProvider();
var factory = provider.GetRequiredService<IHttpClientFactory>();
var client = factory.CreateClient( "MyApi" );
// GET https://my.api.com/data?api-version=1.0
var response = await client.GetAsync( "data" );
You can add or replace the default IApiVersionWriter with:
var services = new ServiceCollection();
services.AddSingleton<IApiVersionWriter>( new UrlSegmentApiVersionWriter( "{ver}" ) );
services.AddHttpClient(
"MyApi",
client => client.BaseAddress = new Uri( "https://my.api.com/v{ver}") )
.AddApiVersion( 1 );
var provider = services.BuildServiceProvider();
var factory = provider.GetRequiredService<IHttpClientFactory>();
var client = factory.CreateClient( "MyApi" );
// GET https://my.api.com/v1/data
var response = await client.GetAsync( "data" );
The following implementations are provided out-of-the-box:
QueryStringApiVersionWriterHeaderApiVersionWriterMediaTypeApiVersionWriterUrlSegmentApiVersionWriter
Specifying multiple API versions is typically unnecessary; however, if this is a capability you need or want, multiple writers can be composed together:
var writer = ApiVersionWriter.Combine(
new QueryApiVersionWriter( "api-version" ),
new HeaderApiVersionWriter( "x-ms-api-version" ) );
Your application might have multiple clients that communicate to services which use different API versioning methods. To accommodate these differences, you can specify a specific writer per client.
var services = new ServiceCollection();
services.AddHttpClient(
"SomeApi",
client => client.BaseAddress = new Uri( "https://some.api.com/") )
.AddApiVersion( 1.0, new QueryApiVersionWriter() );
services.AddHttpClient(
"OtherApi",
client => client.BaseAddress = new Uri( "https://other.api.com/v{ver}/") )
.AddApiVersion( 2, new UrlSegmentApiVersionWriter( "{ver}" ) );
If you’re not using dependency injection or the IHttpClientFactory, you can still configure writers by explicitly
configuring the ApiVersionHandler:
using var client = new HttpClient(
new ApiVersionHandler(
new QueryApiVersionWriter(),
new ApiVersion( 1, 0 ) )
{
InnerHandler = new HttpClientHandler(),
} );
Notifications
API clients always have a few common questions:
- “How do I know when an API version is deprecated?”
- “How do I know when an API version will be sunset?”
- “How do I know when a new API version is available?”
These questions can now be answered via:
public interface IApiNotification
{
Task OnApiDeprecatedAsync( ApiNotificationContext context, CancellationToken cancellationToken );
Task OnNewApiAvailableAsync( ApiNotificationContext context, CancellationToken cancellationToken );
}
Where the notification information provided is:
public class ApiNotificationContext
{
public HttpResponseMessage Response { get; }
public ApiVersion ApiVersion { get; }
public SunsetPolicy SunsetPolicy { get; }
}
If the API reports its versions, then the ApiVersionHandler will detect when these events occur and invoke the
appropriate notification. The ApiVersionHandler will look for the api-supported-versions and
api-deprecated-versions HTTP headers by default, but alternate headers may be configured. If a deprecation or sunset
policy is specified by the API, then the deprecation date will be read from the deprecation HTTP header and the sunset
date will be read from the sunset HTTP header. Any link HTTP headers where the relation type is
rel="deprecation" or rel="sunset" will also be read.
No notifications or actions occur by default. The most logical action to perform when a notification occurs is to log
it. The ApiVersionHandlerLogger<T> implements an IApiNotification that is paired with an ILogger<T> that will:
- Log a warning message when an API reports that the version requested is deprecated.
- Log an informational message when an API reports that a newer version than the one requested is available.
Logged messages can be connected to alerts to notify developers when these events occur in an automated fashion.
If you configuration uses dependency injection and ILogger<ApiVersionHandler> is a resolvable service,
ApiVersionHandlerLogger<ApiVersionHandler> will be used as the default IApiNotification implementation unless
configured otherwise.
API Information
Using API information provided in responses is useful, but not always provided for every request. Furthermore, if you’re onboarding to an API, how do you know which API versions are available or deprecated? How do you know the policies around these APIs? Detailed information might be provided by OpenAPI, but how do you know where the OpenAPI documents are?
The most logical way for an API to expose this information is to provide an OPTIONS method, which may be
version-specific or version-neutral, that returns all of the available API information. This information is useful for
automation and client tooling.
The GetApiInformationAsync extension method for the HttpClient provides a prescribed implementation to make the
appropriate OPTIONS request and parse its response into:
using var client = new HttpClient()
{
BaseAddress = new Uri( "https://my.api.com" ),
};
var info = await client.GeApiInformationAsync( "/?api-version=1.0" );
Request API information
OPTIONS /?api-version=1.0 HTTP/2
host: my.api.com
HTTP request sent
HTTP/2 200
api-supported-versions: 2.0
api-deprecated-versions: 1.0
deprecation: @1688169600
sunset: Mon, 01 Jan 2024 00:00:00 GMT
link: <https://api.docs.com/policies/deprecation.html>; rel="deprecation"; type="text/html"
link: <https://api.docs.com/policies/sunset.html>; rel="sunset"; type="text/html"
link: <openapi/v1.json>; rel="openapi"; type="application/json"; api-version="1.0"
HTTP response received
public class ApiInformation
{
public IReadOnlyList<ApiVersion> SupportedApiVersions { get; }
public IReadOnlyList<ApiVersion> DeprecatedApiVersions { get; }
public SunsetPolicy SunsetPolicy { get; }
public IReadOnlyDictionary<ApiVersion, Uri> OpenApiDocumentUrls { get; }
}
Parsed API information
Known Limitations
URL Path Segment
API versioning does not fundamentally change how routing works in ASP.NET. When you elect to support API versioning via a URL path segment, the API version is part of the path considered in routing. There is currently no built-in method to match a route where the API version URL path segment has not be specified.
The recommended method to enable this scenario is to use Double Route Registration by providing multiple routes for the corresponding controller actions as follows:
[ApiVersion( 1.0)]
[RoutePrefix( "api" )]
public class ValuesController : ApiController
{
// ~/api/values
// ~/api/v1/values
[Route( "values" )]
[Route( "v{version:apiVersion}/values" )]
public IHttpActionResult Get() => Ok();
}
[ApiVersion( 2.0 )]
[RoutePrefix( "api" )]
public class Values2Controller : ApiController
{
// ~/api/v2/values
[Route( "v{version:apiVersion}/values" )]
public IHttpActionResult Get() => Ok();
}
Alternative
Y&ou can also choose to implement a custom IDirectRouteProvider as suggested in Issue #73.
Routing
The Direct Route routing mechanism (aka attribute routing) was bolted onto the existing routing infrastructure. The design of the out-of-the-box router does not account for nor support overlapping routes between convention-based and attribute-based routes. The result of this behavior is that for each given route, all of the versioned controllers must use convention-based or attribute-based routes. Mixing the two routing strategies for the same route is not guaranteed to resolve correctly.
While it would be ideal to implement a router that could support both approaches, the level of effort to achieve this is high. Furthermore, it’s significantly easier to rationalize about versioned controllers from a service author’s perspective if all of the versioned routes follow the same routing strategy.
OData
The OData support in ASP.NET Web API uses convention-based routing under the hood. If you want to support transitioning
to, or from, OData using API versioning, your ApiController types that match the same routes must also use
convention-based routing.
FAQ
What is the difference between the DefaultApiVersion and ApiVersionSelector options?
There are subtle differences between these two options. Typically, you only need to configure one or the other, but not both.
The DefaultApiVersion has the following uses:
- The API version defined for a controller that does not have any explicit attribution or conventions
- The fallback API version used when no other API version can be resolved
It’s important to understand that once you opt into API versioning, every controller has an API version, even if you do not apply an explicit definition via attributes or conventions. This behavior can also be thought of as the initial API Version.
The DefaultApiVersion value is 1.0, but that may not be your starting API version. For example, you might use the
date-only API versioning scheme. This configuration option prevents the value from being hard-coded and makes it easy
to change the API version for your initial set of services.
The ApiVersionSelector option has a familiar, but different purpose. Any implementation of the IApiVersionSelector
is used to select the best API version given the current HTTP request and API version model. The provided API version
model will already be aggregated across all known service versions.
While this component could be used for a number of different purposes, it is currently only used to select the API
version that should be used when a client does not provide an API version. This option is thus only used when the
AssumeDefaultVersionWhenUnspecified option is also true. The default, configured value for this option is a
instance of the DefaultApiVersionSelector, which always returns the value of DefaultApiVersion. Most of the built-in
IApiVersionSelector implementations accept the ApiVersioningOptions in their constructors so that they can use the
DefaultApiVersion as the final fallback value.
It’s recommended that the IApiVersionSelector implementation you use provides stable, deterministic results. This is particularly important for existing clients that may not be aware that you have introduced API versioning. Contrary to this guidance, a number of service authors have requested granular control over how the API versions should be selected. As an example, a service author might want to allow a client to never specify an API version and use an internal client-to-version mapping that is maintained on the server after the first client connects. How this is implemented in an IApiVersionSelector is up to the service author, but it likely requires information from the current HTTP request and the available API versions for a service.
Examples
Complete, runnable sample projects live in the examples folder of the repository.