DDD: существует ли элегантный способ передачи информации аудита при обновлении совокупного корня? - PullRequest
1 голос
/ 16 июня 2019

Предположим, у меня есть команда 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».Это чисто техническая проблема, потому что ни одна из моих бизнес-логик не зависит от этих полей.

Я нашел связанный вопрос здесь , где было предложено вызвать событие для обработкиэтот.Это был бы хороший способ сделать это, потому что я также сохраняю изменения, основанные на событиях домена, вызванных агрегатом, используя подход, аналогичный описанному здесь .

(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 для каждого отдельного варианта использования в агрегате, чтобы его можно было передать в событие домена.

например,

Метод DoSomething, описанный выше, изменится следующим образом:

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

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

Недостатком этого метода является то, что идентификатор пользователя на самом деле не имеет ничего общего с доменом, однако его необходимо передавать в каждый отдельный случай использования для каждого агрегата, для которого нужны поля аудита.

Вариант 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 Ответ

0 голосов
/ 16 июня 2019

Это будет зависеть от конкретного случая.Я попытаюсь объяснить пару разных проблем и способы их решения.

  1. У вас есть система, в которой информация об аудите, естественно, является частью домена.

Давайте рассмотримПростой пример:

Банковская система, заключающая договоры между Банком и Лицом . Банк представлен BankEmployee .Когда Контракт либо подписан, либо изменен, вам необходимо включить информацию о том, кто это сделал, в контракт.

public class Contract {

    public void AddAdditionalClause(BankEmployee employee, Clause clause) {
        AddEvent(new AdditionalClauseAdded(employee, clause));
    }
}
У вас есть система, в которой информация об аудите не является естественной частью домена.

Здесь есть несколько вещей, которые необходимо решить.Например, могут ли пользователи давать команды только вашей системе?Иногда другая система может вызывать команды.

Решение: Записать все поступающие команды и их состояние после обработки: успешно, сбой, отклонено и т. Д.

Включить информацию о командеэмитент.

Запишите время, когда произошла команда.Вы можете включить информацию об эмитенте в команду или нет.

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

профи : это приведет к удалению вашего домена из области знаний, которую кто-то выполняет команды

cons : если вам нужна более подробная информация оизменения и команды сопоставления с событиями, которые вам понадобятся для сопоставления меток времени и другой информации.В зависимости от сложности системы это может выглядеть ужасно

Решение: Запишите все входящие команды в объекте / агрегате с соответствующими событиями.Проверьте эту статью для подробного примера.Вы можете включить 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 информацию в агрегаты

Если нетзатем:

Имеет ли база данных хорошую поддержку транзакций?

  • Да: сохранить Commands и CommandIssuer вне агрегатов.

  • Нет: сохранить Commands и CommandIssuer в совокупностях.

...