IMediatr с Autofac в объектах домена DDD

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

Теперь моя проблема заключается в том, как вызвать эти события из самих объектов модели предметной области. В настоящее время я использую для этого статический класс DomainEvents Уди Дахана (мне нужно, чтобы события обрабатывались именно тогда, когда они происходят, а не в последнее время). События используются для многих вещей, таких как ведение журнала, обновление данных в связанных службах и других объектах модели предметной области и БД, публикация сообщений в шине MassTransit и т. д.

Статический класс DomainEvents использует область действия Autofac, которую я добавляю в него в какой-то момент, чтобы найти экземпляр IMediatr и опубликовать события, например:

public static class DomainEvents
{
    private static ILifetimeScope Scope;

    public async static Task RaiseAsync<TDomainEvent>(TDomainEvent @event) where TDomainEvent : IDomainEvent
    {
        var mediator = Scope?.Resolve<IMediatorBus>();
        if (mediator != null)
        {
            await mediator!.Publish(@event).ConfigureAwait(false);
        }
        else
        {
            Debug.WriteLine("Mediator not set for DomainEvents!");
        }
    }

    public static void SetScope(ILifetimeScope scope)
    {
        Scope = scope;
    }
}

Все это работает нормально в однопоточной среде, но метод DomainEvents.SetScope() является возможной проблемой гонок в многопоточной среде. Т.е. Когда я представлю MassTransit и создам потребителей сообщений, каждый потребитель сообщений установит текущую LifetimeScope в DomainEvents с помощью этого метода, и вот проблема: каждый потребитель перезапишет область времени жизни новой.

Почему я использую статический класс DomainEvents? Потому что я не хочу загрязнять свои объекты модели предметной области инфраструктурой. Я думал о том, чтобы сделать DomainEvents нестатичным (определить интерфейс), но затем мне нужно, чтобы они вводились в каждый объект модели предметной области, и я все еще думаю об этом, но, возможно, есть лучший способ.

Я хочу знать, есть ли лучший способ справиться с этим? Может быть, какие-то изменения в классе DomainEvents? Или, возможно, удалите статический класс DomainEvents и используйте для этого интерфейс или DomainService. Проблема в том, что мне не нравятся статические классы, но мне также не нравится помещать неспецифические для предметной области зависимости в объекты модели предметной области.

Пожалуйста помоги.

ОБНОВЛЕНИЕ

Чтобы лучше прояснить процесс и то, для чего я использую DomainEvents... У меня есть длительный процесс, который может занять от нескольких минут до нескольких часов/дней. Итак, процесс происходит следующим образом:

  1. Я получаю сообщение от MassTransit, т.е. ProcessStartMessage(processId)
  2. Получите ProcessData для (processId) из базы данных.
  3. Создайте модель предметной области в памяти ProcessTracker (singleton) и поместите в нее все данные, которые я загрузил из БД. (кэш в памяти)
  4. Я получаю другое сообщение от Masstransit т.е. ProcessStatusChanged(идентификатор процесса, данные).
  5. Перешлите данные этого сообщения в одноэлементный ProcessTracker в памяти для обработки.
  6. ProcessTracker обрабатывает данные.

Чтобы ProcessTracker мог обрабатывать эти данные, он создает множество объектов модели предметной области, каждый из которых отвечает за обработку некоторой части данных. (Обратите внимание, что больше НЕТ вызовов БД и гидратации объектов из БД, все это происходит в памяти, а также модель домена не сопоставляется с каким-либо объектом, она не подключена ни к какому объекту БД). В какой-то момент мне нужно зарегистрировать, что сделал объект модели предметной области в цепочке, закончилась или началась его работа, достигнута ли какая-то веха и т. д. Это делается путем вызова DomainEvents. Мне также нужно уведомить графический интерфейс об этих событиях, поэтому они также используются для отправки сообщений Masstransit.

То есть (псевдокод):

public class ProcessTracker 
{
    private Step _currentStep;
    
    public void ProcessData(data)
    {
        _currentStep.ProcessData(data);
        DomainEvents.Raise(new ProcesTrackerDataProcessed());
        ...
    }
 }

 public class Step 
 {
    public Phase _currentPhase;
  
    public void ProcessData(data)
    {
        if (data.IsManual && _someOtherCondition())
        {
            DomainEvents.Raise(new StepDataEvent1());
            ...
        }

        if(data.CanTransition)
        {
            DomainEvents.Raise(new TransitionToNewPhase(this, data));
        }

        _currentPhase.DoSomeWork(data);
        DomainEvents.Raise(new StepDataProcessed(this, data));
        ...
    }
 }

