Web API versioning

Let's start by saying all API's exposed to the public or to third parties should implement some kind of versioning. Why? API's tend to evolve as the world moves on, so when making changes we don't want to break stuff using our api every time we do so.

Well then, if we need versioning, what is "the right way" of implementing it? Short answer: there is none. Long answer: there are various ways of doing so, but every single one has downsides.

Most common ways of API versioning

There are lots of ways to implement versioning, below are the most common ones.

Host name versioning

In this case the version number is put in the host name:

GET https://v1.awesomeapi.com/foo/bar HTTP/1.1
Host: v1.awesomeapi.com
Accept: application/json

This can be tricky in debug mode if you are accessing raw IP addresses such as 127.0.0.1 or using IIS express to debug your application. But, this method of versioning can be usefull if you want to route incomming requests to different servers as you can create DNS records for different versions of your API.

URL versioning

This is basically putting the version number somewhere in the url:

GET https://awesomeapi.com/v1/foo/bar HTTP/1.1
Host: awesomeapi.com
Accept: application/json

If we concur that the URL should represent the resource, then that would mean every time the data is changed the api version should be changed as well. Changing the data of your resource should not affect the API version.

Query string parameter versioning

The version is put in the querystring like so:

GET https://awesomeapi.com/foo/bar?version=1 HTTP/1.1
Host: awesomeapi.com
Accept: application/json

Query string parameter versioning has pretty much the same issues as URL versioning, but also the risk of someone not specifying it. What happens if someone doesn't? The API might throw an exception or use some kind of default version you wouldn't expect.

Accept header versioning

Accept header versioning is usually done by either adding a parameter:

GET https://awesomeapi.com/foo/bar HTTP/1.1
Host: awesomeapi.com
Accept: application/json; version=1

Or by specifying a custom vendor media type:

GET /foo/bar HTTP/1.1
Host: awesomeapi.com
Accept: application/vnd.producer.product-v1+json

Accept header versioning with a parameter is generally considered as best practice, although some clients may not understand this format.

Using custom media types seems logical because you describe how you'd like the data. But, if you have a lot of entities this approach might cause a lot of kludge which can result in a lot of maintenance overtime. If you consider using custom media types, be sure to include detailed documentation about them so everyone knows how they work and what they represent.

Custom header versioning

And then there is versioning using a custom header:

GET https://awesomeapi.com/foo/bar HTTP/1.1
Host: awesomeapi.com
Accept: application/json
X-Api-Version: 1

Using a custom header is not really a semantic way of describing the resource. The HTTP spec tells us to use the accept header to describe how to present the resource the way we want it to, why reproduce this with a custom header?

Then again on using headers, it could be difficult for certain clients to set them. Imagine a JavaScript client making a JsonP call, in this case it is likely to be a real pain to set a custom header value.

One route constraint to rule them all!

Because we can't really agree on one specific type of versioning to be the best way for all clients out there, i've created a HttpRouteConstraint that supports all of the above with the exception of host name versioning. The full code is available on GitHub, the link is found at the end of this article.

By using route contraints, we can be in control of where requests are being routed to. To be able to do that, we have to create a class that implements IHttpRouteConstraint.

public class VersionConstraint : IHttpRouteConstraint  
{
    private readonly string[] _allowedVersions;

    public VersionConstraint( string allowedVersions )
    {
        _allowedVersions = !string.IsNullOrWhiteSpace( allowedVersions ) ? 
            allowedVersions.Split( new[] { ',' }, StringSplitOptions.RemoveEmptyEntries ).Select( x => x.Trim() ).ToArray();
    }

    public bool Match( HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary values, HttpRouteDirection routeDirection )
    {
        if( routeDirection != HttpRouteDirection.UriResolution ) return true;

        // Do some matching versions overhere
    }
}

The version constraint takes an allowedVersions parameter to specify which api versions should be allowed to use. Now for URL versioning, the default route should be something like this:

// Allow version to be part of the url  
config.Routes.MapHttpRoute(  
    name: "VersionedApi",
    routeTemplate: "api/{version}/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional },
    constraints: new { version = new VersionConstraint("1,2") }
);

Where the version parameter is used in the route constraint we just made. Matching the version from the URL is fairly easy by adding this to the match method:

public bool Match( HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary values, HttpRouteDirection routeDirection )  
{
    if( routeDirection != HttpRouteDirection.UriResolution ) return true;

    if( !values.ContainsKey( parameterName ) ) return false;

    return allowedVersions.Contains( values[parameterName].ToLower().TrimStart( 'v' ), StringComparer.InvariantCultureIgnoreCase );
}

To get the version from a query string parameter, a little more code is required. So i've created the following function:

