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

3

18

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.

Dishrag answered 13/6, 2018 at 10:29 Comment(2)
I'm not sure how this relates to DDD - maybe CQRS at best. Regarding MediatR handlers replacing services, yes, I guess it's the way you'd use it.Swore
Playing devil’s advocate here is a post on why you need to think twice before bringing it into the project - alex-klaus.com/mediatorZion
D
6

Partly this was answered here: MediatR when and why I should use it? vs 2017 webapi

The biggest benefit of using MediaR(or MicroBus, or any other mediator implementation) is isolating and/or segregating your logic (one of the reasons its popular way to use CQRS) and a good foundation for implementing decorator pattern (so something like ASP.NET Core MVC filters). From MediatR 3.0 there's an inbuilt support for this (see Behaviours) (instead of using IoC decorators)

You can use the decorator pattern with services (classes like FooService) too. And you can use CQRS with services too (FooReadService, FooWriteService)

Other than that it's opinion-based, and use what you want to achieve your goal. The end result shouldn't make any difference except for code maintenance.

Additional reading:

Dishrag answered 23/8, 2018 at 12:8 Comment(0)
S
47

Update: I'm preserving the answer, but my position on this has changed somewhat as indicated in this blog post.

Update 2: Seriously, I no longer endorse what I wrote here.


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.

Samaria answered 29/5, 2020 at 21:58 Comment(4)
Two blog posts on this topic: my "Is Mediator/MediatR still cool?" and Scott's "No, MediatR Didn't Run Over My Dog"Zion
1. Find references to the command/query and you can jump to it easily. 2. Generic Decorators are where the power is. If you wanted built in validation/retry/error handling, you implement it on the generic interface once and you are done. If you have all these one offs, you'd have to decorate each one manually. Useless. 3. Any class can implement any interface. To say that one class could have more than one handler implemented is a silly argument. That is what code reviews are for. Each command only knows about the data it contains.Leialeibman
The Decorator pattern existed before MediatR, and it’s possible to add AOP decorators without building separate classes for each interface. The ability to do something with a tool does not make it a feature unique to that tool. / Each command only knows about the data it contains, but adding a dependency on IMediator effectively adds a dependency on every command and query handler. Sure you can catch violations in code review. But I’d rather not introduce a vague, inaccurately named dependency that can invoke any command or query in the first place. Hopefully we catch that in code review.Samaria
I'm torn on whether to update this because my position on this isn't quite as strong as it was. If lots of people agree with it as-is then I guess I should leave it. But I updated my corresponding blog post to show where I see this a little bit differently. scotthannen.org/blog/2020/06/20/mediatr-didnt-run-over-dog.htmlSamaria
D
6

Partly this was answered here: MediatR when and why I should use it? vs 2017 webapi

The biggest benefit of using MediaR(or MicroBus, or any other mediator implementation) is isolating and/or segregating your logic (one of the reasons its popular way to use CQRS) and a good foundation for implementing decorator pattern (so something like ASP.NET Core MVC filters). From MediatR 3.0 there's an inbuilt support for this (see Behaviours) (instead of using IoC decorators)

You can use the decorator pattern with services (classes like FooService) too. And you can use CQRS with services too (FooReadService, FooWriteService)

Other than that it's opinion-based, and use what you want to achieve your goal. The end result shouldn't make any difference except for code maintenance.

Additional reading:

Dishrag answered 23/8, 2018 at 12:8 Comment(0)
A
2

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

A third option has not been discussed yet:

  1. Adding the handlers to the service collection yourself. (Current status.)
  2. Using a library like MediatR to scan your assemblies and add the handlers to the service collection. (Is it worth it?)
  3. Not adding the handlers to your service collection. (New option.)

In this answer, I will discuss the third option via a comparison of a few parts of MediatR and IGet.

First, let's check out how similar the code can look:


MediatR

With MediatR an update request of a user's email address could look like:

public class ChangeEmailRequest : IRequest<SomeResult>
{
    public int UserId { get; set; }
    public string NewEmailAddress { get; set; }
}

The handler could look like:

public class ChangeEmailRequestHandler : IRequestHandler<ChangeEmailRequest, SomeResult>
{
    // Here would be the constructor with dependencies that are injected by the service provider

    public async Task<SomeResult> Handle(ChangeEmailRequest request, CancellationToken cancellationToken)
    {
        // trim and validate input
        // return if not valid (with reason)
        // save new email address in database
        // send e-mail to the new address for verification
        // return a success result
    }
}

Then, invoking the handler would look like

var result = await mediator.Send(request);

IGet

With IGet an update request of a user's email address could look like:

public class ChangeEmailRequest
{
    public int UserId { get; set; }
    public string NewEmailAddress { get; set; }
}

The handler could look like:

public class ChangeEmailRequestHandler
{
    // Here would be the constructor with dependencies that are injected by the service provider

    public async Task<SomeResult> Handle(ChangeEmailRequest request, CancellationToken cancellationToken)
    {
        // trim and validate input
        // return if not valid (with reason)
        // save new email address in database
        // send e-mail to the new address for verification
        // return a success result
    }
}

Then, invoking the handler would look like:

var result = await i.Get<ChangeEmailRequestHandler>().Handle(request);

Discussing the code examples above

  • Both MediatR and IGet can be used to reduce the injected constructor arguments of a PageModel or Controller to one: IMediator mediator vs IGet i.
  • MediatR requires the IRequest and IRequestHandler interfaces to be implemented. This allows MediatR to scan your assemblies and add the handlers to your service collection at startup. MediatR gets the handlers from your service collection and it can therefore decorate the handlers via so-called Behaviours before calling Handle.
  • IGet uses generics and the ActivatorUtilities class to instantiate your handlers and then you call the Handle method yourself - the Handle method can therefore be used for easier code navigation in your editor and IGet gives compile-time safety checks that your handler exists. Adding shared behaviour to handlers should be done more explicitly when using IGet, because it cannot be done by a service provider or a "mediator" on getting the handler. You could add shared behaviour via a base class or via decorators as is shown in the readme of IGet.
Aday answered 8/4, 2023 at 12:35 Comment(3)
Are there any performance problems when using ActivatorUtilities.CreateInstance?Brassy
Also MediatR uses ActivatorUtilities.CreateInstance internally, but that is in github.com/jbogard/MediatR/blob/master/src/MediatR/Mediator.cs and not for instantiating handlers. The implementation of IServiceProvider is responsible for instantiating the handlers and that is not part of the MediatR library. I did not compare the performance of an IServiceProvider with ActivatorUtilities.CreateInstance. A pull request to fix some performance issues has been merged in 2019: github.com/dotnet/extensions/pull/1796Aday
For people in doubt: note that IGet.Get<Handler>() does not need to do any assembly-scanning for finding the handler. Not at startup and not at runtime. If that was the case, it would hit performance. But the performance difference we’re talking about here is much smaller: it’s only about matching contructor parameters with the services to inject. IGet might be faster than MediatR - I didn’t test that because I guess that the difference is no issue.Aday

© 2022 - 2025 — McMap. All rights reserved.