Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

PackageVersionTFM
Microsoft.AspNetCore.Mvc.Versioning<= 5.x.xnetcoreapp3.1, net5.0
Microsoft.AspNetCore.Mvc.ApiExplorer<= 5.x.xnetcoreapp3.1, net5.0
Microsoft.AspNetCore.OData<= 5.x.xnetcoreapp3.1, net5.0
Microsoft.AspNetCore.OData.ApiExplorer<= 5.x.xnetcoreapp3.1, net5.0

All new features and platform support will use the Asp.Versioning.* prefix:

PackageVersionTFM
Asp.Versioning.Abstractions6.0.0+net6.0+, netstandard1.0, netstandard2.0
Asp.Versioning.Http16.0.0+net6.0+
Asp.Versioning.Mvc26.0.0+net6.0+
Asp.Versioning.Mvc.ApiExplorer36.0.0+net6.0+
Asp.Versioning.OData6.0.0+net6.0+
Asp.Versioning.OData.ApiExplorer6.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) → ApiVersionMetadata
  • GetApiVersionModel() → 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 controllers
  • IApiControllerSpecification - 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.UseApiBehaviors has been removed
  • ApiVersioningOptions.Conventions has been moved to MvcApiVersioningOptions.Conventions as API Versioning no longer requires MVC Core
    • To configure conventions, use .AddMvc(options => options.Conventions = ?) via the IApiVersioningBuilder extension method
  • ApiVersioningOptions.ControllerNameConvention has 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>()

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 v prefix 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
DescriptionExamples
FFull API version as
[group version][.major[.minor]][-status]
2017-05-01.1-RC ->
2017-05-01.1-RC
FFFull API version with optional minor version as
[group version][.major[.minor,0]][-status]
2017-05-01.1-RC ->
2017-05-01.1.0-RC
GGroup version as yyyy-MM-dd2017-05-01.1-RC ->
2017-05-01
GGGroup version as yyyy-MM-dd with status2017-05-01.1-RC ->
2017-05-01-RC
yGroup version year from 0 to 992001-05-01.1-RC -> 1
yyGroup version year from 00 to 992001-05-01.1-RC -> 01
yyyGroup version year with a minimum of three digits2017-05-01.1-RC -> 017
yyyyGroup version year as a four-digit number2017-05-01.1-RC -> 2017
MGroup version month from 1 through 122001-05-01.1-RC -> 5
MMGroup version month from 01 through 122001-05-01.1-RC -> 05
MMMGroup version abbreviated name of the month2001-06-01.1-RC -> Jun
MMMMGroup version full name of the month2001-06-01.1-RC -> June
dGroup version day of the month, from 1 through 312001-05-01.1-RC -> 1
ddGroup version day of the month, from 01 through 312001-05-01.1-RC -> 01
dddGroup version abbreviated name of the day of the week2001-05-01.1-RC -> Mon
ddddGroup version full name of the day of the week2001-05-01.1-RC -> Monday
vMinor version2001-05-01.1-RC -> 1
1.1 -> 1
VMajor version1.0-RC -> 1
2.0 -> 2
VVMajor and minor version1-RC -> 1
1.1-RC -> 1.1
1.1 -> 1.1
VVVMajor, optional minor version, and status1-RC -> 1-RC
1.1 -> 1.1
VVVVMajor, minor version, and status1-RC -> 1.0-RC
1.1 -> 1.1
1 -> 1.0
SStatus1.0-Beta -> Beta
pPadded minor version with default of two digits1.1 -> 01
1 -> 00
p[n]Padded minor version with N digitsp2: 1.1 -> 01
p3: 1.1 -> 001
PPadded major version with default of two digits2.1 -> 02
2 -> 02
P[n]Padded major version with N digitsP2: 2.1 -> 02
P3: 2.1 -> 002
PPPadded major and minor version with a default of two digits2.1 -> 02.01
2 -> 02.00
PPPPadded major, optional minor version, and status with a default of two digits1-RC -> 01-RC
1.1-RC -> 01.01-RC
PPPPPadded major, minor version, and status with a default of two digits1-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" or rel="sunset". The default implementation can be replaced or extended or you can use the LinkHeaderValue directly 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 MethodSupported
Attribute-based routingYes
Convention-based routingYes
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:

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 URLMatched Controller
/api/helloworld?api-version=1.0HelloWorldController
/api/helloworld?api-version=2.0HelloWorld2Controller
/api/People?api-version=1.0PeopleController
/api/People?api-version=2.0People2Controller

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
    • IInputFormatter to the custom media type
    • IOutputFormatter to 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.AssumedDefaultVersionWhenUnspecified is 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 URLMatched ControllerMatched Action
