Replacing service layer with MediatR - is it worth to do it?

Konrad picture Konrad · Jun 13, 2018 · Viewed 10.3k times · Source

Do you think it might be reasonable to replace my service layer or service classes with MediatR? For example, my service classes look like this:

public interface IEntityService<TEntityDto> where TEntityDto : class, IDto
{
    Task<TEntityDto> CreateAsync(TEntityDto entityDto);
    Task<bool> DeleteAsync(int id);
    Task<IEnumerable<TEntityDto>> GetAllAsync(SieveModel sieveModel);
    Task<TEntityDto> GetByIdAsync(int id);
    Task<TEntityDto> UpdateAsync(int id, TEntityDto entityDto);
}

I want to achieve some sort of modular design so other dynamically loaded modules or plugins can write their own notification or command handlers for my main core application.

Currently, my application is not event-driven at all and there's no easy way for my dynamically loaded plugins to communicate.

I can either incorporate MediatR in my controllers removing service layer completely or use it with my service layer just publishing notifications so my plugins can handle them.

Currently, my logic is mostly CRUD but there's a lot of custom logic going on before creating, updating, deleting.

Possible replacement of my service would look like:

public class CommandHandler : IRequestHandler<CreateCommand, Response>, IRequestHandler<UpdateCommand, Response>, IRequestHandler<DeleteCommand, bool>
{
    private readonly DbContext _dbContext;

    public CommandHandler(DbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public Task<Response> Handle(CreateCommand request, CancellationToken cancellationToken)
    {
        //...
    }

    public Task<Response> Handle(UpdateCommand request, CancellationToken cancellationToken)
    {
        //...
    }

    public Task<bool> Handle(DeleteCommand request, CancellationToken cancellationToken)
    {
        ///...
    }
}

Would it be something wrong to do?

Basically, I'm struggling what to choose for my logic flow:

