Cutting Through Component Spaghetti
If you've ever had a UI component that talks directly to three other components, which each talk to two more, you know how quickly interactions become tangled. Adding a feature means updating every component in the chain because they all know about each other.
The Mediator pattern centralizes communication. Instead of components referencing each other directly, they send messages through a mediator. The mediator handles routing and coordination. Components stay loosely coupled because they only depend on the mediator interface, not on each other.
You'll build a chat room where users send messages without knowing about other users. The mediator manages who receives what. By the end, you'll know when centralized coordination beats direct component interaction.
Components Using the Mediator
Each component holds a reference to the mediator but not to other components. When a component needs to communicate, it tells the mediator. The mediator decides what happens next.
namespace MediatorDemo;
public class User
{
private readonly IChatMediator _mediator;
public string Name { get; }
public User(string name, IChatMediator mediator)
{
Name = name;
_mediator = mediator;
_mediator.RegisterUser(this);
}
public void Send(string message)
{
Console.WriteLine($"{Name} sends: {message}");
_mediator.SendMessage(message, this);
}
public void ReceiveMessage(string message, string senderName)
{
Console.WriteLine($"{Name} received from {senderName}: {message}");
}
}
Users send messages through the mediator. They don't call other users directly. This decoupling means you can add users, change routing rules, or introduce filters without touching the User class.
Try It Yourself
Build a simple mediator that coordinates button clicks in a dialog. This shows how mediators manage interactions between UI components without direct coupling.
Steps
- Init:
dotnet new console -n MediatorLab
- Enter:
cd MediatorLab
- Edit Program.cs
- Configure .csproj
- Start:
dotnet run
var dialog = new DialogMediator();
var okBtn = new Button("OK", dialog);
var cancelBtn = new Button("Cancel", dialog);
okBtn.Click();
cancelBtn.Click();
interface IDialogMediator
{
void Notify(string sender, string ev);
}
class DialogMediator : IDialogMediator
{
public void Notify(string sender, string ev)
{
if (sender == "OK" && ev == "click")
Console.WriteLine("Mediator: Validating and closing dialog");
else if (sender == "Cancel" && ev == "click")
Console.WriteLine("Mediator: Discarding changes and closing");
}
}
class Button
{
private readonly string _name;
private readonly IDialogMediator _mediator;
public Button(string name, IDialogMediator mediator)
{
_name = name;
_mediator = mediator;
}
public void Click() => _mediator.Notify(_name, "click");
}
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Output
Mediator: Validating and closing dialog
Mediator: Discarding changes and closing
Real-World Usage
In ASP.NET Core applications, libraries like MediatR provide a mediator implementation with built-in dependency injection support. You define request and handler classes, and MediatR routes requests to the appropriate handlers. This pattern works particularly well for CQRS, where commands and queries flow through a central pipeline.
Register MediatR in your DI container and inject IMediator into controllers or services. Define requests as simple classes and create corresponding handler classes. MediatR discovers handlers automatically and provides pipeline behaviors for cross-cutting concerns like logging, validation, and transactions.
For observability, add logging in the mediator or use pipeline behaviors that wrap handlers. Log when requests start and finish, along with their types and execution times. Use structured logging with request properties as log context. Add counters for request throughput and timers for handler duration to track performance trends over time.