Understanding the Mediator Pattern in .NET

Why Do We Need the Mediator Pattern?

In complex applications, components often need to communicate with each other. Traditionally, this can be achieved by having components directly interact with one another. However, this approach can lead to a tightly coupled system where changes in one component might necessitate changes in others, leading to a maintenance nightmare. This is where the Mediator pattern comes into play.

The Mediator pattern promotes loose coupling by ensuring that instead of components communicating directly, they communicate through a mediator. This mediator handles the communication logic, resulting in a more decoupled and maintainable system.

Image description

Example Without Using MediatR

Let’s start with an example without using MediatR to illustrate the complexity and tight coupling.

Original Implementation

ComponentB.cs

public class ComponentB
{ public void SomeOperation() { // Original operation Console.WriteLine("ComponentB: Performing some operation."); }
}
Enter fullscreen mode
Exit fullscreen mode

ComponentC.cs

public class ComponentC
{ public void AnotherOperation() { // Original operation Console.WriteLine("ComponentC: Performing another operation."); }
}
Enter fullscreen mode
Exit fullscreen mode

ComponentA.cs

public class ComponentA
{ private readonly ComponentB _componentB; private readonly ComponentC _componentC; public ComponentA(ComponentB componentB, ComponentC componentC) { _componentB = componentB; _componentC = componentC; } public void Operation() { // ComponentA communicates directly with ComponentB and ComponentC _componentB.SomeOperation(); _componentC.AnotherOperation(); }
}
Enter fullscreen mode
Exit fullscreen mode

Program.cs

public class Program
{ public static void Main(string[] args) { var componentB = new ComponentB(); var componentC = new ComponentC(); var componentA = new ComponentA(componentB, componentC); componentA.Operation(); }
}
Enter fullscreen mode
Exit fullscreen mode

When you run this code, the output will be:

ComponentB: Performing some operation.
ComponentC: Performing another operation.
Enter fullscreen mode
Exit fullscreen mode

Changes to Components

Now, let’s say we need to update ComponentB and ComponentC:

  1. ComponentB: Change SomeOperation to ExecuteOperation and modify its implementation.
  2. ComponentC: Add a parameter to AnotherOperation.

Updated ComponentB.cs

public class ComponentB
{ public void ExecuteOperation() { // Updated operation Console.WriteLine("ComponentB: Executing updated operation."); }
}
Enter fullscreen mode
Exit fullscreen mode

Updated ComponentC.cs

public class ComponentC
{ public void AnotherOperation(string message) { // Updated operation with a parameter Console.WriteLine($"ComponentC: Performing another operation with message: {message}"); }
}
Enter fullscreen mode
Exit fullscreen mode

Impact on ComponentA

Due to the changes in ComponentB and ComponentC, ComponentA must also be updated to accommodate these changes:

Updated ComponentA.cs

public class ComponentA
{ private readonly ComponentB _componentB; private readonly ComponentC _componentC; public ComponentA(ComponentB componentB, ComponentC componentC) { _componentB = componentB; _componentC = componentC; } public void Operation() { // ComponentA must be updated to use the new methods and parameters _componentB.ExecuteOperation(); _componentC.AnotherOperation("Hello from ComponentA"); }
}
Enter fullscreen mode
Exit fullscreen mode

Updated Program.cs

public class Program
{ public static void Main(string[] args) { var componentB = new ComponentB(); var componentC = new ComponentC(); var componentA = new ComponentA(componentB, componentC); componentA.Operation(); }
}
Enter fullscreen mode
Exit fullscreen mode

Running this updated code will produce the following output:

ComponentB: Executing updated operation.
ComponentC: Performing another operation with message: Hello from ComponentA.
Enter fullscreen mode
Exit fullscreen mode

Pros and Cons of Tight Coupling

Pros:

  1. Simplicity: Direct communication between components can be straightforward to implement initially.
  2. Performance: There is no intermediary, which can sometimes result in faster execution.

Cons:

