C# tutorials > Frameworks and Libraries > Other Important Libraries > MediatR for in-process messaging (CQRS)

MediatR for in-process messaging (CQRS)

MediatR is a simple and powerful .NET library that implements the Mediator pattern. It allows you to decouple request handling from the actual request processing. It's commonly used to implement CQRS (Command Query Responsibility Segregation) within a single application, making your code more maintainable, testable, and extensible. This tutorial will guide you through using MediatR in C#.

What is MediatR?

MediatR is an in-process messaging system. It lets components within your application communicate without needing direct references to each other. It achieves this through a mediator that dispatches commands or queries to their respective handlers. Think of it as a traffic controller for your application's internal communications.

Installation

First, you need to install the MediatR NuGet packages. Open your Package Manager Console and run these commands.

The first package, MediatR, provides the core functionality. The second package, MediatR.Extensions.Microsoft.DependencyInjection, helps to integrate MediatR with the .NET dependency injection container, which is crucial for managing your handlers.

Install-Package MediatR
Install-Package MediatR.Extensions.Microsoft.DependencyInjection

Defining a Request (Command)

A request represents an action you want to perform, typically a command or a query. Here, we define a command to create a product. It implements the IRequest<TResponse> interface, where TResponse is the type of the result the handler will return. In this case, we expect the new product's ID (a Guid) to be returned.

public class CreateProductCommand : IRequest<Guid>
{
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
}

Defining a Request (Query)

Similarly, a query retrieves data. Here, we define a query to get a product by its ID. It also implements IRequest<TResponse>, but this time, TResponse is the Product type. This query expects a Product object to be returned.

public class GetProductByIdQuery : IRequest<Product>
{
    public Guid Id { get; set; }
}

Defining a Handler (Command)

A handler is responsible for processing a specific request. The CreateProductCommandHandler handles the CreateProductCommand. It implements the IRequestHandler<TRequest, TResponse> interface, where TRequest is the type of the request it handles and TResponse is the type of the response it returns. This handler depends on an IProductRepository to persist the new product.

public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, Guid>
{
    private readonly IProductRepository _productRepository;

    public CreateProductCommandHandler(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }

    public async Task<Guid> Handle(CreateProductCommand request, CancellationToken cancellationToken)
    {
        var product = new Product
        {
            Id = Guid.NewGuid(),
            Name = request.Name,
            Description = request.Description,
            Price = request.Price
        };

        await _productRepository.Add(product);
        return product.Id;
    }
}

Defining a Handler (Query)

The GetProductByIdQueryHandler handles the GetProductByIdQuery. It uses the IProductRepository to retrieve the product based on the provided ID.

public class GetProductByIdQueryHandler : IRequestHandler<GetProductByIdQuery, Product>
{
    private readonly IProductRepository _productRepository;

    public GetProductByIdQueryHandler(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }

    public async Task<Product> Handle(GetProductByIdQuery request, CancellationToken cancellationToken)
    {
        return await _productRepository.GetById(request.Id);
    }
}

Registering MediatR and Handlers with Dependency Injection

You need to register MediatR and your handlers with the dependency injection container. The AddMediatR extension method registers all handlers in the specified assembly (the assembly where your commands/queries are defined). You also need to register any dependencies your handlers need, such as the IProductRepository.

using Microsoft.Extensions.DependencyInjection;

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddMediatRServices(this IServiceCollection services)
    {
        services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(CreateProductCommand).Assembly));
        services.AddScoped<IProductRepository, ProductRepository>(); // Assuming you have a concrete ProductRepository
        return services;
    }
}

Using MediatR to Send Requests

To use MediatR, inject the IMediator interface into your controller (or other component). Use the Send method to send a command or a query to its handler. The Send method will automatically find the correct handler based on the type of the request and return the result.

using MediatR;

public class ProductController : ControllerBase
{
    private readonly IMediator _mediator;

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

