DDD:有没有一种优雅的方法可以在更新聚合根时传递审计信息?

DDD: Is there an elegant way to pass auditing information through while updating an aggregate root?

假设我有一个如下所示的 CQRS 命令:

public sealed class DoSomethingCommand : IRequest
{
    public Guid Id { get; set; }

    public Guid UserId { get; set; }

    public string A { get; set; }

    public string B { get; set; }
}

这是在以下命令处理程序中处理的:

public sealed class DoSomethingCommandHandler : IRequestHandler<DoSomethingCommand, Unit>
{
    private readonly IAggregateRepository _aggregateRepository;

    public DoSomethingCommand(IAggregateRepository aggregateRepository)
    {
       _aggregateRepository = aggregateRepository;
    }

    public async Task<Unit> Handle(DoSomethingCommand request, CancellationToken cancellationToken)
    {
        // Find aggregate from id in request
        var id = new AggregateId(request.Id);

        var aggregate = await _aggregateRepository.GetById(id);

        if (aggregate == null)
        {
            throw new NotFoundException();
        }

        // Translate request properties into a value object relevant to the aggregate
        var something = new AggregateValueObject(request.A, request.B);

        // Get the aggregate to do whatever the command is meant to do and save the changes
        aggregate.DoSomething(something);

        await _aggregateRepository.Save(aggregate);

        return Unit.Value;
    }
}

我有保存审计信息的需求,例如"CreatedByUserID"和"ModifiedByUserID"。这纯粹是技术问题,因为我的业务逻辑 none 依赖于这些字段。

我发现了一个相关问题 here, where there was a suggestion to raise an event to handle this. This would be a nice way to do it because I'm also persisting changes based on the domain events raised from an aggregate using an approach similar to the one described here

(TL;DR: 将事件添加到聚合中的每个操作的集合中,将聚合传递给存储库中的单个 Save 方法,在该存储库方法中使用模式匹配处理存储在聚合中的每个事件类型以持久化更改)

例如
上面的 DoSomething 行为看起来像这样:

public void DoSomething(AggregateValueObject something)
{
    // Business logic here
    ...

    // Add domain event to a collection
    RaiseDomainEvent(new DidSomething(/* required information here */));
}

AggregateRepository 将具有如下所示的方法:

public void Save(Aggregate aggregate)
{
   var events = aggregate.DequeueAllEvents();

   DispatchAllEvents(events);
}

private void DispatchAllEvents(IReadOnlyCollection<IEvent> events)
{
    foreach (var @event in events)
    {
        DispatchEvent((dynamic) @event);
    }
}

private void Handle(DidSomething @event)
{
    // Persist changes from event
}

因此,向每个域事件添加 RaisedByUserID 似乎是允许存储库中的每个事件处理程序保存 "CreatedByUserID" 或 "ModifiedByUserID" 的好方法。在一般持久化域事件时,这似乎也是很好的信息。

我的问题是关于是否可以轻松地使 DoSomethingCommand 中的 UserId 流入域事件,或者我是否应该费心这样做。

目前,我认为有两种方法可以做到这一点:

选项 1:

UserId 传递到聚合上的每个单个用例中,以便它可以传递到域事件中。

例如

上面的DoSomething方法会像这样改变:

public void DoSomething(AggregateValueObject something, Guid userId)
{
    // Business logic here
    ...

    // Add domain event to a collection
    RaiseDomainEvent(new DidSomething(/* required information here */, userId));
}

此方法的缺点是用户 ID 实际上与域无关,但需要将其传递到需要审计字段的每个聚合上的每个用例中。

选项 2:

改为将 UserId 传递到存储库的 Save 方法中。这种方法将避免向域模型引入不相关的细节,即使在所有事件处理程序和存储库上重复要求 userId 参数仍然存在。

例如

上面的AggregateRepository会变成这样:

public void Save(Aggregate aggregate, Guid userId)
{
   var events = aggregate.DequeueAllEvents();

   DispatchAllEvents(events, userId);
}

private void DispatchAllEvents(IReadOnlyCollection<IEvent> events, Guid userId)
{
    foreach (var @event in events)
    {
        DispatchEvent((dynamic) @event, Guid userId);
    }
}

private void Handle(DidSomething @event, Guid userId)
{
    // Persist changes from event and use user ID to update audit fields
}

