Implement the Mediator Pattern in a .NET Web API

Introduction

Sometimes one object/component needs to communicate with another object/component within an application. If the components are minimal, there are no issues. However, in real-time applications, there are many components, and each component has to communicate with other components. The complexity increases with the number of components in the application. In this article, we will explore how we can solve this problem using the Mediator pattern and how to implement it in a .NET Web API.

The Real-Time Problem

Let's understand the real-time problem using the following example.

We have four components.

  • Component A
  • Component B
  • Component C
  • Component D

Component

These components need to communicate with each other. For example, if Component A wants to communicate with Component B, Component A must have a reference to Component B and use that reference to call Component B's methods. Similarly, if any component wants to send a message to another component, it must know the reference of the other component and use it to call its methods.

The objects are tightly coupled, meaning many objects know each other. In the example, we have only four objects. However, in real-world applications, you might have hundreds of objects that need to communicate with each other. It will be very difficult and will increase the complexity of the application.

How to Reduce the Coupling Between the Components Using Mediator Pattern?

A mediator design pattern is a behavioral design pattern that restricts the direct communication between entities and forces them to interact through a mediator object. It reduces the communication complexity between the components.

In the Mediator pattern, direct communication between components is restricted. Instead, all components communicate indirectly through a dedicated mediator object. The Mediator object serves as the communication hub for all components. Each component interacts with the mediator object, which then routes the messages to the appropriate destination component.

Let's consider the above example and re-design the application using the Mediator pattern as described below.

Mediator pattern

Real-world use cases for Mediator Pattern

Chat Application is a perfect example of the Mediator design pattern. In this application, there are users and groups. Users can join groups and share their messages within the group. When 'Person A' shares a message in the chat group, it is sent to all members who have joined the group. In this scenario, the chat group acts as the Mediator.

Components involved in Mediator Pattern

  • Component: Components are various classes with their own business logic. Each component references a mediator, as defined by the mediator interface.
  • Mediator: The Mediator interface defines methods for communication with components, mainly featuring a notification method.
  • Concrete Mediator: Concrete Mediators manage the interactions between various components.
namespace MediatorDesignPattern
{
    public interface IMediator
    {
        // This Method is used to send the Message to users who are registered with the group
        void SendMessage(string message, IUser user);       
        // This method is used to register a user with the group
        void RegisterChatUser(IUser user); 
    }
}
namespace MediatorDesignPattern
{
    public interface IUser
    {
        int UserId { get; }
        void SendMessage(string message);
        void RecieveMessage(string message);
    }
}
namespace MediatorDesignPattern
{
    public class ChatUser : IUser
    {
        private readonly IMediator _mediator;
        private readonly string _username;
        private readonly int _userId;
        public int UserId => _userId;
        public ChatUser(IMediator mediator, string username, int userId)
        {
            _mediator = mediator;
            _username = username;
            _userId = userId;
        }
        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, this);
        }
    }
}
namespace MediatorDesignPattern
{
    public class ChatMediator : IMediator
    {
        private readonly Dictionary<int, IUser> UsersList; // List of users to whom we want to communicate
        public ChatMediator()
        {
            UsersList = new Dictionary<int, IUser>();
        }
        // To register the user within the group
        public void RegisterChatUser(IUser user)
        {
            if (!UsersList.ContainsKey(user.UserId))
            {
                UsersList.Add(user.UserId, user);
            }
        }
        // To send the message in the group
        public void SendMessage(string message, IUser chatUser)
        {
            foreach (var user in UsersList.Where(u => u.Key != chatUser.UserId))
            {
                user.Value.RecieveMessage(message);
            }
        }
    }
}
namespace MediatorDesignPattern
{
    class Program
    {
        static void Main(string[] args)
        {
             var chatGroup = new ChatMediator();
             var PersonA = new User(chatGroup, "Person A", 1);
             var PersonB = new User(chatGroup, "Person B", 2);
             var PersonC = new User(chatGroup, "Person C", 3);
             chatGroup.RegisterChatUser(PersonA);
             chatGroup.RegisterChatUser(PersonB);
             chatGroup.RegisterChatUser(PersonC);
             PersonA.SendMessage("Any one is available for trip this week?");
             PersonB.SendMessage("I am in");
             PersonC.SendMessage("I am not available");
             Console.Read();
        }
    }
}

Output

Output

How to Implement Mediator Pattern in .NET Web API?

MediatR is a widely used library that simplifies the implementation of the Mediator pattern in .NET applications.

Step 1. Install the MediatR package.

You can do this through the NuGet Package Manager or by running the following command in the Package Manager Console.

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

Step 2. Define the Request and Response Models.

public class SendMessageCommand : IRequest<string>
{
    public string Sender { get; set; }
    public string Message { get; set; }
}

This class represents the request model for sending a message. It implements the IRequest<string> interface from MediatR, indicating that it expects a response of type string.

Step 3. Create the Handler.

public class SendMessageHandler : IRequestHandler<SendMessageCommand, string>
{
    public Task<string> Handle(SendMessageCommand request, CancellationToken cancellationToken)
    {
        // Logic to handle the message (e.g., routing it to other components)
        string response = $"[{request.Sender}]: {request.Message}";
        return Task.FromResult(response);
    }
}

This class handles the processing of the SendMessageCommand. It implements the IRequestHandler<SendMessageCommand, string> interface, which indicates it handles SendMessageCommand requests and returns a string response. The Handle method processes the request and returns a formatted string containing the sender and the message.

Step 4. Configure MediatR in the Startup.

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

In the Startup class, MediatR is added to the service collection using services.AddMediatR(typeof(Startup).Assembly). This registers all MediatR handlers found in the specified assembly.

Step 5. Send a Message Using MediatR.

public class ChatController : ControllerBase
{
    private readonly IMediator _mediator;
    public ChatController(IMediator mediator)
    {
        _mediator = mediator;
    }
    [HttpPost("send")]
    public async Task<IActionResult> SendMessage([FromBody] SendMessageCommand command)
    {
        string result = await _mediator.Send(command);
        return Ok(result);
    }
}

This controller handles incoming HTTP requests for sending messages. It injects an instance of IMediator via its constructor. The SendMessage action method receives a SendMessageCommand object as its parameter, sends the command using MediatR, and returns the result as an HTTP response.

Flow

  • When a POST request is made to the sending endpoint of the ChatController, the SendMessageCommand is created with the sender's name and message content.
  • The SendMessageCommand is sent to MediatR using _mediator.Send(command).
  • MediatR locates the appropriate handler (SendMessageHandler) for the command.
  • The SendMessageHandler processes the command by formatting the sender's name and message into a string and returns this string as the response.
  • The ChatController returns the formatted string as the HTTP response.

This approach centralizes communication through MediatR, reducing direct dependencies between components and making the code more modular and easier to maintain.

Summary

In this article, you have learned the following topics.

  • What is the Mediator pattern?
  • How to implement the Mediator pattern?
  • How to implement a Mediator pattern using the MediatR package in .NET Web API?