    [HttpPost]
    public async Task<IActionResult> CreateProduct([FromBody] CreateProductCommand command)
    {
        var productId = await _mediator.Send(command);
        return CreatedAtAction(nameof(GetProduct), new { id = productId }, productId);
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetProduct(Guid id)
    {
        var product = await _mediator.Send(new GetProductByIdQuery { Id = id });
        if (product == null)
        {
            return NotFound();
        }
        return Ok(product);
    }
}

Concepts Behind the Snippet

This snippet demonstrates the core concepts of MediatR: decoupling request handling using commands and queries, the Mediator pattern, and leveraging dependency injection. By separating the request definition from its execution, you gain flexibility and maintainability.

Real-Life Use Case Section

Imagine an e-commerce application. When a user places an order, several things need to happen: the order needs to be saved to the database, inventory needs to be updated, and notifications need to be sent. Using MediatR, you can define a PlaceOrderCommand. Multiple handlers can then subscribe to this command: one handler saves the order, another updates inventory, and another sends notifications. The controller (or service) only needs to send the PlaceOrderCommand; it doesn't need to know about the specific actions that need to be performed.

Best Practices

  • Keep your requests and handlers focused on a single responsibility.
  • Use asynchronous operations (async/await) for I/O-bound operations to avoid blocking the thread.
  • Handle exceptions gracefully within your handlers.
  • Use dependency injection to manage dependencies and make your handlers testable.
  • Consider using validators to validate your requests before they are handled.

Interview Tip

When discussing MediatR in an interview, emphasize its role in decoupling components and promoting separation of concerns. Be prepared to explain the Mediator pattern and how it relates to CQRS. Also, be prepared to discuss the benefits of using MediatR, such as improved testability and maintainability. Be able to explain how to define a request and its handler.

When to Use Them

Use MediatR when you have complex business logic that involves multiple steps or when you want to decouple your application's components. It's particularly useful in CQRS architectures where you want to separate read and write operations. Avoid using MediatR for simple operations where direct method calls are sufficient.

Memory Footprint

MediatR itself has a relatively small memory footprint. The primary concern regarding memory would be the objects passed as requests and the data handled within the handlers. Be mindful of the size of data you're passing around, especially in high-throughput scenarios. Avoid large data structures in your requests unless absolutely necessary.

Alternatives

Alternatives to MediatR include:

  • Direct Method Calls: Simple, but can lead to tight coupling.
  • Event Aggregators: Similar to MediatR, but often used for notifications rather than commands/queries.
  • Message Queues (e.g., RabbitMQ, Azure Service Bus): For distributed systems where components are running in different processes or machines.

Pros

  • Decoupling: Reduces dependencies between components, making code more modular.
  • Testability: Handlers can be easily tested in isolation.
  • Maintainability: Easier to modify and extend code without affecting other parts of the application.
  • CQRS Implementation: Simplifies the implementation of CQRS patterns.

Cons

  • Increased Complexity: Adds a layer of abstraction, which can make the codebase more complex, especially for simple scenarios.
  • Performance Overhead: A slight performance overhead due to the indirection of the Mediator pattern (though often negligible).
  • Learning Curve: Requires developers to understand the Mediator pattern and CQRS concepts.

FAQ

  • What is the difference between MediatR and a message queue?

    MediatR is an in-process messaging system, meaning the communication happens within the same application process. Message queues (like RabbitMQ or Azure Service Bus) are for inter-process or inter-service communication, where the sender and receiver might be running on different machines. MediatR is typically used for internal communication within a single application, while message queues are used for distributed systems.

  • Can I use MediatR without dependency injection?

    While technically possible, it's highly recommended to use MediatR with dependency injection. It allows you to easily manage your handlers and their dependencies, making your code more testable and maintainable. Without DI, you'd need to manually create and manage your handlers, which would negate many of the benefits of using MediatR.

  • How do I handle exceptions in MediatR?

    You should handle exceptions within your handlers. You can use try-catch blocks to catch specific exceptions and handle them appropriately. You can also implement a custom pipeline behavior to handle exceptions globally, such as logging or retrying operations. For example:

    public async Task<Guid> Handle(CreateProductCommand request, CancellationToken cancellationToken)
    {
    try
    {
        // Your logic here
    }
    catch (Exception ex)
    {
        // Log the exception
        // Handle the error
        throw;
    }
    }