Reputation: 51
I saw online posts or vid such as https://www.youtube.com/watch?v=ykC3Ty-3U7g&t=588s which promotes MediatR or Mediator pattern in general.
I don't quite see the benefit of using MediatR or the Mediator pattern
With Mediator pattern:
Controler.Get() => await _sender.send(new GetAllProducts());
GetProductsHandler.Handle(...) => await _repo.GetAllProducts();
Without
Controler.GetProducts() => await _productServices.GetProducts();
ProductService.GetProducts() => await await _repo.GetAllProducts();
Seems mediator is just a fancier version of the good old service?
Upvotes: 3
Views: 4562
Reputation: 16
The mediator behavioral pattern is one that simply prescribes that you restrict direct communication between objects and force them to talk via a mediator object.
In your example, both the handler and service style shown actually follow the mediator pattern. In both, you are preventing direct communication between the controller and the repo, forcing them to go through the MediatR handler or service, respectively.
However, the purpose of the mediator object in the mediator pattern is to reduce dependencies between those objects, and other objects that may need to deal with them.
Note, however, that the mediator pattern is a behavioral pattern.
MediatR is a .NET implementation of the mediator pattern, allowing you to ensure this behavior is followed in your application. But, it can also help with facilitating other patterns, such a the adapter, facade, or proxy structural design patterns. The handlers are a kind of mini-service as well, but I encourage only using mediator handlers as a way of handling communication as their single purpose.
The service, as you included it in your example, has a different purpose in most designs. It's purpose is to provide a place for you to put business logic. Of course, often for simple 'GetAll' kind of endpoints, there is no business logic at the start, so we just have it there as a 'just in case', but really, you could just call the repo from your controller and save yourself some code and complexity. But, if you were to implement the mediator pattern as you have above, when you DO need to add business logic in there, you can then have the handler call a service instead of the repo, and the controller would be none the wiser and require zero changes (in theory). In true mediator implementation, the service wouldn't call the repo either, but rather the go back through the mediator get the info it needs from the repo, too.
Refactoring Guru has some good information on the various patterns and their uses.
https://refactoring.guru/design-patterns/mediator
Upvotes: 0
Reputation: 181
It depends on your team/personal preference really. Obviously, in the example you give, there would be no benefits to using MediatR vs just a regular good-old constructor injection. However, I will highlight a few points which in my humbled opinions where MediatR really shines:
You need to do a lot of constructor injections (think 4-5 services). At this point, the command/query handler becomes the orchestration between all these services and you can keep the controller really lean and minimal. This won't really do much but at some point, if you decide to port the project over to something else let's say from a ASP.NET controller to minimal API or Azure Function or even something like a WinForm app. The only thing you need to do is to refactor the controller to something that would send the request to the appropriate handler, and you are all set. All your application/business logic remains the same.
MediatR pattern is usually used with layer architecture, so your solution could have several project responsible for different things, which to be honest, any medium-sized project will eventually need some layering/slicing. Then usually without MediatR, you still need to separate the presentation layer from the application layer or any other layers via some sort of service. Let just say GetProductService in this case, then you need to inject that service into the controller. This does not make a lot of difference than using MediatR so that it could scan the assembly and wire up the correct handler for you, so at this point it is up to you to either do it yourself or use something like MediatR. The only downside I can see is if you somehow delete the handler and without integration testing, you could potentially run into issues or when you are debugging, you are unsure where your handler is (which can be mitigated with a sound folder structure).
You need to solve cross-cutting concerns (caching/authentication/authorization/logging/validation, etc...). If you dive deeper into the MediatR library you will discover something called pipeline behavior which when implemented will look something like this
public class LoggingBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly ILogger<TRequest> _logger;
public LoggingBehavior(ILogger<TRequest> logger)
=> _logger = logger;
// Log the request (command) and whether it succeeded or failed
public async Task<TResponse> Handle(
TRequest request,
CancellationToken ct,
RequestHandlerDelegate<TResponse> next)
{
// Get the name of the request
string name = request.GetType().Name;
try
{
// Log the request
_logger.LogInformation($"Executing request {name}", name);
// Execute the request
var result = await next();
_logger.LogInformation($"Request {name} executed successfully", name);
return result;
}
catch (Exception exception)
{
_logger.LogError(
exception,
$"Request {name} failed",
name);
throw;
}
}
}
This example automatically performs logging for your, and you do not need to repeat this code again in any of the handlers. This helps big time with code duplication and can be used for lots of things (authorization/logging/validation,etc...). Without MediatR, you would need to structure your project to do something similar to avoid code duplication or have duplicated code to perform cross-cutting concerns all over the places.
public class AddNewProductDomainEventHandler: INotificationHandler<AddNewProductDomainEvent>
{
// your code to send email or whatever side effect that needed to do when a new product is added
}
In your handler for adding product, now you only need to do this
// This will add new product to your db
await _repo.AddAsync(newProduct, ct)
// This will use IPublisher from MediatR to publish event
// As long as you have a handler for this event it will execute your side effect
await _publisher.Publish(new AddNewProductDomainEvent(product.Id))
Note, this is only an example, in real life you will need to architecture this better for resiliency (outbox pattern or something that would handle failure gracefully). This is a very common pattern in event-driven architecture and when you need to do something like this in your application, you will start to appreciate the MediatR. Of course, all of this can be implemented by your self/team without having to add another external dependency. However, the behavior would be very similar so it will again be up to you to use something that is very popular like MediatR or a do-it-yourself approach.
Upvotes: 3
Reputation: 169
based on my experience there are some benefits in using Mediator
first of all you don't need to inject lots of services and interfaces in your controller ( just need to inject IMediator service and use it )
second and more important one is that you can use a very useful feature of this amazing library which is PipelineBehavior check this out
Upvotes: 3