4
votes

I am implementing Web API versioning as in Web API Versioning. My controllers are in 2 separate namespaces, and I've used a custom SelectController method to choose which version to use based on a a query parameter. e.g.

http://myapi/api/values?version=1.0

This all works fine but some actions in the controllers use the Route attribute

[Route("api/values/getNames")]
public HttpResponseMessage Get() { ... }

Which are mapped to the correct controller by default using

config.MapHttpAttributeRoutes();

in WebApiConfig.cs

This will not work if I have multiple versions of the API with the same route. Am I able to provide a custom implementation for config.MapHttpAttributeRoutes() so I can select the correct version of the API to use, or is there a better way of doing this?

3

3 Answers

11
votes

There is an example for this in the official WebApi 2.1 examplex on Codeplex. It relies on a request header value to store the version.

I think it's a lot nicer, since it allows the routes to stay the same for all versions. Clients select the version simply by including an HTTP header in the request (in this case the version number).

This sample shows how to use Attribute Routing and Constraints in ASP.NET Web API to dynamically filter controllers by an 'api-version' HTTP header. When a route uses Constraints, each constraint has a chance to prevent the route from matching a given request. In this sample, a custom RouteFactoryAttribute (VersionedRoute) adds a constraint to each attribute route.

...

The custom constraint implementation (VersionConstraint) is implemented based on the value of 'api-version' matching an integer value. The value of the allowed version for the constraint is provided by the VersionedRoute attribute placed on each controller. When a request comes in, the header value of 'api-version' is matched against the expected version. This example uses a header but a constraint implementation could use any criteria to decided if the request is valid for the route.

Anyway, the end result would end up looking like this:

[VersionedRoute("api/Customer", 1)]
public class CustomerVersion1Controller : ApiController
{
    // controller code goes here
}
[VersionedRoute("api/Customer", 2)]
public class CustomerVersion2Controller : ApiController
{
    // controller code goes here
}
7
votes

Here is a solution that will let you use the Web API 2 way of versioned routes (headers), in addition to query parameter support (i.e. use a header called 'api-version' or a querystring parameter named '?api-version=XXX'.

The HTTP Route constraint does the work:

/// <summary>
/// Add a route constraint to detect version header or by query string
/// </summary>
public class RouteVersionHttpConstraint : IHttpRouteConstraint
{
    public const string VersionHeaderName = "api-version";
    private const int DefaultVersion = 1;
    /// <summary>
    /// Add a route constraint to detect version header or by query string
    /// </summary>
    /// <param name="allowedVersion"></param>
    public RouteVersionHttpConstraint(int allowedVersion)
    {
        AllowedVersion = allowedVersion;
    }

    public int AllowedVersion
    {
        get;
        private set;
    }

    /// <summary>
    /// Perform the controller match
    /// </summary>
    /// <param name="request"></param>
    /// <param name="route"></param>
    /// <param name="parameterName"></param>
    /// <param name="values"></param>
    /// <param name="routeDirection"></param>
    /// <returns></returns>
    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection)
    {
        if (routeDirection == HttpRouteDirection.UriResolution)
        {
            int version = GetVersionHeaderOrQuery(request) ?? DefaultVersion;
            if (version == AllowedVersion)
            {
                return true;
            }
        }
        return false;
    }

    /// <summary>
    /// Check the request header, and the query string to determine if a version number has been provided
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    private int? GetVersionHeaderOrQuery(HttpRequestMessage request)
    {
        string versionAsString;
        IEnumerable<string> headerValues;
        if (request.Headers.TryGetValues(VersionHeaderName, out headerValues) && headerValues.Count() == 1)
        {
            versionAsString = headerValues.First();
            int version;
            if (versionAsString != null && Int32.TryParse(versionAsString, out version))
            {
                return version;
            }
        }
        else
        {
            var query = System.Web.HttpUtility.ParseQueryString(request.RequestUri.Query);
            string versionStr = query[VersionHeaderName];
            int version = 0;
            int.TryParse(versionStr, out version);
            if (version > 0)
                return version;
        }
        return null;
    }
}

And the route factory:

/// <summary>
/// Versioning support for the WebAPI controllers
/// </summary>
public class RouteVersionAttribute : RouteFactoryAttribute
{
    public int Version { get; private set; }

    public RouteVersionAttribute() : this(null, 1) 
    { 
    }
    /// <summary>
    /// Specify a version for the WebAPI controller
    /// </summary>
    /// <param name="version"></param>
    public RouteVersionAttribute(int version) : this(null, version)
    {
    }

    public RouteVersionAttribute(string template, int version)
        : base(template)
    {
        Version = version;
    }

    public override IDictionary<string, object> Constraints
    {
        get
        {
            var constraints = new HttpRouteValueDictionary();
            constraints.Add("version", new RouteVersionHttpConstraint(Version));
            return constraints;
        }
    }

    public override IDictionary<string, object> Defaults
    {
        get
        {
            var defaults = new HttpRouteValueDictionary();
            defaults.Add("version", 1);
            return defaults;
        }
    }

}

Usage:

[RouteVersion("api/versiontest", 1)]
public class Version1TestController : BaseApiController
{
    // get: api/versiontest
    [HttpGet]
    public HttpResponseMessage get()
    {
        return Request.CreateResponse(HttpStatusCode.OK, new { Version = "API Version 1 selected" });
    }

}

[RouteVersion("api/versiontest", 2)]
public class Version2TestController : ApiController
{
    // get: api/versiontest
    [HttpGet]
    public HttpResponseMessage get()
    {
        return Request.CreateResponse(HttpStatusCode.OK, new { Version = "API Version 2 selected" });
    }

}
0
votes

