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

31
我正在尝试创建一个通用控制器,代码如下:
[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中,至少在早期,默认控制器工厂DefaultControllerFactory负责发现所有可用的控制器。它测试了“Controller”后缀:

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

显然,在ASP.NET Core中,控制器工厂不再负责此任务。正如我之前所述,我的自定义控制器工厂对于“普通”控制器执行,但对于通用控制器从未被调用。因此,还有其他一些早期评估过程中的东西来管理控制器的发现。
是否有人知道负责该发现的“服务”接口是什么?我不知道自定义接口或“钩子”点。
还有人知道让ASP.NET Core“转储”其发现的所有控制器名称的方法吗?编写单元测试来验证任何我期望的自定义控制器发现是否有效将非常好。
顺便说一句,如果有一个“钩子”允许发现通用控制器名称,则意味着路由替换也必须标准化:
[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 Core能够“看到”或“发现”我的通用控制器,尽管其运行时反射名称没有以预期的“Controller”后缀结尾。
4个回答

29

默认发生了什么

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

我们现在已经为Controller<Account>Controller<Contact>关闭了TypeInfos
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方法。因为您的通用控制器继承了[Controller]属性从ControllerBase,所以它将被认为是发现过程中的控制器,而不管其名称有多么奇怪(“Controller`1”)。
但是,“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”)可以完全查看您的控制器。

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


你能解释一下第一部分吗?我应该在哪里编写那段代码,如果我的控制器是UserController<TIdentityUser>,我应该写什么?谢谢。 - david benalal

28

简短回答

实现 IApplicationFeatureProvider<ControllerFeature> 接口。

问与答

有人知道负责 [发现所有可用控制器] 的 "service" 接口是什么吗?

ControllerFeatureProvider 负责这个。

还有没有办法让 ASP.NET Core "dump" 所发现的所有控制器的名称?

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.

这里是 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
哇!你是通过myget或其他方式找到这些预发布软件包的吗?你从哪里学习了最新的TFM和project.json规则和语义(https://github.com/bigfont/StackOverflow/blob/937b13d1a720b9bc4d1dd3662d09cde8ee8b3c89/AspNetCoreCustomControllerNames/project.json)?在我理解这些细节之前,我无法尝试您的解决方案。:O - Brent Arias
@BrentArias 各种 NuGet 源可以在这里找到:https://github.com/aspnet/Home/wiki/NuGet-feeds - Shaun Luttin
1
我尝试创建一个MyControllerFeatureProvider,但是VS不理解ControllerFeatureProvider类型,并且没有任何有用的建议来解决它。这在mvc 6 rc1中是否可用,还是我需要夜间构建才能访问它? - RonC
1
@ShaunLuttin RC1怎么样? - MacGyver
1
我发现Sebastien Ros提出了另一种ASP.NET通用控制器的方法。我还没有时间与你的进行比较... - Brent Arias
显示剩余6条评论

2
应用程序功能提供者会检查应用程序部分并为这些部分提供功能。以下是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。使用以下属性来修改名称以对应于控制器使用的通用类型:

using Microsoft.AspNetCore.Mvc.ApplicationModels; using System;

注:本文中的html标签已保留。

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

示例:通用控制器功能


0

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

    ApplicationPartManager appManager = <FROM DI>;

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

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接