/api/v1/helloworldHelloWorldControllerGet
/api/v2/helloworldHelloWorld2ControllerGet
/api/v3/helloworldHelloWorld2ControllerGetV3
/api/v1/PeoplePeopleControllerGet
/api/v2/PeoplePeople2ControllerGet
/api/v3/PeoplePeople2ControllerGetV3

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 URLMatched ControllerMatched Action
/api/helloworld?api-version=1.0HelloWorldControllerGet
/api/helloworld?api-version=2.0HelloWorld2ControllerGet
/api/helloworld?api-version=3.0HelloWorld2ControllerGetV3
/api/People?api-version=1.0PeopleControllerGet
/api/People?api-version=2.0People2ControllerGet
/api/People?api-version=3.0People2ControllerGetV3

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:

  1. The correct service API version cannot be selected across deployments.
  2. 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, and LastName are visible in all API versions
  • MiddleName and Email are only visible starting in API version 2.0
  • Phone is only visible starting in API version 3.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:

NotationApplied RuleDescription
1.0x ≥ 1.0Minimum version, inclusive
[1.0,)x ≥ 1.0Minimum version, inclusive
(1.0,)x > 1.0Minimum version, exclusive
[1.0]x == 1.0Exact version match
(,1.0]x ≤ 1.0Maximum version, inclusive
(,1.0)x < 1.0Maximum version, exclusive
[1.0,2.0]1.0 ≤ x ≤ 2.0Exact range, inclusive
(1.0,2.0)1.0 < x < 2.0Exact range, exclusive
[1.0,2.0)1.0 ≤ x < 2.0Mixed inclusive minimum and exclusive maximum version
(1.0)invalidinvalid

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:

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 CodeMeaningDescription
400Bad RequestThe API doesn’t support this version
404Not FoundThe API doesn’t exist
501Not ImplementedThe API isn’t implemented

Remarks

Regardless of the configured option, when versioning by:

  • URL segment, 404 is always returned
  • media type, 406 or 415 is 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();
    } );

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.0
  • Contoso.Api.v1_1.Controllers → 1.1
  • Contoso.Api.v0_9_Beta.Controllers → 0.9-Beta
  • Contoso.Api.v20180401.Controllers → 2018-04-01
  • Contoso.Api.v2018_04_01.Controllers → 2018-04-01
  • Contoso.Api.v2018_04_01_Beta.Controllers → 2018-04-01-Beta
  • Contoso.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 IApiVersionSelector can 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 use AddApiVersioning().AddOData( options => options.AddRouteComponents( ... ) ). The standard AddOData configuration 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 types
  • SingleResult<T> response types
  • ODataValue<T> response types
  • Delta<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

IModelConfiguration instances are automatically discovered through Dependency Injection when you declare IEnumerable<IModelConfiguration> or VersionedODataModelBuilder as a dependent parameter. The ModelConfigurations property 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 ODataModelBuilder via the ModelBuilderFactory
    • Invoke IModelConfiguration.Apply for each item defined in ModelConfigurations, including the DefaultModelConfiguration, with the current model builder and API version
    • Invoke ODataModelBuilder.GetEdmModel to generate the current EDM model
    • Apply the ApiVersionAnnotation with the current API version to the generated EDM model
    • Invoke OnModelCreated with the current model builder and generated EDM model, if defined

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

