Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DDD: Referencing MediatR interface from the domain project

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!

like image 493
platypusjh Avatar asked Nov 14 '17 18:11

platypusjh


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

like image 95
Constantin Galbenu Avatar answered Oct 19 '22 20:10

Constantin Galbenu


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!

like image 37
Dawood Moazzem Avatar answered Oct 19 '22 21:10

Dawood Moazzem