在我的项目中,我实现了自定义路由约束以允许通过自定义头变量(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组?
答案 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。