为什么我的区域特定Web API可以从所有其他区域访问?

我目前正在开发一个ASP.NET MVC 4 Web应用程序项目,该项目必须遵循以下设计决策:

  • 主MVC应用程序驻留在解决方案的根目录中。
  • 所有管理员function都位于单独的区域中。
  • 每个外部方(例如供应商)都有自己的区域。
  • 每个区域,包括根,构成一个分离良好的function块。 一个区域的function可能不会暴露给另一个区域。 这是为了防止未经授权的数据访问。
  • 每个区域(包括根)都有自己的RESTfull API(Web API)。

所有区域中的所有常规控制器(包括根)都按预期工作。 但是,我的一些Web API控制器出现意外行为。 例如,有两个具有相同名称但位于不同区域的Web API控制器会产生以下exception:

找到了多个匹配名为“clients”的控制器的类型。 如果为此请求提供服务的路由(’api / {controller} / {id}’)发现多个控制器定义了相同名称但名称不同的命名空间(不受支持),则会发生这种情况。

对’clients’的请求找到了以下匹配的控制器:MvcApplication.Areas.Administration.Controllers.Api.ClientsController MvcApplication.Controllers.Api.ClientsController

这看起来很奇怪,因为我有不同的路线,应该将两者分开。 这是我的管理部门的区域注册:

public class AdministrationAreaRegistration : AreaRegistration { public override string AreaName { get { return "Administration"; } } public override void RegisterArea(AreaRegistrationContext context) { context.Routes.MapHttpRoute( name: "Administration_DefaultApi", routeTemplate: "Administration/api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); context.MapRoute( "Administration_default", "Administration/{controller}/{action}/{id}", new { action = "Index", id = UrlParameter.Optional } ); } } 

此外,我注意到我可以访问区域特定的Web API,同时省略呼叫区域的名称。

这里发生了什么? 如何让我的Web API控制器像普通的ASP.NET MVC控制器一样运行?

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> _apiControllerTypes; public AreaHttpControllerSelector(HttpConfiguration configuration) : base(configuration) { _configuration = configuration; _apiControllerTypes = new Lazy>(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 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(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> ByAreaName(this IEnumerable> 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> WithoutAreaName(this IEnumerable> query) { return query.Where(x => x.Key.IndexOf(".areas.", StringComparison.OrdinalIgnoreCase) == -1); } public static IEnumerable> ByControllerName(this IEnumerable> query, string controllerName) { var controllerNameToFind = string.Format(CultureInfo.InvariantCulture, ".{0}{1}", controllerName, AreaHttpControllerSelector.ControllerSuffix); return query.Where(x => x.Key.EndsWith(controllerNameToFind, StringComparison.OrdinalIgnoreCase)); } } } 

通过将以下行添加到Global.asax中的Application_Start方法来覆盖DefaultHttpControllerSelector

 GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector), new AreaHttpControllerSelector(GlobalConfiguration.Configuration)); 

恭喜,您的Web API控制器现在将像您的普通MVC控制器一样尊重您所在区域的规则!

更新:2012年9月6日

几个开发人员已经联系了我他们遇到的路由变量的DataTokens属性为null 。 我的实现假定DataTokens属性始终初始化,如果此属性为null ,则无法正常运行。 这种行为很可能是由ASP.NET MVC框架中的最新更改引起的,实际上可能是框架中的错误。 我已更新我的代码来处理这种情况。