DDD:在更新聚合根目录时,是否有一种优雅的方式来传递审核信息?

时间:2019-06-16 10:32:51

标签: c# domain-driven-design repository-pattern cqrs auditing

假设我有一个如下所示的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”。这纯粹是技术问题,因为我的业务逻辑都不依赖于这些字段。

我找到了一个相关的问题here,那里有一个建议引发一个事件来处理此问题的建议。这将是一个很好的方法,因为我还将基于与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”的好方法。一般而言,在保留域事件时,它似乎也具有很好的信息。

我的问题与是否很容易使UserId中的DoSomethingCommand流到域事件或者我是否应该这样做很容易。

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

选项1:

UserId传递到聚合中的每个单个用例中,以便将其传递到domain事件中。

例如

上面的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字段,该字段可以在执行任何操作之前进行设置,但这似乎容易出错,即使它删除了所有重复,因为它需要在每个命令处理程序中完成。 / p>

是否有某种神奇的方法可以通过依赖注入或修饰器来实现类似的目的?

1 个答案:

答案 0 :(得分:0)

这取决于具体情况。我将尝试解释几个不同的问题及其解决方案。

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

让我们举一个简单的例子:

Bank Person 之间签约的银行系统。 Bank BankEmployee 表示。签署或修改合同后,您需要在合同中包括有关谁做的信息。

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

专家:这将从某人发出命令的知识中删除您的域

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

解决方案:记录实体/集合中所有传入的命令以及相应的事件。查看this article以获得详细示例。您可以在事件中加入CommandIssuer

public class SomethingAggregate {

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

您确实包括了一些从外部到聚合的信息,但是至少它是抽象的,因此聚合只记录它。看起来还不错,不是吗?

解决方案:使用一个包含所有与您正在使用的操作有关的信息的传奇。在分布式系统中,大多数时候您将需要执行此操作,因此这将是一个很好的解决方案。在另一个系统中,它将增加复杂性和您可能不希望拥有的开销:)

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和/或CommandIssuer信息传递给聚合

如果否,那么:

dabase是否具有良好的事务支持?

  • 是:将CommandsCommandIssuer保存在聚合之外。

  • 否:将CommandsCommandIssuer保存为摘要。