Simpleinjector: Is this the right way to RegisterManyForOpenGeneric when I have 2 implementations and want to pick one?

后端 未结 1 1620
隐瞒了意图╮
隐瞒了意图╮ 2021-01-03 02:30

Using simple injector with the command pattern described here and the query pattern described here. For one of the commands, I have 2 handler implementations. The first is a

相关标签:
1条回答
  • 2021-01-03 03:13

    There are indeed easier ways to do this. For instance, instead of registering a BatchRegistrationCallback as you did in your last code snippet, you can make use of the OpenGenericBatchRegistrationExtensions.GetTypesToRegister method. This method is used internally by the RegisterManyForOpenGeneric methods, and allows you to filter the returned types before you send them to an RegisterManyForOpenGeneric overload:

    var types = OpenGenericBatchRegistrationExtensions
        .GetTypesToRegister(typeof(IHandleCommands<>), assemblies)
        .Where(t => !t.Name.StartsWith("SendAsync"));
    
    container.RegisterManyForOpenGeneric(
        typeof(IHandleCommands<>), 
        types);
    

    But I think it would be better to make a few changes to your design. When you change your async command handler to a generic decorator, you completely remove the problem altogether. Such a generic decorator could look like this:

    public class SendAsyncCommandHandlerDecorator<TCommand>
        : IHandleCommands<TCommand>
    {
        private IHandleCommands<TCommand> decorated;
    
        public SendAsyncCommandHandlerDecorator(
             IHandleCommands<TCommand> decorated)
        {
            this.decorated = decorated;
        }
    
        public void Handle(TCommand command)
        {
            // WARNING: THIS CODE IS FLAWED!!
            Task.Factory.StartNew(
                () => this.decorated.Handle(command));
        }
    }
    

    Note that this decorator is flawed because of reasons I'll explain later, but let's go with this for the sake of education.

    Making this type generic, allows you to reuse this type for multiple commands. Because this type is generic, the RegisterManyForOpenGeneric will skip this (since it can't guess the generic type). This allows you to register the decorator as follows:

    container.RegisterDecorator(
        typeof(IHandleCommands<>), 
        typeof(SendAsyncCommandHandler<>));
    

    In your case however, you don't want this decorator to be wrapped around all handlers (as the previous registration does). There is an RegisterDecorator overload that takes a predicate, that allows you to specify when to apply this decorator:

    container.RegisterDecorator(
        typeof(IHandleCommands<>), 
        typeof(SendAsyncCommandHandlerDecorator<>),
        c => c.ServiceType == typeof(IHandleCommands<SendEmailMessageCommand>));
    

    With this predicate applied, the SendAsyncCommandHandlerDecorator<T> will only be applied to the IHandleCommands<SendEmailMessageCommand> handler.

    Another option (which I prefer) is to register a closed generic version of the SendAsyncCommandHandlerDecorator<T> version. This saves you from having to specify the predicate:

    container.RegisterDecorator(
        typeof(IHandleCommands<>), 
        typeof(SendAsyncCommandHandler<SendEmailMessageCommand>));
    

    As I noted however, the code for the given decorator is flawed, because you should always build a new dependency graph on a new thread, and never pass on dependencies from thread to thread (which the original decorator does). More information about this in this article: How to work with dependency injection in multi-threaded applications.

    So the answer is actually more complex, since this generic decorator should really be a proxy that replaces the original command handler (or possibly even a chain of decorators wrapping a handler). This proxy must be able to build up a new object graph in a new thread. This proxy would look like this:

    public class SendAsyncCommandHandlerProxy<TCommand>
        : IHandleCommands<TCommand>
    {
        Func<IHandleCommands<TCommand>> factory;
    
        public SendAsyncCommandHandlerProxy(
             Func<IHandleCommands<TCommand>> factory)
        {
            this.factory = factory;
        }
    
        public void Handle(TCommand command)
        {
            Task.Factory.StartNew(() =>
            {
                var handler = this.factory();
                handler.Handle(command);
            });
        }
    }
    

    Although Simple Injector has no built-in support for resolving Func<T> factory, the RegisterDecorator methods are the exception. The reason for this is that it would be very tedious to register decorators with Func dependencies without framework support. In other words, when registering the SendAsyncCommandHandlerProxy with the RegisterDecorator method, Simple Injector will automatically inject a Func<T> delegate that can create new instances of the decorated type. Since the proxy only refences a (singleton) factory (and is stateless), we can even register it as singleton:

    container.RegisterSingleDecorator(
        typeof(IHandleCommands<>), 
        typeof(SendAsyncCommandHandlerProxy<SendEmailMessageCommand>));
    

    Obviously, you can mix this registration with other RegisterDecorator registrations. Example:

    container.RegisterManyForOpenGeneric(
        typeof(IHandleCommands<>),
        typeof(IHandleCommands<>).Assembly);
    
    container.RegisterDecorator(
        typeof(IHandleCommands<>),
        typeof(TransactionalCommandHandlerDecorator<>));
    
    container.RegisterSingleDecorator(
        typeof(IHandleCommands<>), 
        typeof(SendAsyncCommandHandlerProxy<SendEmailMessageCommand>));
    
    container.RegisterDecorator(
        typeof(IHandleCommands<>),
        typeof(ValidatableCommandHandlerDecorator<>));
    

    This registration wraps any command handler with a TransactionalCommandHandlerDecorator<T>, optionally decorates it with the async proxy, and always wraps it with a ValidatableCommandHandlerDecorator<T>. This allows you to do the validation synchronously (on the same thread), and when validation succeeds, spin of handling of the command on a new thread, running in a transaction on that thread.

    Since some of your dependencies are registered Per Web Request, this means that they would get a new (transient) instance an exception is thrown when there is no web request, which is they way this is implemented in the Simple Injector (as is the case when you start a new thread to run the code). As you are implementing multiple interfaces with your EF DbContext, this means Simple Injector will create a new instance for each constructor-injected interface, and as you said, this will be a problem.

    You'll need to reconfigure the DbContext, since a pure Per Web Request will not do. There are several solutions, but I think the best is to make an hybrid PerWebRequest/PerLifetimeScope instance. You'll need the Per Lifetime Scope extension package for this. Also note that also is an extension package for Per Web Request, so you don't have to use any custom code. When you've done this, you can define the following registration:

    container.RegisterPerWebRequest<DbContext, MyDbContext>();
    container.RegisterPerLifetimeScope<IObjectContextAdapter,
        MyDbContext>();
    
    // Register as hybrid PerWebRequest / PerLifetimeScope.
    container.Register<MyDbContext>(() =>
    {
        if (HttpContext.Current != null)
            return (MyDbContext)container.GetInstance<DbContext>();
        else
            return (MyDbContext)container
                .GetInstance<IObjectContextAdapter>();
    });
    

    UPDATE Simple Injector 2 now has the explicit notion of lifestyles and this makes the previous registration much easier. The following registration is therefore adviced:

    var hybrid = Lifestyle.CreateHybrid(
        lifestyleSelector: () => HttpContext.Current != null,
        trueLifestyle: new WebRequestLifestyle(),
        falseLifestyle: new LifetimeScopeLifestyle());
    
    // Register as hybrid PerWebRequest / PerLifetimeScope.
    container.Register<MyDbContext, MyDbContext>(hybrid);
    

    Since the Simple Injector only allows registering a type once (it doesn't support keyed registration), it is not possible to register a MyDbContext with both a PerWebRequest lifestyle, AND a PerLifetimeScope lifestyle. So we have to cheat a bit, so we make two registrations (one per lifestyle) and select different service types (DbContext and IObjectContextAdapter). The service type is not really important, except that MyDbContext must implement/inherit from that service type (feel free to implement dummy interfaces on your MyDbContext if this is convenient).

    Besides these two registrations, we need a third registration, a mapping, that allows us to get the proper lifestyle back. This is the Register<MyDbContext> which gets the proper instance back based on whether the operation is executed inside a HTTP request or not.

    Your AsyncCommandHandlerProxy will have to start a new lifetime scope, which is done as follows:

    public class AsyncCommandHandlerProxy<T>
        : IHandleCommands<T>
    {
        private readonly Func<IHandleCommands<T>> factory;
        private readonly Container container;
    
        public AsyncCommandHandlerProxy(
            Func<IHandleCommands<T>> factory,
            Container container)
        {
            this.factory = factory;
            this.container = container;
        }
    
        public void Handle(T command)
        {
            Task.Factory.StartNew(() =>
            {
                using (this.container.BeginLifetimeScope())
                {
                    var handler = this.factory();
                    handler.Handle(command);
                }            
            });    
        }    
    }
    

    Note that the container is added as dependency of the AsyncCommandHandlerProxy.

    Now, any MyDbContext instance that is resolved when HttpContext.Current is null, will get a Per Lifetime Scope instance instead of a new transient instance.

    0 讨论(0)
提交回复
热议问题