为什么我的区域特定的Web API的所有其他领域的访问? [英] Why are my Area specific Web API’s accessible from all other Areas?

查看:213
本文介绍了为什么我的区域特定的Web API的所有其他领域的访问?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我目前工作必须遵循以下设计决定一个ASP.NET MVC 4 Web应用程序项目:


  • 主要MVC应用程序驻留在解决方案的根。

  • 所有管理员功能位于一个独立的区域。

  • 每个外部方(例如供应商)都有自己的区域。

  • 每个区域,包括根,构成了很好的分离
    功能块。从一个区域的功能可以不暴露于
    另一个领域。这是数据prevent未经授权的访问。

  • 每个区域,包括根,有它自己的REST风格的API(网页API)。

在各个领域,包括根,按预期方式工作一切正常控制器。然而,一些我的Web API控制器出现意外行为。例如,具有相同的名称,但在不同的领域两个Web API控制器产生以下异常:


  

多种类型的发现命名为客户控制器匹配。
  如果路由服务这个请求会发生这种情况
  (API / {控制器} / {ID}')发现定义多个控制器
  同名但不同的命名空间,这是不支持的。


  
  

对于'客户'发现下列匹配控制器请求:
  MvcApplication.Areas.Administration.Controllers.Api.ClientsController
  MvcApplication.Controllers.Api.ClientsController


这似乎很奇怪,因为我有不同的路线应该分开两者。这里是我的管理部分AreaRegistration:

 公共类AdministrationAreaRegistration:AreaRegistration
{
    公众覆盖字符串AREANAME
    {
        得到
        {
            返回管理;
        }
    }    公共覆盖无效RegisterArea(AreaRegistrationContext上下文)
    {
        context.Routes.MapHttpRoute(
            名称:Administration_DefaultApi
            routeTemplate:管理/ API / {}控制器/(编号),
            默认:新{ID = RouteParameter.Optional}
        );        context.MapRoute(
            Administration_default
            管理/ {控制器} / {行动} / {ID}
            新{行动=索引,ID = UrlParameter.Optional}
        );
    }
}

此外,我注意到,虽然省略从呼叫区域的名字,我可以访问区特定Web的API。

这是怎么回事?
我如何得到我的Web API控制器的行为就像正常的ASP.NET MVC控制器?


解决方案

  

ASP.NET MVC 4不支持跨区域的Web API控制器的分区。


您可以将在不同的API文件夹的WebAPI控制器在不同的领域,但ASP.NET MVC将把好像他们都在同一个地方。

幸运的是,你可以通过重写ASP.NET MVC基础设施的一部分克服这个限制。有关限制和解决方案的更多信息,请阅读我的博客文章 ASP.NET MVC 4 RC:获得的WebAPI和领域,发挥很好的。如果你只在解决方案感兴趣,请继续阅读:

第1步。你的路由区意识到

下面的扩展方法添加到您的ASP.NET MVC应用程序,并确保它们可以访问您的AreaRegistration类:

 公共静态类AreaRegistrationContextExtensions
{
    公共静态路由MapHttpRoute(这AreaRegistrationContext背景下,字符串名称,字符串routeTemplate)
    {
        返回context.MapHttpRoute(姓名,routeTemplate,NULL,NULL);
    }    公共静态路由MapHttpRoute(这AreaRegistrationContext背景下,字符串名称,字符串routeTemplate,对象的默认值)
    {
        返回context.MapHttpRoute(姓名,routeTemplate,默认值,NULL);
    }    公共静态路由MapHttpRoute(这AreaRegistrationContext背景下,字符串名称,字符串routeTemplate,对象违约,对象约束)
    {
        VAR路线= context.Routes.MapHttpRoute(姓名,routeTemplate,默认值约束);
        如果(route.DataTokens == NULL)
        {
            route.DataTokens =新RouteValueDictionary();
        }
        route.DataTokens.Add(区域,context.AreaName);
        返回路线;
    }
}

要使用新的扩展方法,删除路线从调用链属性:

  context.MapHttpRoute(/ * LT;  -  .Routes删除* /
    名称:Administration_DefaultApi
    routeTemplate:管理/ API / {}控制器/(编号),
    默认:新{ID = RouteParameter.Optional}
);

第2步:使Web API控制器选择区意识到

添加下面的类到你的ASP.NET MVC应用程序,并确保它是在Global.asax

访问

 命名空间MvcApplication.Infrastructure.Dispatcher
{
    使用系统;
    使用System.Collections.Concurrent;
    使用System.Collections.Generic;
    使用System.Globalization;
    使用System.Linq的;
    使用System.Net.Http;
    使用System.Web.Http;
    使用System.Web.Http.Controllers;
    使用System.Web.Http.Dispatcher;    公共类AreaHttpControllerSelector:DefaultHttpControllerSelector
    {
        私人常量字符串AreaRouteVariableName =区域;        私人只读HttpConfiguration _configuration;
        私人只读懒< ConcurrentDictionary<字符串类型>> _apiControllerTypes;        公共AreaHttpControllerSelector(HttpConfiguration配置)
            :基部(配置)
        {
            _configuration =配置;
            _apiControllerTypes =新懒人< ConcurrentDictionary<字符串类型>>(GetControllerTypes);
        }        公众覆盖HttpControllerDescriptor SelectController(HTT prequestMessage要求)
        {
            返回this.GetApiController(请求);
        }        私人静态字符串GetAreaName(HTT prequestMessage要求)
        {
            VAR数据= request.GetRouteData();
            如果(data.Route.DataTokens == NULL)
            {
                返回null;
            }
            其他
            {
                反对AREANAME;
                返回data.Route.DataTokens.TryGetValue(AreaRouteVariableName,出AREANAME)? areaName.ToString():空;
            }
        }        私有静态ConcurrentDictionary<字符串类型> GetControllerTypes()
        {
            无功组件= AppDomain.CurrentDomain.GetAssemblies();            VAR类型=组件
                .SelectMany(一个=>一种
                    .GetTypes(),其中(T =>
                        !t.IsAbstract&放大器;&安培;
                        t.Name.EndsWith(ControllerSuffix,StringComparison.OrdinalIgnoreCase)及&放大器;
                        typeof运算(IHttpController).IsAssignableFrom(T)))
                .ToDictionary(T => t.FullName,T =>吨);            返回新ConcurrentDictionary<字符串类型>(种);
        }        私人HttpControllerDescriptor GetApiController(HTT prequestMessage要求)
        {
            VAR AREANAME = GetAreaName(请求);
            VAR controllerName = GetControllerName(请求);
            VAR类型= GetControllerType(AREANAME,controllerName);            返回新HttpControllerDescriptor(_configuration,controllerName,类型);
        }        私有类型GetControllerType(字符串AREANAME,串controllerName)
        {
            变种查询= _apiControllerTypes.Value.AsEnumerable();            如果(string.IsNullOrEmpty(AREANAME))
            {
                查询= query.WithoutAreaName();
            }
            其他
            {
                查询= query.ByAreaName(AREANAME);
            }            返回查询
                .ByControllerName(controllerName)
                。选择(X => x.Value)
                。单();
        }
    }    公共静态类ControllerTypeSpecifications
    {
        公共静态的IEnumerable< KeyValuePair<字符串类型>> ByAreaName(这IEnumerable的< KeyValuePair<字符串类型>>查询字符串AREANAME)
        {
            VAR areaNameToFind =的String.Format(CultureInfo.InvariantCulture,AREANAME{0});            返回query.Where(X =>!x.Key.IndexOf(areaNameToFind,StringComparison.OrdinalIgnoreCase)= -1);
        }        公共静态的IEnumerable< KeyValuePair<字符串类型>> WithoutAreaName(这IEnumerable的< KeyValuePair<字符串类型>>查询)
        {
            返回query.Where(X =>中。区x.Key.IndexOf(,StringComparison.OrdinalIgnoreCase)== - 1);
        }        公共静态的IEnumerable< KeyValuePair<字符串类型>> ByControllerName(这IEnumerable的< KeyValuePair<字符串类型>>查询字符串controllerName)
        {
            VAR controllerNameToFind =的String.Format(CultureInfo.InvariantCulture,controllerName,AreaHttpControllerSelector.ControllerSuffix{0} {1});            返回query.Where(X => x.Key.EndsWith(controllerNameToFind,StringComparison.OrdinalIgnoreCase));
        }
    }
}

重写 DefaultHttpControllerSelector 通过添加以下行来在Global.asax的的Application_Start 方法。

  GlobalConfiguration.Configuration.Services.Replace(typeof运算(IHttpControllerSelector),新AreaHttpControllerSelector(GlobalConfiguration.Configuration));

恭喜你,你的Web API的控制器现在会尊重你的领域,就像正常的MVC控制器做的规则!

更新:2012年9月6日

有一些开发人员联系我,他们遇到过的路线变量 DataTokens 属性是的场景。我的实现假定 DataTokens 属性总是初始化,如果这个属性将不能正常工作。这种行为很可能是在ASP.NET MVC框架中最近的变化造成的,实际上可能是在框架中的错误。我已经更新了我的code来处理这种情况。

I am currently working on a ASP.NET MVC 4 Web Application project that must adhere to the following design decisions:

  • The main MVC application resides in the root of the solution.
  • All administrator functionality resides in a separate area.
  • Each external party (e.g. suppliers) has its own area.
  • Each area, including the root, constitutes a well separated functional block. Functionality from one area may not be exposed to another area. This is to prevent unauthorized access of data.
  • Each area, including the root, has its own RESTfull API (Web API).

All normal controllers in all areas, including the root, work as expected. However, some of my Web API controllers exhibit unexpected behaviour. For instance, having two Web API controllers with the same name but in different areas produces the following exception:

Multiple types were found that match the controller named ‘clients’. This can happen if the route that services this request (‘api/{controller}/{id}’) found multiple controllers defined with the same name but differing namespaces, which is not supported.

The request for ‘clients’ has found the following matching controllers: MvcApplication.Areas.Administration.Controllers.Api.ClientsController MvcApplication.Controllers.Api.ClientsController

This seems strange since I have distinct routes that should separate both. Here is my AreaRegistration for the Administration section:

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 }
        );
    }
}

