5

使用带有此处描述的命令模式和此处描述的查询模式的简单注入器。对于其中一个命令,我有 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 对我大喊大叫,告诉我它找到了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 抛出的现有异常重用于所有其他实现,而不必在回调中显式抛出它们。

更新对史蒂文的回答的回复

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

和都由实体框架类实现。根据 http 上下文注册,使用ICommandEntities此处描述的方法IUnitOfWorkDbContextDbContext

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

RegisterPerWebRequestsimpleinjector wiki 中扩展方法的默认行为是在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 实体已经拥有发送电子邮件所需的所有信息。这个命令只是从数据库中抓取一个未发送的,发送它,然后记录动作的日期时间。这样的命令可以通过从不同的进程/应用程序轮询来执行,但我不会接受这个问题的这样的答案。现在,当某种 http 请求事件触发它时,我们需要启动这个命令。

4

1 回答 1

9

确实有更简单的方法可以做到这一点。例如BatchRegistrationCallback,您可以使用该OpenGenericBatchRegistrationExtensions.GetTypesToRegister方法,而不是像在上一个代码片段中那样注册 a 。此方法由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>));

然而,正如我所指出的,给定装饰器的代码是有缺陷的,因为您应该始终在新线程上构建新的依赖关系图,并且永远不要在线程之间传递依赖关系(原始装饰器会这样做)。本文中有关此的更多信息:如何在多线程应用程序中使用依赖注入

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

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

Func<T>尽管 Simple Injector 没有对解析工厂的内置支持,但RegisterDecorator方法是例外。这样做的原因是,在没有框架支持的情况下注册具有 Func 依赖项的装饰器会非常繁琐。换句话说,当SendAsyncCommandHandlerProxy使用RegisterDecorator方法注册时,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<>));

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

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

您需要重新配置DbContext,因为纯粹的 Per Web Request 是不行的。有几种解决方案,但我认为最好的方法是制作一个混合 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>();
});

更新 Simple Injector 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.

现在,任何在为 nullMyDbContext时解析的实例都HttpContext.Current将获得一个 Per Lifetime Scope 实例,而不是一个新的瞬态实例。

于 2012-04-24T20:08:18.377 回答