Mixed lifestyle for Per Thread and Per Web Request

2019-01-17 02:12发布

问题:

I'm using SimpleInjector as my IoC library. I register DbContext as per web request and it works fine. But there is one task that I run it in a background thread. So, I have a problem to create DbContext instances. e.g.

  1. Service1 has an instance of DbContext
  2. Service2 has an instance of DbContext
  3. Service1 and Service2 run from background thread.
  4. Service1 fetches an entity and pass it to Service2
  5. Service2 uses that entity, but entity is detached from DbContext

Actually the problem is here: Service1.DbContext is difference from Service2.DbContext.

It seems when I run a task in a separate thread in ASP.NET MVC, SimpleInjector creates a new instance of DbContext for each call. While some IoC libraries (for example StructureMap) have a mixed lifestyle for per-thread-per-webrequest, it seems SimpleInjector hasn't one. Am I right?

Have you any idea to solve this problem in SimpleInjector? Thanks in advance.

EDIT:

My services are here:

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"
}

I user Service2 sometimes and AsyncService2 other times.

回答1:

It seems when I run a task in a separate thread in ASP.NET MVC, SimpleInjector creates a new instance of DbContext for each call.

The behavior of the RegisterPerWebRequest lifestyle of Simple Injector v1.5 and below is to return a transient instance when instances are requested outside the context of a web request (where HttpContext.Current is null). Returning a transient instance was a design flaw in Simple Injector, since this makes it easy to hide improper usage. Version 1.6 of the Simple Injector will throw an exception instead of incorrectly returning a transient instance, to communicate clearly that you have mis-configured the container.

While some IoC libraries (for example StructureMap) have a mixed lifestyle for per-thread-per-webrequest, it seems Simple Injector hasn't one

It is correct that Simple Injector has no built-in support for mixed lifestyles because of a couple reasons. First of all it's quite an exotic feature that not many people need. Second, you can mix any two or three lifestyles together, so that would be almost an endless combination of hybrids. And last, it is (pretty) easy do register this yourself.

Although you can mix Per Web Request with Per Thread lifestyles, it would probably be better when you mix Per Web Request with Per Lifetime Scope, since with the Lifetime Scope you explicitly start and finish the scope (and can dispose the DbContext when the scope ends).

From Simple Injector 2 and on, you can easily mix any number of lifestyles together using the Lifestyle.CreateHybrid method. Here is an example:

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

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

There is another Stackoverflow question that goes into this subject a bit deeper, you might want to take a look: Simple Injector: multi-threading in MVC3 ASP.NET

UPDATE

About your update. You are almost there. The commands that run on a background thread need to run within a Lifetime Scope, so you will have to start it explicitly. The trick here is to call BeginLifetimeScope on the new thread, but before the actual command handler (and its dependencies) is created. In other words, the best way to do this is inside a decorator.

The easiest solution is to update your AsyncCommandHandlerDecorator to add the scope:

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

Purists that advocate the SOLID principles will shout that this class is violating the Single Responsibility Principle, since this decorator both runs commands on a new thread and starts a new lifetime scope. I wouldn't worry much about this, since I think that there is a close relationship between starting a background thread and starting a lifetime scope (you wouldn't use one without the other anyway). But still, you could easily leave the AsyncCommandHandlerDecorator untouched and create a new LifetimeScopedCommandHandlerDecorator as follows:

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

The order in which these decorators are registered is of course essential, since the AsyncCommandHandlerDecorator must wrap the LifetimeScopedCommandHandlerDecorator. This means that the LifetimeScopedCommandHandlerDecorator registration must come first:

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

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

This old Stackoverflow question talks about this in more detail. You should definitely take a look.