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

Discovering Generic Controllers in ASP.NET Core

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

[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 核心期望有效控制器以后缀 "Controller" 结尾,但我的通用控制器却具有(基于反射的)后缀 "Controller`1"。因此,它声明的基于属性的路由不会被注意到。

在 ASP.NET MVC 中,至少在早期,the DefaultControllerFactory was responsible for discovering all the available controllers。它测试了 "Controller" 后缀:

The MVC framework provides a default controller factory (aptly named DefaultControllerFactory) that will search through all the assemblies in an appdomain looking for all types that implement IController and whose name ends with "Controller."

显然,在 ASP.NET Core 中,控制器工厂不再承担此责任。如前所述,我的自定义控制器工厂为 "normal" 个控制器执行,但从不为通用控制器调用。因此,在评估过程的早期,还有其他因素控制着控制器的发现。

有谁知道 "service" 接口负责那个发现?不知道自定义界面还是"hook"点

有没有人知道一种方法可以让 ASP.NET Core "dump" 成为它发现的所有控制器的名称?编写一个单元测试来验证我期望的任何自定义控制器发现确实有效。

顺便说一句,如果有一个 "hook" 允许发现通用控制器名称,则意味着路由替换也必须规范化:

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

无论 T 的值是多少,[controller] 名称必须保持为简单的基本通用名称。以上面的代码为例,[controller] 的值为 "Orders"。它不会是 "Orders`1" 或 "OrdersOfSomething".

备注

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

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 核心才能 "see" 或 "discover" 我的通用控制器,尽管它的 运行 时间反射名称不以预期 "Controller" 后缀。

简答

实施IApplicationFeatureProvider<ControllerFeature>

问答

Does anyone know what "service" interface is responsible for [discovering all available controllers]?

ControllerFeatureProvider 对此负责。

And does anyone know of a way to make ASP.NET Core "dump" the names of all the controllers it discovered?

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

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

    ApplicationPartManager appManager = <FROM DI>;

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

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

默认情况下会发生什么

在控制器发现过程中,您的开放泛型 Controller<T> class 将在候选类型中。但是 IApplicationFeatureProvider<ControllerFeature> 接口的默认实现 DefaultControllerTypeProvider 将消除您的 Controller<T>,因为它排除了任何具有开放通用参数的 class。

为什么覆盖 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();

我们现在已经关闭了 TypeInfos Controller<Account>Controller<Contact>

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

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

自定义应用部分:

public class GenericControllerApplicationPart : ApplicationPart, IApplicationPartTypeProvider
{
    public GenericControllerApplicationPart(IEnumerable<TypeInfo> typeInfos)
    {
        Types = typeInfos;
    }

    public override string Name => "GenericController";
    public IEnumerable<TypeInfo> Types { get; }
}

在 MVC 服务中注册(Startup.cs):

services.AddMvc()
    .ConfigureApplicationPartManager(apm =>
        apm.ApplicationParts.Add(new GenericControllerApplicationPart(closedControllerTypes)));

只要您的控制器派生自内置 Controller class,就没有实际需要覆盖 ControllerFeatureProviderIsController 方法。因为您的通用控制器从 ControllerBase 继承了 [Controller] 属性,所以它会在发现过程中被接受为控制器,而不管它的名称有点奇怪 ("Controller`1")。

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

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

模型约定属性:

public class GenericControllerAttribute : Attribute, IControllerModelConvention
{
    public void Apply(ControllerModel controller)
    {
        Type entityType = controller.ControllerType.GetGenericArguments()[0];

        controller.ControllerName = entityType.Name;
    }
}

应用于控制器class:

[GenericController]
public class Controller<T> : Controller
{
}

结论

此解决方案与整体 ASP.NET 核心架构保持接近,除此之外,您将通过 API 资源管理器(想想 "Swagger")保持对控制器的完全可见性。

它已成功通过常规路由和基于属性的路由测试。

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

  • 控制器
  • 元数据参考
  • 标签助手
  • 查看组件

功能提供者继承自 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; 使用系统;

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

通用控制器class:

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