我试图指示我的 ASP.NET Core MVC 应用程序使用第 3 方 DI 容器。 我没有编写适配器,而是尝试按照这篇文章
中的建议插入库这工作得很好 - 我可以用我自己的使用 DI 容器的内置
IControllerActivator
替换。 但是,在尝试实例化也依赖于注入依赖项的自定义中间件时,我遇到了障碍。 ASP.NET 无法解析这些依赖项,因为它没有使用我的第 3 方 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);
}
}
我发现我有两个问题:
对于第一个问题的示例,这是一个无法实例化的控制器,因为我正在使用
IUrlHelper
(另请注意 ILogger
,它也无法实例化):
public class SystemController : Controller
{
public SystemController(ILogger logger, IUrlHelper urlHelper)
{
/*...*/
}
}
这是自定义中间件的第二个问题的示例:
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)
{
/* ... */
}
}
我意识到理论上 ASP.NET 组件应该位于自己的管道中,而我的应用程序组件应该位于另一个管道中,但实际上我经常需要以横切的方式使用组件(如上面的示例所示)。
SOLID 原则规定:
摘要归上层/政策层所有(DIP)
这意味着我们的应用程序代码不应该直接依赖于框架代码,即使它们是抽象的。相反,我们应该定义为我们的应用程序的使用量身定制的角色接口。
因此,SOLID 原则不是依赖于可能适合也可能不适合我们的应用程序特定需求的
Microsoft.Framework.Logging.ILogger
抽象,而是引导我们走向应用程序拥有的抽象(端口),并使用挂钩到框架代码的适配器实现。以下是您自己的 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) { ... }
}
此适配器可以在您的应用程序容器中注册,如下所示:
container.RegisterSingleton<MyApp.ILogger>(new MsLoggerAdapter(
app.ApplicationServices.GetRequiredService<Microsoft.Framework.Logging.ILogger>));
更新:在 ASP.NET Core 的 Simple Injector 集成包的更高版本中,Simple Injector 能够自动从内置 ASP.NET 配置系统中提取这些依赖项。也就是说,不再需要之前的手工注册了。
大警告:不要直接复制框架抽象。这几乎永远不会带来好的结果。您应该指定根据您的应用程序定义的抽象。这甚至可能意味着适配器变得更加复杂,并且需要多个框架组件来履行其合同,但这会导致应用程序代码更干净、更易于维护。
但是,如果应用 SOLID 对您来说太麻烦,并且您只想直接依赖外部组件,那么您始终可以在应用程序容器中交叉连接所需的依赖项,如下所示:
container.Register<Microsoft.Framework.Logging.ILogger>(
app.ApplicationServices.GetRequiredService<Microsoft.Framework.Logging.ILogger>);
就这么简单,但请注意,为了保持应用程序的整洁和可维护性,最好定义遵循 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) { /* ... */ }
}
现在您可以将中间件挂接到管道中,如下所示:
app.Use(async (context, next) =>
{
await container.GetInstance<CustomMiddleware>().Invoke(context, next);
});
但不要忘记,您始终可以手动创建中间件,如下所示:
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);
});
ASP.NET 调用自己的服务确实很不幸ApplicationServices
,因为那是你自己的应用程序容器的用途;不是内置的配置系统。