Technology: C# / .NET Core 3.0
Table of Contents
- 3.1 Mediator abstract class
- 3.2 Colleague abstract class
- 3.3 Concrete classes Colleague1 and Colleague2
- 3.4 ConcreteMediator
- 3.5 Entry point - Program.cs
- 4.1 Dynamic management of colleagues with Register
- 4.2 Delegation of the creation of colleagues to the Mediator
- 5.1 ChatApp Architecture
- 5.2 Chatroom abstract class (Mediator)
- 5.3 Abstract class TeamMember (Colleague)
- 5.4 TeamChatroom (ConcreteMediator)
- 5.5 Developer and Tester (Concrete Colleagues)
- 5.6 Use in Program.cs
- 5.7 Targeted Send by Type (SendTo)
- 6.1 Context and use cases
- 6.2 MarkerMediator (Concrete Mediator)
- 6.3 Marker (Colleague inheriting from Label)
- 6.4 Form1 (Mediator host)
- 7.1 Presentation of MediatR
- 7.2 Vertical slice architecture
- 7.3 ASP.NET Core Web API Project Configuration
- 7.4 ContactsContext - Entity Framework InMemory
- 7.5 Startup.cs - Services configuration
- 7.6 ContactsController - Requests, Handler and responses
1. Course Overview
This course covers the Mediator design pattern in C#. The Mediator pattern provides a way to encapsulate interactions and communications between several objects, with loose coupling, regardless of the type of application you are building.
Main topics covered
- Understand what the Mediator pattern is and why we are interested in it
- Implement your own Mediator patterns
- Several variations of the Mediator pattern
- More modern implementations of the pattern with the open source library MediatR
Prerequisites
- Be comfortable with the C# language
- Have basic experience with elementary data structures
2. The Mediator Boss Fundamental Concepts
2.1 What is Mediator boss?
The Mediator pattern encapsulates how objects interact and communicate with each other. This pattern favors weak coupling between objects in order to avoid a complex and unmanageable dependency graph. The Mediator pattern allows objects to avoid directly referencing each other, by encapsulating communication in a central mediator object.
Problem without Mediator
Imagine 10, 100, or even a million objects that each need to communicate with every other object. If each object had to reference and communicate directly with all the others, the situation would become unmanageable very quickly:
- We should keep track of all object references in all objects
- They should be kept synchronized at all times
- Extremely difficult task
Solution with Mediator
With the Mediator pattern, we create a central mediator object. This unique object has the sole responsibility of:
- Maintain references to network objects
- Relay messages or communications between these objects
We can think of the Mediator as a communication hub: all messages pass through it, and the objects only need to know about the mediator, not each other.
2.2 The four components of the pattern
The Mediator pattern typically consists of these four elements:
| Component | Role |
|---|---|
| Mediator | Abstract class that defines the communication contract between colleagues |
| ConcreteMediator | Inherits the abstract Mediator and implements concrete communication |
| Colleague | Basic abstract class representing a participating object. Only knows his mediator |
| ConcreteColleague | Concrete subclasses inheriting from abstract Colleague, with specific behavior |
Two-way relationship
The relationship between the Mediator and the Colleagues is bidirectional:
- The Mediator knows all the colleagues
- Every Colleague knows its unique Mediator
2.3 Bosses vs strict rules
“These examples are patterns, not prescribed code examples that can only have one correct implementation.”
Patterns can have many different variations. For example, we can define a class as a Mediator and another as a Colleague without these classes necessarily needing to inherit from a base abstract class. This is still a valid Mediator pattern.
One of the main advantages of the Mediator pattern is the ability to encapsulate the interaction between objects with loose coupling, so that the colleague objects do not all have to know each other.
3. Structural implementation of the Mediator pattern
This section presents the classic implementation, close to the original definition of Gang of Four (GoF), in a .NET Core 3.0 console application.
3.1 Mediator abstract class
The Mediator class defines the communication contract. It declares an abstract method Send which takes as parameters the message and the sending Colleague.
// MediatorDemo/Structural/Mediator.cs
namespace MediatorDemo.Structural
{
public abstract class Mediator
{
public abstract void Send(string message, Colleague colleague);
}
}
3.2 Colleague abstract class
The Colleague class represents a participant in the communication network. Each Colleague knows its Mediator, can send messages to it and must implement the receive method.
// MediatorDemo/Structural/Colleague.cs
namespace MediatorDemo.Structural
{
public abstract class Colleague
{
protected Mediator mediator;
// Version alternative (variation) : setter au lieu du constructeur
// Cela permet au Colleague de s'enregistrer lui-même
internal void SetMediator(Mediator mediator)
{
this.mediator = mediator;
}
// Envoyer un message au Mediator
public virtual void Send(string message)
{
this.mediator.Send(message, this);
}
// Recevoir un message (doit être implémenté par les classes concrètes)
public abstract void HandleNotification(string message);
}
}
Important points:
- The Colleague only has a reference to its Mediator (not to other Colleagues)
Senddelegates sending to MediatorHandleNotificationis the receiving point for messages- We used
SetMediatorrather than a constructor to give more flexibility
3.3 Concrete classes Colleague1 and Colleague2
Each concrete Colleague implements HandleNotification with its specific behavior.
// MediatorDemo/Structural/Colleague1.cs
namespace MediatorDemo.Structural
{
public class Colleague1 : Colleague
{
public override void HandleNotification(string message)
{
Console.WriteLine($"Colleague1 receives notification message: {message}");
}
}
}
// MediatorDemo/Structural/Colleague2.cs
namespace MediatorDemo.Structural
{
public class Colleague2 : Colleague
{
public override void HandleNotification(string message)
{
Console.WriteLine($"Colleague2 receives notification message: {message}");
}
}
}
3.4 ConcreteMediator
The ConcreteMediator inherits from the abstract Mediator and implements message routing logic. It maintains the list of all registered Colleagues.
// MediatorDemo/Structural/ConcreteMediator.cs
namespace MediatorDemo.Structural
{
public class ConcreteMediator : Mediator
{
// Liste dynamique de colleagues (version évoluée)
private List<Colleague> colleagues = new List<Colleague>();
// Enregistrement manuel d'un Colleague existant
public void Register(Colleague colleague)
{
colleague.SetMediator(this);
this.colleagues.Add(colleague);
}
// Création et enregistrement via génériques
// T doit hériter de Colleague et avoir un constructeur par défaut
public T CreateColleague<T>() where T : Colleague, new()
{
var c = new T();
c.SetMediator(this);
this.colleagues.Add(c);
return c;
}
// Routage du message : envoyer à tous sauf l'expéditeur
public override void Send(string message, Colleague colleague)
{
this.colleagues
.Where(c => c != colleague)
.ToList()
.ForEach(c => c.HandleNotification(message));
}
}
}
Routing logic: The Mediator sends the message to all Colleagues except the sender. This is the central responsibility of the Mediator: knowing where to send messages.
3.5 Entry point - Program.cs
// MediatorDemo/Program.cs
class Program
{
static void Main(string[] args)
{
// Exemple ChatApp (main demo)
var teamChat = new TeamChatroom();
var steve = new Developer("Steve");
var justin = new Developer("Justin");
var jenna = new Developer("Jenna");
var kim = new Tester("Kim");
var julia = new Tester("Julia");
teamChat.RegisterMembers(steve, justin, jenna, kim, julia);
steve.Send("Hey everyone, we're going to be deploying at 2pm today.");
kim.Send("Ok, thanks for letting us know.");
Console.WriteLine();
steve.SendTo<Developer>("Make sure to execute your unit tests before checking in!");
}
// Exemple structurel mis de côté pour référence
private static void StructuralExample()
{
var mediator = new ConcreteMediator();
// Version avec CreateColleague (la plus élégante)
var c1 = mediator.CreateColleague<Colleague1>();
var c2 = mediator.CreateColleague<Colleague2>();
c1.Send("Hello, World! (from c1)");
c2.Send("hi, there! (from c2)");
}
}
Console output of the structural example
Colleague2 receives notification message: Hello, World! (from c1)
Colleague1 receives notification message: hi, there! (from c2)
4. Variations and Alternative Considerations
4.1 Dynamic management of colleagues with Register
The initial version (canonical GoF) had explicit references to Colleague1 and Colleague2 in the ConcreteMediator. Problem: if we have 10, 50 or 100 Colleagues, we don’t want 100 properties.
Solution: Replace the explicit references with a dynamic list and a Register method.
Implementation progress:
// Avant (version canonique GoF) - références explicites
public class ConcreteMediator : Mediator
{
public Colleague1 Colleague1 { get; set; }
public Colleague2 Colleague2 { get; set; }
public override void Send(string message, Colleague colleague)
{
if (colleague == this.Colleague1)
{
this.Colleague2.HandleNotification(message);
}
else
{
this.Colleague1.HandleNotification(message);
}
}
}
// Après (version avec liste dynamique) - plus flexible
public class ConcreteMediator : Mediator
{
private List<Colleague> colleagues = new List<Colleague>();
public void Register(Colleague colleague)
{
colleague.SetMediator(this);
this.colleagues.Add(colleague);
}
public override void Send(string message, Colleague colleague)
{
this.colleagues
.Where(c => c != colleague)
.ToList()
.ForEach(c => c.HandleNotification(message));
}
}
Use with Register:
var mediator = new ConcreteMediator();
var c1 = new Colleague1();
var c2 = new Colleague2();
mediator.Register(c1);
mediator.Register(c2);
c1.Send("Hello, World!");
c2.Send("hi, there!");
Benefit: Colleagues now have control over if and when they check in with a Mediator.
4.2 Delegation of the creation of colleagues to Mediator
We can go even further by also delegating the creation of Colleagues to the Mediator using C# generics.
public T CreateColleague<T>() where T : Colleague, new()
{
var c = new T();
c.SetMediator(this);
this.colleagues.Add(c);
return c;
}
The constraint where T: Colleague, new() means:
Tshould inherit fromColleagueTmust have a default constructor (no parameters)
Usage:
var mediator = new ConcreteMediator();
var c1 = mediator.CreateColleague<Colleague1>();
var c2 = mediator.CreateColleague<Colleague2>();
Advantage: In a single line, we create the Colleague, we establish the bidirectional reference and we save it in the list. Everything is encapsulated in one place.
5. Real World Example – Team Chat App
5.1 ChatApp Architecture
This example uses a paradigm closer to the real world to illustrate the Mediator pattern:
| Boss (GoF) | Example Cat |
|---|---|
| Mediator (abstract) | Chatroom |
| ConcreteMediator | TeamChatroom |
| Colleague (abstract) | TeamMember |
| ConcreteColleague | Developer, Tester |
The Chatroom has a reference to all team members in the chat. The members of the team are the Colleagues. Developer and Tester are concrete Colleagues that inherit from TeamMember.
5.2 Chatroom abstract class (Mediator)
// MediatorDemo/ChatApp/Chatroom.cs
namespace MediatorDemo.ChatApp
{
public abstract class Chatroom
{
public abstract void Register(TeamMember member);
public abstract void Send(string from, string message);
public abstract void SendTo<T>(string from, string message) where T : TeamMember;
}
}
Three abstract methods:
Register: register a new memberSend: send a message to all membersSendTo<T>: send a message only to members of a specific type (allDevelopers, or allTesters)
5.3 Abstract class TeamMember (Colleague)
// MediatorDemo/ChatApp/TeamMember.cs
namespace MediatorDemo.ChatApp
{
public abstract class TeamMember
{
private Chatroom chatroom;
public TeamMember(string name)
{
this.Name = name;
}
public string Name { get; }
// Setter interne pour établir la référence bidirectionnelle
internal void SetChatroom(Chatroom chatroom)
{
this.chatroom = chatroom;
}
// Envoyer un message à tous via le Mediator
public void Send(string message)
{
this.chatroom.Send(this.Name, message);
}
// Recevoir un message (peut être surchargé)
public virtual void Receive(string from, string message)
{
Console.WriteLine($"from {from}: '{message}'");
}
// Envoyer un message à un sous-groupe par type
public void SendTo<T>(string message) where T : TeamMember
{
this.chatroom.SendTo<T>(this.Name, message);
}
}
}
Key points:
- The
TeamMemberonly knows hisChatroom, not the other members Senddelegates to Mediator (chatroom)Receivecan be overridden in concrete classes to customize the displaySendTo<T>allows sending to a subgroup
5.4 TeamChatroom (ConcreteMediator)
// MediatorDemo/ChatApp/TeamChatroom.cs
namespace MediatorDemo.ChatApp
{
public class TeamChatroom : Chatroom
{
private List<TeamMember> members = new List<TeamMember>();
// Enregistrement d'un membre individuel
public override void Register(TeamMember member)
{
member.SetChatroom(this); // référence bidirectionnelle
this.members.Add(member);
}
// Diffuser à tous les membres
public override void Send(string from, string message)
{
this.members.ForEach(m => m.Receive(from, message));
}
// Méthode de commodité : enregistrer plusieurs membres en une fois
public void RegisterMembers(params TeamMember[] teamMembers)
{
foreach (var member in teamMembers)
{
this.Register(member);
}
}
// Diffuser seulement aux membres du type T (Developer ou Tester)
public override void SendTo<T>(string from, string message)
{
this.members.OfType<T>().ToList().ForEach(m => m.Receive(from, message));
}
}
}
Note on OfType<T>(): This LINQ method filters the collection to only return elements of type T. This is an elegant way to target a subgroup of members.
5.5 Developer and Tester (Concrete Colleagues)
// MediatorDemo/ChatApp/Developer.cs
namespace MediatorDemo.ChatApp
{
public class Developer : TeamMember
{
public Developer(string name) : base(name)
{
}
public override void Receive(string from, string message)
{
// Affiche le nom du développeur et son rôle avant le message
Console.Write($"{this.Name} ({nameof(Developer)}) has received: ");
base.Receive(from, message);
}
}
}
// MediatorDemo/ChatApp/Tester.cs
namespace MediatorDemo.ChatApp
{
public class Tester : TeamMember
{
public Tester(string name) : base(name)
{
}
public override void Receive(string from, string message)
{
// Affiche le nom du testeur et son rôle avant le message
Console.Write($"{this.Name} ({nameof(Tester)}) has received: ");
base.Receive(from, message);
}
}
}
Note on nameof: The nameof operator returns the name of the identifier as a string (here "Developer" or "Tester"). This is preferable to a string literal because it is refactoring-safe.
5.6 Use in Program.cs
var teamChat = new TeamChatroom();
// Créer les membres de l'équipe
var steve = new Developer("Steve");
var justin = new Developer("Justin");
var jenna = new Developer("Jenna");
var kim = new Tester("Kim");
var julia = new Tester("Julia");
// Enregistrement en une ligne grâce à RegisterMembers (params)
teamChat.RegisterMembers(steve, justin, jenna, kim, julia);
// Diffusion générale : tous les membres reçoivent
steve.Send("Hey everyone, we're going to be deploying at 2pm today.");
kim.Send("Ok, thanks for letting us know.");
Console output
Steve (Developer) has received: from Steve: 'Hey everyone, we're going to be deploying at 2pm today.'
Justin (Developer) has received: from Steve: 'Hey everyone, we're going to be deploying at 2pm today.'
Jenna (Developer) has received: from Steve: 'Hey everyone, we're going to be deploying at 2pm today.'
Kim (Tester) has received: from Steve: 'Hey everyone, we're going to be deploying at 2pm today.'
Julia (Tester) has received: from Steve: 'Hey everyone, we're going to be deploying at 2pm today.'
Steve (Developer) has received: from Kim: 'Ok, thanks for letting us know.'
Justin (Developer) has received: from Kim: 'Ok, thanks for letting us know.'
Jenna (Developer) has received: from Kim: 'Ok, thanks for letting us know.'
Kim (Tester) has received: from Kim: 'Ok, thanks for letting us know.'
Julia (Tester) has received: from Kim: 'Ok, thanks for letting us know.'
5.7 Targeted sending by type (SendTo)
// Envoyer seulement aux développeurs
steve.SendTo<Developer>("Make sure to execute your unit tests before checking in!");
Console output
Steve (Developer) has received: from Steve: 'Make sure to execute your unit tests before checking in!'
Justin (Developer) has received: from Steve: 'Make sure to execute your unit tests before checking in!'
Jenna (Developer) has received: from Steve: 'Make sure to execute your unit tests before checking in!'
Only three developers receive the message. The testers (Kim and Julia) do not receive it. This is the power of OfType<T>() combined with the Mediator pattern.
6. Advanced Example Marker Positions (Windows Forms)
6.1 Context and use cases
This example demonstrates that the Mediator pattern is not limited to chat applications. This is a Windows Forms application on .NET Core 3.0 where markers (Colleagues) communicate their position in real time to each other via a Mediator.
Similar concrete use cases:
- Vehicles in a traffic system
- Airplanes in an air traffic control system
- Team of people who want to share their GPS location in real time
Behavior:
- When a marker is moved, it communicates its position in real time to all other markers
- If the distance between two markers is less than 100 units: color red (proximity)
- If the distance is 100 units or more: color green (distance)
Note: In this example, we abandon the formality of basic abstract classes to go directly to a concrete Mediator and Colleagues.
6.2 MarkerMediator (Concrete Mediator)
// MarkerPositions/MarkerMediator.cs
namespace MarkerPositions
{
public class MarkerMediator
{
private List<Marker> markers = new List<Marker>();
// Créer un marqueur et établir les références bidirectionnelles
public Marker CreateMarker()
{
var m = new Marker();
m.SetMediator(this);
this.markers.Add(m);
return m;
}
// Diffuser la position à tous les marqueurs sauf l'expéditeur
public void Send(Point location, Marker marker)
{
this.markers
.Where(m => m != marker)
.ToList()
.ForEach(m => m.ReceiveLocation(location));
}
}
}
Key points:
CreateMarker()encapsulates creation + registration (pattern seen previously)Send()receives aPoint(XY coordinates) and sender marker- Other markers receive position via
ReceiveLocation
6.3 Marker (Colleague inheriting from Label)
The Marker inherits from Label (Windows Forms) to take advantage of the existing visual control. This is a convenience for the demo.
// MarkerPositions/Marker.cs
namespace MarkerPositions
{
public class Marker : Label
{
private MarkerMediator mediator;
private Point mouseDownLocation;
internal void SetMediator(MarkerMediator mediator)
{
this.mediator = mediator;
}
public Marker()
{
this.Text = "{Drag me}";
this.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
// Abonnement aux événements de souris
this.MouseDown += OnMouseDown;
this.MouseMove += OnMouseMove;
}
// Capture la position de la souris au moment du clic
private void OnMouseDown(object sender, MouseEventArgs e)
{
if (e.Button == System.Windows.Forms.MouseButtons.Left)
{
this.mouseDownLocation = e.Location;
}
}
// Pendant le déplacement : mise à jour de la position et notification aux autres
private void OnMouseMove(object sender, MouseEventArgs e)
{
if (e.Button == System.Windows.Forms.MouseButtons.Left)
{
// Affiche les coordonnées XY dans le label
this.Text = this.Location.ToString();
// Calcul du déplacement relatif
this.Left = e.X + this.Left - this.mouseDownLocation.X;
this.Top = e.Y + this.Top - this.mouseDownLocation.Y;
// Notification au Mediator avec la nouvelle position
this.mediator.Send(this.Location, this);
}
}
// Reçoit la position d'un autre marqueur et change sa couleur selon la distance
public void ReceiveLocation(Point location)
{
var distance = CalcDistance(location);
if (distance < 100 && this.BackColor != Color.Red)
{
this.BackColor = Color.Red; // Trop proche
}
else if (distance >= 100 && this.BackColor != Color.Green)
{
this.BackColor = Color.Green; // Distance sécuritaire
}
// Fonction locale (C# 7+) pour calculer la distance euclidienne
double CalcDistance(Point point) =>
Math.Sqrt(Math.Pow(point.X - this.Location.X, 2) +
Math.Pow(point.Y - this.Location.Y, 2));
}
}
}
Important concept - Local functions (C# 7):
CalcDistance is a local function, a feature added in C# 7. It is defined inside another method and is only accessible inside that method. The formula used is the classical Euclidean distance:
distance = √((x2-x1)² + (y2-y1)²)
6.4 Form1 (Mediator host)
The Windows Forms Form hosts the Mediator and adds new markers on button click.
// MarkerPositions/Form1.cs
namespace MarkerPositions
{
public partial class Form1 : Form
{
// Le Mediator est hébergé dans la Form
private MarkerMediator mediator = new MarkerMediator();
private Button addButton;
public Form1()
{
InitializeComponent();
// Configuration du bouton "Add Marker"
this.addButton = new Button();
this.addButton.Click += OnAddClick;
this.addButton.Text = "Add Marker";
this.addButton.Dock = DockStyle.Bottom;
this.Controls.Add(this.addButton);
}
private void OnAddClick(object sender, EventArgs e)
{
// Crée un nouveau marqueur (déjà enregistré auprès du Mediator)
var m = this.mediator.CreateMarker();
// Ajoute le marqueur comme contrôle visuel sur le formulaire
this.Controls.Add(m);
}
}
}
Execution flow:
- User clicks “Add Marker”
OnAddClickcallsmediator.CreateMarker()- The Mediator creates the
Marker, establishes the bidirectional reference, adds it to its list - The
Markeris added to the form controls - User drags a marker
OnMouseMovedetects the move and callsmediator.Send(location, this)- The Mediator sends the position to all other markers
- Each marker receives
ReceiveLocation, calculates its distance and changes color
// MarkerPositions/Program.cs - Point d'entrée de l'application Windows Forms
static class Program
{
[STAThread]
static void Main()
{
Application.SetHighDpiMode(HighDpiMode.SystemAware);
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
7. The MediatR library
7.1 Presentation of MediatR
MediatR is an open source library created by Jimmy Bogard that has gained popularity in the C# ecosystem. It describes itself as: “a simple, unambitious mediator implementation in .NET”.
Statistics: Over 7 million downloads on NuGet at the time of class.
Differences with the GoF boss: MediatR has a rather different implementation of the original Gang of Four Mediator pattern. This isn’t a bad thing - it illustrates the fact that patterns can take many different forms.
Use case:
- MediatR is particularly suited to message processing (send/receive and broadcast/command)
- It ensures that messages are routed to the right places
- Think of it as an in-memory messaging system
- If building a stateful chat application, this is probably NOT the library to use
GitHub repository: https://github.com/jbogard/MediatR
7.2 Vertical slice architecture
MediatR is most commonly used in a vertical slice architecture. In this architecture, everything needed to process a request is collocated in a single place (file), rather than being distributed in horizontal layers (repository, service, controller).
Processing flow:
API Controller
→ Query (objet de requête)
→ IMediator.Send(query)
→ [MediatR sélectionne le bon Handler]
→ ContactHandler.Handle(query)
→ [Business logic / accès BD]
→ Response object
→ [MediatR retourne au Controller]
→ Réponse HTTP
7.3 ASP.NET Core Web API Project Configuration
The MediatRDemo project is an ASP.NET Core Web API application on .NET Core 3.0.
Project file (.csproj)
<!-- MediatRDemo/MediatRDemo.csproj -->
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<!-- La bibliothèque MediatR principale -->
<PackageReference Include="MediatR" Version="7.0.0" />
<!-- Extension pour l'injection de dépendances Microsoft -->
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="7.0.0" />
<!-- Entity Framework InMemory pour la démo sans vraie BD -->
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.0.0" />
</ItemGroup>
</Project>
Program.cs entry point
// MediatRDemo/Program.cs
namespace MediatRDemo
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
7.4 ContactsContext - Entity Framework InMemory
To simplify the demo, we use an in-memory database (InMemory) via Entity Framework Core. No need for a real SQL database.
// MediatRDemo/Data/ContactsContext.cs
namespace MediatRDemo.Data
{
public class ContactsContext : DbContext
{
public ContactsContext(DbContextOptions<ContactsContext> options) : base(options)
{
}
// Table Contacts
public DbSet<Contact> Contacts { get; set; }
// Données de départ (seed data)
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Contact>().HasData(
new Contact { Id = 1, FirstName = "Steve", LastName = "Michelotti" },
new Contact { Id = 2, FirstName = "Bill", LastName = "Gates" },
new Contact { Id = 3, FirstName = "Satya", LastName = "Nadella" });
}
}
// Entité Contact (POCO)
public class Contact
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
}
7.5 Startup.cs - Configuring Services
// MediatRDemo/Startup.cs
namespace MediatRDemo
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// Enregistrement de MediatR - scanne l'assembly de Startup pour trouver les Handlers
services.AddMediatR(typeof(Startup));
// Configuration de la BD InMemory pour Entity Framework
services.AddDbContext<ContactsContext>(options =>
options.UseInMemoryDatabase(databaseName: "ContactsDB"));
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
// Initialisation des données de départ au démarrage
using (var serviceScope = app.ApplicationServices.CreateScope())
{
var context = serviceScope.ServiceProvider.GetService<ContactsContext>();
context.Database.EnsureCreated();
}
}
}
}
Key point: services.AddMediatR(typeof(Startup)) registers MediatR and scans the assembly to automatically find all Handlers (IRequestHandler).
7.6 ContactsController - Requests, Handler and Responses
This is the heart of the MediatR demo. Everything is collocated in a single file thanks to the vertical slice architecture.
// MediatRDemo/Controllers/ContactsController.cs
namespace MediatRDemo.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ContactsController : ControllerBase
{
private IMediator mediator;
// Injection de dépendances : IMediator fourni par MediatR
public ContactsController(IMediator mediator) => this.mediator = mediator;
// GET api/contacts/{id}
// Expression-bodied member : retourne directement le résultat du mediator
[HttpGet("{id}")]
public async Task<Contact> GetContact([FromRoute] Query query) =>
await this.mediator.Send(query);
#region Nested Classes (Architecture Vertical Slice)
// L'objet de requête - ce qui entre dans le pipeline MediatR
// IRequest<Contact> indique que ce message retournera un Contact
public class Query : IRequest<Contact>
{
public int Id { get; set; }
}
// Le Handler - gère la requête Query et retourne un Contact
// IRequestHandler<TRequest, TResponse>
public class ContactHandler : IRequestHandler<Query, Contact>
{
private ContactsContext db;
// Injection de dépendances dans le Handler
public ContactHandler(ContactsContext db) => this.db = db;
// Logique métier : cherche le contact par Id en BD
public Task<Contact> Handle(Query request, CancellationToken cancellationToken)
{
return this.db.Contacts
.Where(c => c.Id == request.Id)
.SingleOrDefaultAsync();
}
}
#endregion
}
}
Decomposition of the MediatR flow
- HTTP request:
GET /api/contacts/1 - Controller: Creates a
Query { Id = 1 }object (automatically populated from the route) this.mediator.Send(query): Passes the query to MediatR- MediatR: Finds the right handler (
ContactHandler) based on the type ofQuery ContactHandler.Handle(): Queries the database, returns theContact- JSON response: The
Contactis serialized and returned to the client
Testing with Postman
GET https://localhost:XXXX/api/contacts/1
→ { "id": 1, "firstName": "Steve", "lastName": "Michelotti" }
GET https://localhost:XXXX/api/contacts/2
→ { "id": 2, "firstName": "Bill", "lastName": "Gates" }
Advantage of vertical slice architecture
Instead of looking for a method in a large Repository class, everything we need to process this request is present in the same file. This improves readability, maintainability, and code navigation.
8. Summary and conclusion
What we covered
This course covered many aspects of the Mediator pattern:
-
Conceptual overview: what the Mediator pattern is, why we use it, how it encapsulates the interaction between objects
-
Basic structural example: canonical implementation close to the Gang of Four to solidify the concepts
- Abstract class
Mediatorwith methodSend - Abstract class
ColleaguewithSetMediator,SendandHandleNotification ConcreteMediatorwith dynamic Colleague list and message routing- Variations:
RegisterandCreateColleague<T>
- Team chat application: example closer to the real world
Chatroom/TeamChatroomlike MediatorTeamMember/Developer/Testerlike ColleaguesSendTo<T>()to target subgroups by type
- Windows Forms application - Marker positions: advanced example showing that the pattern applies to other areas
- Markers that communicate their position in real time
- Color proximity indicator (red/green)
- C# 7 Local Functions
- MediatR Library: modern and popular implementation of the Mediator concept
- Vertical slice architecture
- Pipeline
IRequest/IRequestHandler - Integration with ASP.NET Core and Entity Framework
Key message of the course
“When thinking about the Mediator pattern, think about encapsulating object interaction between multiple Colleagues objects, doing this with loose coupling so that the Colleagues objects do not all have to know about each other.”
Summary table of components
| Component | Role | Example Cat | Example Markers | MediatR example |
|---|---|---|---|---|
| Mediator | Communication contract | Chatroom | - | IMediator |
| ConcreteMediator | Routing Implementation | TeamChatroom | MarkerMediator | MediatR (internal) |
| Colleague | Network participant | TeamMember | Marker | - |
| ConcreteColleague | Specific behavior | Developer, Tester | - | ContactHandler |
| Message | Data exchanged | string message | Point location | Query, Contact |
Important principles to remember
- Patterns are guides, not rules: multiple valid implementations coexist
- Weak coupling: the Colleagues do not know each other directly, they only know their Mediator
- Sole responsibility: the Mediator has sole responsibility for maintaining referrals and routing messages
- Flexibility: the dynamic list + generics allow you to add/remove Colleagues without modifying the Mediator
- Scalability: with MediatR, each new feature is a new independent Handler
Resources to go further
- MediatR on GitHub: https://github.com/jbogard/MediatR
- MediatR on NuGet: Search for
MediatRandMediatR.Extensions.Microsoft.DependencyInjection - Vertical slices architecture: Jimmy Bogard’s concept for organizing code by functionality rather than by technical layer
- Gang of Four Design Patterns: Original book defining classic patterns including the Mediator
- Entity Framework course on Pluralsight: To deepen the data persistence part
Search Terms
c-sharp · design · patterns · mediator · testing · architecture · c# · .net · development · abstract · class · colleague · colleagues · concrete · console · mediatr · output · program.cs · boss · components · concretemediator · covered · entry · marker