Mediator - Rades98/AlzaCaseStudy GitHub Wiki

What is it

Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping the objects from referring to each other explicitly, and it lets you vary their interaction independently. In object-oriented implementations, the behaviour is split across many objects. There is a natural requirement to have an object depend on others. Quickly, this dependency can grow and have each object depend on all others.

The Mediator design pattern strives to break such complex dependency chain from being formed. It makes all the objects to depend on a single object, the Mediator. The Mediator depends on and knows all the objects. This keeps the individual component objects re-usable and maintainable. It also lets us add new objects into the system easily.

It is used to handle communication between related objects (Colleagues)

All communication is handled by the Mediator and the Colleagues don't need to know anything about each other

Allows loose coupling by encapsulating the way disparate sets of objects interact and communicate with each other

Allows for the actions of each object set to vary independetly of one another

So this robust and complex architecture

image

will become easy to understand and clear as is shown bellow

image

Advantages x Disadvantages

Advantages Disadvantages
It decouples the component classes. A component only depends on the mediator. It makes a m:n interaction a n:1 interaction. As you would have noticed, the mediator implementation is heavy. It would seem to know a lot of things. If not careful, it can become a God Object.
It makes the components reusable.
Since a mediator has a clear interface, we can create and use a new mediator in the future.

An example – A Smart Home System

Let us say we have a smart home control system application. Consider we have an Air conditioner, a fan and a smart control as part of this application. If the smart control is on, then it controls and optimizes the air-based components (the AC and the fan). If one of the air components is switched on, the other would be switched off automatically (if the other was also running).

Key words

  • Mediator - Defines an abstract class for the mediator. There may be more specific mediators.
  • Colleague - The abstract class is then also defined for colleagues, objects in interaction. Thanks to the abstract class, specific colleagues can easily inherit a link to a mediator.
  • ConcreteColleague - Specific objects implement arbitrary operations and have a link to the mediator.
  • ConcreteMediator - Specific mediators maintain a bond with all colleagues so that they can mediate communication.

image

Usage

Mediator and CQRS

We know how a traditional web API works. It mainly consists of the CRUD operations. All the four operations are tied together inside an API controller.

  • C – Create
  • R - Read
  • U – Update
  • D – Delete. To decouple the application we create interfaces for our data access layer, inject it in the constructor of API controller and perform the actions. This works fine and all looks good. But as your application grow in size the number of dependencies also increases. Now we need to inject multiple interfaces in the API controller and hence the application complexity increases.

In the following example, we will show how a mediator could be used in practice to obtain products from a database using a nugget MediatR. For proper implementation, we need an object implementing IRequest and some object that is for data return. Let's name them GetProductRequest and GetProductResponse.

public class GetProductResponse
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

The request itself is created as follows. As we can see, the Request includes input parameters and a handler where the functionality we implemented takes place.

public class GetProductRequest: IRequest<GetProductResponse?> //Definition of returning object type
{
    public Guid Id { get; set; } //Params
 
    public class Handler : IRequestHandler<GetProductRequest, GetProductResponse?> //Handler returning response
    {
        private readonly ProductEntityRepository _repo; //Product repository
 
        public Handler(ProductEntityRepository  repo) => _repo = repo;
 
        public async Task<GetProductResponse?> Handle(GetProductRequest request, CancellationToken cancellationToken)
        {
            var product = await _repo.GetAsync(request.Id, cancellationToken); // Get product by id
 
            return (GetProductResponse)product;
        }
    }
}

Use of created requests and responses

Registration in the IoC is ensured by calling the following method when registering services

...
services.AddMediatR(Assembly.GetExecutingAssembly());
...

The use, for example, in the controller is then as follows

await Mediator.Send(new ProductGetRequest() { Id = someGuid}, cancellationToken);

As you can see, the controller does not need to know the repository, but only the mediator, which in the future means clarity and simplicity of editing.

Mediator and middleware for Requests

Some middleware can run around the mediator, so logging, caching, validation and so on can be used without the need to write complex handlers. The registration then takes place by creating the required pipeline, for example, for logging the request resolution time, which can look like this:

public class RequestLoggingPipeline<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> 
where TRequest : IRequest<TResponse> //Additional interfaces can be added here to specify requests using this pipeline
{
    private readonly Stopwatch _stopwatch;
    private readonly ILogger<TRequest> _logger;
 
    public RequestLoggingPipeline(ILogger<TRequest> logger) => (_stopwatch, _logger) = (new(), logger);
 
    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        _stopwatch.Start();
        var reqResponse = await next();
        _stopwatch.Stop();
 
        var duration = _stopwatch.ElapsedMilliseconds;
 
        if (duration > 2000)
        {
            _logger.LogWarning($"Request {request.GetType().Name} is slow ({duration} ms) : {request}");
        }
        else
        {
            _logger.LogInformation($"Request {request.GetType().Name} ({duration} ms) : {request}");
        }
 
        return reqResponse;
    }
}

And we'll register it

services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestLoggingPipeline<,>));

Several such pipelines can be used at once. Their use can be clearly determined by what interfaces the Request implements.


To use such a level of an abstraction, the basic IoC container that .net provides us is not enough, but we need to get some a more sophisticated one like:Autofac

⚠️ **GitHub.com Fallback** ⚠️