ASP.NET MVC 4 不支持跨区域划分 Web API 控制器。
您可以将 WebApi 控制器放置在不同区域的不同 Api 文件夹中,但 ASP.NET MVC 会将它们视为都在同一个位置。
幸运的是,您可以通过覆盖 ASP.NET MVC 基础结构的一部分来克服这个限制。有关限制和解决方案的更多信息,请阅读我的博文“ ASP.NET MVC 4 RC:让 WebApi 和区域发挥得很好”。如果您只对解决方案感兴趣,请继续阅读:
步骤 1. 让您的路线了解区域
将以下扩展方法添加到您的 ASP.NET MVC 应用程序并确保它们可以从您的 AreaRegistration 类中访问:
public static class AreaRegistrationContextExtensions
{
public static Route MapHttpRoute(this AreaRegistrationContext context, string name, string routeTemplate)
{
return context.MapHttpRoute(name, routeTemplate, null, null);
}
public static Route MapHttpRoute(this AreaRegistrationContext context, string name, string routeTemplate, object defaults)
{
return context.MapHttpRoute(name, routeTemplate, defaults, null);
}
public static Route MapHttpRoute(this AreaRegistrationContext context, string name, string routeTemplate, object defaults, object constraints)
{
var route = context.Routes.MapHttpRoute(name, routeTemplate, defaults, constraints);
if (route.DataTokens == null)
{
route.DataTokens = new RouteValueDictionary();
}
route.DataTokens.Add("area", context.AreaName);
return route;
}
}
要使用新的扩展方法,请Routes
从调用链中删除该属性:
context.MapHttpRoute( /* <-- .Routes removed */
name: "Administration_DefaultApi",
routeTemplate: "Administration/api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
步骤 2. 使 Web API 控制器选择器区域感知
将以下类添加到您的 ASP.NET MVC 应用程序并确保它可以从 Global.asax 访问
namespace MvcApplication.Infrastructure.Dispatcher
{
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Dispatcher;
public class AreaHttpControllerSelector : DefaultHttpControllerSelector
{
private const string AreaRouteVariableName = "area";
private readonly HttpConfiguration _configuration;
private readonly Lazy<ConcurrentDictionary<string, Type>> _apiControllerTypes;
public AreaHttpControllerSelector(HttpConfiguration configuration)
: base(configuration)
{
_configuration = configuration;
_apiControllerTypes = new Lazy<ConcurrentDictionary<string, Type>>(GetControllerTypes);
}
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
return this.GetApiController(request);
}
private static string GetAreaName(HttpRequestMessage request)
{
var data = request.GetRouteData();
if (data.Route.DataTokens == null)
{
return null;
}
else
{
object areaName;
return data.Route.DataTokens.TryGetValue(AreaRouteVariableName, out areaName) ? areaName.ToString() : null;
}
}
private static ConcurrentDictionary<string, Type> GetControllerTypes()
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
var types = assemblies
.SelectMany(a => a
.GetTypes().Where(t =>
!t.IsAbstract &&
t.Name.EndsWith(ControllerSuffix, StringComparison.OrdinalIgnoreCase) &&
typeof(IHttpController).IsAssignableFrom(t)))
.ToDictionary(t => t.FullName, t => t);
return new ConcurrentDictionary<string, Type>(types);
}
private HttpControllerDescriptor GetApiController(HttpRequestMessage request)
{
var areaName = GetAreaName(request);
var controllerName = GetControllerName(request);
var type = GetControllerType(areaName, controllerName);
return new HttpControllerDescriptor(_configuration, controllerName, type);
}
private Type GetControllerType(string areaName, string controllerName)
{
var query = _apiControllerTypes.Value.AsEnumerable();
if (string.IsNullOrEmpty(areaName))
{
query = query.WithoutAreaName();
}
else
{
query = query.ByAreaName(areaName);
}
return query
.ByControllerName(controllerName)
.Select(x => x.Value)
.Single();
}
}
public static class ControllerTypeSpecifications
{
public static IEnumerable<KeyValuePair<string, Type>> ByAreaName(this IEnumerable<KeyValuePair<string, Type>> query, string areaName)
{
var areaNameToFind = string.Format(CultureInfo.InvariantCulture, ".{0}.", areaName);
return query.Where(x => x.Key.IndexOf(areaNameToFind, StringComparison.OrdinalIgnoreCase) != -1);
}
public static IEnumerable<KeyValuePair<string, Type>> WithoutAreaName(this IEnumerable<KeyValuePair<string, Type>> query)
{
return query.Where(x => x.Key.IndexOf(".areas.", StringComparison.OrdinalIgnoreCase) == -1);
}
public static IEnumerable<KeyValuePair<string, Type>> ByControllerName(this IEnumerable<KeyValuePair<string, Type>> query, string controllerName)
{
var controllerNameToFind = string.Format(CultureInfo.InvariantCulture, ".{0}{1}", controllerName, AreaHttpControllerSelector.ControllerSuffix);
return query.Where(x => x.Key.EndsWith(controllerNameToFind, StringComparison.OrdinalIgnoreCase));
}
}
}
DefaultHttpControllerSelector
通过将以下行添加到Application_Start
Global.asax 中的方法来覆盖。
GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector), new AreaHttpControllerSelector(GlobalConfiguration.Configuration));
恭喜,您的 Web API 控制器现在将像您的普通 MVC 控制器一样尊重您所在区域的规则!
更新:2012 年 9 月 6 日
一些开发人员就他们遇到DataTokens
的路由变量属性为 的场景与我联系null
。我的实现假定该DataTokens
属性始终被初始化,并且如果该属性为null
. 此行为很可能是由 ASP.NET MVC 框架中的最新更改引起的,实际上可能是框架中的错误。我已经更新了我的代码来处理这种情况。