private static string GetVersionFromQueryString( HttpRequestMessage request, string key )  
{
    // Get version parameter value from querystring
    return request.GetQueryNameValuePairs().Any( x => x.Key.Equals( key ) ) ? 
        request.GetQueryNameValuePairs().First( x => x.Key.Equals( key, StringComparison.InvariantCultureIgnoreCase ) ).Value : 
        null;
}

And added it to the match function:

public bool Match( HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary values, HttpRouteDirection routeDirection )  
{
    if( routeDirection != HttpRouteDirection.UriResolution ) return true;

    var version = values.ContainsKey( parameterName ) ? 
        (string) values[parameterName].ToLower().TrimStart( 'v' ) :
        GetVersionFromQueryString( request, "version" );

    return allowedVersions.Contains( version, StringComparer.InvariantCultureIgnoreCase );
}

The method checks if a query string parameter version exists, and returns the value if so. You,ve probably noticed i've made the match function prefer URL versioning over query string versioning. I will be doing the same with the other implementations as well.

Now for parsing the accept header for accept header versioning, the following function is used:

private static string GetVersionFromAcceptHeader( HttpRequestMessage request, string paramName )  
{
    if ( !request.Headers.Accept.Any() ) return null;

    foreach ( var acceptHeader in request.Headers.Accept )
    {
        // Support for application/json; [paramName]=[VERSION]
        if ( acceptHeader.Parameters.Any( x => x.Name.Equals( paramName, StringComparison.InvariantCultureIgnoreCase ) ) )
            return acceptHeader.Parameters.First( x => x.Name.Equals( paramName, StringComparison.InvariantCultureIgnoreCase ) ).Value;
        // Support for application/vnd.yourvendorname-v[VERSION]+json
        if ( !string.IsNullOrEmpty( acceptHeader.MediaType ) && 
              Regex.IsMatch( acceptHeader.MediaType, @"application\/vnd\..*-v([\d]+(\.\d+)*)\+(json|xml)" ) )
            return Regex.Match( acceptHeader.MediaType, @"(?<=-v)[\d]+(\.\d+)*" ).Value;
    }

    return null;
}

The function will match application/json; version=1 as well as application/vnd.awesomeapi-v1+json. Again added it to the match method:

public bool Match( HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary values, HttpRouteDirection routeDirection )  
{
    if( routeDirection != HttpRouteDirection.UriResolution ) return true;

    var version = values.ContainsKey( parameterName ) ? 
        (string) values[parameterName].ToLower().TrimStart( 'v' ) :
        GetVersionFromQueryString( request, "version" ) ??
        GetVersionFromAcceptHeader( request, "version" );

    return allowedVersions.Contains( version, StringComparer.InvariantCultureIgnoreCase );
}

Last but not least, the custom header versioning uses the following function:

private static string GetVersionFromCustomHeader( HttpRequestMessage request, string name )  
{
    // Get version from custom header
    IEnumerable keys;
    return !request.Headers.TryGetValues( name, out keys ) ? null : keys.First();
}

Added to the match method:

public bool Match( HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary values, HttpRouteDirection routeDirection )  
{
    if( routeDirection != HttpRouteDirection.UriResolution ) return true;

    var version = values.ContainsKey( parameterName ) ? 
        (string) values[parameterName].ToLower().TrimStart( 'v' ) :
        GetVersionFromQueryString( request, "version" ) ??
        GetVersionFromAcceptHeader( request, "version" ) ??
        GetVersionFromCustomHeader( request, "X-Api-Version" );

    return allowedVersions.Contains( version, StringComparer.InvariantCultureIgnoreCase );
}

There you have it, one route contstraint that supports the four most common implementations of versioning. But wait! What about a default version? If no version is supplied you probably would want to force a default version upon your client. One way of doing this is, is by creating an AppSetting with de default version in your web.config and using that value in our VersionedRouteConstraint:

<appSettings>  
  <add key="DefaultApiVersion" value="1" />
</appSettings>

I would recommend using the lowest version of your API available as the default version. This version is the only version guaranteed not to break when changes are pushed.

Now to use the AppSetting in our RouteConstraint, i've created a private property that gets the value from the config and defaults to 1 if no setting is found:

private static string DefaultVersion  
{
    get
    {
        var defaultVersion = ConfigurationManager.AppSettings["DefaultApiVersion"];
        return !string.IsNullOrWhiteSpace( defaultVersion ) ? defaultVersion : "1";
    }
}

Added to the match method:

public bool Match( HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary values, HttpRouteDirection routeDirection )  
{
    if( routeDirection != HttpRouteDirection.UriResolution ) return true;

    var version = values.ContainsKey( parameterName ) ? 
        (string) values[parameterName].ToLower().TrimStart( 'v' ) :
        GetVersionFromQueryString( request, "version" ) ??
        GetVersionFromAcceptHeader( request, "version" ) ??
        GetVersionFromCustomHeader( request, "X-Api-Version" ) ??
        DefaultVersion;

    return allowedVersions.Contains( version, StringComparer.InvariantCultureIgnoreCase );
}