  • Controller -> Service -> MediatR -> Notification handlers -> Repository
  • Controller -> MediatR -> Command handlers -> Repository

It seems like with MediatR I can't have a single model for Create, Update and Delete, so one way to re-use it I'd need to derive requests like:

public CreateRequest : MyDto, IRequest<MyDto> {}        
public UpdateRequest : MyDto, IRequest<MyDto> {} 

or embed it in my command like:

public CreateRequest : IRequest<MyDto>
{
    MyDto MyDto { get; set; }
}

One advantage of MediatR is the ability to plug logic in and plug it out easily which seems like a nice fit for modular architecture but still, I'm a bit confused how to shape my architecture with it.

Answer

Scott Hannen picture Scott Hannen · May 29, 2020

If you have a class, let's say an API controller, and it depends on

IRequestHandler<CreateCommand, Response>

What is the benefit of changing your class so that it depends on IMediator,

and instead of calling

return requestHandler.HandleRequest(request);

it calls

return mediator.Send(request);

The result is that instead of injecting the dependency we need, we inject a service locator which in turn resolves the dependency we need.

Quoting Mark Seeman's article,

In short, the problem with Service Locator is that it hides a class' dependencies, causing run-time errors instead of compile-time errors, as well as making the code more difficult to maintain because it becomes unclear when you would be introducing a breaking change.

It's not exactly the same as

var commandHandler = serviceLocator.Resolve<IRequestHandler<CreateCommand, Response>>();
return commandHandler.Handle(request);

because the mediator is limited to resolving command and query handlers, but it's close. It's still a single interface that provides access to lots of other ones.

It makes code harder to navigate

After we introduce IMediator, our class still indirectly depends on IRequestHandler<CreateCommand, Response>. The difference is that now we can't tell by looking at it. We can't navigate from the interface to its implementations. We might reason that we can still follow the dependencies if we know what to look for - that is, if we know the conventions of command handler interface names. But that's not nearly as helpful as a class actually declaring what it depends on.

Sure, we get the benefit of having interfaces wired up to concrete implementations without writing the code, but the savings are trivial and we'll likely lose whatever time we save because of the added (if minor) difficulty of navigating the code. And there are libraries which will register those dependencies for us anyway while still allowing us to inject abstraction we actually depend on.

It's a weird, skewed way of depending on abstractions

It's been suggested that using a mediator assists with implementing the decorator pattern. But again, we already gain that ability by depending on an abstraction. We can use one implementation of an interface or another that adds a decorator. The point of depending on abstractions is that we can change such implementation details without changing the abstraction.

To elaborate: The point of depending on ISomethingSpecific is that we can change or replace the implementation without modifying the classes that depend on it. But if we say, "I want to change the implementation of ISomethingSpecific (by adding a decorator), so to accomplish that I'm going to change the classes that depend on ISomethingSpecific, which were working just fine, and make them depend on some generic, all-purpose interface", then something has gone wrong. There are numerous other ways to add decorators without modifying parts of our code that don't need to change.

Yes, using IMediator promotes loose coupling. But we already accomplished that by using well-defined abstractions. Adding layer upon layer of indirection doesn't multiply that benefit. If you've got enough abstraction that it's easy to write unit tests, you've got enough.

Vague dependencies make it easier to violate the Single Responsibility Principle

Suppose you have a class for placing orders, and it depends on ICommandHandler<PlaceOrderCommand>. What happens if someone tries to sneak in something that doesn't belong there, like a command to update user data? They'll have to add a new dependency, ICommandHandler<ChangeUserAddressCommand>. What happens if they want to keep piling more stuff into that class, violating the SRP? They'll have to keep adding more dependencies. That doesn't prevent them from doing it, but at least it shines a light on what's happening.

On the other hand, what if you can add all sorts of random stuff into a class without adding more dependencies? The class depends on an abstraction that can do anything. It can place orders, update addresses, request sales history, whatever, and all without adding a single new dependency. That's the same problem you get if you inject an IoC container into a class where it doesn't belong. It's a single class or interface that can be used to request all sorts of dependencies. It's a service locator.

IMediator doesn't cause SRP violations, and its absence won't prevent them. But explicit, specific dependencies guide us away from such violations.

The Mediator Pattern

Curiously, using MediatR doesn't usually have anything to do with the mediator pattern. The mediator pattern promotes loose coupling by having objects interact with a mediator rather than directly with each other. If we're already depending on an abstraction like an ICommandHandler then the tight coupling that the mediator pattern prevents doesn't exist in the first place.

The mediator pattern also encapsulates complex operations so that they appear simpler from the outside.

return mediator.Send(request);

is not simpler than

return requestHandler.HandleRequest(request);

The complexity of the two interactions is identical. Nothing is "mediated." Imagine that you're about to swipe your credit card at the grocery store, and then someone offers to simplify your complex interaction by leading you to another register where you do exactly the same thing.

What about CQRS?

A mediator is neutral when it comes to CQRS (unless we have two separate mediators, like ICommandMediator and IQueryMediator.) It seems counterproductive to separate our command handlers from our query handlers and then inject a single interface which in effect brings them back together and exposes all of our commands and queries in one place. At the very least it's hard to say that it helps us to keep them separate.

IMediator is used to invoke command and query handlers, but it has nothing to do with the extent to which they are segregated. If they were segregated before we added a mediator, they still are. If our query handler does something it shouldn't, the mediator will still happily invoke it.


I hope it doesn't sound like a mediator ran over my dog. But it's certainly not a silver bullet that sprinkles CQRS on our code or even necessarily improves our architecture.

We should ask, what are the benefits? What undesirable consequences could it have? Do I need that tool, or can I obtain the benefits I want without those consequences?

What I am asserting is that once we're already depending on abstractions, further steps to "hide" a class's dependencies usually add no value. They make it harder to read and understand, and erode our ability to detect and prevent other code smells.