为Web Api版本控制自定义MapHttpAttributeRoutes

时间:2014-08-14 04:06:04

标签: c# asp.net-mvc api asp.net-web-api routing

我正在Web API Versioning中实现Web API版本控制。我的控制器位于2个独立的命名空间中,我使用自定义SelectController方法根据查询参数选择要使用的版本。 例如

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

这一切都运行正常,但控制器中的某些操作使用Route属性

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

默认使用

映射到正确的控制器
config.MapHttpAttributeRoutes();
WebApiConfig.cs中的

如果我有多个版本的API使用相同的路由,这将无效。我是否能够为config.MapHttpAttributeRoutes()提供自定义实现,以便我可以选择要使用的API的正确版本,还是有更好的方法来实现这一目标?

3 个答案:

答案 0 :(得分:11)

在官方WebApi 2.1 examplex on Codeplex中有一个例子。 它依赖于请求标头值来存储版本。

我认为它更好,因为它允许所有版本的路由保持不变。客户端只需在请求中包含HTTP标头(在本例中为版本号)即可选择版本。

  

此示例显示如何使用属性路由和约束   ASP.NET Web API通过'api-version'动态过滤控制器   HTTP标头。当路由使用约束时,每个约束都有一个   有可能阻止路由匹配给定的请求。在这   sample,自定义RouteFactoryAttribute(VersionedRoute)添加一个   对每个属性路由的约束。

...

  

自定义约束实现(VersionConstraint)是   基于匹配整数的'api-version'的值实现   值。提供了约束的允许版本的值   通过放置在每个控制器上的VersionedRoute属性。当一个   请求进来,'api-version'的标头值匹配   预期的版本。此示例使用标头但使用约束   实现可以使用任何标准来决定请求是否   对路线有效。

无论如何,最终结果看起来像这样:

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

答案 1 :(得分:5)

这是一个解决方案,除了查询参数支持之外,还允许您使用Web API 2版本化路由(标题)(即使用名为' api-version'或查询字符串参数的标题命名'?api-version = XXX'。

HTTP Route约束可以完成工作:

/// <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;
    }
}

路线工厂:

/// <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;
        }
    }

}

用法:

[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" });
    }

}

答案 2 :(得分:0)

我扩展了Michael Brown's answer以允许设置默认版本:

仅现在我在考虑如何使其与Swashbuckle招摇工具一起使用。

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://stackoverflow.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://stackoverflow.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;
        }
    }
}

用法(可以在控制器或操作方法上使用):

#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