Skip to content

Versioning by Media Type

Chris Martinez edited this page Dec 29, 2022 · 6 revisions

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:

ASP.NET 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;
    }
}

ASP.NET Core with 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;
    }
}

ASP.NET Core with Minimal APIs

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 );

Configuration

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

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 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.

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; specifically:

  • Mapping
    • MediaTypeFormatter in ASP.NET Web API to the custom media type
    • IInputFormatter or IOutputFormatter 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.

Clone this wiki locally