获取Web API帮助页面以使用自定义路由约束

时间:2016-03-29 20:03:22

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

在我的项目中,我实现了自定义路由约束以允许通过自定义头变量(api-version)进行API版本控制,类似于this sample project on Codeplex,尽管我修改了约束以允许major.minor约定。

这涉及创建两个独立的控制器,其路由通过FullVersionedRoute属性区分:

Sample1Controller.cs

/// <summary>
/// v1.0 Controller
/// </summary>
public class Sample1Controller : ApiController
{
    [FullVersionedRoute("api/test", "1.0")]
    public IEnumerable<string> Get()
    {
        return new[] { "This is version 1.0 test!" };
    }
}

Sample2Controller.cs

/// <summary>
/// v2.0 Controller
/// </summary>
public class Sample2Controller : ApiController
{
    [FullVersionedRoute("api/test", "2.0")]
    public IEnumerable<string> Get()
    {
        return new[] { "This is version 2.0 test!" };
    }
}

FullVersionedRoute.cs

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

namespace HelperClasses.Versioning
{
    /// <summary>
    /// Provides an attribute route that's restricted to a specific version of the api.
    /// </summary>
    internal class FullVersionedRoute : RouteFactoryAttribute
    {
        public FullVersionedRoute(string template, string allowedVersion) : base(template)
        {
            AllowedVersion = allowedVersion;
        }

        public string AllowedVersion
        {
            get;
            private set;
        }

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

FullVersionConstraint.cs

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

namespace HelperClasses.Versioning
{
    /// <summary>
    /// A Constraint implementation that matches an HTTP header against an expected version value.
    /// </summary>
    internal class FullVersionConstraint : IHttpRouteConstraint
    {
        public const string VersionHeaderName = "api-version";

        private const string DefaultVersion = "1.0";

        public FullVersionConstraint(string allowedVersion)
        {
            AllowedVersion = allowedVersion;
        }

        public string AllowedVersion
        {
            get;
            private set;
        }

        public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection)
        {
            if (routeDirection == HttpRouteDirection.UriResolution)
            {
                var version = GetVersionHeader(request) ?? DefaultVersion;
                return (version == AllowedVersion);
            }

            return false;
        }

        private string GetVersionHeader(HttpRequestMessage request)
        {
            IEnumerable<string> headerValues;

            if (request.Headers.TryGetValues(VersionHeaderName, out headerValues))
            {
                // enumerate the list once
                IEnumerable<string> headers = headerValues.ToList();

                // if we find once instance of the target header variable, return it
                if (headers.Count() == 1)
                {
                    return headers.First();
                }
            }

            return null;
        }
    }
}

这很好用,但是auto-generated help files无法区分两个控制器中的动作,因为它们看起来像是相同的路径(如果你只注意网址路径,那就是它通过默认)。因此,Sample2Controller.cs中的操作会覆盖Sample1Controller.cs中的操作,因此只有Sample2 API显示在帮助页面上。

有没有办法配置Web API帮助页面包以识别自定义约束并识别出有两个独立的API,然后在帮助页面上将它们显示为单独的API组?

1 个答案:

答案 0 :(得分:1)

我发现this article描述了如何通过实现IApiExplorer来实现这一点。

简而言之,您要做的是添加一个新的VersionedApiExplorer类,实现IApiExplorer,如此

using System;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Description;
using System.Web.Http.Routing;

namespace HelperClasses.Versioning
{
    public class VersionedApiExplorer<TVersionConstraint> : IApiExplorer
    {
        private IApiExplorer _innerApiExplorer;
        private HttpConfiguration _configuration;
        private Lazy<Collection<ApiDescription>> _apiDescriptions;
        private MethodInfo _apiDescriptionPopulator;

        public VersionedApiExplorer(IApiExplorer apiExplorer, HttpConfiguration configuration)
        {
            _innerApiExplorer = apiExplorer;
            _configuration = configuration;
            _apiDescriptions = new Lazy<Collection<ApiDescription>>(
                new Func<Collection<ApiDescription>>(Init));
        }

        public Collection<ApiDescription> ApiDescriptions
        {
            get { return _apiDescriptions.Value; }
        }

        private Collection<ApiDescription> Init()
        {
            var descriptions = _innerApiExplorer.ApiDescriptions;

            var controllerSelector = _configuration.Services.GetHttpControllerSelector();
            var controllerMappings = controllerSelector.GetControllerMapping();

            var flatRoutes = FlattenRoutes(_configuration.Routes);
            var result = new Collection<ApiDescription>();

            foreach (var description in descriptions)
            {
                result.Add(description);

                if (controllerMappings != null && description.Route.Constraints.Any(c => c.Value is TVersionConstraint))
                {
                    var matchingRoutes = flatRoutes.Where(r => r.RouteTemplate == description.Route.RouteTemplate && r != description.Route);

                    foreach (var route in matchingRoutes)
                        GetRouteDescriptions(route, result);
                }
            }
            return result;
        }

        private void GetRouteDescriptions(IHttpRoute route, Collection<ApiDescription> apiDescriptions)
        {
            var actionDescriptor = route.DataTokens["actions"] as IEnumerable<HttpActionDescriptor>;

            if (actionDescriptor != null && actionDescriptor.Count() > 0)
                GetPopulateMethod().Invoke(_innerApiExplorer, new object[] { actionDescriptor.First(), route, route.RouteTemplate, apiDescriptions });
        }

        private MethodInfo GetPopulateMethod()
        {
            if (_apiDescriptionPopulator == null)
                _apiDescriptionPopulator = _innerApiExplorer.GetType().GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).FirstOrDefault(
                   m => m.Name == "PopulateActionDescriptions" && m.GetParameters().Length == 4);

            return _apiDescriptionPopulator;
        }

        public static IEnumerable<IHttpRoute> FlattenRoutes(IEnumerable<IHttpRoute> routes)
        {
            var flatRoutes = new List<HttpRoute>();

            foreach (var route in routes)
            {
                if (route is HttpRoute)
                    yield return route;

                var subRoutes = route as IReadOnlyCollection<IHttpRoute>;
                if (subRoutes != null)
                    foreach (IHttpRoute subRoute in FlattenRoutes(subRoutes))
                        yield return subRoute;
            }
        }
    }
}

然后将其添加到您的WebAPIConfig

var apiExplorer = config.Services.GetApiExplorer();
config.Services.Replace(typeof(IApiExplorer), new VersionedApiExplorer<FullVersionConstraint>(apiExplorer, config));

然后,您应该在Web API帮助页面上看到Sample1和Sample2 API。