I extended Michael Brown's answer to allow setting a default version:

Only now I'm thinking how to make it work with Swashbuckle swagger.

RouteVersionAttribute:

using System.Collections.Generic;
using System.Web.Http.Routing;

namespace YourNameSpace.Filters
{
    /// <summary>
    /// Here is a solution that will let you use the Web API 2 way of versioned routes (headers),
    /// in addition to query parameter support (i.e.use a header called 'api-version' or 
    /// a querystring parameter named '?api-version=XXX'.
    /// <para>https://stackoverflow.com/a/28934352/3187389</para>
    /// <para>https://stackguides.com/questions/25299889/customize-maphttpattributeroutes-for-web-api-versioning</para>
    /// </summary>
    public class RouteVersionAttribute : RouteFactoryAttribute
    {
        public int Version { get; private set; }
        public int VersionDefault { get; private set; }

        public RouteVersionAttribute() : this(null, 1, true)
        {
        }

        /// <summary>
        /// Specify a version for the WebAPI controller or an action method
        /// for example: [RouteVersion("Test", 1)] or [RouteVersion("Test", 1, true)]
        /// </summary>
        /// <param name="version"></param>
        /// <param name="isDefault"></param>
        public RouteVersionAttribute(int version, bool isDefault = false) : this(null, version, isDefault)
        {
        }

        /// <summary>
        /// Specify a version for the WebAPI controller or an action method
        /// for example: [RouteVersion("Test", 1)] or [RouteVersion("Test", 1, true)]
        /// </summary>
        /// <param name="template"></param>
        /// <param name="version"></param>
        /// <param name="isDefault"></param>
        public RouteVersionAttribute(string template, int version, bool isDefault = false)
            : base(template)
        {
            Version = version;
            if (isDefault)
                VersionDefault = version;
        }

        public override IDictionary<string, object> Constraints
        {
            get
            {
                var constraints = new HttpRouteValueDictionary();
                constraints.Add("version", new RouteVersionHttpConstraint(Version, VersionDefault));
                return constraints;
            }
        }

        public override IDictionary<string, object> Defaults
        {
            get
            {
                var defaults = new HttpRouteValueDictionary();
                defaults.Add("version", VersionDefault);
                return defaults;
            }
        }
    }
}

RouteVersionHttpConstraint:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Web.Http.Routing;

namespace Boyd.Core.Filters
{
    /// <summary>
    /// Here is a solution that will let you use the Web API 2 way of versioned routes (headers),
    /// in addition to query parameter support (i.e.use a header called 'api-version' or 
    /// a querystring parameter named '?api-version=XXX'.
    /// <para>https://stackoverflow.com/a/28934352/3187389</para>
    /// <para>https://stackguides.com/questions/25299889/customize-maphttpattributeroutes-for-web-api-versioning</para>
    /// </summary>
    public class RouteVersionHttpConstraint : IHttpRouteConstraint
    {
        public const string VersionHeaderName = "api-version";
        private readonly int VersionDefault = 1;

        /// <summary>
        /// Add a route constraint to detect version header or by query string
        /// </summary>
        /// <param name="allowedVersion"></param>
        public RouteVersionHttpConstraint(int allowedVersion, int versionDefault)
        {
            AllowedVersion = allowedVersion;
            VersionDefault = versionDefault;
        }

        public int AllowedVersion
        {
            get;
            private set;
        }

        /// <summary>
        /// Perform the controller match
        /// </summary>
        /// <param name="request"></param>
        /// <param name="route"></param>
        /// <param name="parameterName"></param>
        /// <param name="values"></param>
        /// <param name="routeDirection"></param>
        /// <returns></returns>
        public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection)
        {
            if (routeDirection == HttpRouteDirection.UriResolution)
            {
                int version = GetVersionHeaderOrQuery(request) ?? VersionDefault;
                if (version == AllowedVersion)
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Check the request header, and the query string to determine if a version number has been provided
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        private int? GetVersionHeaderOrQuery(HttpRequestMessage request)
        {
            string versionAsString;
            if (request.Headers.TryGetValues(VersionHeaderName, out IEnumerable<string> headerValues) 
                && headerValues.Count() == 1)
            {
                versionAsString = headerValues.First();
                if (versionAsString != null && Int32.TryParse(versionAsString, out int version))
                {
                    return version;
                }
            }
            else
            {
                var query = System.Web.HttpUtility.ParseQueryString(request.RequestUri.Query);
                string versionStr = query[VersionHeaderName];
                int.TryParse(versionStr, out int version);
                if (version > 0)
                    return version;
            }
            return null;
        }
    }
}

Usage (can be used on the controller or action methods):

#region Temporary Tests

// {{BaseUrl}}Test?api-version=1

[HttpGet]
[RouteVersion("Test", 1)]
public async Task<IHttpActionResult> Test1([FromBody]GetCustomerW2GsForPropertyRequest request)
{
    return await Task.FromResult(Ok("API Version 1 selected"));
}

[HttpGet]
[RouteVersion("Test", 2)]
[RouteVersion("Test", 3)]
[RouteVersion("Test", 4)]
public async Task<IHttpActionResult> Test4([FromBody]GetCustomerW2GsForPropertyRequest request)
{
    return await Task.FromResult(Ok("API Version 2, 3 or 4 selected"));
}

[HttpGet]
[RouteVersion("Test", 5, true)]
public async Task<IHttpActionResult> Test5([FromBody]GetCustomerW2GsForPropertyRequest request)
{
    return await Task.FromResult(Ok("API Version 5 selected"));
}

#endregion Temporary Tests