The Mediator is an object that encapsulates how objects interact with each other. Instead of having objects take a direct dependency on each other, they instead interact with a “mediator”, who is in charge of sending those interactions to the other party: SomeService sends a message to the Mediator, and the Mediator then invokes multiple services to handle the message. It enables “loose coupling”, as the dependency graph is minimized and therefore code is simpler and easier to test. This is very similar to how a message broker works in the “publish/subscribe” pattern. If we wanted to add another handler we could, and the producer wouldn’t have to be modified.
You can think of MediatR as an “in-process” Mediator implementation, that helps us build CQRS systems. All communication between the user interface and the data store happens via MediatR. Since it’s a .NET library that manages interactions within classes on the same process, it’s not an appropriate library to use if we wanted to separate the commands and queries across two systems. In those circumstances, a better approach would be to a message broker such as Kafka or Azure Service Bus. However for the sae of simpilicity, we are going to stick with a simple single-process CQRS system, so MediatR fits the bill perfectly.
First off, let’s open Visual Studio and create a new ASP.NET Core Web Application, selecting API as the project type. We are going to name it CqrsMediatrExample.
builder.Services.AddMediatR(typeof(Program));
Now MediatR is configured and ready to go.
Just before we move to the controller creation, we are going to modify the launchSettings.json file:
{
"profiles": {
"CqrsMediatrExample": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"launchUrl": "weatherforecast",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
the controller will send messages to MediatR. In the Controllers folder, let’s add an “API Controller – Empty”, with the name ProductsController.cs. We then end up with the following class:
[Route("api/products")]
[ApiController]
public class ProductsController : ControllerBase
{
}
Let’s then add a constructor that initializes a IMediatR instance:
[Route("api/products")]
[ApiController]
public class ProductsController : ControllerBase
{
private readonly IMediator _mediator;
public ProductsController(IMediator mediator) => _mediator = mediator;
}
The IMediatR interface allows us to send messages to MediatR, which then dispatches to the relevant handlers. Because we already installed the dependency injection package, the instance will be resolved automatically. ( From the MediatR version 9.0, the IMediator interface is split into two interfaces – ISender and IPublisher. So, even though we can still use the IMediator interface to send requests to a handler, if we want to be more strict about that, we can use the ISender interface instead. You don’t have to change anything else. This interface contains the Send method to send requests to the handlers. ) Of course, for the notifications, you should use the IPublisher interface that contains the Publish method:
public interface ISender
{
Task Send(IRequest request, CancellationToken cancellationToken = default);
Task
Usually, we’d want to interact with a real database. But for this article, let’s create a fake class that encapsulates this responsibility, and simply interacts with some Product values. But before we do that, we have to create a simple Product class:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
}
Just as simple as that. Now, let’s add a new FakeDataStore class, and modify it:
public class FakeDataStore
{
private static List _products;
public FakeDataStore()
{
_products = new List
{
new Product { Id = 1, Name = "Test Product 1" },
new Product { Id = 2, Name = "Test Product 2" },
new Product { Id = 3, Name = "Test Product 3" }
};
}
public async Task AddProduct(Product product)
{
_products.Add(product);
await Task.CompletedTask;
}
public async Task> GetAllProducts() => await Task.FromResult(_products);
}
Here we’re simply interacting with a static list of products, which is enough for our purposes. Let’s update ConfigureServices in Startup.cs to configure our DataStore as a singleton:
services.AddSingleton< FakeDataStore >();
Or in .NET 6, we have to update the Program class:
builder.Services.AddSingleton< FakeDataStore >();
Now that our data store is implemented, let’s set up our app for CQRS.
MediatR Requests are very simple request-response style messages, where a single request is synchronously handled by a single handler (synchronous from the request point of view, not C# internal async/await). Good use cases here would be returning something from a database or updating a database. There are two types of requests in MediatR. One that returns a value, and one that doesn’t. Often this corresponds to reads/queries (returning a value) and writes/commands (usually doesn’t return a value). We’ll use the FakeDataStore we created earlier to implement some MediatR requests. First, let’s create a request that returns all the products from our FakeDataStore. GetProductsQuery Since this is a query, let’s add a class called GetValuesQuery to the “Queries” folder, and implement it:
public record GetProductsQuery() : IRequest>;
Here, we create a record called GetProductsQuery, which implements IRequest
public class GetProductsHandler : IRequestHandler>
{
private readonly FakeDataStore _fakeDataStore;
public GetProductsHandler(FakeDataStore fakeDataStore) => _fakeDataStore = fakeDataStore;
public async Task> Handle(GetProductsQuery request,
CancellationToken cancellationToken) => await _fakeDataStore.GetAllProducts();
}
A bit is going on here, so let’s break it down a bit.
We create a class called GetProductsHandler,
which inherits from IRequestHandler
[HttpGet]
public async Task GetProducts()
{
var products = await _mediator.Send(new GetProductsQuery());
return Ok(products);
}
That’s how simple it is to send a request to MediatR. Notice we’re not taking a dependency on FakeDataStore, or have any idea on how the query is handled. This is one of the principles of the Mediator pattern, and we can see it implemented firsthand here with MediatR. Now let’s make sure everything is working as expected. First, let’s hit CTRL+F5 to build and run our app. Let’s then fire up Postman and create a new request: Get https://localhost:5001/api/roducts
To create our first “Command”, let’s add a request that takes a single product and updates our FakeDataStore. Inside our “Commands” folder, let’s add a record called AddProductCommand: public record AddProductCommand(Product Product) : IRequest; So, our record has a single Product property and inherits from the IRequest interface. Notice this time the IRequest signature doesn’t have a type parameter. This is because we aren’t returning a value. Take a note that due to the simplicity of this example we are using domain entity (Product) as the return type for our query and as a parameter for the command. In real-world apps, we wouldn’t do that, we would use DTOs to hide a domain entity from the public API. If you want to see how to use DTOs with Web API actions, you can read part 5 and part 6 articles of our .NET Core Web API series. Then, in the Handlers folder, we are going to add our handler:
public class AddProductHandler : IRequestHandler
{
private readonly FakeDataStore _fakeDataStore;
public AddProductHandler(FakeDataStore fakeDataStore) => _fakeDataStore = fakeDataStore;
public async Task Handle(AddProductCommand request, CancellationToken cancellationToken)
{
await _fakeDataStore.AddProduct(request.Product);
return Unit.Value;
}
}
We create the Handler class, which inherits from the IRequestHandler
[HttpPost]
public async Task AddProduct([FromBody]Product product)
{ {
await _mediator.Send(new AddProductCommand(product));
return StatusCode(201);
}
Again very similar to our Get method. But this time, we are setting a value on our AddProductCommand, and we don’t return a value. To test our command, let’s run our app again and add a new request to Postman:
To test it actually worked, let’s run our GetAllProducts request again:
As you can see, our POST action just returns a 201 status code. But that is not enough. There is a much better way of informing our client that this action succeeded. But to do that, we have to create GetProductById action. Of course, before we do that, we have to create a new query record:
public record GetProductByIdQuery(int Id) : IRequest;
Modify the FakeDataStore class by adding a new method:
public async Task GetProductById(int id) =>
await Task.FromResult(_products.Single(p => p.Id == id));
And create a new handler:
public class GetProductByIdHandler : IRequestHandler
{
private readonly FakeDataStore _fakeDataStore;
public GetProductByIdHandler(FakeDataStore fakeDataStore) => _fakeDataStore = fakeDataStore;
public async Task Handle(GetProductByIdQuery request, CancellationToken cancellationToken) =>
await _fakeDataStore.GetProductById(request.Id);
}
Wanna join Code Maze Team, help us produce more awesome .NET/C# content and get paid? >> JOIN US! <<
Now we can add a new action in the controller:
[HttpGet("{id:int}", Name = "GetProductById")]
public async Task
So we’ve only seen a single request being handled by a single handler.
However, what if we want to handle a single request by multiple handlers?
That’s where notifications come in. In these situations,
we usually have multiple independent operations that need to occur after some event.
Examples might be:
Sending an email
Invalidating a cache
To demonstrate this, we will update the AddProductCommand flow we created previously to publish a notification
and have it handled by two handlers.
Sending an email and invalidating a cache is out of the scope of this article,
but to demonstrate the behavior of notifications,
let’s instead simply update our fake values list to signify that something was handled.
Updating our FakeDataStore
Let’s open up our FakeDataStore and add a new method:
public async Task EventOccured(Product product, string evt)
{
_products.Single(p => p.Id == product.Id).Name = $"{product.Name} evt: {evt}";
await Task.CompletedTask;
}
Very simply, we are looking for a particular product and updating it to signify an event that occurred on it.
Now that we’ve modified our store, let’s create the notification and handlers in the next section.
Creating the Notification and Handlers
Let’s define a notification message that encapsulates the event we would like to define.
First, let’s add a new folder called Notifications.
Inside that folder, let’s add a record called ProductAddedNotification:
public record ProductAddedNotification(Product Product) : INotification;
Here, we create a class called ProductAddedNotification which implements INotification, with a single propertyProduct. This is the equivalent of IRequest we saw earlier, but for Notifications.
Now, we can create our two handlers:
public class EmailHandler : INotificationHandler