Advanced .NET Core with MediatR Pattern

Introduction

The MediatR library in .NET Core is a powerful tool for implementing the mediator pattern, which helps in decoupling the sending and handling of requests. While the basic usage of MediatR is straightforward, its real power comes into play in more advanced scenarios. This article explores advanced techniques for using MediatR in .NET Core applications, focusing on command/query separation, pipeline behaviors, and notifications.

Why Use MediatR?

MediatR helps to maintain a clean architecture by,

  1. Decoupling request handling from the core application logic.
  2. Promoting the single responsibility principle.
  3. Making it easier to extend and maintain the application.

Setting Up MediatR in .NET Core

Let's start by setting up a .NET Core application with MediatR.

Step 1. Create a .NET Core Project

Create a new .NET Core web API project.

dotnet new webapi -n AdvancedMediatRDemo
cd AdvancedMediatRDemo

Step 2. Install MediatR and MediatR.Extensions.Microsoft.DependencyInjection

dotnet add package MediatR
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection

Step 3. Configure MediatR in `Startup.cs`

Add MediatR services in the `ConfigureServices` method.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddMediatR(typeof(Startup));
}

Advanced MediatR Usage
 

1. Command/Query Separation

Implementing the Command Query Responsibility Segregation (CQRS) pattern helps in separating read and write operations.

Commands

Create a command to handle write operations.

public class CreateOrderCommand : IRequest<Order>
{
    public string ProductName { get; set; }
    public int Quantity { get; set; }
}
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Order>
{
    public Task<Order> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
    {
        var order = new Order { ProductName = request.ProductName, Quantity = request.Quantity };
        // Save order to the database
        return Task.FromResult(order);
    }
}

Queries

Create a query to handle read operations.

public class GetOrderByIdQuery : IRequest<Order>
{
    public int Id { get; set; }
}

public class GetOrderByIdHandler : IRequestHandler<GetOrderByIdQuery, Order>
{
    public Task<Order> Handle(GetOrderByIdQuery request, CancellationToken cancellationToken)
    {
        var order = // Fetch order from the database by request.Id
        return Task.FromResult(order);
    }
}

2. Pipeline Behaviors

Pipeline behaviors allow you to add cross-cutting concerns like logging, validation, and caching.

Create a logging behavior

public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
    public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
    {
        _logger = logger;
    }

    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
    {
        _logger.LogInformation($"Handling {typeof(TRequest).Name}");
        var response = await next();
        _logger.LogInformation($"Handled {typeof(TResponse).Name}");
        return response;
    }
}

Register the behavior in `Startup.cs`

services.AddTransient(
  typeof(IPipelineBehavior<,>),
  typeof(LoggingBehavior<,>)
);

3. Notifications

Notifications in MediatR allow multiple handlers to respond to the same event.

Create a notification

public class OrderCreatedNotification : INotification
{
    public Order Order { get; set; }
}
public class SendEmailHandler : INotificationHandler<OrderCreatedNotification>
{
    public Task Handle(OrderCreatedNotification notification, CancellationToken cancellationToken)
    {
        // Send email
        return Task.CompletedTask;
    }
}
public class UpdateInventoryHandler : INotificationHandler<OrderCreatedNotification>
{
    public Task Handle(OrderCreatedNotification notification, CancellationToken cancellationToken)
    {
        // Update inventory
        return Task.CompletedTask;
    }
}

Publish the notification in the `CreateOrderHandler`

public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Order>
{
    private readonly IMediator _mediator;
    public CreateOrderHandler(IMediator mediator)
    {
        _mediator = mediator;
    }
    public async Task<Order> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
    {
        var order = new Order { ProductName = request.ProductName, Quantity = request.Quantity };
        // Save order to the database
        await _mediator.Publish(new OrderCreatedNotification { Order = order });
        return order;
    }
}

Conclusion

The MediatR library in .NET Core offers a powerful and flexible way to implement the mediator pattern, especially when dealing with complex scenarios. By separating commands and queries, using pipeline behaviors for cross-cutting concerns, and utilizing notifications for event-driven architectures, you can build robust and maintainable applications.