Writing decoupled code with MediatR: The Mediator Pattern

Writing decoupled code with MediatR: The Mediator Pattern

ยท

5 min read

I recently wrote a blog post introducing some of my favourite NuGet packages: My Top 4 NuGet Packages for ASP.NET Core. In that post, I briefly introduced a package called MediatR. Today, I will dedicate this post to revisiting MediatR in further detail.

What is MediatR?

MediatR is an implementation of the mediator pattern. It is a behavioural software design pattern that helps you to build simpler code by making all components communicate via a "mediator" object, instead of directly with each other. This helps the code to remain highly decoupled and reduces the number of complex dependencies between objects.

A good real-world example of the mediator pattern is the air-traffic control (ATC) tower at airports. If every plane had to directly communicate with every other plane, it would be chaos. Instead, they all report to the ATC tower, and the tower decides how to relay those messages to other aircraft. In this, scenario the ATC tower is the mediator object.

Using the MediatR package, one sends some data as an object to the mediator object. Depending on the type of data that is sent to the mediator object, it decides which other objects/services to call. MediatR handles two forms of message:

  • Requests - can only send messages to one other object/service. Can return the result from that object/service to the original caller.
  • Notifications - can send messages to multiple objects/services. Cannot receive any data back.

Setting up MediatR

To get MediatR, install the MediatR package from NuGet. If you are using ASP NET Core, you should also install the MediatR.Extensions.Microsoft.DependencyInjection, which provides an easy way to register all of the MediaR services:

// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
  // other services

  services.AddMediatR(typeof(Startup));
}

If you are using a different method of dependency injection, please check the wiki on how to configure MediatR for your container.

Sending Requests

To send a request, you need to create two objects: a request and a request handler.

The request object should implement the IRequest or IRequest<TResponse> interfaces, depending on whether or not you want to return any data. The request object should consist of any data that you want to send to the handler.

public class AdditionRequest : IRequest<int>
{
    public int A { get; set; }
    public int B { get; set; }
}

The request handler should implement the IRequestHandler<TRequest> or IRequestHandler<TRequest, TResponse> interfaces, where TRequest is the request object that you just created.

public class AdditionRequestHandler : IRequestHandler<AdditionRequest, int>
{
    public Task<int> Handle(AdditionRequest request, CancellationToken cancellationToken)
    {
        var result = request.A + request.B;

        return Task.FromResult(result);
    }
}

To then send the request via MediatR, one must use the Send method on the IMediator instance, passing in an instance of AdditionRequest.

public class MyCalculator
{
    private readonly IMediator _mediator;

    public MyCalculator(IMediator mediator)
    {
        _mediator = mediator;
    }

    public async Task<int> Add(int a, int b)
    {
        var request = new AdditionRequest { A = a, B = b };
        var result = await _mediator.Send(request);

        return result;
    }
}

Note that if you choose not to return anything from a request handler, MediatR actually has a special 'nothing' value that you must return instead, called Unit.

public class MyRequest : IRequest 
{
    // some properties
}

public class MyRequestHandler : IRequestHandler<MyRequest>
{
    public Task<Unit> Handle(MyRequest request, CancellationToken cancellationToken)
    {
        // do stuff

        return Task.FromResult(Unit.Value);
    }
}

Sending Notifications

Sending notifications is very similar to sending requests, in that a notification object and a notification handler object must be created. The difference here is that multiple notification handler objects can be created, which will all be called when a notification is sent to MediatR.

public class MyNotification : INotification
{
    // some properties
}

public class MyNotificationHandler1 : INotificationHandler<MyNotification>
{
    public Task Handle(MyNotification notification, CancellationToken cancellationToken)
    {
        // do stuff

        return Task.CompletedTask;
    }
}

public class MyNotificationHandler2 : INotificationHandler<MyNotification>
{
    public Task Handle(MyNotification notification, CancellationToken cancellationToken)
    {
        // do stuff

        return Task.CompletedTask;
    }
}

Then to actually send the notification, call the the Publish method on the IMediator instance, passing in an instance of your notification object. When Publish is called, both MyNotificationHandler1 and MyNotificationHandler2 will be executed.

public class MyService
{
    private readonly IMediator _mediator;

    public MyService(IMediator mediator)
    {
        _mediator = mediator;
    }

    public async Task Execute()
    {
        var notification = new MyNotification
        {
            // initialise
        };

        await _mediator.Publish(notification);
    }
}

Pipeline Behaviours

Pipeline behaviours are a type of middleware that get executed before and after a request (only supports requests, not notifications). They can be useful for a number of different tasks, such as logging, error handling, request validation etc.

These behaviours work in the same way that you would expect for middleware: you can chain multiple behaviours together, each behaviour gets executed in turn until it reaches the end of the chain, then the actual request handler is executed, then the result of this is passed back up the chain.

For example, a LoggingBehaviour could write to the application log before and after each request:

public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;

    public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
    {
        _logger = logger;
    }

    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        _logger.LogInformation($"Handling {typeof(TRequest).Name}");

        // go to the next behaviour in the chain/to the request handler
        var response = await next();

        _logger.LogInformation($"Handled {typeof(TResponse).Name}");

        return response;
    }
}

In ASP NET Core, to register each PipelineBehaviour with the IoC container, add the following line to the ConfigureServices method of the the Startup class:

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

For other containers, check the wiki.

Conclusion

In this article, I have introduced the mediator pattern and the NuGet package MediatR, which itself is an implementation of the mediator pattern. I have showed you how to send both requests (one-to-one) and notifications (one-to-many), as well as how to write middleware (pipeline behaviours) that gets executed before/after each request.

"Ten++ Ways to Make Money as a Developer" eBook

I post mostly about full stack .NET and Vue web development. To make sure that you don't miss out on any posts, please follow this blog and subscribe to my newsletter. If you found this post helpful, please like it and share it. You can also find me on Twitter.

Did you find this article valuable?

Support Sam Walpole by becoming a sponsor. Any amount is appreciated!