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

Bre*_*ias 16 asp.net-mvc url-routing asp.net-web-api 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)
    {
       //....
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

在ASP.NET MVC中,至少在早期,DefaultControllerFactory负责发现所有可用的控制器.它测试了"Controller"后缀:

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

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

有谁知道什么"服务"界面负责该发现?我不知道自定义界面或"钩子"点.

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

顺便提一下,如果存在允许发现通用控制器名称的"钩子",则意味着还必须规范路由替换:

[Route("api/[controller]")]
public class OrdersController<T> : Controller { }
Run Code Online (Sandbox Code Playgroud)

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

注意

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

public class VanityOrdersController : OrdersController<Vanity> { }
public class ExistingOrdersController : OrdersController<Existing> { }
Run Code Online (Sandbox Code Playgroud)

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

~/api/VanityOrders
~/api/ExistingOrders
Run Code Online (Sandbox Code Playgroud)

我真正想要的是这个:

~/api/Orders/Vanity
~/api/Orders/Existing
Run Code Online (Sandbox Code Playgroud)

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

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

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

Sha*_*tin 16

简答

实施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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在启动期间注册它.

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

这是一些示例输出.

MyControllerFeatureProvider IsController: False.
OrdersFoobar IsController: True.
OrdersFoobarController`1 IsController: True.
Program IsController: False.
<>c__DisplayClass0_0 IsController: False.
<>c IsController: False.
Run Code Online (Sandbox Code Playgroud)

这是GitHub上的一个演示.祝你好运.

编辑 - 添加版本

.NET版本

> dnvm install "1.0.0-rc2-20221" -runtime coreclr -architecture x64 -os win -unstable
Run Code Online (Sandbox Code Playgroud)

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>
Run Code Online (Sandbox Code Playgroud)

.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
Run Code Online (Sandbox Code Playgroud)

还原,构建和运行

> dotnet restore
> dotnet build
> dotnet run
Run Code Online (Sandbox Code Playgroud)

编辑 - 关于RC1与RC2的注释

这可能是不可能的RC1,因为DefaultControllerTypeProvider.IsController()标记为internal.


Mat*_*nda 11

默认情况下会发生什么

在控制器发现过程中,您的开放泛型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();
Run Code Online (Sandbox Code Playgroud)

我们现在已经关闭TypeInfosController<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; }
}
Run Code Online (Sandbox Code Playgroud)

注册MVC服务(Startup.cs):

services.AddMvc()
    .ConfigureApplicationPartManager(apm =>
        apm.ApplicationParts.Add(new GenericControllerApplicationPart(closedControllerTypes)));
Run Code Online (Sandbox Code Playgroud)

只要你的控制器派生自内置Controller类,就没有实际需要覆盖的IsController方法ControllerFeatureProvider.因为您的通用控制器继承了该[Controller]属性ControllerBase,所以它将在发现过程中被接受为控制器,而不管它有多奇怪的名称("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;
    }
}
Run Code Online (Sandbox Code Playgroud)

应用于控制器类:

[GenericController]
public class Controller<T> : Controller
{
}
Run Code Online (Sandbox Code Playgroud)

结论

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

它已通过传统和基于属性的路由成功测试.