  1. Maintenance: Any change in one component necessitates changes in all dependent components, leading to a fragile system.
  2. Scalability: As the system grows, the interdependencies become more complex, making it hard to manage.
  3. Testing: Unit testing becomes more difficult as components are tightly coupled.

Solving the Problem Using MediatR

Now, let’s see how we can solve this problem using MediatR in a .NET 8 console application.

Step-by-Step Implementation

  1. Setup the Console Application

    Create a new .NET console application:

    dotnet new console -n MediatorExample
    cd MediatorExample
    
  2. Install MediatR Packages

    Add the necessary MediatR packages:

    dotnet add package MediatR
    dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
    
  3. Create Request and Handler Classes

    CreateUserRequest.cs

    using MediatR; public class CreateUserRequest : IRequest
    { public string UserName { get; set; } public string Email { get; set; }
    }
    

    CreateUserHandler.cs

    using MediatR;
    using System.Threading;
    using System.Threading.Tasks; public class CreateUserHandler : IRequestHandler<CreateUserRequest>
    { public Task<Unit> Handle(CreateUserRequest request, CancellationToken cancellationToken) { // Simulate user creation logic Console.WriteLine($"User created: {request.UserName} with email: {request.Email}"); return Unit.Task; }
    }
    

    SendWelcomeEmailRequest.cs

    using MediatR; public class SendWelcomeEmailRequest : IRequest
    { public string Email { get; set; }
    }
    

    SendWelcomeEmailHandler.cs

    using MediatR;
    using System.Threading;
    using System.Threading.Tasks; public class SendWelcomeEmailHandler : IRequestHandler<SendWelcomeEmailRequest>
    { public Task<Unit> Handle(SendWelcomeEmailRequest request, CancellationToken cancellationToken) { // Simulate sending a welcome email Console.WriteLine($"Welcome email sent to: {request.Email}"); return Unit.Task; }
    }
    
  4. Register MediatR in the Dependency Injection Container

    Program.cs

    using MediatR;
    using Microsoft.Extensions.DependencyInjection;
    using System;
    using System.Threading.Tasks; class Program
    { static async Task Main(string[] args) { var services = new ServiceCollection(); services.AddMediatR(typeof(Program)); services.AddTransient<UserService>(); var serviceProvider = services.BuildServiceProvider(); var userService = serviceProvider.GetService<UserService>(); await userService.RegisterUser("JohnDoe", "[email protected]"); }
    }
    
  5. Define the UserService

    UserService.cs

    using MediatR;
    using System.Threading.Tasks; public class UserService
    { private readonly IMediator _mediator; public UserService(IMediator mediator) { _mediator = mediator; } public async Task RegisterUser(string userName, string email) { // Create user await _mediator.Send(new CreateUserRequest { UserName = userName, Email = email }); // Send welcome email await _mediator.Send(new SendWelcomeEmailRequest { Email = email }); }
    }
    

Pros and Cons of Using MediatR

Pros:

  1. Decoupling: Components do not need to know about each other, reducing dependencies and making the system more maintainable.
  2. Scalability: Easier to add new features and modify existing ones without affecting other parts of the system.
  3. Testing: Simplifies unit testing by isolating each component’s behavior.

Cons:

  1. Complexity: Introduces additional layers, which can make the initial setup more complex.
  2. Performance: Potentially slower due to the overhead of the mediator, although this is usually negligible.

Conclusion

Using the Mediator pattern with MediatR helps to decouple components, leading to a more flexible and maintainable system. While the initial setup might be more complex, the long-term benefits of reduced dependencies and easier testing outweigh the cons. By applying the Mediator pattern, you can avoid tightly coupled dependencies, leading to a cleaner and more maintainable architecture.


Discover more from Coursity

Subscribe to get the latest posts sent to your email.

Leave a Comment

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

Discover more from Coursity

Subscribe now to keep reading and get access to the full archive.

Continue reading

Scroll to Top
To turn on screen reader adjustments at any time, users need only to press the alt+1 keyboard combination.