ASP.NET 带有简单注入器的核心 2.1 服务定位器返回 null

ASP.NET Core 2.1 Service Locator with Simple Injector returning null

我有一个 .NET MVC 5 .NET Framework 应用程序,我正在将其转换为 .NET Core 2.1

我有一个自定义操作过滤器,它在 .NET Framework 版本中被注册为 Filterconfig class 中的全局过滤器,如下所示:

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new MyCustomActionFilter());
    }
}

在 .NET 版本的自定义操作过滤器中,我使用的是服务定位器模式(我知道它可以被认为是反模式),如下所示:

var myService = DependencyResolver.Current.GetService<IMyService>();

我正在为 DI 使用 Simple Injector,在 .NET 版本中一切正常。对于 .NET Core 版本,我试图获得相同的功能,但 myService 始终为 null

我仍在使用 Simple Injector(因为解决方案中的所有其他项目都在使用它,并且它们没有转移到 .NET Core 项目(只有 Web 项目是)。

我的 Startup.cs class 有这个代码:

services.Configure<MvcOptions>(options =>
{
    options.Filters.Add(new MyCustomActionFilter());
});

SimpleInjectorConfig.IntegrateSimpleInjector(services, container);

在我的服务层,我有一个从 Web 层调用的 SimpleInjector Registartion class - 然后向下调用 DAL 层进行注册

public class SimpleInjectorRegistration
{
    public static void RegisterServices(Container container)
    {
        container.Register<IMyService, MyService>();
        //further code removed for brevity

当我 运行 应用程序在自定义过滤器中有一个断点并且在这个 RegisterServices 方法中有一个断点时,我可以看到 RegisterServices 方法中的断点首先被命中,然后是自定义过滤器中的断点 - 这个让我觉得容器中的一切都已正确连接。

但是我正尝试在自定义过滤器中使用 .NET Core 服务定位器模式再次执行以下操作

var myService = filterContext.HttpContext.RequestServices.GetService<IMyService>();

但结果始终为空?

我在这个设置中遗漏了什么吗?

------------更新-----------------

根据史蒂文斯的评论,我向我的操作过滤器添加了一个构造函数并传入了简单注入器容器。

所以我的启动 class 现在是:

public class Startup
{
    //Simple Injector container
    private Container container = new Container(); 

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();


        services.Configure<MvcOptions>(options =>
        {
           options.Filters.Add(new MyCustomActionFilter(container)); 

我的自定义过滤器现在如下所示,添加了构造函数:

public class MyCustomActionFilter : ActionFilterAttribute
{
    private readonly IMyService _myService;

    public MyCustomActionFilter(Container container)
    {
        _myService = container.GetService<IMyService>();
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
       //actual code of custom filter removed - use of MyService 

我在 MyCustomActionFilter 的构造函数上设置了一个断点,我可以看到它被击中,但我抛出了一个错误:

SimpleInjector.ActivationException: 'The IDbContext is registered as 'Async Scoped' lifestyle, but the instance is requested outside the context of an active (Async Scoped) scope.'

MyService 对注入其中的 DbContext 有依赖性(它正在执行从数据库保存和检索数据的工作。

对于数据库上下文,我注册如下:

public class SimpleInjectorRegistration
{
    public static void RegisterServices(Container container, string connectionString)
    {
        container.Register<IDbContext>(() => new MyDbContext(connectionString),
            Lifestyle.Scoped);
    }

}

在旧的 ASP.NET MVC 和新的 ASP.NET Core 中集成 Simple Injector 的方式有一些重大变化。在旧系统中,您可以替换 IDependencyResolver。 ASP.NET 然而,Core 包含一个完全不同的模型,它有自己的内部 DI 容器。由于用 Simple Injector 替换内置容器是 impossible,因此您将并排放置两个容器 运行。在这种情况下,内置容器将解析框架和第三方组件,其中 Simple Injector 将为您编写应用程序组件。

当您调用 HttpContext.RequestServices.GetService 时,您将请求内置容器的服务,不是 简单注入器。将 IMyService 注册添加到内置容器中,正如 TanvirArjel 的回答所暗示的那样,起初似乎可行,但这完全跳过了等式中的 Simple Injector,这显然不是一个选项,因为您希望使用 Simple注入器作为您的应用程序容器。

要模仿您之前的类似服务定位器的行为,您必须将 SimpleInjector.Container 注入您的过滤器,如下所示:

options.Filters.Add(new MyCustomActionFilter(container));

但是,如您在问题中所示,从构造函数中调用容器是错误的:

public class MyCustomActionFilter : ActionFilterAttribute
{
    private readonly IMyService _myService;

    public MyCustomActionFilter(Container container)
    {
        _myService = container.GetService<IMyService>(); // NEVER DO THIS!!!
    }

    ...
}

WARNING: You should never resolve from the container from the constructor. Or in more general: you should never use any injected dependency from inside the constructor. The constructor should only store the dependency.

正如 Mark Seemann 所解释的,injection constructors should be simple。在这种情况下,情况甚至会变得更糟,因为:

  • 调用MyCustomActionFilter的构造函数期间,没有活动作用域,无法解析IMyService
  • 即使 IMyService 可以解析,MyCustomActionFilter 也是一个 Singleton,将 IMyService 存储在私有字段中将导致隐藏 Captive Dependency。这可能会导致各种麻烦。

您应该存储 Container 依赖项,而不是存储已解析的 IMyService 依赖项:

public class MyCustomActionFilter : ActionFilterAttribute
{
    private readonly Container _container;

    public MyCustomActionFilter(Container container)
    {
        _container = container;
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        myService = container.GetService<IMyService>();
        //actual code of custom filter removed - use of MyService 
    }
}

在调用 OnActionExecuting 期间,将有一个活动的简单注入器 Scope,这将允许解析 IMyService。最重要的是,由于 IMyService 没有存储在私有字段中,因此不会被缓存,也不会导致 Captive Dependency。

在你的问题中你提到了 Service Locator anti-pattern. Whether or not the injection of the Container into your filter is in fact an implementation of the Service Locator anti-pattern depends on where the filter is located. As Mark Seemann puts 它:

A DI container encapsulated in a Composition Root is not a Service Locator - it's an infrastructure component.

换句话说,只要过滤器 class 位于 内部 您的 Composition Root,您就没有应用服务定位器反模式。然而,这确实意味着您必须确保过滤器本身包含尽可能少的有趣行为。该行为应全部移至过滤器解析的服务。

正如@Steven 指出的那样,内置容器将解析框架和第三方组件,其中 Simple Injector 将为您编写应用程序组件。对于内置容器,它无法解析来自简单注入器的服务。对于简单的注入器,你可以尝试 EnableSimpleInjectorCrossWiring 从内置容器解析服务。

对于options.Filters.Add,它也接受MyCustomActionFilter instance,无需将Container作为对MyCustomActionFilter的依赖,您可以尝试在样本注入器中注册MyCustomActionFilter,然后将此实例传递给 options.Filters.Add

  • 注册服务

        private void InitializeContainer(IApplicationBuilder app)
    {
        // Add application presentation components:
        container.RegisterMvcControllers(app);
        container.RegisterMvcViewComponents(app);
    
        // Add application services. For instance:
        container.Register<IMyService, MyService>(Lifestyle.Scoped);
        container.Register<MyCustomActionFilter>(Lifestyle.Scoped);
        // Allow Simple Injector to resolve services from ASP.NET Core.
        container.AutoCrossWireAspNetComponents(app);
    }
    
  • 添加 MyCustomActionFilter

            services.Configure<MvcOptions>(options =>
        {
            using (AsyncScopedLifestyle.BeginScope(container))
            {
                options.Filters.Add(container.GetRequiredService<MyCustomActionFilter>());
            }
        });
        #region SampleInjector
        IntegrateSimpleInjector(services);
        #endregion
    

    注意如果指定container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();,调用container.GetRequiredService<MyCustomActionFilter>()时需要using (AsyncScopedLifestyle.BeginScope(container))