DDD: ссылка на интерфейс MediatR из доменного проекта

Я только начинаю с DDD. Я помещаю события домена в приложение CQRS, и я сталкиваюсь с фундаментальной задачей: как использовать интерфейс маркера MediatR.INotification в проекте домена без создания зависимости домена от инфраструктуры.

Мое решение организовано в четырех проектах следующим образом:

MyApp.Domain
    - Domain events
    - Aggregates
    - Interfaces (IRepository, etc), etc.
MyApp.ApplicationServices
    - Commands
    - Command Handlers, etc.
MyApp.Infrastructure
    - Repository
    - Emailer, etc.
MyApp.Web
    - Startup
    - MediatR NuGet packages and DI here
    - UI, etc.

В настоящее время в проекте UI установлены пакеты MediumR и MediatR.net Core DI, и они добавляются в DI с помощью.AddMediatR() с помощью команды

services.AddMediatR(typeof(MyApp.AppServices.Commands.Command).Assembly);

который сканирует и регистрирует обработчики команд из проекта AppServices.

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

namespace ObApp.Domain.Events
{
    public class NewUserAdded : INotification
    {
        ...
    }

Как правильно пометить мои события в этой ситуации, чтобы они могли использоваться MediatR? Я могу создать свой собственный интерфейс маркеров для событий, но MediatR не распознает их без какого-либо способа автоматически привести их к MediatR.inotification.

Это просто недостаток использования нескольких проектов? Однако даже если бы я использовал один проект, я бы поместил "внешний" интерфейс в домен, если бы использовал MediatR.INotification из раздела домена.

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

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

Спасибо!

6 ответов

Решение

Лучше всего, чтобы уровень вашего домена не зависел от какой-либо инфраструктуры, но это трудно получить в CQRS из-за привязок. Я могу сказать вам по своему опыту. Однако вы можете минимизировать эту зависимость. Один из способов сделать это - сделать свой собственный EventInterface это расширяет MediatR.INotification и использовать этот интерфейс во всем коде домена. Таким образом, если вы когда-нибудь захотите изменить инфраструктуру, вам нужно изменить только в одном месте.

Если вы хотите, чтобы уровень вашего домена был действительно чистым, без каких-либо ссылок на MediatR, создайте свои собственные интерфейсы для событий, посредника и обработчика на уровне домена. Затем на уровне инфраструктуры или приложения создайте классы-оболочки, чтобы обернуть MediatR и передать вызовы через классы-оболочки. При таком подходе вам не нужно получать данные из интерфейсов MediatR. Обязательно зарегистрируйте оболочки в вашем IoC тоже

Вот пример:

в вашем доменном слое:

public interface IDomainMediator
{
    Task Publish<TNotification>(TNotification notification,
        CancellationToken cancellationToken = default(CancellationToken))
        where TNotification : IDomainNotification;
}
public interface IDomainNotification
{}
public interface IDomainNotificationHandler<in TNotification>
    where TNotification : IDomainNotification
{
    Task Handle(TNotification notification, 
        CancellationToken cancellationToken=default(CancellationToken));
}

Затем в вашей инфраструктуре или на уровне приложений, где бы у вас ни был пакет MediatR:

public class MediatRWrapper : IDomainMediator
{
    private readonly MediatR.IMediator _mediator;

    public MediatRWrapper(MediatR.IMediator mediator)
    {
        _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
    }

    public Task Publish<TNotification>(TNotification notification,
        CancellationToken cancellationToken = default(CancellationToken))
        where TNotification : IDomainNotification
    {
        var notification2 = new NotificationWrapper<TNotification>(notification);
        return _mediator.Publish(notification2, cancellationToken);
    }
}

public class NotificationWrapper<T> : MediatR.INotification
{
    public T Notification { get; }

    public NotificationWrapper(T notification)
    {
        Notification = notification;
    }
}

public class NotificationHandlerWrapper<T1, T2> : MediatR.INotificationHandler<T1>
    where T1 : NotificationWrapper<T2>
    where T2 : IDomainNotification
{
    private readonly IEnumerable<IDomainNotificationHandler<T2>> _handlers;

    //the IoC should inject all domain handlers here
    public NotificationHandlerWrapper(
           IEnumerable<IDomainNotificationHandler<T2>> handlers)
    {
        _handlers = handlers ?? throw new ArgumentNullException(nameof(handlers));
    }

    public Task Handle(T1 notification, CancellationToken cancellationToken)
    {
        var handlingTasks = _handlers.Select(h => 
          h.Handle(notification.Notification, cancellationToken));
        return Task.WhenAll(handlingTasks);
    }
}

Я не тестировал его с конвейерами и т. Д., Но он должен работать. Ура!

Это было бы первым призом, чтобы попытаться сначала не иметь зависимости инфраструктуры на уровне домена.

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

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

public class MediatRNotification<T> : INotification
{
    T Instance { get; }

    public MediatRNotification(T instance)
    {
        Instance = instance;
    }
}

Ваша инфраструктура может даже использовать некоторое отражение для создания этой оболочки из события домена.

Если вы хотите воспользоваться полиморфизмом mediatR для уведомления без извлечения события вашего домена с помощью MediatR.INotification, создайте оболочку, как говорит Eben.

public class DomainEventNotification<TDomainEvent> : INotification where TDomainEvent : IDomainEvent
{
    public TDomainEvent DomainEvent { get; }

