DDD: Referencing MediatR interface from the domain project

Viewed 8233

I'm just getting started with DDD. I'm putting domain events into a CQRS application and I'm stumbling on a fundamental task: How to use the MediatR.INotification marker interface within the domain project without creating a domain dependency on infrastructure.

My solution is organized in four projects as follows:

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.

I currently have the MediatR and MediatR .net Core DI packages installed in the UI project and they are added to DI using .AddMediatR(), with the command

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

which scans and registers command handlers from the AppServices project.

The problem comes when I want to define an event. For MediatR to work with my domain events, they need to be marked with the MediatR.INotification interface.

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

What is the proper way to mark my events in this situation so they can be used by MediatR? I can create my own marker interface for events, but MediatR won't recognize those without some way to automatically cast them to MediatR.INotification.

Is this just a shortcoming of using multiple projects? Even if I was using a single project, though, I would be putting an "external" interface in the domain if I used MediatR.INotification from within the domain section.

I've run into the same issue when my User entity inherited from EF's IdentityUser. In that case the web consensus seems to say be pragmatic and go ahead and allow the minor pollution to save a lot of headaches. Is this another similar case? I don't mind sacrificing purity for pragmatism, but not just to be lazy.

This is a fundamental issue that will occur with other packages I use, so I look forward to solving this.

Thank you!

6 Answers

It is best that your domain layer doesn't depend on any infrastructure but that is hard to obtain in CQRS because of the bindings. I can tell you from my experience. You can, however, minimize that dependency. One way to do that is to make your own EventInterface that extends MediatR.INotification and use that interface all over the domain code. In this way, if you ever want to change the infrastructure, you need to change only in one place.

It would be first prize to attempt to first not have an infrastructure dependency in the domain layer.

I don't know MediatR but from what you describe it requires an interface to be implemented on a class that is going to be used in that space.

Is it perhaps an option to create a wrapper class that lives outside your domain?

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

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

Your infrastructure could even use some reflection to create this wrapper from a domain event.

If you want to take advantage of the mediatR polymorphism for notification without derive your domain event with MediatR.INotification, create a wrapper as told by Eben.

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

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

Then create it with the right type instead of the domain event interface by applying dynamic. See this article for more explanation

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

The handler of your domain event type will be called :

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

this is an approach that you can use without using an infrastructure interface https://github.com/Leanwit/dotnet-cqrs

From the GitHub site:

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.

As others mentioned, the consensus seems to be to wrap MediatR.INotification. I found this post from 2020 to be very useful.

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

        }
    }
}

Microsoft's approach

Note that in its CQRS full example, Microsoft suggests to simply reference the MediatR interface within the Domain Entity:

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