RawRequestedApiVersion is the API version as it appear over the wire, whereas RequestedApiVersion will be the parsed ApiVersion type.

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.

TitleUnspecified API version
Typehttps://docs.api-versioning.org/problems#unspecified
Status400
DetailAn API version is required, but was not specified
CodeApiVersionUnspecified

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.

TitleUnsupported API version
Typehttps://docs.api-versioning.org/problems#unsupported
Status4001 2
DetailThe specified API version is not supported
CodeUnsupportedApiVersion

1: Defined by ApiVersioningOptions.UnsupportedApiVersionStatusCode
2: The value is always 404 when 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).

TitleInvalid API version
Typehttps://docs.api-versioning.org/problems#invalid
Status400
DetailAn API version was specified, but it is invalid
CodeInvalidApiVersion

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.

TitleAmbiguous API version
Typehttps://docs.api-versioning.org/problems#ambiguous
Status400
DetailAn API version was specified multiple times with different values
CodeAmbiguousApiVersion

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)

NuGet Package NuGet Package

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

NuGet Package NuGet Package NuGet Package

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

NuGet Package NuGet Package

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:

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.

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:

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.

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:

NameParameter TypeData TypeDescription
$selectquerystringLimits the properties returned in the result.
$orderbyquerystringSpecifies the order in which results are returned. The allowed properties are: firstName, lastName.
$topqueryintegerLimits the number of items returned from a collection. The maximum value is 100.
$skipqueryintegerExcludes 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 AllowedQueryOptions is 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:

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:

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:

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:

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:

  • ApiVersionsBaseAttribute
  • ApiVersionAttribute
  • MapToApiVersionAttribute
  • AdvertiseApiVersionsAttribute

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:

  1. You must extend ApiVersion
  2. You must override:
    • GetHashCode
    • CompareTo
    • ToString(string,IFormatProvider)
  3. You must implement IApiVersionParser
    • It may be possible to extend ApiVersionParser depending on your requirements

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:

  • QueryStringApiVersionWriter
  • HeaderApiVersionWriter
  • MediaTypeApiVersionWriter
  • UrlSegmentApiVersionWriter

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:

  1. Log a warning message when an API reports that the version requested is deprecated.
  2. 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.

