【问题标题】:Getting an ApiController to work with areas?让 ApiController 与区域一起工作?
【发布时间】:2014-10-09 13:17:43
【问题描述】:

我的 ASP .NET MVC 5 项目目前有 2 个区域。一种叫做Supporters,一种叫做Chatter。在这两个区域中的每一个区域中,都有一个名为 CommunicationControllerApiController,由于 ApiController 如何与路由一起使用的性质,这会带来问题。

问题的一个例子

如果我在一个区域中只有一个名为CommunicationControllerApiController,它的路由不会在 URL 中包含该区域,并且 URL 会是这样的:

http://example.com/api/communication/someAction

但是在上面的网址中,区域在哪里?

由于我的两个控制器名称相同,因此它们现在都存在路由问题。

我尝试了什么?

我尝试按照此处的说明进行操作:http://blogs.infosupport.com/asp-net-mvc-4-rc-getting-webapi-and-areas-to-play-nicely/

他们似乎申请了 ASP .NET MVC 4 RC,因为我使用的是 MVC 5,所以它不再相关,这可能就是它不起作用的原因。

不过,回顾那篇博文,这里是我的路由文件。

App_Start\RouteConfig.cs

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapRoute("Default", "{controller}/{action}",
            new {action = "Index", controller = "Home"},
            new[] { "Website.Controllers" }
            );
    }
}

App_Start\WebApiConfig.cs

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.MapHttpAttributeRoutes();

        //the two lines below were added.
        config.Routes.MapHttpRoute("SupportersApi", "api/supporters/{controller}/{id}", new {id = RouteParameter.Optional, area = "Supporters"}
            );
        config.Routes.MapHttpRoute("ChatterApi", "api/chatter/{controller}/{id}", new { id = RouteParameter.Optional, area = "Chatter" }
            );
    }
}

Areas\Chatter\ChatterAreaRegistration.cs

public class ChatterAreaRegistration : AreaRegistration
{
    public override string AreaName
    {
        get { return "Chatter"; }
    }

    public override void RegisterArea(AreaRegistrationContext context)
    {

        //the below line was added.
        context.Routes.MapHttpRoute("Chatter_api", "api/chatter/{controller}/{id}", new { id = RouteParameter.Optional, area = "Chatter" }
            );

        context.MapRoute(
            "Chatter_default",
            "Chatter/{controller}/{action}/{id}",
            new {action = "Index", id = UrlParameter.Optional}
            );
    }
}

Areas\Supporters\SupportersAreaRegistration.cs

public class SupportersAreaRegistration : AreaRegistration 
{
    public override string AreaName 
    {
        get 
        {
            return "Supporters";
        }
    }

    public override void RegisterArea(AreaRegistrationContext context)
    {
        //the below line was added.
        context.Routes.MapHttpRoute("Supporters_api", "api/supporters/{controller}/{id}", new { id = RouteParameter.Optional, area = "Supporters" }
            );

        context.MapRoute(
            "Supporters_default",
            "Supporters/{controller}/{action}/{id}",
            new { action = "Index", controller = "Home", id = UrlParameter.Optional }
        );
    }
}

我在这里做错了什么,我有什么选择?

【问题讨论】:

  • 别忘了您可以使用Glimpse 调试您的路线。它将为您提供有价值的信息,说明您的路线为何未按预期运行。
  • 一瞥对我没有帮助。它只是在我加载页面时崩溃。
  • 这可能不是你想听到的,但我个人永远不会使用区域。这不值得努力。如果我想使用“区域”,我将根据需要设置我想要的文件夹结构和相关的 Views/Web.configs。我发现太多东西只是与区域摩擦,而使用本机 MVC 并使用它会更好。我曾经领导过的最好的项目之一(使用你的术语)我有 /Modules/Chatter/Modules/Supporters 嵌套了 Controllers/Views 文件夹。我只需要花费public ActionResult foo() { return View("~/modules/chatter/foo.cshtml") }

标签: c# asp.net-mvc


【解决方案1】:

使用WebAPI 2 attributes,因为您使用的是 MVC 5,并且您可以通过声明 API 的路由及其实现来摆脱大量样板代码(您还可以为 HTTP 操作指定动词,并且甚至使用属性自动转换为 XML/JSON/月序列化)。

除非您出于其他原因使用区域,否则您确实不需要它们来实现 Web API。

特别是,您想要的是 RoutePrefix 属性。

