Mediator Pattern in ASP.NET Core
Mediator pattern is used to reduce communication complexity between multiple objects or classes. This pattern provides a mediator class which normally handles all the communications between different classes and supports easy maintenance of the code by loose coupling
For the last few years, Command Query Responsibility Segregation (CQRS) and Event Sourcing (ES) emerged as patterns that can help implement large scale systems, with some risky complexity, by having different models to read or mutate data while also using events as a single source of truth.
Example of mediator pattern in real life. Think about a big airport like with many of arriving and departing planes. They all need to be coordinated to avoid crashes. It would be impossible for a plan to talk to all other planes. Instead, they call the tower as a mediator, and the tower talks to all planes and organizes who goes where.
In this post I’m going to focus and show how you can decouple your application layers by mediator
Commands, Queries, Events, Handlers and Mediator
Before showing some code, lets make a simple review of some core concepts about these patterns and the library we are going to use:
- Commands — each action intended to change the system state, by creating, updating or deleting information, should be represented by a class implementing either
ICommand
orICommand<TResult>
, if you are using the mediator just for in-process decoupling and want to return a result synchronously. Examples:CreateProductCommand
orDeleteUserCommand
- Queries — classes implementing
IQuery<TResult>
represent data reads that shouldn’t change the system state or else they must be considered commands. Examples:GetProductByIdQuery
orSearchUsersQuery
- Events — representing system changes over time, these classes implement
IEvent
. Examples:ProductCreatedEvent
orUpdatedUserEmailEvent
- Handlers — responsible for receiving the commands (
ICommandHandler
), queries (IQueryHandler
) or events (IEventHandler
), they implement the business behavior to be run. Examples:CreateProductCommandHandler
orGetProductByIdQueryHandler
- Mediator — decouples the caller from the executor exposing generic methods to send commands, fetch queries or broadcast events, being responsible for finding the correct handler (or handlers, in case of an event) and is represented by the interface
IMediator
.
Implementation of the Mediator Pattern
As I described in my previous post, in In my Productmicroservice, the controllers call all needed services (CQRS mediator) and therefore work as the mediator. Additionally, I installed the MediatR , AutoMapper and MediatR.Extension.Microsoft.DependencyInjection NuGet package, which helps to call the services.
Installing MediatR
I have installed the MediatR , AutoMapper and MediatR.Extension.Microsoft.DependencyInjection packages in my project : ProductMicroservice.
In the Startup class, I registered Mediators using:
services.AddMediatR(Assembly.GetExecutingAssembly()); services.AddAutoMapper(typeof(Startup));
In this project both Controller and CQRS folder (Services) are in the same project (ProductMicroservice) (The other solution shall be creating two other projects one for CQRS and one for Data access and moving data access (in this case Repository) to Data access project and adding references for both new projects to the ProductMicroservice project).
I use the IMediator object with dependency injection in my controllers.
public class ProductController : ControllerBase
{
private readonly IMediator _mediator;
private readonly IMapper _mapper;
public ProductController(IMediator mediator, IMapper mapper)
{
_mediator = mediator;
_mapper = mapper;
}
Using the Mediator pattern
Every call from Controller consists of a request and a handler. The request is sent to the handler which processes this request. A request could be a adding a new object (post action as Insert) which should be saved in the database object (Get action ) which should be retrieved. Here we are using CQRS, which the requests are either a query for read operations or a command for a write operation.
In the ProductController, I have the Post method which will create a new product object. I create a CreateProductCommand and map the Product from the post request to the Product of the CreateProductCommandObject. Then I use the Send method of the mediator.
[HttpPost]
public async Task<ActionResult<Product>> Post([FromBody] Product product)
{
try
{
var prod = new CreateProductCommand
{
product = _mapper.Map<Product>(product)
};
return await _mediator.Send(prod);
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}Copied!
The command (CreateProductCommand) inherit from IRequest interface which where (T =Product) indicates the return value. If you don’t have a return value, then inherit from IRequest.
public class CreateProductCommand : IRequest<Product>
{
public Product Product { get; set; }
}
The send method (mediator.Send(prod)) sends the object to the CreateProductCommmandHandler (in the bellow code). The handler inherits from IRequestHandler<TRequest, TResponse> and implements a Handle method. This Handle method processes the CreateProductCommand . In this case, it calls the InsertProduct(request.Product) method of the repository and passes the Product.
using MediatR;
using ProductMicroservice.Models;
using ProductMicroservice.Repository;
using System.Threading;
using System.Threading.Tasks;
namespace ProductMicroservice.CQRS.Commands
{
public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, Product>
{
private readonly IProductRepository _ProductRepository;
public CreateProductCommandHandler(IProductRepository productRepository)
{
_ProductRepository = productRepository;
}
public async Task<Product> Handle(CreateProductCommand request, CancellationToken cancellationToken)
{
return await Task.FromResult(_ProductRepository.InsertProduct(request.product));
}
}
}
I have implemented Mediator to the a method in the ProductController.
you can can find implementation of all methods in the GitHub.
Advantages
- Less coupling: Since the classes don’t have dependencies on each other, they are less coupled.
- Easier reuse: Fewer dependencies also helps to reuse classes.
- Single Responsibility Principle: The services don’t have any logic to call other services, therefore they only do one thing.
- Open/closed principle: Adding new mediators can be done without changing the existing code.
Disadvantages
- he Mediator often needs to be very intimate with all the different classes, And it makes it really complex.
- Can make it difficult to maintain.
Conclusion
The Mediator pattern is used to reduce communication complexity between multiple objects or classes. This pattern provides a mediator class which normally handles all the communications between different classes and supports easy maintenance of the code by loose coupling. This also helps you to reuse your components in your application and also to keep the Single Responsible Principle, by using MediatR NuGet package
In my Next post I will going to implement RabbitMQ, which enables Microservices to exchange data (communicate with each others) in a decoupled asynchronous way.
The complete code can be find on GitHub.
This post is part of “Microservices-Step by step”.