IDCategoryDescription
AV0001UsageInvalid API version
AV0002UsageInvalid API version range
AV0003UsageInvalid API version status
AV0004UsageInvalid API version number
AV0005UsageInvalid API version year
AV0006UsageInvalid API version month
AV0007UsageInvalid API version day
AV0008UsageInvalid API version date
AV0009UsageInvalid API version format specifier
AV0010UsageUnexpected API version format
AV0011StyleRemove unnecessary default API version
AV0012UsageInvalid default API version
AV0013UsageMissing AddMvc
AV0014UsageMissing API behavior
AV0015PerformanceUse a specific API version reader
AV0016UsageDo not assume default API version
AV0017UsageRemove unnecessary default value
AV0018UsageAll endpoints are version-neutral
AV0019UsageVersioned and version-neutral
AV0020StyleRemove unnecessary API explorer
AV0021UsageUse the versioned API explorer
AV0022UsageMissing AddOData
AV0023UsageRoute components are ignored
AV0024UsageRemove unnecessary API explorer option
AV0025DocumentationMissing OpenAPI document description
AV0026UsageRemove unnecessary group name format
AV0027UsageUse DescribeApiVersions
AV0028UsageSunset policy takes effect before deprecation
AV0029UsageRemove unnecessary OpenAPI services
AV0030UsageMissing WithDocumentPerVersion
AV0031UsageMissing 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 forEntire 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 IDAV0001
CategoryUsage
Fix isBreaking

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 IDAV0002
CategoryUsage
Fix isBreaking

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 IDAV0003
CategoryUsage
Fix isBreaking

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 IDAV0004
CategoryUsage
Fix isBreaking

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 IDAV0005
CategoryUsage
Fix isBreaking

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 IDAV0006
CategoryUsage
Fix isBreaking

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 IDAV0007
CategoryUsage
Fix isBreaking

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 IDAV0008
CategoryUsage
Fix isBreaking

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 IDAV0009
CategoryUsage
Fix isBreaking

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 IDAV0010
CategoryUsage
Fix isNon-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 IDAV0011
CategoryStyle
Fix isNon-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 IDAV0012
CategoryUsage
Fix isBreaking

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 IDAV0013
CategoryUsage
Fix isBreaking

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 IDAV0014
CategoryUsage
Fix isBreaking

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 IDAV0015
CategoryPerformance
Fix isBreaking

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 IDAV0016
CategoryUsage
Fix isNon-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 IDAV0017
CategoryUsage
Fix isNon-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 IDAV0018
CategoryUsage
Fix isBreaking

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 IDAV0019
CategoryUsage
Fix isBreaking

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 IDAV0020
CategoryStyle
Fix isNon-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 IDAV0021
CategoryUsage
Fix isNon-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 IDAV0022
CategoryUsage
Fix isBreaking

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 IDAV0023
CategoryUsage
Fix isBreaking

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 IDAV0024
CategoryUsage
Fix isNon-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 IDAV0025
CategoryDocumentation
Fix isNon-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 IDAV0026
CategoryUsage
Fix isNon-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 IDAV0027
CategoryUsage
Fix isNon-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 IDAV0028
CategoryUsage
Fix isNon-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 IDAV0029
CategoryUsage
Fix isNon-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 IDAV0030
CategoryUsage
Fix isNon-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 IDAV0031
CategoryUsage
Fix isNon-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:

PackageVersionTFM
Microsoft.AspNet.WebApi.Versioning<= 5.x.xnet45
Microsoft.AspNet.WebApi.Versioning.ApiExplorer<= 5.x.xnet45
Microsoft.AspNet.OData.Versioning<= 5.x.xnet45
Microsoft.AspNet.OData.Versioning.ApiExplorer<= 5.x.xnet45

All new features and platform support will use the Asp.Versioning.* prefix:

PackageVersionTFM
Asp.Versioning.Abstractions6.0.0+net6.0+, netstandard1.0, netstandard2.0
Asp.Versioning.WebApi6.0.0+net45, net472
Asp.Versioning.WebApi.ApiExplorer6.0.0+net45, net472
Asp.Versioning.WebApi.OData6.0.0+net45, net472
Asp.Versioning.WebApi.OData.ApiExplorer6.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) → ApiVersionMetadata
  • GetApiVersionModel() → 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" or rel="sunset". The default implementation can be replaced or extended or you can use the LinkHeaderValue directly 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 MethodSupported
Attribute-based routingYes
Convention-based routingYes
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:

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 URLMatched Controller
/api/helloworld?api-version=1.0HelloWorldController
/api/helloworld?api-version=2.0HelloWorld2Controller
/api/People?api-version=1.0PeopleController
/api/People?api-version=2.0People2Controller

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
    • MediaTypeFormatter to the custom media type
    • MediaTypeFormatter to 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.AssumedDefaultVersionWhenUnspecified is 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 URLMatched ControllerMatched Action
