在ASP.NET Core中替换中间件的激活器

Jos*_*ron 8 c# dependency-injection ninject asp.net-core

我试图指示我的ASP.NET Core MVC应用程序使用第三方DI容器.而不是写一个适配器,我试图按照这篇文章中的建议插入库

这很好用 - 我可以IControllerActivator用我自己的内置替换使用DI容器的内置.但是,在尝试实例化依赖注入依赖项的自定义中间件时,我遇到了障碍.ASP.NET无法解决这些依赖关系,因为它没有使用我的第三方DI容器 - 是否有相当于IControllerActivator中间件,或者我是否坚持使用内置DI或编写适配器?

**编辑**

这是我的一些代码 - 我实际上是尝试使用上面的模式使用Ninject.

internal sealed class NinjectControllerActivator : IControllerActivator
{
    private readonly IKernel _kernel;

    public NinjectControllerActivator(IKernel kernel)
    {
        _kernel = kernel;
    }

    [DebuggerStepThrough]
    public object Create(ActionContext context, Type controllerType)
    {
        return _kernel.Get(controllerType);
    }
}
Run Code Online (Sandbox Code Playgroud)

我发现我有两个问题:

  • 我不能将标准的ASP.NET组件注入我的控制器,因为Ninject不知道它们
  • 我使用应用程序服务的中间件无法实例化,因为ASP.NET不知道Ninject.

有关第一个问题的示例,这里是一个无法实例化的控制器,因为我正在使用IUrlHelper(还要注意ILogger,它也无法实例化):

public class SystemController : Controller 
{
    public SystemController(ILogger logger, IUrlHelper urlHelper) 
    {
         /*...*/
    }
}
Run Code Online (Sandbox Code Playgroud)

以下是自定义中间件的第二个问题示例:

public class CustomMiddleware
{
    private RequestDelegate _next;
    // this is an application specific service registered via my Ninject kernel
    private IPersonService _personService; 

    public CustomMiddleware(RequestDelegate next, IPersonService personService)
    {
        _next = next;
        _personService = personService;
    }

    public async Task Invoke(HttpContext context)
    {
        /* ... */
    }
}
Run Code Online (Sandbox Code Playgroud)

我意识到理论上ASP.NET组件应该在他们自己的管道中,而我的应用程序组件应该在另一个组件中,但实际上我经常需要以交叉方式使用组件(如上例所示).

Ste*_*ven 12

SOLID原则规定:

摘要由上层/政策层(DIP)拥有

这意味着我们的应用程序代码不应直接依赖于框架代码,即使它们是抽象的.相反,我们应该定义为我们的应用程序使用而定制的角色接口.

因此Microsoft.Framework.Logging.ILogger,SOLID原则不是依赖于抽象,而是可能适合或不适合我们的应用程序特定需求,而是指导我们实现应用程序所拥有的抽象(端口),并使用挂钩到框架代码中的适配器实现.以下是您自己的ILogger抽象可能如何的示例.

当应用程序代码依赖于您自己的抽象时,您需要一个能够将调用转发给框架提供的实现的适配器实现:

public sealed class MsLoggerAdapter : MyApp.ILogger
{
    private readonly Func<Microsoft.Framework.Logging.ILogger> factory;
    public MsLoggerAdapter(Func<Microsoft.Framework.Logging.ILogger> factory) {
        this.factory = factory;
    }

    public void Log(LogEntry entry) {
        var logger = this.factory();
        LogLevel level = ToLogLevel(entry.Severity);
        logger.Log(level, 0, entry.Message, entry.Exception,
            (msg, ex) => ex != null ? ex.Message : msg.ToString());
    }

    private static LogLevel ToLogLevel(LoggingEventType severity) { ... }
}
Run Code Online (Sandbox Code Playgroud)

此适配器可以在应用程序容器中注册,如下所示:

container.RegisterSingleton<MyApp.ILogger>(new MsLoggerAdapter(
    app.ApplicationServices.GetRequiredService<Microsoft.Framework.Logging.ILogger>));
Run Code Online (Sandbox Code Playgroud)

BIG警告:不要使框架抽象的直接拷贝.这几乎不会带来好结果.您应该指定根据您的应用程序定义的抽象.这甚至可能意味着适配器变得更加复杂并且需要多个框架组件来履行其合同,但这会产生更清晰且更易于维护的应用程序代码.

但是,如果应用SOLID对您来说太麻烦,并且您只想直接依赖外部组件,则可以始终在应用程序容器中交叉连接所需的依赖项,如下所示:

container.Register<Microsoft.Framework.Logging.ILogger>(
    app.ApplicationServices.GetRequiredService<Microsoft.Framework.Logging.ILogger>);
Run Code Online (Sandbox Code Playgroud)

它就像这样简单,但请注意,为了保持应用程序的清洁和可维护性,定义符合SOLID原则的特定于应用程序的抽象要好得多.另请注意,即使您这样做,您也只需要一些交叉连接的依赖项.因此,最好仍然将应用程序容器与vNext配置系统尽可能分开.

使用中间件,这里有一个完全不同的问题.在中间件中,您将运行时数据(next委托)注入到组件(CustomMiddleware类)中.这让您倍感悲痛,因为这会使注册和解析组件变得复杂,并阻止容器对其进行验证和诊断.相反,您应该将next委托移出构造函数并进入Invoke委托,如下所示:

public class CustomMiddleware
{
    private IPersonService _personService;

    public CustomMiddleware(IPersonService personService) {
        _personService = personService;
    }

    public async Task Invoke(HttpContext context, RequestDelegate next) { /* ... */ }
}
Run Code Online (Sandbox Code Playgroud)

现在您可以将中间件挂钩到管道中,如下所示:

app.Use(async (context, next) =>
{
    await container.GetInstance<CustomMiddleware>().Invoke(context, next);
});
Run Code Online (Sandbox Code Playgroud)

但不要忘记,您可以随时手动创建中间件,如下所示:

var frameworkServices = app.ApplicationServices;

app.Use(async (context, next) =>
{
    var mw = new CustomMiddleware(
        container.GetInstance<IPersonService>(),
        container.GetInstance<IApplicationSomething>(),
        frameworkServices.GetRequiredService<ILogger>(),
        frameworkServices.GetRequiredService<AspNetSomething>());

    await mw.Invoke(context, next);
});
Run Code Online (Sandbox Code Playgroud)

非常不幸的是,ASP.NET调用自己的服务ApplicationServices,因为这是您自己的应用程序容器所在的位置; 不是内置的配置系统.

  • 更糟糕的是,这意味着您需要将它们从应用程序容器中取出,以某种方式获取它们并手动将它们传递给适配器.这真的是非常恐怖的IMO.好消息是,这种模式是可行的,但从美学POV来看并不是很好. (3认同)
  • 我理解快乐道路所违反的原则,但另类看起来非常糟糕.遵循该字母的原则意味着将所有抽象包装在控件之外,并将外部依赖项交叉连接到应用程序容器中.你真的认为那更好吗? (2认同)
  • @davidfowl我不建议在应用程序容器AT ALL中交叉连接外部依赖项,因为这意味着应用程序组件仍然可以依赖于外部依赖项,而那些应该由适配器包装.并且不要忘记你只需要包装应用程序实际使用的抽象,通常只需要几个."它是否更好"取决于手头应用程序的大小和使用寿命,但总的来说,我认为这实际上要好得多. (2认同)