-
Notifications
You must be signed in to change notification settings - Fork 704
Versioning by Media Type
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:
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;
}
}
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;
}
}
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 );
ASP.NET Web API and ASP.NET Core would 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
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
.
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.
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" );
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 requirments 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.
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; specifically:
- Mapping
-
MediaTypeFormatter
in ASP.NET Web API to the custom media type -
IInputFormatter
orIOutputFormatter
in ASP.NET Core to the custom media type
-
- OpenAPI
- Listing all of the consumes media types
- Listing all of the produces media types
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.
- Home
- Quick Starts
- Version Format
- Version Discovery
- Version Policies
- How to Version Your Service
- API Versioning with OData
- Configuring Your Application
- Error Responses
- API Documentation
- Extensions and Customizations
- Known Limitations
- FAQ
- Examples