在ASP.NET Core中发现通用控制器

时间:2016-04-17 19:05:37

标签: asp.net-mvc asp.net-web-api asp.net-core url-routing

我正在尝试创建一个这样的通用控制器:

[Route("api/[controller]")]
public class OrdersController<T> : Controller where T : IOrder
{
    [HttpPost("{orderType}")]
    public async Task<IActionResult> Create(
        [FromBody] Order<T> order)
    {
       //....
    }
}

我打算使用{orderType} URI段变量来控制控制器的泛型类型。我正在尝试使用自定义IControllerFactoryIControllerActivator,但没有任何效果。每次我尝试发送请求时,都会收到404响应。我的自定义控制器工厂(和激活器)的代码永远不会执行。

显然问题是ASP.NET Core期望有效的控制器以后缀&#34; Controller&#34;结束,但我的通用控制器却具有(基于反射的)后缀&#34; Controller`1&#34 ;。因此,它声明的基于属性的路线将被忽视。

在ASP.NET MVC中,至少在其早期,the DefaultControllerFactory was responsible for discovering all the available controllers。它测试了&#34;控制器&#34;后缀:

  

MVC框架提供了一个默认的控制器工厂(恰当地命名为DefaultControllerFactory),它将搜索appdomain中的所有程序集,查找实现IController的所有类型,其名称以&#34; Controller结束。&#34;

显然,在ASP.NET Core中,控制器工厂不再承担此责任。正如我之前所说,我的自定义控制器工厂执行&#34; normal&#34;控制器,但从不为通用控制器调用。因此,在评估过程的早期还有其他一些东西可以控制控制器的发现。

有谁知道&#34;服务&#34;接口负责该发现?我不知道自定义界面或&#34; hook&#34;点。

有没有人知道如何制作ASP.NET Core&#34; dump&#34;它发现的所有控制器的名称?编写单元测试可以很好地验证我期望的任何自定义控制器发现确实有效。

顺便提一下,如果有一个&#34; hook&#34;它允许发现通用控制器名称,这意味着路由替换也必须标准化:

[Route("api/[controller]")]
public class OrdersController<T> : Controller { }

无论给出T的值是什么,[controller]名称必须保持简单的基本通用名称。使用上面的代码作为例子,[controller]值将是&#34; Orders&#34;。它不会是&#34; Orders`1&#34;或者&#34; OrdersOfSomething&#34;。

注意

这个问题也可以通过明确声明封闭泛型类型来解决,而不是在运行时生成它们:

public class VanityOrdersController : OrdersController<Vanity> { }
public class ExistingOrdersController : OrdersController<Existing> { }

上述工作,但它产生我不喜欢的URI路径:

~/api/VanityOrders
~/api/ExistingOrders

我真正想要的是:

~/api/Orders/Vanity
~/api/Orders/Existing

另一项调整让我得到了我正在寻找的URI:

[Route("api/Orders/Vanity", Name ="VanityLink")]
public class VanityOrdersController : OrdersController<Vanity> { }
[Route("api/Orders/Existing", Name = "ExistingLink")]
public class ExistingOrdersController : OrdersController<Existing> { }

然而,尽管这似乎有效,但它并没有真正回答我的问题。我想在运行时直接使用我的通用控制器,而不是在编译时间接(通过手动编码)。从根本上说,这意味着我需要ASP.NET Core能够“看到&#34;或者&#34;发现&#34;我的通用控制器,尽管其运行时反射名称并未以预期的&#34; Controller&#34;后缀。

4 个答案:

答案 0 :(得分:14)

简答

实施IApplicationFeatureProvider<ControllerFeature>

问题与答案

  

有谁知道“发现所有可用控制器”的“服务”界面是什么?

ControllerFeatureProvider对此负责。

  

有没有人知道如何让ASP.NET Core“转储”它发现的所有控制器的名称?

ControllerFeatureProvider.IsController(TypeInfo typeInfo)内完成。

实施例

MyControllerFeatureProvider.cs

using System;
using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Controllers;