这对我来说很有意义,因为 userId 用于纯粹的技术问题,但它仍然具有与第一个选项相同的重复性。它还不允许我在不可变的域事件对象中封装一个 "RaisedByUserID",这看起来很不错。

选项 3:

有没有更好的方法可以做到这一点,或者重复真的没有那么糟糕吗?

我考虑在存储库中添加一个 UserId 字段,可以在任何操作之前设置它,但这似乎很容易出错,即使它删除了所有重复项,因为它需要在每个命令处理程序中完成.

是否有一些神奇的方法可以通过依赖注入或装饰器实现类似的东西?

视具体情况而定。我将尝试解释几个不同的问题及其解决方案。

  1. 您有一个系统,其中审计信息自然是域的一部分。

举个简单的例子:

银行个人之间签订合同的银行系统。 BankBankEmployee 表示。当签署或修改 合同 时,您需要在合同中包含有关谁做的信息。

public class Contract {

    public void AddAdditionalClause(BankEmployee employee, Clause clause) {
        AddEvent(new AdditionalClauseAdded(employee, clause));
    }
}
  1. 您的系统中的审核信息不是域的自然组成部分。

这里有几件事需要解决。例如,用户可以只向您的系统发出命令吗?有时另一个系统可以调用命令。

解决方法:记录所有传入的命令及其处理后的状态:成功、失败、拒绝等

包括命令发布者的信息。

记录命令发生的时间。您可以在命令中包含有关发行者的信息,也可以不包含。

public interface ICommand {
    public Datetime Timestamp { get; private set; }
}

public class CommandIssuer {
    public CommandIssuerType Type { get; pivate set; }
    public CommandIssuerInfo Issuer {get; private set; }
}

public class CommandContext {
    public ICommand cmd { get; private set; }
    public CommandIssuer CommandIssuer { get; private set; }
}

public class CommandDispatcher {

    public void Dispatch(ICommand cmd, CommandIssuer issuer){

        LogCommandStarted(issuer, cmd);

        try {
            DispatchCommand(cmd);
            LogCommandSuccessful(issuer, cmd);
        }
        catch(Exception ex){
            LogCommandFailed(issuer, cmd, ex);
        }
    }

    // or
    public void Dispatch(CommandContext ctx) {
        // rest is the same
    }
}

优点:这将从有人发出命令的知识中删除您的域

缺点:如果您需要有关事件更改和匹配命令的更多详细信息,您将需要匹配时间戳和其他信息。根据系统的复杂性,这可能会变得丑陋

解决方法:在entity/aggregate中记录所有的命令和相应的事件。检查 this article 以获取详细示例。您可以在事件中包含 CommandIssuer

public class SomethingAggregate {

    public void Handle(CommandCtx ctx) {
        RecordCommandIssued(ctx);
        Process(ctc.cmd);
    }
}

你确实将一些来自外部的信息包含到你的聚合中,但至少它是抽象的,所以聚合只是记录它。看起来还不错吧?

解决方案: 使用一个 saga 来包含有关您正在使用的操作的所有信息。在分布式系统中,大多数时候你需要这样做,所以它会是一个很好的解决方案。在另一个系统中,它会增加您可能不想拥有的复杂性和开销 :)

public void DoSomethingSagaCoordinator {

    public void Handle(CommandContext cmdCtx) {

        var saga = new DoSomethingSaga(cmdCtx);
        sagaRepository.Save(saga);
        saga.Process();
        sagaRepository.Update(saga);
    }
}

我已经使用了此处描述的所有方法以及您的选项 2 的变体。在我处理请求时的版本中,Repositoires 可以访问包含用户信息的 context,因此当他们保存事件时,此信息包含在 EventRecord 对象中,该对象同时具有事件数据和用户信息。它是自动化的,因此其余代码与它分离。我确实使用 DI 将上下文注入存储库。在这种情况下,我只是将事件记录到事件日志中。我的聚合不是事件来源的。

我使用这些指南来选择方法:

如果是分布式系统 -> 选择 Saga

如果不是:

我需要将详细信息与命令相关联吗?

  • 是:将 Commands and/or CommandIssuer 信息传递给聚合体

如果没有那么:

数据库是否有良好的事务支持?

  • 是:保存CommandsCommandIssuer聚合之外。

  • 否:将CommandsCommandIssuer保存在集合中。