Что касается обновлений базы данных, они не являются транзакционными и не важны для процесса, а состояние объекта модели предметной области сохраняется только в памяти, в случае сбоя процесса процесс ДОЛЖЕН начинаться с самого начала (восстановления НЕТ).

Чтобы завершить процесс:

  1. Я получаю ProcessEnd от MassTransit
  2. Данные сообщения пересылаются в ProcessTracker.
  3. ProcessTracker обрабатывает данные и получает результат процесса.
  4. Результат процесса сохраняется в БД
  5. Другим участникам процесса отправляется сообщение, уведомляющее их о завершении процесса.

person Luka    schedule 25.03.2021    source источник


Ответы (1)


Сначала спросите себя, что вы собираетесь делать, когда вы инициируете событие из вашей модели предметной области?

Обычно это работает так:

  • Получить команду
  • Загрузить объект домена из репозитория
  • Выполнить поведение
  • (тут наверное) Поднять событие
  • Сохранить новое состояние объекта домена

Итак, куда поместятся ваши дополнительные обработчики событий предметной области? Собираетесь ли вы выполнить некоторые другие вызовы базы данных, отправить электронное письмо? Помните, что все это происходит сейчас, когда вы даже не сохранили измененное состояние вашего доменного объекта. Что, если ваша настойчивость не сработает? Это произойдет после выполнения всех обработчиков домена.

Вы не должны выполнять более одной транзакции при обработке одной команды. Шаблон Aggregate ясно говорит вам, что совокупность является границей транзакции. Вы должны вызывать события предметной области после завершения транзакции или внутри той же технической транзакции, но должны сохраняться только совокупное состояние и событие . Реакции на события домена потенциально инициируют транзакции для других объектов домена, и это должно быть сделано за пределами области обработки текущей команды.

Проблема вовсе не техническая, это проблема дизайна.

Если вы используете MassTransit, вы можете сделать его (относительно) надежным, только если вы обрабатываете команду в потребителе сообщений. Затем вы можете использовать исходящие сообщения в памяти, которые не отправят событие, если потребитель преуспел. По-прежнему не гарантируется, что событие будет опубликовано в случае сбоя брокера.

Если вы не перейдете к Event Sourcing, у вас есть два 100% надежных варианта:

  • Используйте шаблон транзакционного исходящего ящика (он есть в NServiceBus, и он довольно сложный). Он имеет ограничения на тип используемой базы данных.
  • Сохраните событие в той же базе данных, что и объект домена, в другой таблице в той же транзакции. Опросите таблицу с помощью DELETE INTO и оттуда отправляйте события брокеру.
person Alexey Zimarev    schedule 25.03.2021
comment
Я обновил вопрос с дополнительной информацией (см. ОБНОВЛЕНИЕ) - person Luka; 25.03.2021
comment
Я позволяю агрегировать корневой список событий, а затем поднимаю их из обработчика mediatr. Почему бы не внедрить eventSender в обработчик mediatr? Он не должен быть статичным. - person Christian Johansen; 29.03.2021
comment
@ Лука, ты намеренно создаешь проблемы для себя в будущем. просто подумайте о запуске этого приложения в двух экземплярах, а затем попытайтесь выяснить, будет ли оно работать. - person Alexey Zimarev; 30.03.2021
comment
@ChristianJohansen это не MediatR. Это функция посредника MassTransit. В любом случае, обработка событий предметной области в процессе с дополнительными побочными эффектами (не являющимися частью обработки команды) — прямой путь к бесчисленным проблемам. Я не уверен, какой смысл во всем этом. - person Alexey Zimarev; 30.03.2021
comment
Спасибо за указание на это, @alexey my Bad. EventSender может отправлять внепроцессный транспорт из commandHandler, но я думаю, что это не имеет отношения к вопросу. - person Christian Johansen; 30.03.2021
comment
@ChristianJohansen Как я уже писал, мне нужно, чтобы они выполнялись именно тогда, когда они происходят, а не в конце запроса/транзакции. Насчет внедрения eventSender в доменные события - вот вопрос - хорошее ли это решение? Потому что я внедряю специфичную для инфраструктуры логику в свои объекты домена. Это нормально? - person Luka; 31.03.2021
comment
@AlexeyZimarev Я говорю не о посреднике MassTransit, а о самой библиотеке MediatR. Приложение представляет собой службу Windows, которая ДОЛЖНА/БУДЕТ размещаться только на одном компьютере. Я знаю, что мое решение проблематично, поэтому я задал вопрос. Вопрос в том, как правильно публиковать события домена из самого объекта домена без использования статического класса, является ли хорошей практикой внедрение класса инфраструктуры для отправки событий в объекты домена? - person Luka; 31.03.2021