namespace CustomControllerNames 
{
    public class MyControllerFeatureProvider : ControllerFeatureProvider 
    {
        protected override bool IsController(TypeInfo typeInfo)
        {
            var isController = base.IsController(typeInfo);

            if (!isController)
            {
                string[] validEndings = new[] { "Foobar", "Controller`1" };

                isController = validEndings.Any(x => 
                    typeInfo.Name.EndsWith(x, StringComparison.OrdinalIgnoreCase));
            }

            Console.WriteLine($"{typeInfo.Name} IsController: {isController}.");

            return isController;
        }
    }
}

在启动期间注册它。

public void ConfigureServices(IServiceCollection services)
{
    services
        .AddMvcCore()
        .ConfigureApplicationPartManager(manager => 
        {
            manager.FeatureProviders.Add(new MyControllerFeatureProvider());
        });
}

这是一些示例输出。

MyControllerFeatureProvider IsController: False.
OrdersFoobar IsController: True.
OrdersFoobarController`1 IsController: True.
Program IsController: False.
<>c__DisplayClass0_0 IsController: False.
<>c IsController: False.

And here is a demo on GitHub。祝你好运。

编辑 - 添加版本

.NET版

> dnvm install "1.0.0-rc2-20221" -runtime coreclr -architecture x64 -os win -unstable

NuGet.Config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear/>
    <add key="AspNetCore" 
         value="https://www.myget.org/F/aspnetvnext/api/v3/index.json" />  
  </packageSources>
</configuration>

.NET CLI

> dotnet --info
.NET Command Line Tools (1.0.0-rc2-002429)

Product Information:
 Version:     1.0.0-rc2-002429
 Commit Sha:  612088cfa8

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.10586
 OS Platform: Windows
 RID:         win10-x64

恢复,构建和运行

> dotnet restore
> dotnet build
> dotnet run

编辑 - 关于RC1与RC2的注释

这可能是RC1无法实现的,因为DefaultControllerTypeProvider.IsController()被标记为internal

答案 1 :(得分:10)

默认情况下会发生什么

在控制器发现过程中,您的开放式通用Controller<T>类将成为候选类型之一。但IApplicationFeatureProvider<ControllerFeature>界面DefaultControllerTypeProvider的默认实现会消除您的Controller<T>,因为它会排除任何具有开放通用参数的类。

为什么覆盖IsController()不起作用

替换IApplicationFeatureProvider<ControllerFeature>接口的默认实现,以覆盖DefaultControllerTypeProvider.IsController(),将无法正常工作。因为您实际上不希望发现过程接受您的开放通用控制器(Controller<T>)作为有效控制器。 本身就是一个有效的控制器,并且控制器工厂无论如何都不知道如何实例化它,因为它不知道应该是什么T

需要做什么

1。生成封闭的控制器类型

在控制器发现过程开始之前,您需要使用反射从打开的通用控制器生成封闭的泛型类型。这里有两个样本实体类型,名为AccountContact

Type[] entityTypes = new[] { typeof(Account), typeof(Contact) };
TypeInfo[] closedControllerTypes = entityTypes
    .Select(et => typeof(Controller<>).MakeGenericType(et))
    .Select(cct => cct.GetTypeInfo())
    .ToArray();

我们现在已关闭TypeInfosController<Account> {。}}。{/ p>

2。将它们添加到应用程序部分并注册

应用程序部分通常包含在CLR程序集中,但我们可以实现一个自定义应用程序部分,它提供在运行时生成的类型集合。我们只需要让它实现Controller<Contact>接口。因此,我们的运行时生成的控制器类型将像任何其他内置类型一样进入控制器发现过程。

自定义应用程序部分:

IApplicationPartTypeProvider

在MVC服务中注册(public class GenericControllerApplicationPart : ApplicationPart, IApplicationPartTypeProvider { public GenericControllerApplicationPart(IEnumerable<TypeInfo> typeInfos) { Types = typeInfos; } public override string Name => "GenericController"; public IEnumerable<TypeInfo> Types { get; } } ):

Startup.cs

只要您的控制器派生自内置services.AddMvc() .ConfigureApplicationPartManager(apm => apm.ApplicationParts.Add(new GenericControllerApplicationPart(closedControllerTypes))); 类,就不需要覆盖Controller的{​​{1}}方法。因为您的通用控制器从IsController继承了ControllerFeatureProvider属性,所以它将在发现过程中被接受为控制器,而不管它有多奇怪的名称(“Controller`1”)。

3。覆盖应用程序模型中的控制器名称

