多线程 – Simpleinjector:当我有2个实现并希望选择一个时,这是RegisterManyForOpenGeneric的正确方法吗?

使用简单的注入器with the command pattern described herethe query pattern described here.对于其中一个命令,我有2个处理程序实现.第一个是同步执行的“正常”实现:

public class SendEmailMessageHandler
    : IHandleCommands<SendEmailMessageCommand>
{
    public SendEmailMessageHandler(IProcessQueries queryProcessor
        , ISendMail mailSender
        , ICommandEntities entities
        , IUnitOfWork unitOfWork
        , ILogExceptions exceptionLogger)
    {
        // save constructor args to private readonly fields
    }

    public void Handle(SendEmailMessageCommand command)
    {
        var emailMessageEntity = GetThisFromQueryProcessor(command);
        var mailMessage = ConvertEntityToMailMessage(emailMessageEntity);
        _mailSender.Send(mailMessage);
        emailMessageEntity.SentOnUtc = DateTime.UtcNow;
        _entities.Update(emailMessageEntity);
        _unitOfWork.SaveChanges();
    }
}

另一个类似于命令装饰器,但显式包装前一个类以在单独的线程中执行命令:

public class SendAsyncEmailMessageHandler 
    : IHandleCommands<SendEmailMessageCommand>
{
    public SendAsyncEmailMessageHandler(ISendMail mailSender, 
        ILogExceptions exceptionLogger)
    {
        // save constructor args to private readonly fields
    }

    public void Handle(SendEmailMessageCommand command)
    {
        var program = new SendAsyncEmailMessageProgram
            (command, _mailSender, _exceptionLogger);
        var thread = new Thread(program.Launch);
        thread.Start();
    }

    private class SendAsyncEmailMessageProgram
    {
        internal SendAsyncEmailMessageProgram(
            SendEmailMessageCommand command
            , ISendMail mailSender
            , ILogExceptions exceptionLogger)
        {
            // save constructor args to private readonly fields
        }

        internal void Launch()
        {
            // get new instances of DbContext and query processor
            var uow = MyServiceLocator.Current.GetService<IUnitOfWork>();
            var qp = MyServiceLocator.Current.GetService<IProcessQueries>();
            var handler = new SendEmailMessageHandler(qp, _mailSender, 
                uow as ICommandEntities, uow, _exceptionLogger);
            handler.Handle(_command);
        }
    }
}

有一段时间,simpleinjector对我大吼大叫,告诉我它找到了2个IHandleCommands< SendEmailMessageCommand>的实现.我发现以下工作,但不确定它是否是最佳/最佳方式.我想显式注册这个接口以使用Async实现:

container.RegisterManyForOpenGeneric(typeof(IHandleCommands<>), 
    (type, implementations) =>
    {
        // register the async email handler
        if (type == typeof(IHandleCommands<SendEmailMessageCommand>))
            container.Register(type, implementations
                .Single(i => i == typeof(SendAsyncEmailMessageHandler)));

        else if (implementations.Length < 1)
            throw new InvalidOperationException(string.Format(
                "No implementations were found for type '{0}'.",
                    type.Name));
        else if (implementations.Length > 1)
            throw new InvalidOperationException(string.Format(
                "{1} implementations were found for type '{0}'.",
                    type.Name, implementations.Length));

        // register a single implementation (default behavior)
        else
            container.Register(type, implementations.Single());

    }, assemblies);

我的问题:这是正确的方式,还是有更好的东西?例如,我想重用Simpleinjector抛出的所有其他实现的现有异常,而不是必须在回调中显式抛出它们.

更新回复史蒂文的答案

我已将我的问题更新为更明确.我以这种方式实现它的原因是因为作为操作的一部分,该命令更新System.Nullable< DateTime> MailMessage成功发送后,在db实体上调用SentOnUtc属性.

ICommandEntities和IUnitOfWork都由实体框架DbContext类实现.DbContext是根据http上下文注册的,使用the method described here

container.RegisterPerWebRequest<MyDbContext>();
container.Register<IUnitOfWork>(container.GetInstance<MyDbContext>);
container.Register<IQueryEntities>(container.GetInstance<MyDbContext>);
container.Register<ICommandEntities>(container.GetInstance<MyDbContext>);

simpleinjector wiki中RegisterPerWebRequest扩展方法的默认行为是在HttpContext为null(它将在新启动的线程中)时注册一个瞬态实例.

