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

Posted

技术标签:

【中文标题】在 ASP.NET Core 中发现通用控制器【英文标题】:Discovering Generic Controllers in ASP.NET Core 【发布时间】:2016-08-09 10:06:06 【问题描述】:

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

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

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

MVC 框架提供了一个默认控制器工厂(恰当地命名为 DefaultControllerFactory),它将搜索应用程序域中的所有程序集,以查找所有实现 IController 并且名称以“Controller”结尾的类型。

显然,在 ASP.NET Core 中,控制器工厂不再有这个责任。正如我之前所说,我的自定义控制器工厂为“普通”控制器执行,但从不为通用控制器调用。因此,在评估过程的早期,还有其他一些东西控制着控制器的发现。

有谁知道负责该发现的“服务”接口是什么?我不知道自定义界面或“钩子”点。

有没有人知道一种方法可以让 ASP.NET Core “转储”它发现的所有控制器的名称?编写一个单元测试来验证我期望的任何自定义控制器发现确实有效,这将是很棒的。

顺便说一句,如果存在允许发现通用控制器名称的“钩子”,则意味着路由替换也必须被规范化:

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

无论T 的值是什么,[控制器] 名称都必须是一个简单的基本通用名称。以上面的代码为例,[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 Core 能够“看到”或“发现”我的通用控制器,尽管它的运行时反射名称不以预期的“控制器”后缀结尾。

【问题讨论】:

【参考方案1】:

应用程序功能提供者检查应用程序部分并为这些部分提供功能。以下 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  

在 Startup 中添加功能提供程序:

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;
        
    

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

【讨论】:

【参考方案2】:

默认情况下会发生什么

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

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

替换IApplicationFeatureProvider&lt;ControllerFeature&gt; 接口的默认实现以覆盖DefaultControllerTypeProvider.IsController() 将不起作用。因为您实际上并不希望发现过程接受您的开放通用控制器 (Controller&lt;T&gt;) 作为有效控制器。它本身不是一个有效的控制器,而且控制器工厂也不知道如何实例化它,因为它不知道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&lt;Account&gt;Controller&lt;Contact&gt;

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 类,实际上就不需要重写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;
    

应用于控制器类:

[GenericController]
public class Controller<T> : Controller


结论

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

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

【讨论】:

你能解释一下第 1 部分吗?如果我的控制器是 UserController 我应该在哪里写代码?谢谢【参考方案3】:

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

    ApplicationPartManager appManager = <FROM DI>;

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

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

【讨论】:

【参考方案4】:

简答

实现IApplicationFeatureProvider&lt;ControllerFeature&gt;

问答

有谁知道什么“服务”接口负责[发现所有可用的控制器]?

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

【讨论】:

哇!您是否通过myget 或其他方式找到这些预发布包?你从哪里学习bleeding edge TFM's and project.json rules and semantics?在我赶上这些细节之前,我无法尝试您的解决方案。 :O @BrentArias 各种 NuGet 提要在这里:github.com/aspnet/Home/wiki/NuGet-feeds 我试图创建一个 MyControllerFeatureProvider 但 VS 不理解 ControllerFeatureProvider 类型,也没有任何有用的解决它的建议。这在 mvc 6 rc1 中可用还是我需要每晚构建才能访问它? @ShaunLuttin RC1 怎么样? 我发现了 Sebastien Ros 建议的另一种 ASP.NET generic controller 方法。我还没来得及和你的比较……

以上是关于在 ASP.NET Core 中发现通用控制器的主要内容,如果未能解决你的问题,请参考以下文章

在 ASP.NET Core 控制器中模拟 HttpRequest

ASP.Net Core 在运行时注册控制器

ASP.NET Core 中的通用存储库,Startup.cs 中的每个表没有单独的 AddScoped 行?

ASP.NET Core基于滑动窗口算法实现限流控制

ASP.Net Web Api Core - 无法在抽象基类中使用 CreatedAtRoute

获取 ASP.NET-Core 2.2 控制器中的控制器名称和方法名称