9

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

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

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

找到了与名为“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 控制器一样工作?

4

1 回答 1

19

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_StartGlobal.asax 中的方法来覆盖。

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

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

更新:2012 年 9 月 6 日

一些开发人员就他们遇到DataTokens的路由变量属性为 的场景与我联系null。我的实现假定该DataTokens属性始终被初始化,并且如果该属性为null. 此行为很可能是由 ASP.NET MVC 框架中的最新更改引起的,实际上可能是框架中的错误。我已经更新了我的代码来处理这种情况。

于 2012-08-19T08:52:22.530 回答