使用工作单元装饰特定的命令处理程序

本文关键字:命令处理程序 工作 单元 | 更新日期: 2023-09-27 18:31:23

我正在尝试将我的应用程序从服务模式重写为命令和查询模式(在我移动到 CQRS 之前)。目前我被困在这个博客上。

它显示了他将工作单元提交从基本命令移动到PostCommitCommandHandlerDecorator的位置,然后使用简单注入器将它们绑定起来。作者还表示,并非所有命令都需要使用工作单元,这在我的情况下是正确的,因为并非每个命令都与数据库通信,但有些命令发送电子邮件等。

如何构建命令和绑定,以便只有那些需要包装在工作单元提交的命令才会被 IoC 容器绑定?

使用工作单元装饰特定的命令处理程序

如何构建命令和绑定,以便只有那些需要包装在工作单元提交的命令才会被 IoC 容器绑定?

首先,并非所有处理程序都使用工作单元真的重要吗?当创建工作单元而未使用它时,这是一个问题吗?因为当没有性能问题时,就没有必要使代码更加复杂。

但让我们假设它确实很重要。在这种情况下,诀窍是查询容器是否将工作单元注入到某处。您可以使用Lazy<T>来使其正常工作。请查看以下注册:

Func<IUnitOfWork> uowFactory = 
    () => new MyUnitOfWork(connectionString);
// Register the factory as Lazy<IUnitOfWork>
container.Register<Lazy<IUnitOfWork>>(
    () => new Lazy<IUnitOfWork>(uowFactory), 
    Lifestyle.Scoped);
// Create a registration that redirects to Lazy<IUnitOfWork>
container.Register<IUnitOfWork>(
    () => container.GetInstance<Lazy<IUnitOfWork>>().Value, 
    Lifestyle.Scoped);

对于本文的其余部分,我假设您正在构建一个 Web 应用程序,但想法是相同的。

通过此注册,当容器解析具有依赖于IUnitOfWork的组件的对象图时,它将在幕后解析Lazy<IUnitOfWork>并获取其值。我们缓存每个请求的Lazy<IUnitOfWork>,因此这允许我们拥有另一个依赖于Lazy<IUnitOfWork>的组件,并检查其IsValueCreated属性以查看IUnitOfWork是否在任何地方注入。

现在,您的装饰器可能如下所示:

public class TransactionCommandHandlerDecorator<TCommand>
    : ICommandHandler<TCommand>
{
    private readonly ICommandHandler<TCommand> decorated;
    private readonly Lazy<IUnitOfWork> lazyUnitOfWork;
    public TransactionCommandHandlerDecorator(
        ICommandHandler<TCommand> decorated,
        Lazy<IUnitOfWork> lazyUnitOfWork)
    {
        this.decorated = decorated;
        this.lazyUnitOfWork = lazyUnitOfWork;
    }
    public void Handle(TCommand command)
    {
        this.decorated.Handle(command);
        if (this.lazyUnitOfWork.IsValueCreated)
        {
            this.lazyUnitOfWork.Value.SubmitChanges();
        }
    }
}

但是请注意,您仍然不知道工作单元是否实际使用,但我认为可以安全地假设工作单元将在注入时使用。您不想注入未使用的依赖项。

如果这不能削减它,并且您想检查它是否已创建,则必须注入一个允许您检查的代理工作单元。例如:

public class DelayedUnitOfWorkProxy : IUnitOfWork
{
    private Lazy<IUnitOfWork> uow;
    public DelayedUnitOfWorkProxy(Lazy<IUnitOfWork> uow)
    {
        this.uow = uow;
    }
    void IUnitOfwork.SubmitChanges()
    {
        this.uow.Value.SubmitChanges();
    }
    // TODO: Implement All other IUnitOfWork methods
}

您的配置现在将如下所示:

Func<IUnitOfWork> uowFactory = 
    () => new MyUnitOfWork(connectionString);
// Register the factory as Lazy<IUnitOfWork>
container.Register<Lazy<IUnitOfWork>>(
    () => new Lazy<IUnitOfWork>(uowFactory), 
    Lifestyle.Scoped);
// Register the proxy that delays the creation of the UoW
container.Register<IUnitOfWork, DelayedUnitOfWorkProxy>(
    Lifestyle.Scoped);

当命令或任何其他依赖项需要IUnitOfWork时,它们将获得DelayedUnitOfWorkProxy,并注入Lazy<IUnitOfWork>。所以在创建对象图之后,工作单元本身不会创建。仅当调用其中一个DelayedUnitOfWorkProxy方法时,才会创建此类实例。装饰器将保持不变。

