In this section, we are exploring MediatR, an open-source mediator implementation.What is MediatR? Let’s start with its maker’s description from its GitHub repository, which brands it as this:

“Simple, unambitious mediator implementation in .NET”

MediatR is a simple but very powerful tool doing in-process communication through messaging. It supports a request/response flow through commands, queries, notifications, and events, synchronously and asynchronously.We can install the NuGet package using the .NET CLI: dotnet add package MediatR.Now that I have quickly introduced the tool, we are going to explore the migration of our Clean Architecture sample but instead use MediatR to dispatch the StocksController requests to the core use cases. We use a similar pattern with MediatR than what we built in the CQS project.

Why migrate our Clean Architecture sample? The primary reason we are building the same project using different models is for ease of comparison. It is much easier to compare the changes of the same features than if we were building completely different projects.

What are the advantages of using MediatR in this case? It allows us to organize the code around use cases (vertically) instead of services (horizontally), leading to more cohesive features. We remove the service layer (the StockService class) and replace it with multiple use cases (features) instead (the AddStocks and RemoveStock classes). MediatR also enables a pipeline we can extend by programming behaviors. Those extensibility points allow us to manage cross-cutting concerns, such as requests validation centrally, without impacting the consumers and use cases. We explore request validation in Chapter 17, Getting Started with Vertical Slice Architecture.Let’s jump into the code now to see how it works.

Project – Clean Architecture with MediatR

Context: We want to break some more of the coupling in the Clean Architecture project we built in Chapter 14, Understanding Layering, by leveraging the Mediator pattern and a CQS approach.The clean architecture solution was already solid, but MediatR will pave the way to more good things later. The only “major” change is the replacement of the StockService with two feature objects, AddStocks and RemoveStocks, which we explore soon.First, we must install the MediatR NuGet package in the Core project, where the features will live. Moreover, it will transiently cascade to the Web project, allowing us to register MediatR with the IoC container. In the Program.cs file, we can register MediatR like this:

builder.Services
    // Core Layer
    .AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<NotEnoughStockException>())
;

That code scans the Core assembly for MediatR-compatible pieces and registers them with the IoC Container. The NotEnoughStockException class is part of the core project.

I picked the NotEnoughStockException class but could have chosen any class from the Core assembly. There are more registration options.

MediatR exposes the following types of messages (as of version 12):

  • Request/response that has one handler; perfect for commands and queries.
  • Notifications that support multiple handlers; perfect for an event-based model applying the Publish-Subscribe pattern where a notification represents an event.
  • Request/response streams that are similar to request/response but stream the response through the IAsyncEnumerable<T> interface.

We cover the Publish-Subscribe pattern in Chapter 19, Introduction to Microservices Architecture.

Now that everything we need related to MediatR is “magically” registered, we can look at the use cases that replace the StockService. Let’s have a look at the updated AddStocks code first:

namespace Core.UseCases;
public class AddStocks
{
    public class Command : IRequest<int>
    {
        public int ProductId { get; set; }
        public int Amount { get; set; }
    }
    public class Handler : IRequestHandler<Command, int>
    {
        private readonly IProductRepository _productRepository;
        public Handler(IProductRepository productRepository)
        {
            _productRepository = productRepository ??
throw new ArgumentNullException(nameof(productRepository));
        }
        public async Task<int> Handle(Command request, CancellationToken cancellationToken)
        {
            var product = await _productRepository.FindByIdAsync(request.ProductId, cancellationToken);
            if (product == null)
            {
                throw new ProductNotFoundException(request.ProductId);
            }
            product.AddStock(request.Amount);
            await _productRepository.UpdateAsync(product, cancellationToken);
            return product.QuantityInStock;
        }
    }
}

Since we covered both use cases in the previous chapters and the changes are very similar, we will analyze both together, after the RemoveStocks use case code:

namespace Core.UseCases;
public class RemoveStocks
{
    public class Command : IRequest<int>
    {
        public int ProductId { get; set; }
        public int Amount { get; set; }
    }
    public class Handler : IRequestHandler<Command, int>
    {
        private readonly IProductRepository _productRepository;
        public Handler(IProductRepository productRepository)
        {
            _productRepository = productRepository ??
throw new ArgumentNullException(nameof(productRepository));
        }
        public async Task<int> Handle(Command request, CancellationToken cancellationToken)
        {
            var product = await _productRepository.FindByIdAsync(request.ProductId, cancellationToken);
            if (product == null)
            {
                throw new ProductNotFoundException(request.ProductId);
            }
            product.RemoveStock(request.Amount);
            await _productRepository.UpdateAsync(product, cancellationToken);
            return product.QuantityInStock;
        }
    }
}