The Mediator pattern is a behavioral design pattern that aims to minimize complex dependencies among objects. This pattern limits direct communication between entities, compelling them to interact solely through a mediator object. In this blog, we’ll explore MediatR which is the implementation of the mediator pattern in C#.

What is the Mediator Pattern

The Mediator pattern is a behavioral design pattern that helps to reduce the direct connections between components by introducing a mediator object that handles the communication between them.

According to the Mediator pattern, direct communication between components that should remain independent should be discontinued. Instead, these components should communicate indirectly by invoking a dedicated mediator object. The mediator object, in turn, manages and directs the communication to the relevant components. Consequently, the components rely on a single mediator class, avoiding tight coupling with each other.

Incorporating a mediator into an application can lead to increased complexity over time. For smaller applications, employing numerous design patterns may introduce unnecessary intricacies. However, as the application expands, accumulating business logic and adhering to principles like KISS (Keep It Simple, Stupid) and DRY (Don’t Repeat Yourself) may involve making direct calls between services or managers. While dependency injection can address tight coupling, it may still result in classes becoming cumbersome with various dependencies.

  • Chat Application — An exemplary use case of the mediator design pattern is illustrated in a chat application. In this scenario, numerous participants are involved, and establishing direct connections between each participant could compromise privacy, security, and escalate the complexity of connections. A more secure and efficient alternative is to employ a central mediator class, acting as a hub where all participants can connect.
  • Air Traffic Control — An analogous real-world scenario showcasing the mediator design pattern is found in the traffic control rooms at airports. If every flight were to communicate directly with one another to coordinate arrivals, it would be chaotic. Instead, flights transmit their status to the central tower. The tower, serving as the mediator, orchestrates the signals to determine the feasibility of takeoffs or landings. It’s crucial to note that these towers don’t exert control over the entire flight but rather impose restrictions within terminal areas.

Software uses cases for Mediator Pattern

  • ESB — enterprise service bus
  • Message brokers like RabbitMQ and Kafka
  • CQRS implementation

Lot of times people hear mediator pattern used with CQRS and assume that for CQRS needs mediator pattern. That is not true you can implement CQRS without mediator pattern as well. Just using mediator pattern in CQRS reduces the coupling between command/queries and handlers. We will see CQRS and Event sourcing example in future blog.

Misconception

Components in Mediator Pattern

  • Component: Components encompass diverse classes housing distinct business logic. Each component holds a reference to a mediator, as declared through the mediator interface. The component remains unaware of the specific mediator class, allowing for reusability by associating it with a different mediator. Components should operate without knowledge of other components. If a significant event occurs within or involving a component, it must inform the mediator. Upon receiving the notification, the mediator can discern the sender’s identity, enabling it to determine which component should be activated.
  • Mediator: The Mediator interface outlines communication methods with components, primarily featuring a notification method. Components can provide any context as arguments for this method, including their objects, ensuring that no direct coupling ensues.
  • Concrete Mediator: Concrete Mediators encapsulate relationships among various components. They typically retain references to all managed components and, at times, oversee their lifecycles.

Sample Chat Application using Mediator Pattern in .Net

Below is sample of Mediator interface.

namespace MediatorPattern.Mediator
{
    public interface IMediator
    {
        public void SendMessage(string message, int chatId);

        public void RegisterChatUser(IUser user);
    }
}

Below is sample of Components, which our case are Users

namespace MediatorPattern.Components
{
    public interface IUser
    {
        int ChatId { get; }

        void SendMessage(string message);

        void RecieveMessage(string message);
    }
}

using MediatorPattern.Mediator;

namespace MediatorPattern.Components
{
    public class User : IUser
    {
        private readonly IMediator _mediator;

        private string _username;

        private int _chatId;

        public User(IMediator mediator, string username, int chatId)
        {
            _mediator = mediator;
            _username = username;
            _chatId = chatId;
        }

        public int ChatId => _chatId;

        public void RecieveMessage(string message)
        {
            Console.WriteLine($"{_username} received message: {message}");
        }

        public void SendMessage(string message)
        {
            Console.WriteLine($"{_username} sending message: {message}");
            _mediator.SendMessage(message, _chatId);
        }
    }
}

Below is sample of Concrete Mediator nothing but chat application

using MediatorPattern.Components;

namespace MediatorPattern.Mediator
{
    public class ChatMediator : IMediator
    {
        private readonly Dictionary<int, IUser> _users;

        public ChatMediator()
        {
             _users = new Dictionary<int, IUser>();
        }

        public void RegisterChatUser(IUser user)
        {
            if (!_users.ContainsKey(user.ChatId))
            {
                _users.Add(user.ChatId, user);
            }
        }

        public void SendMessage(string message, int chatId)
        {
            foreach (var user in _users.Where(x=>x.Key != chatId))
            {
                user.Value.RecieveMessage(message);
            }
        }
    }
}

Below is program for demonstrating chat application using Mediator Pattern

