DDD: Referencing MediatR interface from the domain project
Asked Answered
H

6

16

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!

Historiated answered 14/11, 2017 at 18:33 Comment(2)
Interestingly, someone has made a similar comment on the mediatR Github.Gynandry
That being said, I'm not sure mediatR's creator intended it as a heavy framework, more as a lightweight library that can be referenced directly in the domain layer (see the description : Simple in-process messaging with no dependencies)Gynandry
S
14

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.

Sun answered 14/11, 2017 at 20:29 Comment(0)
P
8

If you want to keep your domain layer really pure, without having any reference to MediatR, create your own interfaces for events, mediator and handler in the domain layer. Then in the infrastructure or application layer, create wrapper classes to wrap MediatR and pass the calls through the wrapper classes. With this approach, you wont need to derive from the MediatR interfaces. Make sure to register the wrappers in your IoC too

Here's an example:

in your domain layer:

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

Then in your infrastructure or application layer, wherever you have the MediatR package:

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

I haven't tested it with pipelines etc, but it should work. Cheers!

Parrott answered 12/3, 2018 at 8:39 Comment(5)
sadly i cannot get this to work any more. i get a "$exception {"The number of generic arguments provided doesn't equal the arity of the generic type definition. (Parameter 'instantiation')"} System.ArgumentException"Reluctivity
Worked fine for me. BTW, Dawood, I know you, and a good answer! :-)Norge
for some reason, i am unable to get this to work. the generic NotificationHandlerWrapper doesn't get instantiated. A non-generic handler wrapper does, however. I'm using the built-in .Net Core DI container.Untoward
Hey, could you provide any pointers with wiring this up in IoC. .net built in service collection. Really struggling to get this to work, but the principle looks nice. Thx.Oocyte
@DawoodMoazzem great idea! I have picked that up and created a proof-of-concept (youtu.be/JGgeoB-tXJw). IoC setup is a challenge but I was able to solve it using AutofacHuntsman
A
3

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.

Andres answered 15/11, 2017 at 5:43 Comment(1)
Thanks Eben and @Constantin. I've got the code working with Constantin's answer, which was to be pragmatic and include the interface, but I'm going to chew on it in my mind for a day or two before I decide on the final route to take. I'll think about how to do a wrapper like you mentioned.Historiated
S
1

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 ...
}
Sewellyn answered 28/5, 2018 at 8:56 Comment(0)
K
0

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.

Kaka answered 22/5, 2020 at 22:59 Comment(0)
R
0

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
}
Renter answered 18/11, 2021 at 12:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.