var context = HttpContext.Current;
if (context == null)
{
    // No HttpContext: Let's create a transient object.
    return _instanceCreator();
...

这就是Launch方法使用服务定位器模式获取DbContext的单个实例,然后将其直接传递给同步命令处理程序构造函数的原因.为了使_entities.Update(emailMessageEntity)和_unitOfWork.SaveChanges()行能够工作,两者都必须使用相同的DbContext实例.

注意:理想情况下,发送电子邮件应由单独的轮询工作人员处理.这个命令基本上是一个队列清算所.数据库中的EmailMessage实体已具有发送电子邮件所需的所有信息.此命令只是从数据库中获取未发送的一个,发送它,然后记录操作的DateTime.这样的命令可以通过从不同的进程/应用程序进行轮询来执行,但我不会接受这个问题的答案.现在,当某种http请求事件触发它时,我们需要启动此命令.

最佳答案
确实有更简单的方法可以做到这一点.例如,您可以使用OpenGenericBatchRegistrationExtensions.GetTypesToRegister方法,而不是像在上一个代码段中那样注册BatchRegistrationCallback. RegisterManyForOpenGeneric方法在内部使用此方法,并允许您在将返回的类型发送到RegisterManyForOpenGeneric重载之前对其进行过滤:

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

container.RegisterManyForOpenGeneric(
    typeof(IHandleCommands<>), 
    types);

但我认为对您的设计进行一些更改会更好.当您将异步命令处理程序更改为通用装饰器时,您可以完全删除该问题.这样的通用装饰器可能如下所示:

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

请注意,这个装饰器是有缺陷的,因为我稍后会解释的原因,但是为了教育,让我们继续这样做.

使此类型通用,允许您将此类型重用于多个命令.因为这种类型是通用的,所以RegisterManyForOpenGeneric将跳过这个(因为它无法猜测泛型类型).这允许您注册装饰器,如下所示:

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

但是,在你的情况下,你不希望这个装饰器被所有处理程序包裹起来(就像之前的注册一样).有一个RegisterDecorator重载,它接受一个谓词,允许你指定何时应用这个装饰器:

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

应用此谓词后,SendAsyncCommandHandlerDecorator< T>将仅应用于IHandleCommands< SendEmailMessageCommand>处理程序.

另一个选择(我更喜欢)是注册SendAsyncCommandHandlerDecorator< T>的封闭通用版本.版.这使您不必指定谓词:

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

然而,正如我所指出的,给定装饰器的代码是有缺陷的,因为你应该总是在新线程上构建一个新的依赖图,并且永远不会将依赖从线程传递给线程(原始装饰器所做的).有关此内容的更多信息,请参阅:How to work with dependency injection in multi-threaded applications.

所以答案实际上更复杂,因为这个通用装饰器应该是一个代替原始命令处理程序的代理(或者甚至可能是包装处理程序的装饰器链).此代理必须能够在新线程中构建新的对象图.这个代理看起来像这样:

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

尽管Simple Injector没有内置支持来解析Func< T>.在Factory中,RegisterDecorator方法是例外.这样做的原因是,在没有框架支持的情况下,使用Func依赖项注册装饰器会非常繁琐.换句话说,当使用RegisterDecorator方法注册SendAsyncCommandHandlerProxy时,Simple Injector将自动注入Func< T>.可以创建装饰类型的新实例的委托.由于代理只重新启用(单例)工厂(并且是无状态的),我们甚至可以将其注册为单例:

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

显然,您可以将此注册与其他RegisterDecorator注册混合使用.例:

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

此注册使用TransactionalCommandHandlerDecorator< T>包装任何命令处理程序,可选地使用异步代理装饰它,并始终使用ValidatableCommandHandlerDecorator< T>包装它.这允许您同步地执行验证(在同一个线程上),并且当验证成功时,在新线程上旋转处理命令,在该线程上的事务中运行.

由于某些依赖项是按Web请求注册的,这意味着它们会获得一个新的(瞬态)实例,当没有Web请求时抛出异常,这就是它在Simple Injector中实现的方式(就像这样)当你启动一个新线程来运行代码时).当您使用EF DbContext实现多个接口时,这意味着Simple Injector将为每个构造函数注入的接口创建一个新实例,正如您所说,这将是一个问题.

您需要重新配置DbContext,因为纯Per Web请求不会这样做.有几种解决方案,但我认为最好是制作混合PerWebRequest / PerLifetimeScope实例.你需要Per Lifetime Scope扩展包.另请注意,它也是Per Web Request的扩展包,因此您不必使用任何自定义代码.完成此操作后,您可以定义以下注册:

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
简单的注射器2现在具有生活方式的明确概念,这使得先前的注册更容易.因此,建议进行以下注册:

var hybrid = Lifestyle.CreateHybrid(
    lifestyleSelector: () => HttpContext.Current != null,
    trueLifestyle: new WebRequestLifestyle(),
    falseLifestyle: new LifetimeScopeLifestyle());

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

由于Simple Injector只允许注册一次类型(它不支持键控注册),因此无法使用PerWebRequest生活方式和PerLifetimeScope生活方式注册MyDbContext.所以我们不得不作弊,所以我们进行两次注册(每种生活方式一次)并选择不同的服务类型(DbContext和IObjectContextAdapter).服务类型并不重要,除了MyDbContext必须从该服务类型实现/继承(如果方便的话,随意在MyDbContext上实现虚拟接口).

除了这两个注册,我们需要第三次注册,一个映射,让我们能够恢复正常的生活方式.这是Register< MyDbContext>它根据操作是否在HTTP请求中执行而获得正确的实例.

您的AsyncCommandHandlerProxy必须启动一个新的生命周期范围,其操作如下:

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

请注意,该容器是作为AsyncCommandHandlerProxy的依赖项添加的.

现在,当HttpContext.Current为null时解析的任何MyDbContext实例将获得Per Lifetime Scope实例而不是新的瞬态实例.

转载注明原文:多线程 – Simpleinjector:当我有2个实现并希望选择一个时,这是RegisterManyForOpenGeneric的正确方法吗? - 代码日志