Mediator itself is a pretty old and well-known design pattern. It promotes loosely-coupled communication between objects which reduces chaotic dependencies and forces the communication through a single mediator object.

In modern applications, the mediator tries to solve the problem of decoupling the in-process sending of messages from handling messages. This is especially helpful when building applications with a focus on clean architecture where the core of your application is not dependent on any specific framework (e.g., ORM like Entity Framework or NHibernate). The mediator, in this instance, serves more as an in-memory message bus for request/response tasks.

In the following examples, I will be using an open-source MediatR library that implements mediator pattern in .NET (github link). In MediatR, each request requires a definition of input, output, and handler that does the actual logic. So, for example, let’s say we need to implement a request that obtains some elements from the database based on the provided filter. In order to do so, we need to create an input class for a mediator that implements IRequest<T> interface where T is the type of request output; in this case a collection of elements.

    public class GetElements : IRequest<IEnumerable<Element>>
    {
        // any parameters of request in form of public properties
        public ElementFilter Filter { get; set; }
    }

To define a handler for this request, we need to create a class that implements IRequestHandler<Tin, Tout> interface where Tin is the type of the request and Tout is the type of output.

    public class GetElementsHandler : IRequestHandler<GetElements, IEnumerable<Element>>
    {
        private readonly ElementsContext _elementsContext;

        public GetElementsHandler(ElementsContext elementsContext)
        {
            _elementsContext = elementsContext;
        }

        public async Task<IEnumerable<Element>> Handle(GetElements request, CancellationToken cancellationToken) {
            var elementsQuery = ElementFilterQueryBuilder.Build(_elementsContext, request.Filter);
            var elements = await elementsQuery
                               .AsNoTracking()
                               .ToListAsync(cancellationToken);

            return elements;
        }
    }

The IRequestHandler interface requires the method Handle to be implemented in a derived class, which is, in fact, the place where your logic of the request is. The Handle method has two arguments. The first argument is of the type Tin — the place where metadata of the request are stored, in this case, a filter. The second argument is the standard cancellation token.

As you can see from the code, it is extremely clean and simple, honoring the KISS (keep it stupid simple) principle. More importantly, it also honors the single responsibility principle because each single handler has only one specific purpose. Every developer that looks at this code should know in a few seconds what the code does (if he has some knowledge of Entity Framework, in this case). As you might have noticed, the database context is injected into the constructor using dependency injection. For this to work, MediatR needs to be registered into the service provider of your choice. For Microsoft’s service provider it is as easy as calling one line of code in the Startup file of your ASP.NET Core application.

public void ConfigureServices(IServiceCollection services) {
	. . .
    services.AddMediatR(typeof(GetElements));
	. . .
}

The only parameter of this method is an Assembly or a type in a given assembly where your handlers are located, registration of each individual handler is then done for you by using reflection in the background. Apart from ASP.NET Core service provider, other supported containers are Autofac, Castle Windsor, DryIoc, Lamar, LightInject, Ninject, Simple Injector, StructureMap, and Unity.

So far, we have defined and implemented request GetElements and we also have it registered into our service provider, so the only thing left is… to use it!

To use the mediator object, simply inject IMediator into the constructor and let it be resolved by your service provider. IMediator object has two methods: Send and Publish. Send method is used for the command type of requests, the Publish method serves more as an event system. In this article, we will focus on the Send method.

To create a new request, simply call the Send method and provide an instance of the desired request.

private readonly IMediator _mediator;
 
public ElementsController(IMediator mediator)
{
    _mediator = mediator;
} 
 
[HttpGet(nameof(GetElements))]
public async Task<IActionResult> GetElements([FromQuery] ElementFilter filter)
{
    IEnumerable<Element> response = await _mediator.Send(new GetElements {Filter = filter});
    return Ok(response);
} 

The mediator acquires the request, finds an appropriate handler, instantiates it using the service provider (so all dependencies are met), calls the Handle method, and returns the result to the caller (in our example it is the GetElements method in the controller). It is good to know that the reflection for handler registration is done only at the start, so you do not need to worry about performance at all.

In a case where the return value of request is not needed (e.g., delete, update, or create operations), the handler class should inherit from the abstract class AsyncRequestHandler<T> where T is the type of the request and the request class should implement plain IRequest interface (without any generics).

    public class DeleteElementById : IRequest
    {
        public int Id { get; set; }
    }
 
    public class DeleteElementByIdHandler : AsyncRequestHandler<DeleteElementById>
    {
        private readonly ElementsContext _elementsContext;
 
        public DeleteElementByIdHandler(ElementsContext elementsContext)
        {
            _elementsContext = elementsContext;
        }
 
        protected override async Task Handle(DeleteElementById request, CancellationToken cancellationToken) {
            var element = await _elementsContext.ScreenElements.FindAsync(request.Id);
            if (element == null)
            {
                return;
            }
            _elementsContext.ScreenElements.Remove(element);
            await _elementsContext.SaveChangesAsync(cancellationToken);
        }
    }

Although consistent naming of requests is not required, it is nevertheless recommended. The name of your request should start with the type of operation (e.g. Delete), followed by the domain (Element) and optionally some other specification (by ID), so, for example, UpdateUser, GetPrintJobById, CreateUniverse. I think you get the point. If you keep the naming consistent, it will be easier to navigate through your code and it will also greatly improve the code readability.

Another example of the tradeoff of a mediator is when a mediator is used inside of the request handler. The mediator object is injected into the constructor and the Handle method is used to call some other request. In the example below, we need to check first when creating a new element if an element with the same metadata does not already exist. In order to do so, we can call another request using the mediator object, which is perfectly fine; however, it violates the Inversion of Control pattern because it is not transparent from the constructor definition that the GetElementByFilter request would be called. In my opinion, this is a minor tradeoff worth mentioning.

    public class CreateElementHandler : AsyncRequestHandler<CreateElement>
    {
        private readonly ElementsContext _elementsContext;
        private readonly IMediator _mediator;
 
        public CreateElementHandler(ElementsContext elementsContext, IMediator mediator)
        {
            _elementsContext = elementsContext;
            _mediator = mediator;
        }
 
        protected override async Task Handle(CreateElement request, CancellationToken cancellationToken) {
            var element = request.Element;
 
            var filter = new ElementFilter(){. . .};
            var response = await _mediator.Send(new GetElementByFilter() { Filter = filter }, cancellationToken);
            if (response.Element != null)
            {
                throw new ElementAlreadyExistException(element.Name);
            }
            _elementsContext.ScreenElements.Add(element);
            await _elementsContext.SaveChangesAsync(cancellationToken);
        }
    }

Pros:

  • Naturally promotes SRP
  • Easily maintainable, testable and readable code

Cons:

  • Sometimes antipattern to IoC
  • Requires a lot of classes

In my experience, this approach is perfect for applications that handle CRUD operations of a few domains, e.g., microservices with 1-3 domains. For larger applications or monoliths, I would be careful when using the Mediator because you could easily end up creating a God object.

Comments

LEAVE A REPLY

Please enter your comment!
Please enter your name here