    public DomainEventNotification(TDomainEvent domainEvent)
    {
        DomainEvent = domainEvent;
    }
}

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

public class DomainEventDispatcher : IDomainEventChangesConsumer
{
    private readonly IMediator _mediator;

    public DomainEventDispatcher(IMediator mediator)
    {
        _mediator = mediator;
    }

    public void Consume(IAggregateId aggregateId, IReadOnlyList<IDomainEvent> changes)
    {
        foreach (var change in changes)
        {
            var domainEventNotification = CreateDomainEventNotification((dynamic)change);

            _mediator.Publish(domainEventNotification);
        }
    }

    private static DomainEventNotification<TDomainEvent> CreateDomainEventNotification<TDomainEvent>(TDomainEvent domainEvent) 
        where TDomainEvent : IDomainEvent
    {
        return new DomainEventNotification<TDomainEvent>(domainEvent);
    }
}

Будет обработан тип события вашего домена:

public class YourDomainEventHandler
    : INotificationHandler<DomainEventNotification<YourDomainEvent>>
{
    public Task Handle(DomainEventNotification<YourDomainEvent> notification, CancellationToken cancellationToken)
    {
        // Handle your domain event
    }
}

public class YourDomainEvent : IDomainEvent
{
    // Your domain event ...
}

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

Нам нужно решить небольшую проблему, связанную с тем, что наше событие домена не является действительным INotification MediatR. Мы преодолеем это, создав общий INotification для переноса нашего события предметной области.

Создайте собственный универсальный INotification.

      using System;
using MediatR;
using DomainEventsMediatR.Domain;

namespace DomainEventsMediatR.Application
{
    public class DomainEventNotification<TDomainEvent> : INotification where TDomainEvent : IDomainEvent
    {
        public TDomainEvent DomainEvent { get; }

        public DomainEventNotification(TDomainEvent domainEvent)
        {
            DomainEvent = domainEvent;
        }
    }
}

Создайте диспетчер, который упаковывает события домена в уведомления MediatR и публикует их:

      using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using MediatR;
using DomainEventsMediatR.Domain;

namespace DomainEventsMediatR.Application
{
    public class MediatrDomainEventDispatcher : IDomainEventDispatcher
    {
        private readonly IMediator _mediator;
        private readonly ILogger<MediatrDomainEventDispatcher> _log;
        public MediatrDomainEventDispatcher(IMediator mediator, ILogger<MediatrDomainEventDispatcher> log)
        {
            _mediator = mediator;
            _log = log;
        }

        public async Task Dispatch(IDomainEvent devent)
        {

            var domainEventNotification = _createDomainEventNotification(devent);
            _log.LogDebug("Dispatching Domain Event as MediatR notification.  EventType: {eventType}", devent.GetType());
            await _mediator.Publish(domainEventNotification);
        }
       
        private INotification _createDomainEventNotification(IDomainEvent domainEvent)
        {
            var genericDispatcherType = typeof(DomainEventNotification<>).MakeGenericType(domainEvent.GetType());
            return (INotification)Activator.CreateInstance(genericDispatcherType, domainEvent);

        }
    }
}

Подход Microsoft

Обратите внимание, что в своем полном примере CQRS Microsoft предлагает просто ссылаться на интерфейс MediatR внутри объекта домена:

В C# событие домена — это просто структура или класс хранения данных, например DTO, со всей информацией, относящейся к тому, что только что произошло в домене, как показано в следующем примере:

      public class OrderStartedDomainEvent : INotification
{
    public string UserId { get; }
    public string UserName { get; }
    public int CardTypeId { get; }
    public string CardNumber { get; }
    public string CardSecurityNumber { get; }
    public string CardHolderName { get; }
    public DateTime CardExpiration { get; }
    public Order Order { get; }

    public OrderStartedDomainEvent(Order order, string userId, string userName,
                                   int cardTypeId, string cardNumber,
                                   string cardSecurityNumber, string cardHolderName,
                                   DateTime cardExpiration)
    {
        Order = order;
        UserId = userId;
        UserName = userName;
        CardTypeId = cardTypeId;
        CardNumber = cardNumber;
        CardSecurityNumber = cardSecurityNumber;
        CardHolderName = cardHolderName;
        CardExpiration = cardExpiration;
    }
}

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

      public abstract class Entity
{
     //...
     private List<INotification> _domainEvents;
     public List<INotification> DomainEvents => _domainEvents;

     public void AddDomainEvent(INotification eventItem)
     {
         _domainEvents = _domainEvents ?? new List<INotification>();
         _domainEvents.Add(eventItem);
     }

     public void RemoveDomainEvent(INotification eventItem)
     {
         _domainEvents?.Remove(eventItem);
     }
     //... Additional code
}

Это подход, который можно использовать без использования интерфейса инфраструктуры https://github.com/Leanwit/dotnet-cqrs

С сайта GitHub:

Этот проект показывает чистый способ использования CQRS без использования библиотеки MediatR.

В C# обычно используется библиотека MediatR для реализации CQRS. Это потрясающая библиотека, но она заставляет вас реализовать интерфейс INotification, INotificationHandler и IRequestHandler на уровне домена / приложения, связав его с библиотекой инфраструктуры. Это другой подход, чтобы избежать этой связи.

Другие вопросы по тегам