Moreover, I notice that I can access Area specific Web API’s while omitting the name of the area from the call.

What is going on here? How do I get my Web API Controllers to behave just like normal ASP.NET MVC controllers?

解决方案

ASP.NET MVC 4 does not support the partitioning of Web API controllers across Areas.

You may place WebApi controllers in different Api folders in different Areas, but ASP.NET MVC will treat as if they are all in the same place.

Fortunately, you can overcome this limitation by overriding a part of the ASP.NET MVC infrastructure. For more information about the limitation and the solution, please read my blog post 'ASP.NET MVC 4 RC: Getting WebApi and Areas to play nicely'. If you are only interested in the solution, read on:

Step 1. Make your routes Area aware

Add the following extension methods to your ASP.NET MVC application and make sure they are accessible from your AreaRegistration classes:

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;
    }
}

To use the new extension method, remove the Routes property from the call chain:

context.MapHttpRoute( /* <-- .Routes removed */
    name: "Administration_DefaultApi",
    routeTemplate: "Administration/api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

Step 2. Make Web API controller selector Area aware

Add the following class to your ASP.NET MVC application and make sure it is accessible from the 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));
        }
    }
}

Override the DefaultHttpControllerSelector by adding the following line to the Application_Start method in the Global.asax.

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

Congratulations, your Web API controllers will now respect the rules of your areas just like your normal MVC controllers do!

UPDATE: 6 september 2012

Several developers have contacted me about a scenario they encountered where the DataTokens property of the route variable is null. My implementation assumes that the DataTokens property is always initialized and will not function properly if this property is null. This behavior is most likely caused by recent changes in the ASP.NET MVC framework and may be actually be a bug in the framework. I’ve updated my code to handle this scenario.

这篇关于为什么我的区域特定的Web API的所有其他领域的访问?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