/api/v1/helloworldHelloWorldControllerGet
/api/v2/helloworldHelloWorld2ControllerGet
/api/v3/helloworldHelloWorld2ControllerGetV3
/api/v1/PeoplePeopleControllerGet
/api/v2/PeoplePeople2ControllerGet
/api/v3/PeoplePeople2ControllerGetV3

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 URLMatched ControllerMatched Action
/api/helloworld?api-version=1.0HelloWorldControllerGet
/api/helloworld?api-version=2.0HelloWorld2ControllerGet
/api/helloworld?api-version=3.0HelloWorld2ControllerGetV3
/api/People?api-version=1.0PeopleControllerGet
/api/People?api-version=2.0People2ControllerGet
/api/People?api-version=3.0People2ControllerGetV3

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:

  1. The correct service API version cannot be selected across deployments.
  2. 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:

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 CodeMeaningDescription
400Bad RequestThe API doesn’t support this version
404Not FoundThe API doesn’t exist
501Not ImplementedThe API isn’t implemented

Remarks

Regardless of the configured option, when versioning by:

  • URL segment, 404 is always returned
  • media type, 406 or 415 is 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.0
  • Contoso.Api.v1_1.Controllers → 1.1
  • Contoso.Api.v0_9_Beta.Controllers → 0.9-Beta
  • Contoso.Api.v20180401.Controllers → 2018-04-01
  • Contoso.Api.v2018_04_01.Controllers → 2018-04-01
  • Contoso.Api.v2018_04_01_Beta.Controllers → 2018-04-01-Beta
  • Contoso.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 IApiVersionSelector can 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 types
  • SingleResult<T> response types
  • ODataValue<T> response types
  • Delta<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 ODataModelBuilder via the ModelBuilderFactory
    • Invoke IModelConfiguration.Apply for each item defined in ModelConfigurations, including the DefaultModelConfiguration, with the current model builder and API version
    • Invoke ODataModelBuilder.GetEdmModel to generate the current EDM model
    • Apply the ApiVersionAnnotation with the current API version to the generated EDM model
    • Invoke OnModelCreated with the current model builder and generated EDM model, if defined

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.

TitleUnspecified API version
Typehttps://docs.api-versioning.org/problems#unspecified
Status400
DetailAn API version is required, but was not specified
CodeApiVersionUnspecified

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.

TitleUnsupported API version
Typehttps://docs.api-versioning.org/problems#unsupported
Status4001 2
DetailThe specified API version is not supported
CodeUnsupportedApiVersion

1: Defined by ApiVersioningOptions.UnsupportedApiVersionStatusCode
2: The value is always 404 when 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).

TitleInvalid API version
Typehttps://docs.api-versioning.org/problems#invalid
Status400
DetailAn API version was specified, but it is invalid
CodeInvalidApiVersion

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.

TitleAmbiguous API version
Typehttps://docs.api-versioning.org/problems#ambiguous
Status400
DetailAn API version was specified multiple times with different values
CodeAmbiguousApiVersion

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

NuGet Package

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

NuGet Package

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:

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.

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:

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.

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:

NameParameter TypeData TypeDescription
$selectquerystringLimits the properties returned in the result.
$orderbyquerystringSpecifies the order in which results are returned. The allowed properties are: firstName, lastName.
$topqueryintegerLimits the number of items returned from a collection. The maximum value is 100.
$skipqueryintegerExcludes 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 AllowedQueryOptions is 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:

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:

  • ApiVersionsBaseAttribute
  • ApiVersionAttribute
  • MapToApiVersionAttribute
  • AdvertiseApiVersionsAttribute

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:

  1. You must extend ApiVersion
  2. You must override:
    • GetHashCode
    • CompareTo
    • ToString(string,IFormatProvider)
  3. You must implement IApiVersionParser
    • It may be possible to extend ApiVersionParser depending on your requirements

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:

  • QueryStringApiVersionWriter
  • HeaderApiVersionWriter
  • MediaTypeApiVersionWriter
  • UrlSegmentApiVersionWriter

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:

  1. Log a warning message when an API reports that the version requested is deprecated.
  2. 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.

  • Web API
    • Includes controllers with Web API
    • Includes OpenAPI/Swagger
  • OData
    • Includes controllers with Web API and OData
    • Includes OpenAPI/Swagger