Now that all that is done, what if we want to route a specific version to a specific controller action. We can't, because we don't have a RouteFactoryAttribute that uses our newly made route constraint. So we have to make one:

public class VersionedRoute : RouteFactoryAttribute  
{
    private readonly string _allowedVersions;

    public VersionedRoute( string template, string allowedVersions ) : base( template )
    {
        _allowedVersions = allowedVersions;
    }

    public override IDictionary Constraints
    {
        get
        {
            var constraints = new HttpRouteValueDictionary {{"version", new VersionConstraint( _allowedVersions )}};
            return constraints;
        }
    }
}

Now we can do awesome stuff like this to route the request using the version constraint:

[VersionedRoute( "api/product/{id}", "v1" )]  
public Product Get( int id ) {  
    // Return some product for API v1
}

There is one but, when using custom media types with accept header versioning. When your API supports multiple formatters, we must make them aware of our custom media types so the appropriate formatter is used to serialize the response.

To make sure JSON is outputted when using the accept header application/vnd.webapiversioning-v1+json and XML is outputted when using application/vnd.webapiversioning-v1+xml using the default formatters, the media types must be added to the corresponding formatters in the Global.asax:

GlobalConfiguration.Configuration.Formatters.JsonFormatter.SupportedMediaTypes.Add(  
    new MediaTypeHeaderValue( "application/vnd.webapiversioning-v1+json" ) );
GlobalConfiguration.Configuration.Formatters.XmlFormatter.SupportedMediaTypes.Add(  
    new MediaTypeHeaderValue( "application/vnd.webapiversioning-v1+xml" ) );

GlobalConfiguration.Configuration.Formatters.JsonFormatter.SupportedMediaTypes.Add(  
    new MediaTypeHeaderValue( "application/vnd.webapiversioning-v2+json" ) );
GlobalConfiguration.Configuration.Formatters.XmlFormatter.SupportedMediaTypes.Add(  
    new MediaTypeHeaderValue( "application/vnd.webapiversioning-v2+xml" ) );

Remember that you have to do this for every version your API supports. However, these media types are not typed and do not really describe the represented resource. To be able to create typed formatters, i've extended JsonMediaTypeFormatter and XmlMediaTypeFormatter:

public class TypedXmlMediaTypeFormatter : XmlMediaTypeFormatter  
{
    private readonly Type _resourceType;

    public TypedXmlMediaTypeFormatter( Type resourceType, MediaTypeHeaderValue mediaType )
    {
        _resourceType = resourceType;
        SupportedMediaTypes.Clear();
        SupportedMediaTypes.Add( mediaType );
    }

    public override bool CanReadType( Type type )
    {
        return _resourceType == type || _resourceType == type.GetEnumerableType();
    }

    public override bool CanWriteType( Type type )
    {
        return _resourceType == type || _resourceType == type.GetEnumerableType();
    }
}

public class TypedJsonMediaTypeFormatter : JsonMediaTypeFormatter  
{
    private readonly Type _resourceType;

    public TypedJsonMediaTypeFormatter( Type resourceType, MediaTypeHeaderValue mediaType )
    {
        _resourceType = resourceType;
        SupportedMediaTypes.Clear();
        SupportedMediaTypes.Add( mediaType );
    }

    public override bool CanReadType( Type type )
    {
        return _resourceType == type || _resourceType == type.GetEnumerableType();
    }

    public override bool CanWriteType( Type type )
    {
        return _resourceType == type || _resourceType == type.GetEnumerableType();
    }
}

And then you are able to created typed formatters like this in the Global.asax:

// Add typed formatters  
GlobalConfiguration.Configuration.Formatters.Add(  
    new TypedJsonMediaTypeFormatter( typeof( Product ),
        new MediaTypeHeaderValue( "application/vnd.vendor.product-v1.0+json" ) ) );
GlobalConfiguration.Configuration.Formatters.Add(  
    new TypedJsonMediaTypeFormatter( typeof( ProductV2 ),
        new MediaTypeHeaderValue( "application/vnd.vendor.product-v2.0+json" ) ) );

GlobalConfiguration.Configuration.Formatters.Add(  
    new TypedXmlMediaTypeFormatter( typeof( Product ),
        new MediaTypeHeaderValue( "application/vnd.vendor.product-v1.0+xml" ) ) );
GlobalConfiguration.Configuration.Formatters.Add(  
    new TypedXmlMediaTypeFormatter( typeof( ProductV2 ),
        new MediaTypeHeaderValue( "application/vnd.vendor.product-v2.0+xml" ) ) );

Conclusion

If you have the luxury of being in control over the API and the client consuming it, you can choose one type of versioning that suits best for both ends. If you are not, you probably want to pick two or three variants to widely support all clients out there.

Check out the full source at GitHub.