MediatR is an implementation of the Mediator pattern. It doesn’t match the pattern exactly, but as the creator, Jimmy Bogard states that “It matches the problem description (reducing chaotic dependencies), the implementation doesn’t exactly match…”. It’s worth reading his post You Probably Don’t Need to Worry About MediatR.
This pattern is aimed at decoupling the likes of business logic from a UI layer or request/response’s.
There are several ways we can already achieve this in our code, for example, using interfaces to decouple the business logic from the UI or API layers as “services” as we’ve probably all done for years. The only drawback of this approach is it requires the interfaces to be either passed around in our code or via DI and is a great way to do things. Another way to do this is, as used within UI, using WPF, Xamarin Forms, MAUI and others where we often use in-process message queues to send messages around our application tell it to undertake some task and this is essentially what MediatR is giving us.
Let’s have a look at using MediatR. I’m going to create an ASP.NET web API (obviously you could use MediatR in other types of solutions)
At this point we have MediatR registering services for us at startup. We can passing multiple assemblies to the RegisterServicesFromAssembly method, so if we have all our reqeust/response code in multiple assemblies we can supply just those assemblies. Obviously this makes our life simpler but at the cost of reflecting across our code at startup.
The ASP.NET Core Web API creates the WeatherForecast example, we’ll just use this for our sample code as well.
The first thing you’ll notice is that the route to the weatherforecast is tightly coupled to the sample code. Ofcourse it’s an example, so this is fine, but we’re going to clean things up here and move the implementation into a file named GetWeatherForecastHandler but before we do that…
Note: Ofcourse we could just move the weather forecast code into an WeatherForecastService, create an IWeatherForecastService interface and there’s no reason not to do that, MediatR just offers and alternative way of doing things.
MediatR will try to find a matching handler for your request. In this example we have no request parameters. This begs the question as to how MediatR will match against our GetWeatherForecastHandler. It needs a unique request type to map to our handler, in this case the simplest thing to do is create yourself the request type. Mine’s named GetWeatherForecast and looks like this
public record GetWeatherForecast : IRequest<WeatherForecast[]>
{
public static GetWeatherForecast Default { get; } = new();
}
Note: I’ve created a static method so we’re not creating an instance for every call, however this is not required and obviously when you are passing parameters you will be creating an instance of a type each time – this does obviously concern me a little if we need high performance and are trying to write allocation free code, but then we’d do lots differently then including probably not using MediatR.
Now we’ll create the GetWeatherForecastHandler file and the code looks like this
public class GetWeatherForecastHandler : IRequestHandler<GetWeatherForecast, WeatherForecast[]>
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
public Task<WeatherForecast[]> Handle(GetWeatherForecast request, CancellationToken cancellationToken)
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
return Task.FromResult(forecast);
}
}
At this point we’ve created a way for MediatR to find the required handler (i.e. using the GetWeatherForecast type) and we’ve created a handler to create the response. In this example we’re not doing any async work, so we just wrap the result in a Task.FromResult.
Next go back to the Program.cs or if you’ve used controllers, go to your controller. If using controller you’ll need the constructor to take the parameters IMediator mediator and assign to a readonly field in the usually way.
For our minimal API example, go back to the Program.cs file remove the summaries variable/code and then change the route code to look like this
app.MapGet("/weatherforecast", (IMediator mediator) =>
mediator.Send(GetWeatherForecast.Default))
.WithName("GetWeatherForecast")
.WithOpenApi();
We’re not really playing too nice in the code above, in that we’re not returning results code, so let’s add some basic result handling
app.MapGet("/weatherforecast", async (IMediator mediator) =>
await mediator.Send(GetWeatherForecast.Default) is var results
? Results.Ok(results)
: Results.NotFound())
.WithName("GetWeatherForecast")
.WithOpenApi();
Now for each new HTTP method call, we would create a request object and a handler object. In this case we send no parameters, but as you can no doubt see, for a request that takes (for example) a string for your location, we’d create a specific type for wrapping that parameter and the handler can then be mapped to that request type.
In our example we used the MediatR Send method. This sends a request to a single handler and expects a response of some type, but MediatR also has the ability to Publish to multiple handlers. These types of handlers are different, firstly they need to implement the INotificationHandler interface and secondly no response is expected when using Publish. These sorts of handlers are more like event broadcasts, so you might use then to send a message to an email service or database code which sends out an email upon request or updates a database.
Or WeatherForecast sample doesn’t give me any good ideas for using Publish in it’s current setup, so let’s just assume we have a way to set the current location. Like I said this example’s a little contrived as we’re going to essentially set the location for everyone connecting to this service, but you get the idea.
We’re going to add a SetLocation request type that looks like this
public record SetLocation(string Location) : INotification;
Notice that for publish our type is implementing the INotification interface. Our handles look like this (my file is named SetLocationHandler.cs but I’ll put both handlers in there just to be a little lazy)
public class UpdateHandler1 : INotificationHandler<SetLocation>
{
public Task Handle(SetLocation notification, CancellationToken cancellationToken)
{
Console.WriteLine(nameof(UpdateHandler1));
return Task.CompletedTask;
}
}
public class UpdateHandler2 : INotificationHandler<SetLocation>
{
public Task Handle(SetLocation notification, CancellationToken cancellationToken)
{
Console.WriteLine(nameof(UpdateHandler2));
return Task.CompletedTask;
}
}
As you can see, the handlers need to implement INotificationHandler with the correct request type. In this sample we’ll just write messages to console, but you might have a more interesting set of handlers in mind.
Finally let’s add the following to the Program.cs to publish a message
app.MapGet("/setlocation", (IMediator mediator, string location) =>
mediator.Publish(new SetLocation(location)))
.WithName("SetLocation")
.WithOpenApi();
When you run up your server and use Swagger or call the setlocation method via it’s URL you’ll see that all your handlers that handle the request get called.
Ofcourse we can also Send and Post messages/request from our handlers, so maybe we get the weather forecast data then publish a message for some logging system to update the logs.
MediatR also includes the ability to stream from a requests where our request type implements the IStreamRequest and our handlers implement IStreamRequestHandler.
If we create a simple request type but this one implements IStreamRequest for example
public record GetWeatherStream : IStreamRequest<WeatherForecast>;
and now add a handler which implements IStreamRequestHandler, something like this (which delay’s to just give a feel of getting data from somewhere else)
public class GetWeatherStreamHandler : IStreamRequestHandler<GetWeatherStream, WeatherForecast>
{
public async IAsyncEnumerable<WeatherForecast> Handle(GetWeatherStream request,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var index = 0;
while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(500, cancellationToken);
yield return new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Data.Summaries[Random.Shared.Next(Data.Summaries.Length)]
};
index++;
if(index > 10)
break;
}
}
}
Finally we can declare our streaming route using Minimal API very simply, for example
app.MapGet("/stream", (IMediator mediator) =>
mediator.CreateStream(new GetWeatherStream()))
.WithName("Stream")
.WithOpenApi();