【讨论】:

  • 其实这行不通。我现在设置了 RouteRoutePrefix 属性,但是当我尝试访问 URL 时,我得到了 404。
  • 试试本指南asp.net/web-api/overview/web-api-routing-and-actions/… 在 CLASS 上方使用 [RoutePrefix("/api/whatever")],然后在 ACTION 上方使用 [Route("myname")]。在操作上方也使用 [HttpGet] 或 [HttpPost]。
  • 这也不行。我已经这样做了,但我仍然得到 404。This 是我的配置。
  • 别忘了添加 config.MapHttpAttributeRoutes();在 App_Start 的 WebApiConfig.cs 中
  • @Mathias 可能存在几个问题,并且不知道您的项目中的所有内容,我无法真正形成一个好的理论来解释您为什么会得到 404。在我发送的链接中,他们有将您的解决方案从 WebAPI1 升级到 2 的指南。我建议根据 MSDN 上的教程设置一个准系统示例,然后与您的代码进行比较。
【解决方案2】:

如果两个或多个区域有同名的apicontroller,那么为了调用特定区域的控制器,区域名称必须包含在URL中。

所以 http://example.com/api/communication/someAction 不会工作。

在这种情况下,可以是
http://example.com/supporters/api/communication/someActionhttp://example.com/chatters/api/communication/someAction

http://blogs.infosupport.com/asp-net-mvc-4-rc-getting-webapi-and-areas-to-play-nicely 中给出的自定义 httpcontrollerselector 也适用于 mvc5。

删除 webapiconfig 中的以下行

config.Routes.MapHttpRoute("SupportersApi", "api/supporters/{controller}/{id}", new {id = RouteParameter.Optional, area = "Supporters"}
        );        

    config.Routes.MapHttpRoute("ChatterApi", "api/chatter/{controller}/{id}", new { id = RouteParameter.Optional, area = "Chatter" }
        );

步骤如下,效果很好

1.将以下扩展方法添加到项目中。

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

}

2。在每个 AreaRegistration 文件中,在 routeTemplate 中添加包含区域名称的路由

到 SupportAreaRegistration,添加

 context.MapHttpRoute(
name: "Supporters_DefaultApi",
routeTemplate: "supporters/api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);

到 ChatterAreaRegistration,添加

 context.MapHttpRoute(
name: "Chatters_DefaultApi",
routeTemplate: "chatters/api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);

它的 context.MapHttpRoute,而不是 context.Routes

3。添加自定义 HttpControllerSelector

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

4.更改 Global.Asax 文件中的 Application_Start 方法,以便使用 AreaHttpControllerSelector 而不是 DefaultHttpControllerSelector

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

【讨论】:

  • 这似乎是一个非常合理的解决方案(不是我实际测试了代码),但这正是我不使用区域的原因。如此多的额外提升却没有任何价值收益。
  • 我尝试按照这些确切的说明进行操作,但仍然得到 404。
  • in line var types = assemblies .SelectMany(a =&gt; a .GetTypes().Where(t =&gt; !t.IsAbstract &amp;&amp; t.Name.EndsWith(ControllerSuffix, StringComparison.OrdinalIgnoreCase) &amp;&amp; typeof(IHttpController).IsAssignableFrom(t))) .ToDictionary(t =&gt; t.FullName, t =&gt; t); 我得到一个异常 {"Unable to load one or more of the requested types. Retrieve the LoaderExceptions property for more information."} ,知道为什么吗?
【解决方案3】:

试试下面的配置。这里的技巧是注册命名空间以在路由匹配时搜索 API 控制器。

config.Routes.MapHttpRoute(
            name: "chatterApi",
            routeTemplate: "api/chatter/{controller}/{action}",
            defaults: new { action = "", controller = "", namespaces = new string[] { "WebApplication.chatter.api" } }
        );

config.Routes.MapHttpRoute(
            name: "supportersApi",
            routeTemplate: "api/supporters/{controller}/{action}",
            defaults: new { action = "", controller = "", namespaces = new string[] { "WebApplication.supporters.api" } }
        );

【讨论】:

  • API 控制器不能这样做。
  • 还有ApiController 吗?
【解决方案4】:

由于您使用的是 MVC5 并且它带有 WebAPI 2.0,因此您可以使用

[RoutePrefix("api/Supporters/Communication")]

将区域指定为提到的@Clever Neologism。但别忘了打电话

config.MapHttpAttributeRoutes();

在 Global.asax.cs 中配置路由时。另见this答案

【讨论】:

  • 这不起作用。我仍然得到 404。This 是我的配置。
  • 使用Route("AllSessions") 而不是使用Route(Name="AllSessions"),因为Name 参数用于不同的目的。
【解决方案5】:

它有错误,路线。 DataTokens 是只读的,不能设置值

【讨论】:

    猜你喜欢
    • 2016-12-13
    • 2016-10-06
    • 2011-11-30
    • 2014-02-19
    • 2011-09-03
    • 2013-05-01
    • 2018-05-30
    • 2014-03-17
    • 2011-09-29
    相关资源
    最近更新 更多