using MediatorPattern.Components;
using MediatorPattern.Mediator;

Console.WriteLine("Mediator Pattern Demo Chat Application");

var chatApp = new ChatMediator();

var johnDoe = new User(chatApp, "John Doe", 1);
var jackSmith = new User(chatApp, "Jack Smith", 2);
var bobMartin = new User(chatApp, "Bob Martin", 3);

chatApp.RegisterChatUser(johnDoe);
chatApp.RegisterChatUser(jackSmith);
chatApp.RegisterChatUser(bobMartin);

johnDoe.SendMessage("Hello, everyone!");
jackSmith.SendMessage("Any one got extra phone charger?");
bobMartin.SendMessage("Got one Extra Phone Charger");

Prospects and Consequences

Advantages (Pros):

  • Principle of Single Responsibility: It simplifies communication paths among multiple components by consolidating them into a single location, enhancing clarity and manageability.
  • Principle of Open/Closed: Additional mediators can be added without altering the existing components.
  • Reuse of Individual Components: It avoids sub-classing by directing actions that would otherwise be dispersed among various objects. Colleague classes can be reused without modifications.
  • Simplicity: Components can be replaced without modifying classes or interfaces.

Disadvantages (Cons):

  • Centralization of Control: While it reduces interaction complexity, a mediator may become more intricate than individual colleagues, potentially becoming a challenging-to-maintain monolith.
  • Complexity: The mediator handles all interactions among participants’ objects, leading to potential complexity issues, especially with many participants and diverse participant classes.
  • Difficult to Maintain: The centralized and complex nature of a mediator makes it challenging to maintain over time.

What is MediatR in .NET Core?

MediatR is a popular open-source library in the .NET ecosystem, specifically designed for implementing the Mediator pattern in .NET applications. MediatR is a .NET implementation of the Mediator pattern that offers support for both synchronous and asynchronous requests/responses, commands, queries, notifications, and events. It employs intelligent dispatch using C# generic variance. Simplifying the adoption of the Command Query Responsibility Segregation (CQRS) pattern, MediatR provides a straightforward approach to managing command and query handlers. Functioning as a mediator, MediatR efficiently directs commands and queries to their designated handlers.

Another scenario where the mediator pattern finds utility is in .NET controllers. Instead of injecting all managers into controllers, you can simply inject a mediator, allowing it to handle communication with other managers.

Why we need to use MediatR?

In the context of MediatR in .NET Core, here’s what it provides and why you might want to use it:

1. Mediator Pattern Implementation:
— MediatR simplifies the implementation of the Mediator pattern in your application. It allows you to define requests (commands and queries) and their corresponding handlers separately. This separation helps in organizing and maintaining your application logic.

2. Loose Coupling:
— By using MediatR, you can achieve loose coupling between different components of your application. Components communicate through the mediator, and they don’t need to have direct references to each other. This makes the codebase more maintainable and flexible.

3. Simplified Request-Response Handling:
— MediatR provides an easy way to handle requests (commands or queries) and their responses. You define request objects and corresponding handler classes, which encapsulate the logic for processing those requests. This leads to cleaner and more modular code.

4. Pipeline Behaviors:
— MediatR allows you to extend and customize the behavior of the mediator pipeline through pipeline behaviors. You can inject additional logic before or after the request is handled, such as logging, validation, or authorization.

5. Easy to Test:
— Because of the separation of concerns, it becomes easier to unit test individual handlers in isolation. You can test the logic for handling specific requests without having to set up the entire application context.

6. Consistent Architecture:
— Adopting MediatR can lead to a more consistent and organized architecture. With commands and queries clearly defined, and their handlers encapsulating the business logic, your codebase becomes easier to understand and maintain.

7. Support for Dependency Injection:
— MediatR integrates well with the dependency injection framework in .NET Core. This allows you to easily inject mediator instances and handler dependencies into your classes, promoting good dependency injection practices.

Here’s a simple example of how MediatR is typically used:

// Define a command
public class CreateUserCommand : IRequest<int>
{
    public string UserName { get; set; }
    public string Email { get; set; }
}

// Implement a handler for the command
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, int>
{
    public Task<int> Handle(CreateUserCommand request, CancellationToken cancellationToken)
    {
        // Business logic for creating a user
        // ...

        // Return the user ID or any relevant result
        return Task.FromResult(userId);
    }
}

// In your application code or controller
var createUserCommand = new CreateUserCommand { UserName = "JohnDoe", Email = "john@example.com" };
var userId = await mediator.Send(createUserCommand);

In summary, MediatR in .NET Core provides a clean and organized way to implement the Mediator pattern, leading to a more maintainable and flexible codebase, especially in applications with complex interactions and business logic.

