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