DDD:从域项目中引用 MediatR 接口

DDD: Referencing MediatR interface from the domain project

我刚刚开始使用 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 项目中安装了 MediatR 和 MediatR .net Core DI 包,它们是使用 .AddMediatR() 添加到 DI 的,使用命令

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,我也会在域中放置一个 "external" 接口。

当我的用户实体继承自 EF 的 IdentityUser 时,我 运行 遇到了同样的问题。在那种情况下,网络共识似乎是务实的,继续前进,让轻微的污染避免很多麻烦。这是另一个类似的案例吗?我不介意为了实用主义而牺牲纯洁,但不是为了偷懒。

这是我使用的其他软件包会出现的一个基本问题,因此我期待解决这个问题。

谢谢!

您的领域层最好不依赖于任何基础设施,但由于绑定的原因,很难在 CQRS 中获得。我可以根据我的经验告诉你。但是,您可以最大限度地减少这种依赖性。一种方法是制作自己的 EventInterface 扩展 MediatR.INotification 并在整个域代码中使用该接口。这样,如果你想改变基础设施,你只需要改变一个地方。

尝试首先在域层中具有基础结构依赖性将是一等奖。

我不知道 MediatR,但根据您的描述,它需要在 class 上实现一个接口,该接口将用于 space。

是否可以选择创建一个位于您域外的包装器 class?

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

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

您的基础架构甚至可以使用一些反射来从域事件创建此包装器。

如果你想保持你的域层真正纯粹,而不需要任何对 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 多态性进行通知而不用 MediatR.INotification 派生您的域事件,请按照 Eben 的指示创建一个包装器。

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

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

然后使用正确的类型而不是域事件接口通过应用动态来创建它。有关更多说明,请参阅 this article

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

这是一种您可以在不使用基础结构接口的情况下使用的方法 https://github.com/Leanwit/dotnet-cqrs

来自 GitHub 网站:

This project shows a clean way to use CQRS without using the MediatR library.

In C# is common to use a library named MediatR to implement CQRS. This is an amazing library but forces you to implement the interface INotification, INotificationHandler and IRequestHandler in your domain/application layer coupling this with an infrastructure library. This is a different approach to avoid add this coupling.

正如其他人所提到的,共识似乎是包装 MediatR.INotification。我发现 this post from 2020 非常有用。

We do have to deal with the small issue of our Domain Event not being a valid MediatR INotification. We’ll overcome this by creating a generic INotification to wrap our Domain Event.

Create a custom generic 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;
        }
    }
}

Create a Dispatcher that wraps Domain Events in MediatR notificatoins and publishes them:

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);

        }
    }
}

微软的做法

请注意,在其 CQRS full example, Microsoft suggests 中仅引用域实体中的 MediatR 接口:

In C#, a domain event is simply a data-holding structure or class, like a DTO, with all the information related to what just happened in the domain, as shown in the following example:

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

First, you add the events happening in your entities into a collection or list of events per entity. That list should be part of the entity object, or even better, part of your base entity class, as shown in the following example of the Entity base class:

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
}