使用简单注入器的每线程和每Web请求的混合生活方式

时间:2012-10-30 20:59:16

标签: c# dependency-injection inversion-of-control simple-injector

我正在使用SimpleInjector作为我的IoC库。我根据网络请求注册DbContext,它运行正常。但是我有一个任务是在后台线程中运行它。因此,我在创建DbContext个实例时遇到问题。 e.g。

  1. Service1的实例为DbContext
  2. Service2的实例为DbContext
  3. Service1Service2从后台线程运行。
  4. Service1获取实体并将其传递给Service2
  5. Service2使用该实体,但实体与DbContext
  6. 分离

    实际上问题出在此处:Service1.DbContextService2.DbContext不同。

    当我在ASP.NET MVC中的一个单独的线程中运行任务时,SimpleInjector为每个调用创建一个DbContext的新实例。虽然一些IoC库(例如StructureMap)对于每个网络的每个线程请求具有混合的生活方式,但似乎SimpleInjector没有一个。{1}}。我是对的吗?

    您有任何想法在SimpleInjector解决此问题吗? 提前谢谢。

    修改

    我的服务在这里:

    class Service1 : IService1 {
        public Service1(MyDbContext context) { }
    }
    
    class Service2 : IService2 {
        public Service2(MyDbContext context, IService1 service1) { }
    }
    
    class SyncServiceUsage {
        public SyncServiceUsage(Service2 service2) {
            // use Service2 (and Service1 and DbContext) from HttpContext.Current
        }
    }
    
    class AsyncServiceUsage {
        public AsyncServiceUsage(Service2 service2) {
            // use Service2 (and Service1 and DbContext) from background thread
        }
    }
    
    public class AsyncCommandHandlerDecorator<TCommand> 
        : ICommandHandler<TCommand> where TCommand : ICommand {
    
        private readonly Func<ICommandHandler<TCommand>> _factory;
    
        public AsyncCommandHandlerDecorator(Func<ICommandHandler<TCommand>> factory) {
            _factory = factory;
        }
    
        public void Handle(TCommand command) {
            ThreadPool.QueueUserWorkItem(_ => {
                // Create new handler in this thread.
                var handler = _factory();
                handler.Handle(command);
            });
        }
    }
    
    void InitializeSimpleInjector() {
        register AsyncCommandHandlerDecorator for services (commands actually) that starts with "Async"
    }
    

    我有时会用Service2,有时会AsyncService2

1 个答案:

答案 0 :(得分:17)

  

当我在ASP.NET MVC中的一个单独的线程中运行任务时,   SimpleInjector为每次调用创建一个新的DbContext实例。

Simple Injector v1.5及更低版本的RegisterPerWebRequest生活方式的行为是在Web请求的上下文之外请求实例时返回临时实例(其中HttpContext.Current为null)。返回瞬态实例是Simple Injector中的一个设计缺陷,因为这样可以很容易地隐藏不正确的用法。 Version 1.6 of the Simple Injector将抛出异常而不是错误地返回瞬态实例,以清楚地表明您错误配置了容器。

  

虽然有些IoC库(例如StructureMap)有混合   每个网络请求的每个线程的生活方式,它似乎简单的注入器   没有一个

由于几个原因,Simple Injector没有内置的混合生活方式支持是正确的。首先,它是一个非常奇特的功能,没有多少人需要。其次,你可以将任何两种或三种生活方式混合在一起,这样几乎是混合动力的无穷无尽的组合。最后,自己注册(非常)很容易。

虽然您可以将Per Web RequestPer Thread生活方式混合使用,但将每个Web请求与Per Lifetime Scope混合使用可能会更好,因为使用Lifetime Scope可以明确地开始和完成范围(并且可以在范围结束时处置DbContext。)

Simple Injector 2开始,您可以使用Lifestyle.CreateHybrid方法轻松混合任意数量的生活方式。这是一个例子:

var hybridLifestyle = Lifestyle.CreateHybrid(
    () => HttpContext.Current != null,
    new WebRequestLifestyle(),
    new LifetimeScopeLifestyle());

// Register as hybrid PerWebRequest / PerLifetimeScope.
container.Register<DbContext, MyDbContext>(hybridLifestyle);

还有另一个Stackoverflow问题深入讨论这个主题,你可能想看看:Simple Injector: multi-threading in MVC3 ASP.NET

<强>更新

关于您的更新。你快到了。在后台线程上运行的命令需要在 Lifetime Scope 中运行,因此您必须明确地启动它。这里的技巧是在新线程上调用BeginLifetimeScope,但在创建实际命令处理程序(及其依赖项)之前。换句话说,执行此操作的最佳方法是在装饰器内。

最简单的解决方案是更新您的AsyncCommandHandlerDecorator以添加范围:

public class AsyncCommandHandlerDecorator<TCommand> 
    : ICommandHandler<TCommand> where TCommand : ICommand 
{
    private readonly Container _container;
    private readonly Func<ICommandHandler<TCommand>> _factory;

    public AsyncCommandHandlerDecorator(Container container,
        Func<ICommandHandler<TCommand>> factory) 
    {
        _container = container;
        _factory = factory;
    }

    public void Handle(TCommand command) 
    {
        ThreadPool.QueueUserWorkItem(_ => 
        {
            using (_container.BeginLifetimeScope())
            {
                // Create new handler in this thread
                // and inside the lifetime scope.
                var handler = _factory();
                handler.Handle(command);
            }
        });
    }
}

提倡SOLID原则的纯粹主义者会大声说这个类违反了Single Responsibility Principle,因为这个装饰器都在新线程上运行命令并启动新的生命周期范围。我不会担心这一点,因为我认为启动后台线程和启动生命周期范围之间存在密切关系(不管怎样,你都不会使用其中一个)。但是,您仍然可以轻松地保持AsyncCommandHandlerDecorator不变,并创建一个新的LifetimeScopedCommandHandlerDecorator,如下所示:

public class LifetimeScopedCommandHandlerDecorator<TCommand> 
    : ICommandHandler<TCommand> where TCommand : ICommand 
{
    private readonly Container _container;
    private readonly Func<ICommandHandler<TCommand>> _factory;

    public LifetimeScopedCommandHandlerDecorator(Container container,
        Func<ICommandHandler<TCommand>> factory)
    {
        _container = container;
        _factory = factory;
    }

    public void Handle(TCommand command)
    {
        using (_container.BeginLifetimeScope())
        {
            // The handler must be created inside the lifetime scope.
            var handler = _factory();
            handler.Handle(command);
        }
    }
}

这些装饰者的注册顺序当然是必不可少的,因为AsyncCommandHandlerDecorator 必须包裹LifetimeScopedCommandHandlerDecorator。这意味着必须首先注册LifetimeScopedCommandHandlerDecorator

container.RegisterDecorator(typeof(ICommandHandler<>),
    typeof(LifetimeScopedCommandHandlerDecorator<>),
    backgroundCommandCondition);

container.RegisterDecorator(typeof(ICommandHandler<>),
    typeof(AsyncCommandHandlerDecorator<>),
    backgroundCommandCondition);

This old Stackoverflow question更详细地讨论了这一点。你一定要看看。