Предположим, у меня есть команда 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
в репозиторий, которое можно установить перед любыми действиями, но которое кажется подверженным ошибкамдаже если он удаляет все повторения, как это должно быть сделано в каждом обработчике команд.
Может ли быть какой-то магический способ добиться чего-то подобного с помощью внедрения зависимостей или декоратора?