但即使这样也可能不够好。MVC 控制器(假设您正在构建一个 ASP.NET MVC 应用程序)可能依赖于使用工作单元的查询,但命令处理程序不依赖于使用工作单元的查询。在这种情况下,您可能仍然不想提交工作单元,因为命令处理程序(或其依赖项之一)仍然不使用工作单元。

在这种情况下,您实际尝试做的是将命令处理程序的执行隔离在其自己的范围内。就好像它们在不同的应用程序域中运行一样。您希望它们独立于执行它们的 Web 请求。

在这种情况下,您需要混合生活方式。使用Simple Injector,您可以保持所有代码和配置不变,但切换到这样的混合生活方式:

container.Options.DefaultScopedLifestyle = Lifestyle.CreateHybrid(
    () => container.GetCurrentLifetimeScope() != null,
    new LifetimeScopeLifestyle(),
    new WebRequestLifestyle());
Func<IUnitOfWork> uowFactory = 
    () => new MyUnitOfWork(connectionString);
// Register the factory as Lazy<IUnitOfWork>
container.Register<Lazy<IUnitOfWork>>(
    () => new Lazy<IUnitOfWork>(uowFactory), 
    Lifestyle.Scoped);
// Register a proxy that depends on Lazy<IUnitOfWork>    
container.Register<IUnitOfWork, DelayedUnitOfWorkProxy>(
    Lifestyle.Scoped);

混合生活方式是两种(或多种)生活方式的组合,它包含一个谓词委托,容器将调用该委托来检查应应用哪种生活方式。

仅使用此配置,不会发生任何事情,因为LifetimeScopeLifestyle要求您显式启动和停止新作用域。如果没有作用域,container.GetCurrentLifetimeScope() 方法将始终返回 null,这意味着混合生活方式将始终选择 WebRequestLifestyle。

您需要在解析新的命令处理程序之前启动新的生存期范围。与往常一样,这可以通过定义装饰器来完成:

private sealed class LifetimeScopeCommandHandlerDecorator<T>
    : ICommandHandler<T>
{
    private readonly Container container;
    private readonly Func<ICommandHandler<T>> decorateeFactory;
    public LifetimeScopeCommandHandlerDecorator(Container container,
        Func<ICommandHandler<T>> decorateeFactory)
    {
        this.decorateeFactory = decorateeFactory;
        this.container = container;
    }
    public void Handle(T command)
    {
        using (this.container.BeginLifetimeScope())
        {
            var decoratee = this.decorateeFactory.Invoke();
            decoratee.Handle(command);
        }
    }
}

您应该将此装饰器注册为最后一个装饰器(最外层的装饰器)。这个装饰器不依赖于ICommandHandler<T>而是依赖于Func<ICommandHandler<T>>。这可确保仅在调用 Func<T> 委托时解析修饰的命令处理程序。这会推迟创建,并允许首先创建生命周期范围。

由于此修饰器依赖于两个单例(容器和Func<T>都是单例),因此装饰器本身也可以注册为单例。您的配置可能如下所示:

// Batch register all command handlers
container.Register(
    typeof(ICommandHandler<>), 
    typeof(ICommandHandler<>).Assembly);
// Register one or more decorators
container.RegisterDecorator(
    typeof(ICommandHandler<>), 
    typeof(TransactionCommandHandlerDecorator<>));
// The the lifetime scope decorator last (as singleton).
container.RegisterDecorator(
    typeof(ICommandHandler<>), 
    typeof(LifetimeScopeCommandHandlerDecorator<>),
    Lifestyle.Singleton);

这将有效地将命令使用的工作单元与请求其余部分中在命令处理程序上下文之外创建的任何工作单元隔离开来。

有一种简单的方法可以实现您的要求。RegisterDecorator扩展方法有一些重载版本,它们接受一个Predicate,该与标记接口结合使用,可用于有选择地应用装饰器。

下面是一个代码示例:

public interface ICommandHandler<T> where T : class { }
public interface IDontUseUnitOfWork { }
public class MyCommand { }
public class MyCommandHandler : 
    ICommandHandler<MyCommand>, IDontUseUnitOfWork { }
public sealed class UnitOfWorkCommandDecorator<T> :
    ICommandHandler<T> where T : class
{
    public UnitOfWorkCommandDecorator(ICommandHandler<T> decorated) { }
}

以及将UnitOfWorkCommandDecorator应用于命令处理程序的注册,但那些使用 IDontUseUnitOfWork 接口标记的处理程序除外

container.RegisterDecorator(
    typeof(ICommandHandler<>), 
    typeof(UnitOfWorkCommandDecorator<>),
    x => !typeof(IDontUseUnitOfWork).IsAssignableFrom(x.ImplementationType));

这个谓词功能非常有用,非常值得掌握。