Key Features of MediatR

  • Decoupling: MediatR facilitates the separation of the request sender (command or query) from its recipient (handler), contributing to more maintainable and modular code.
  • Pipeline Behaviors: It accommodates the incorporation of pipeline behaviors, allowing for the easy addition of cross-cutting concerns like validation, logging, and authentication.
  • Automatic Handler Discovery: MediatR possesses the capability to automatically identify and register handlers, reducing the need for explicit configuration.

Implement MediatR Example

Let’s explore the functionality of MediatR in C# by constructing a sample Customer API within an e-commerce application. In this context, we will refer to commands and queries as requests, and the corresponding classes responsible for handling them will be termed handlers.

Step 1: Create projects and install required package

For this we will create 2 projects, one is called MediatRAPI which will server Customer API. Another class library called MediatRHandlers where we configure requests and request handlers. Also install MediatR nuget package using below command.

dotnet add package MediatR

Step 2: Create Requests

Let us create two requests as shown below for creating a Customer and retrieving the Customer by Customer Id

public class Customer
    {
        public string FirstName { get; set; }

        public string LastName { get; set; }

        public string EmailAddress { get; set; }

        public string Address { get; set; }
    }

using MediatR;
using MediatRHandlers.Entities;

namespace MediatRHandlers.Requests
{
    public class CreateCustomerRequest : IRequest<int>
    {
        public Customer Customer { get; set; }
    }
}

using MediatR;
using MediatRHandlers.Entities;

namespace MediatRHandlers.Requests
{
    public class GetCustomerRequest : IRequest<Customer?>
    {
        public int CustomerId { get; set; }
    }
}

Step 3: Create Handlers

For each of the above requests create handlers as shown below.

using MediatR;
using MediatRHandlers.Repositories;
using MediatRHandlers.Requests;

namespace MediatRHandlers.RequestHandlers
{
    public class CreateCustomerHandler : IRequestHandler<CreateCustomerRequest, int>
    {
        //Inject Validators 
        private readonly ICustomerRepository _customerRepository;

        public CreateCustomerHandler(ICustomerRepository customerRepository)
        {
            _customerRepository = customerRepository;
        }

        public async Task<int> Handle(CreateCustomerRequest request, 
            CancellationToken cancellationToken)
        {
            // First validate the request
            return await _customerRepository.CreateCustomer(request.Customer);
        }
    }
}

using MediatR;
using MediatRHandlers.Entities;
using MediatRHandlers.Repositories;
using MediatRHandlers.Requests;

namespace MediatRHandlers.RequestHandlers
{
    public class GetCustomerHandler : IRequestHandler<GetCustomerRequest, Customer?>
    {
        private readonly ICustomerRepository _customerRepository;

        public GetCustomerHandler(ICustomerRepository customerRepository)
        {
            _customerRepository = customerRepository;
        }

        public async Task<Customer?> Handle(GetCustomerRequest request, CancellationToken cancellationToken)
        {
            return await _customerRepository.GetCustomer(request.CustomerId);
        }
    }
}

Step 4: Create Controller

Create a Customer Controller as shown below, if you notice we are not injecting all the handlers instead we are injecting only the mediator.

using MediatR;
using MediatRHandlers.Entities;
using MediatRHandlers.Requests;
using Microsoft.AspNetCore.Mvc;

namespace MediatRAPI.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class CustomerController : ControllerBase
    {
        private readonly IMediator _mediator;

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

        [HttpGet("customerId")]
        public async Task<Customer?> GetCustomerAsync(int customerId)
        {
            var customerDetails = await _mediator.Send(new GetCustomerRequest() { CustomerId = customerId});

            return customerDetails;
        }

        [HttpPost]
        public async Task<int> CreateCustomerAsync(Customer customer)
        {
            var customerId = await _mediator.Send(new CreateCustomerRequest() { Customer = customer});
            return customerId;
        }
    }
}

Step 5: Wire up the registrations

Register the MediatR registrations in program or start up file as shown below.

using Microsoft.Extensions.DependencyInjection;

namespace MediatRHandlers
{
    public static class MediatRDependencyHandler
    {
        public static IServiceCollection RegisterRequestHandlers(
        this IServiceCollection services)
        {
            return services
                .AddMediatR(cf => cf.RegisterServicesFromAssembly(typeof(MediatRDependencyHandler).Assembly));
        }
    }
}

using MediatRHandlers;
using MediatRHandlers.Repositories;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddScoped<ICustomerRepository, CustomerRepository>();
builder.Services.RegisterRequestHandlers();

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();



var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseAuthorization();

app.MapControllers();

app.Run();

Step 6: Run the API

Once hit run for the API project you will see Customer API swagger, where you can test create customer and get customer as shown below.

It is crucial to thoroughly evaluate the pros and cons of the Mediator pattern before incorporating it into your project. Although it can serve as a robust solution for orchestrating communication between objects in your system, it may not be the optimal choice for every scenario.

Download Source Code

$ git clone https://github.com/favtuts/dotnet-core-tutorials.git
$ cd mediator-pattern-impl

Leave a Reply

Your email address will not be published. Required fields are marked *