If you want to implement the Mediator pattern in C# you could of course implement the mediator yourself. Another option is to use a standard library for this, a popular choice with us is MediatR since it’s simple to implement and doesn’t impose any rules on you. That’s why it’s tagline is “Simple, unambitious mediator implementation in .NET”. In this blog post we’ll take a quick look at what the mediator pattern is, and how to implement this with C#. Please note that this whole blog was written on a Linux machine, so the examples assume a normal Bash and curl.
What is the mediator pattern
According to Wikipedia:
With the mediator pattern, communication between objects is encapsulated within a mediator object. Objects no longer communicate directly with each other, but instead communicate through the mediator. This reduces the dependencies between communicating objects, thereby reducing coupling.
The mediator pattern is meant to split responsibilities between a caller and the callee. So instead of having an instance of a class and calling a method directly on it, you ask the mediator to do this for you. While this might sound a lot like doing dependency injection, it actually goes a step further. You don’t even know the interface of the callee. This makes it even more decoupled than you’d have with dependency injection.
The result is that the objects are loosely coupled. They only know about the mediator object, not about each other. Besides the mediator object they’ll likely also have some kind of data transfer object to send parameters for the callee.
For a comparison in the real world, imagine only being able to communicatie with someone or a department through their secretary. You give them a message (the data transfer object) and they pass it on to who you want to reach. Once there’s an answer, the secretary will give it back to you. This way you have no knowledge of who actually answered it.
How to implement it in C#
For this part I’m assuming you are starting from scratch. If you are implementing this in an already existing solution the steps should be the same, but the order of things in the Startup.cs
might be different. If you’re just looking for a full example, head on over to this GitHub repository. As we’re mostly doing web application development here at Sentia, this example will be in the form of a web application, based on .NET Core 3.1. There is nothing that’s stopping you from implementing something like this in a console application or a GUI application.
Setting up the project
First things first, we’ll make a new ASP.NET Core project. Because we don’t really need either MVC or Web API for this example, we’ll just use an empty ASP.NET Core project. This is created by the command dotnet new web
in an empty directory. This will use the folder name as name for the project. On my machine, the output was as follows:
$ dotnet new web
The template "ASP.NET Core Empty" was created successfully.
Processing post-creation actions...
Running 'dotnet restore' on /home/sanne/MediatRDemo/MediatRDemo.csproj...
Determining projects to restore...
Restored /home/sanne/MediatRDemo/MediatRDemo.csproj (in 81 ms).
Restore succeeded.
In other words, we now have a new project named MediatRDemo.csproj
. This is the project we’ll use in this blog post. Of course all of this could also be done in your favorite IDE, be it Visual Studio or Jetbrains Rider.
Next up, we need to install MediatR
. This is a NuGet package, so we’ll install it using dotnet add package MediatR
, giving us the following output:
$ dotnet add package MediatR
Determining projects to restore...
Writing /tmp/tmpZaLe9j.tmp
info : Adding PackageReference for package 'MediatR' into project '/home/sanne/MediatRDemo/MediatRDemo.csproj'.
info : Restoring packages for /home/sanne/MediatRDemo/MediatRDemo.csproj...
info : GET https://api.nuget.org/v3-flatcontainer/mediatr/index.json
info : OK https://api.nuget.org/v3-flatcontainer/mediatr/index.json 108ms
info : GET https://api.nuget.org/v3-flatcontainer/mediatr/8.0.1/mediatr.8.0.1.nupkg
info : OK https://api.nuget.org/v3-flatcontainer/mediatr/8.0.1/mediatr.8.0.1.nupkg 6ms
info : Installing MediatR 8.0.1.
info : Package 'MediatR' is compatible with all the specified frameworks in project '/home/sanne/MediatRDemo/MediatRDemo.csproj'.
info : PackageReference for package 'MediatR' version '8.0.1' added to file '/home/sanne/MediatRDemo/MediatRDemo.csproj'.
info : Committing restore...
info : Writing assets file to disk. Path: /home/sanne/MediatRDemo/obj/project.assets.json
log : Restored /home/sanne/MediatRDemo/MediatRDemo.csproj (in 1.15 sec).
Great! So, now we’ve got MediatR installed, but this way it isn’t as easy to use. To make it easier to use, we’ll also install MediatR.Extensions.Microsoft.DependencyInjection
the same way. After this, we need to add MediatR to the services collection. To do this, open up Startup.cs
and add the code below to the ConfigureServices(IServiceCollection services)
method:
public void ConfigureServices(IServiceCollection services)
{
services.AddMediatR(typeof(Startup));
}
This makes it so you can get the IMediator
interface from dependency injection in .NET Core. Don’t forget to add using MediatR;
to the top of the file if your IDE didn’t prompt you for it. We’re passing in the type in which MediatR should look for implementations of IRequest
and IRequestHandler
. In this case, that’s the same project as the Startup
class. In a bigger application, this might be another assembly.
Short recap of what we’ve done so far:
- Create a new project
- Add the MediatR and dependency injection packages
- Add MediatR to the ServiceCollection using the dependency injection package
Adding the MediatR objects
The next step is adding an IRequest
and IRequestHandler
. These represent a request to the mediator, and a way to handle this. Convention for MediatR is to have these two classes in the same file, which we’ll follow for convenience. So, let’s create a new folder named RequestHandlers
which’ll contain the new file. In this folder, create a file named PingHandler.cs
. I’d advise doing this with your IDE so you’ll have the correct namespace and some basic using
statements. In this file, create two classes:
- A class named
Ping
which implements theMediatR.IRequest<string>
interface - A class named
PingHandler
which implements theMediatR.IRequestHandler<Ping, string>
The IRequestHandler
interface takes one or two type arguments. If you give it just one, you cannot return a value. If you give it two, you can return a value. In the Ping
class, add one public property of type string
with the name ResponseMessage
. In the PingHandler
class, implement the interface and return the ResponseMessage
from the request. Your code should now look like this:
using System.Threading;
using System.Threading.Tasks;
using MediatR;
namespace MediatRDemo.RequestHandlers
{
public class Ping : IRequest<string>
{
public string ResponseMessage { get; set; }
}
public class PingHandler : IRequestHandler<Ping, string>
{
public Task<string> Handle(Ping request, CancellationToken cancellationToken)
{
return Task.FromResult(request.ResponseMessage);
}
}
}
So, now we have both the request and the handler for that request. The handler will simply reply with the response message that was request. So, how do we call this? Simple! We’ll add a controller that’ll do the work for us. Before we get to that part though, we’ll have to add controller support in the Startup.cs
. To do this, add the line below to the ConfigureServices
method:
services.AddControllers()
Also, in the Configure
method we need to modify the lambda function inside app.UseEndpoints
. At the moment it’ll always return the text “Hello World” when calling the website. Not what we want. Instead, we want to use controllers to handle the requests. To do this, modify it to look like this:
app.UseEndpoints(endpoints => endpoints.MapControllers());
With that done, we’re ready to create our controllers and have then work as well. Create a folder named Controllers
in the root of your project and add an API controller named PingController.cs
. Add a private readonly IMediator _mediator
and initialize it from the constructor. Next, add a Get
method and have it call _mediator.Send(new Ping { ResponseMessage = "Pong!" })
. Your controller should look like this:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using MediatRDemo.RequestHandlers;
using MediatR;
namespace MediatRDemo.Controllers
{
[ApiController]
[Route("[controller]")]
public class PingController : Controller
{
private readonly IMediator _mediator;
public PingController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet]
public Task<string> Get()
{
return _mediator.Send(new Ping { ResponseMessage = "Pong!" });
}
}
}
If you now run dotnet run
in the root of the project, everything should compile and you should be able to access http://localhost:5000/ping. This should show you the text you entered for the ReponseMessage
in your controller. So far so good, but where are you going to put validation?
Adding validations
A significant part of time in web applications is spend validating user input. You’ll never now what the user will send you. To make this simpler we’ll use FluentValidations. This is available as a NuGet package as well. In this case, we’re directly installing the dependency injection extensions, this will download and install FluentValidation as a dependency.
Install this with dotnet add package FluentValidation.DependencyInjectionExtensions
in the root of your project. Next, we need to update the ConfigureServices
to load the validators. Add the following line to that method, and don’t forget to add using FluentValidation;
at the top of the file.
services.AddValidatorsFromAssemblyContaining<Startup>();
You need to pass in the assembly in which your validators are. In this case, it’s the same as the Startup
class, but just like with adding MediatR it might be somewhere else in a bigger project. So change it where necessary.
We’ll add validators to the same file as the IRequest
and IRequestHandler
, this’ll keep everything nice and contained for this demo. In reality, you could have a completely different namespace or assembly for the validators.
To add a validator, open the RequestHandlers/PingHandler.cs
file again, and add a PingValidator
class. This class must inherit from the FluentValidation.AbstractValidator<Ping>
class. Ping
, in this case, is referring to the IRequest
we want to validate. As an example, we’ll check that the ResponseMessage
is not empty but also not longer than 512 characters. We’ll do that by adding a constructor with these validations so it’ll look like this:
public class PingValidator : AbstractValidator<Ping>
{
public PingValidator()
{
RuleFor(x => x.ResponseMessage)
.NotEmpty()
.WithMessage("We need to know what you want from us")
.DependentRules(() =>
RuleFor(x => x.ResponseMessage.Length)
.LessThanOrEqualTo(512)
.WithMessage("We will not reply with more than 512 characters"));
}
}
Here, too, you must add the using FluentValidation
line to the list of usings or it won’t compile. So, now we have the IRequest
, the IRequestHandler
and the validation set up, but we still need to add something that’ll call your validations on each request to MediatR. To do this, MediatR has something called IPipelineBehavior
. This is an interface that gives you access to the MediatR pipeline, and using this we’ll validate the incoming IRequest
. There are a lot of examples for this online, but I’m using a combination from multiple sources that looks like this:
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FluentValidation;
using MediatR;
namespace MediatRDemo
{
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
var context = new ValidationContext(request);
var failures = _validators
.Select(v => v.Validate(context))
.SelectMany(result => result.Errors)
.Where(f => f != null)
.ToList();
if (failures.Count != 0)
{
throw new ValidationException(failures);
}
return next();
}
}
}
This’ll get all the registered IValidator<TRequest>
instances from the dependency injection framework, and then call all of them. If none of these give a failure, the request passes on to the next stage in the pipeline via the call to next()
. But if there is a failure, we’ll throw a new ValidationException
containing a list of all failures. There are improvements to be had, for example if you have a lot of validators you want to call for each request you might want to call them in parallel.
To have MediatR call this IPipelineBehavior
on each request, add the following line to the ConfigureServices
method in the Startup
class:
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
We’re adding it as a transient here to make sure we don’t run the wrong validators. With this done, all we need to do is give the outside world a way to trigger this validator. The current Get
method on the controller isn’t likely to trigger a failure on the validator because it has a constant string that’s being sent. To make it a bit more interesting, let’s add a Post
method that’ll accept an input and try to put it into the ResponseMessage
. Add this to your PingController
:
[HttpPost]
public Task<string> Post([FromBody]string responseMessage)
{
return _mediator.Send(new Ping { ResponseMessage = responseMessage });
}
If you now run the application again using dotnet run
, it should still work just fine when doing a simple GET request via the browser. However, we can now also POST data to the controller and have it return it to us. Let’s use curl to do some POST requests to the controller.
Let’s start with something simple, something that’ll surely pass the validator:
$ curl \
--request POST \
--header "Content-Type: application/json" \
--data '"hello"' \
http://localhost:5000/ping
hello
This weird escaping around the hello
is necessary, because otherwise it isn’t a proper JSON string. The request should just return hello
back to you. So far so good. What happens when we do the same request, but with an empty the --data
argument? Let’s try it:
$ curl \
--request POST \
--header "Content-Type: application/json" \
--data '\"\"' \
http://localhost:5000/ping
FluentValidation.ValidationException: Validation failed:
-- ResponseMessage: We need to know what you want from us
at MediatRDemo.ValidationBehavior`2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate`1 next) in /home/sanne/MediatRDemo/ValidationBehaviour.cs:line 33
Yikes, that gives us an error. Note how it returns the message you told it to return in the PingValidator
, but it also includes the complete stacktrace with it. This is something that could use some improvement. For example, you could have a generic TResponse
implementation that always has an Error
attribute and set that. Then you could simply return a new TResponse
with that set.
Unit testing
Last but definitely not least, you probably should unit test your code. The IRequestHandler
instances are probably the easiest, just call them from your test and get the results you want. The FluentValidation parts take a little more work, but are still relatively easy. When instantiating them you can just call Validate()
on the instance and pass data in via one of the arguments. A test method for the PingValidator
could look something like this:
[TestMethod]
public void EmptyResponseMessage_ShouldThrowValidationException()
{
var _sut = new PingValidator();
var ping = new Ping { ResponseMessage = null };
var validationResult = _sut.Validate(ping);
validationResult.IsValid.Should().BeFalse();
validationResult.Errors.Should().NotBeEmpty();
validationResult.Errors[0].ErrorMessage.Should().Be("We need to know what you want from us");
}
This is using the Microsoft.VisualStudio.TestTools.UnitTesting framework and FluentValidations for the assertions. In this case we instantiate the PingValidator
and give it a Ping
instance with the ResponseMessage
set to null
. We then specify that it shouldn’t be valid, that the Errors
property of the result shouldn’t be empty and that the first error should be the text we expect.
Wrapping it all up
So, by now you should have some idea of what the mediator pattern is, and how to implement it using the MediatR package. Also, we’ve shown you how to add some validations to it using FluentValidators. Finally, a short look into unit testing validators to wrap it up. If you want to know more about MediatR, there are a bunch of examples and a some documentation on their own wiki on GitHub. You can find that over at https://github.com/jbogard/MediatR/wiki. Also, there is a small demo project containing everything I’ve shown you here over on my personal GitHub, you can find that at https://github.com/sentialabs/MediatR.