为Web API版本控制定制MapHttpAttributeRoutes

4

我正在实现Web API版本控制,就像Web API Versioning中所述。我的控制器位于两个不同的命名空间中,我使用了自定义的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个回答

11

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

我认为这样做更好,因为它允许所有版本都保持相同的路由。客户端只需在请求中包括一个HTTP头(在本例中是版本号),即可选择版本。

此示例演示了如何使用Attribute Routing和ASP.NET Web API中的约束条件,通过“api-version”HTTP头动态筛选控制器。当路由使用约束条件时,每个约束条件都有机会阻止路由与给定请求匹配。在此示例中,自定义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
}

这看起来是一个不错的解决方案,但不幸的是我必须使用一个查询参数,它不会是一个整数,它将具有“major-version.minor-version”或日期格式。 - nickela125
@user2609051:我认为你可以调整他们的示例,改用查询参数,格式随意。 - Marcel N.

7
这里有一个解决方案,可以让你使用Web API 2版本路由的方式(头部),同时支持查询参数(即使用名为“api-version”的标题或名为“?api-version=XXX”的查询字符串参数)。
HTTP路由约束实现了这一功能:
/// <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" });
    }

}

我在该方法上调用了此属性,并尝试执行一个没有此属性的方法,结果却调用了该属性的方法。 - aj go

0
我扩展了Michael Brown的答案,以允许设置默认版本:
现在我正在考虑如何使其与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://dev59.com/qoLba4cB1Zd3GeqPe3iI#28934352</para>
    /// <para>https://dev59.com/qoLba4cB1Zd3GeqPe3iI</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://dev59.com/qoLba4cB1Zd3GeqPe3iI#28934352</para>
    /// <para>https://dev59.com/qoLba4cB1Zd3GeqPe3iI</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

你是否找到了一种方法使其与Swashbuckle Swagger一起工作?或者,是否有人有链接到一个更新的API版本控制解决方案,该解决方案是兼容的? - jaygeek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接