尽管如此,“Controller`1”并不是用于路由目的的好名称。您希望每个封闭的通用控制器都具有独立的[Controller]。在这里,我们将用实体类型的名称替换控制器的名称,以匹配两个独立的“AccountController”和“ContactController”类型会发生的情况。

模型约定属性:

ControllerBase

应用于控制器类:

RouteValues

结论

此解决方案与整体ASP.NET核心架构保持接近,除此之外,您还可以通过API Explorer保持对控制器的完全可见性(想想“Swagger”)。

已经使用传统和基于属性的路由成功测试了它。

答案 2 :(得分:1)

应用程序功能提供程序检查应用程序部件并为这些部件提供功能。有以下MVC功能的内置功能提供程序:

  • 控制器
  • 元数据参考
  • Tag Helpers
  • 查看组件

功能提供程序继承自IApplicationFeatureProvider,其中T是功能的类型。您可以为上面列出的任何MVC功能类型实现自己的功能提供程序。 ApplicationPartManager.FeatureProviders集合中的功能提供程序的顺序可能很重要,因为以后的提供程序可以对先前提供程序采取的操作做出反应。

默认情况下,ASP.NET Core MVC忽略通用控制器(例如,SomeController)。此示例使用在默认提供程序之后运行的控制器功能提供程序,并为指定的类型列表添加通用控制器实例(在EntityTypes.Types中定义):

public class GenericControllerFeatureProvider : IApplicationFeatureProvider<ControllerFeature>
{
    public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
    {
        // This is designed to run after the default ControllerTypeProvider, 
        // so the list of 'real' controllers has already been populated.
        foreach (var entityType in EntityTypes.Types)
        {
            var typeName = entityType.Name + "Controller";
            if (!feature.Controllers.Any(t => t.Name == typeName))
            {
                // There's no 'real' controller for this entity, so add the generic version.
                var controllerType = typeof(GenericController<>)
                    .MakeGenericType(entityType.AsType()).GetTypeInfo();
                feature.Controllers.Add(controllerType);
            }
        }
    }
}

实体类型:

public static class EntityTypes
{
    public static IReadOnlyList<TypeInfo> Types => new List<TypeInfo>()
    {
        typeof(Sprocket).GetTypeInfo(),
        typeof(Widget).GetTypeInfo(),
    };

    public class Sprocket { }
    public class Widget { }
}

功能提供程序添加在启动:

services.AddMvc()
    .ConfigureApplicationPartManager(p => 
        p.FeatureProviders.Add(new GenericControllerFeatureProvider()));

默认情况下,用于路由的通用控制器名称的格式为GenericController`1 [Widget]而不是Widget。以下属性用于修改名称以对应于控制器使用的泛型类型:

使用Microsoft.AspNetCore.Mvc.ApplicationModels; 使用System;

namespace AppPartsSample
{
    // Used to set the controller name for routing purposes. Without this convention the
    // names would be like 'GenericController`1[Widget]' instead of 'Widget'.
    //
    // Conventions can be applied as attributes or added to MvcOptions.Conventions.
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
    public class GenericControllerNameConvention : Attribute, IControllerModelConvention
    {
        public void Apply(ControllerModel controller)
        {
            if (controller.ControllerType.GetGenericTypeDefinition() != 
                typeof(GenericController<>))
            {
                // Not a GenericController, ignore.
                return;
            }

            var entityType = controller.ControllerType.GenericTypeArguments[0];
            controller.ControllerName = entityType.Name;
        }
    }
}

GenericController类:

using Microsoft.AspNetCore.Mvc;

namespace AppPartsSample
{
    [GenericControllerNameConvention] // Sets the controller name based on typeof(T).Name
    public class GenericController<T> : Controller
    {
        public IActionResult Index()
        {
            return Content($"Hello from a generic {typeof(T).Name} controller.");
        }
    }
}

<强> Sample: Generic controller feature

答案 3 :(得分:0)

要获取RC2中的控制器列表,只需从DependencyInjection获取ApplicationPartManager并执行此操作:

    ApplicationPartManager appManager = <FROM DI>;

    var controllerFeature = new ControllerFeature();
    appManager.PopulateFeature(controllerFeature);

    foreach(var controller in controllerFeature.Controllers)
    {
        ...
    }