Hero image

Software Development

Mastering Mediator Pattern Implementations Part 1

Home

>

Blog

>

Software Development

>

Mastering Mediator Pattern Implementations Part 1

Published: 2023/10/05

9 min read

Have you ever wondered how to achieve flexibility and adaptability in implementing the Mediator pattern in your software projects? This is something I analyzed with Kamil Bytner, the co-creator of our Pipelines library, and we concluded that, while on the one hand it’s simple, on the other hand, it needs to be implemented, wrapped and customized each time. 

Before we dive into our considerations, let’s start from the beginning – what is a Mediator? The Mediator pattern is a solution that helps organize and reduce the complexity of dependencies between objects in an application. In practice, this means it eliminates direct communication between objects and requires them to collaborate only through a special mediator object. The theory may seem simple, but what makes this pattern truly exceptional is its ability to seamlessly integrate with other popular design patterns, such as Observer, CQRS, Event Aggregator, or Chain of Responsibility. 

The Mediator pattern in .NET can be implemented in many ways, though in my experience two approaches – a pragmatic approach using the MediatR library and creating a custom mediator implementation – are the most common. From my observations, in most cases, MediatR proves to be effective in a project, although sometimes certain adapters or markers need to be implemented. On the other hand, I have increasingly come across the approach of implementing a custom mediator. However, is creating a custom mediator not reinventing the wheel? Let’s analyze some facts. 

In simple and small applications, MediatR is an excellent choice, especially when you handle a single pipeline for the entire application. The challenge arises in more complex architectures, such as Clean Architecture, where multiple mediator abstractions are used to handle various communication pipelines. So, can you use MediatR in Clean Architecture? Of course, there’s nothing preventing you from using MediatR, but you need to consider some limitations: 

  • Violation of the Dependency Rule – having dependencies on an external library in both the Application and Domain layers. This can be reduced by using adapters and interface abstraction, but it requires additional implementations. However, keep in mind that the abstracted interfaces will still have dependencies on MediatR. 
  • MediatR is global for the entire application – this can create difficulties in implementing multiple distinct communication pipelines, especially when implementing Behaviors/Decorators. It is worth noting that MediatR does not support the creation of multiple independent pipelines. You need to always maintain a single pipeline for different use cases. 

Interestingly, recent years have shown that awareness of Clean Architecture is increasing among developers, based on this Google Trends chart:

Chart 3

However, if I were to act as the moderator of a panel discussion at a conference and presented my considerations, I assume it would spark quite a debate. I imagine that the entire room would quickly divide into these three main camps, all vying for recognition in the context of choosing the best approach.: 

  • Pragmatists of Clean Code – “We don’t want to waste time on implementation; after all, it’s just a mediator. Let’s use a library, and that’s it.” 
  • Experts of Clean Code – “We don’t want to break the Dependency Rule, but we want to use an existing solution. So, we’ll create adapters for the MediatR library.” 
  • Masters of Clean Code – “We believe it’s not worth the effort to customize the MediatR library by preparing adapters. We’ll write our own implementation.” 

What if there’s a way to reconcile all the camps and bring order to the chaos? How can we create a camp that is both pragmatic and a master of clean code? 

Developing our pipelines library 

One evening, my colleague Kamil (mentioned earlier) and I met to start a discussion with the guiding question: “Is it possible to give developers complete freedom in building solutions based on the Mediator pattern?” All beginnings are difficult, and this was no different. However, after a quick proof of concept, we confirmed that it is indeed possible. That is how our fascinating journey with the Pipelines library began. 

We believe that a good library should adapt to an application, not the other way round. That’s why Pipelines grants you the freedom to create any number of mediators within your application. Each of them can be tailored to specific use cases, ensuring maximum flexibility in programming. This is possible because: 

  • Pipelines does not expose types – it does not require implementing an interface or inheriting a class. You have absolute control over input data and operation results. 
  • Each mediator built with Pipelines is independent and decoupled from the others. 
  • To maintain the best performance, we utilized the Source Generator mechanism, which minimizes the use of reflection. 
  • Decorator support enables you to add additional effects like validation, logging, or even using the Unit of Work pattern. Furthermore, it enables the construction of the Chain of Responsibility pattern. 

I would like to add a few sentences here about why using Source Generator in Pipelines is a significant advantage. Source generator in C# is a mechanism useful for code generation based on metadata, annotations and other information available within the code. It allows developers to generate code during compilation, which can improve performance and simplify code management. It is thanks to Source Generator that we boost our efficiency and convenience in creating mediators. Source Generator enables us to approach this task in a more pragmatic way, eliminating the need for manual implementation of each mediator. Instead, you only need to provide the appropriate interfaces, and Source Generator takes care of the rest. So, by using the Pipelines library, you can promote from an expert of clean code to a master of clean code without the need for manual mediator implementations. 

Before we move on to practical examples, let’s examine a few key facts. In the Mediator pattern, we need an object that acts as a bridge between the place of invocation and its handling. In this library’s case, such an entity is called the “Dispatcher” – its role is to forward the Input and find the class that will handle it, known as the Handler. Now, what about decorators? Here, it is best to use an example from childhood – Matryoshka dolls. 

Matryoshka dolls

The construction of a Matryoshka doll is designed in such a way that you need to open each doll individually to reach the smallest one inside. However, when we finish playing and want to clean up, we need to assemble the dolls in reverse order. So, translating this to the language of the Pipelines library, the dolls that are opened are the decorators, and the smallest doll inside is the Handler. So, decorators allow us to apply certain effects before and after executing the logic of the Handler. 

To summarize, here’s a diagram of what a happy programmer using Pipelines looks like: 

DIAGRAM

Turning theory into practice 

What does this look like in real life?? Is it efficient? Do you really have full flexibility in defining types? When it comes to flexibility, there are some guidelines, which you will read about in the practical example below. But first, let me explain how it works. Initially, we used reflection as the mechanism, but during our initial performance tests, we discovered that it was not a satisfying solution, certainly not one that would satisfy the representatives of our camps. If not reflection, then what? Well, that is where our Source Generator mechanism came to the rescue. It allowed us to generate dispatcher code based on a configuration, which does not rely on reflection. This means that we achieve performance comparable to the implementation of MediatR, while still preserving the ability to use custom types. 

Enough theory; let’s try to analyze two use cases of this library – the classic CQRS pattern where we’ll implement the command part and a more advanced approach where we’ll use the Chain of Responsibility pattern with an example order processing workflow. You can find all the examples mentioned here in our repository, where we have prepared working projects ready for execution: Pipelines Examples. 

We’ll focus now on the part responsible for handling commands. But before we start, let’s see how to build a mediator for command handling. According to the requirements, we need to define three interfaces:

public interface ICommand
{ }

public interface ICommandHandler where TCommand : ICommand
{
 public Task HandleAsync(TCommand command, CancellationToken token);
}

public interface ICommandDispatcher
{
 public Task SendAsync(ICommand command, CancellationToken token);
}

With these components, we can register a new pipeline in the dependency container using the “AddPipeline” extension method. 

services.AddPipeline()
 .AddInput(typeof(ICommand))
 .AddHandler(typeof(ICommandHandler<>), commandsAssembly)
 .AddDispatcher(infrastructureAssembly)
 .Build();

What happens when you call the “Build” method? 

  1. The library will check whether the types you provided are compatible, which means: 
  2. The Input type must be the first parameter in the methods of the Handler and Dispatcher. 
  3. The returned result type must be the same for both Dispatcher and Handler methods. 
  4. The parameters for Dispatcher and Handler methods must be the same. 
  5. The library will then register all Handler implementations in the dependency container. 

People from the camps of experts and masters will nod in agreement here; they also had to create their own markers, where experts override interfaces from the MediatR library. 

Now, all the camps will come together because we are moving on to the implementation of the Command and Handler: 

In our example, we will use a simple “ToDo” domain, and in this case, the Command has one parameter, which is the “Title.”

public record CreateToDoCommand(string Title) : ICommand;

Next, we have a simple Handler with one task to execute adding a ToDo object to the repository and sending a domain event. This Handler handles the previously prepared “CreateToDoCommand.”

public class CreateToDoCommandHandler : ICommandHandler
{
 private readonly IToDoRepository _toDoRepository;
 private readonly IDomainEventsDispatcher _domainEventsDispatcher;

 public CreateToDoCommandHandler(IToDoRepository toDoRepository,  IDomainEventsDispatcher domainEventsDispatcher)
 {
  _toDoRepository = toDoRepository;
  _domainEventsDispatcher = domainEventsDispatcher;
 }

 public async Task HandleAsync(CreateToDoCommand command, CancellationToken token)
 {
  var toDo = ToDo.Create(command.Title);
  await _toDoRepository.AddAsync(toDo, token);

  await _domainEventsDispatcher.SendAsync(toDo.DomainEvents, token);

  return toDo.Id;
 }
}

Let’s use the ICommandDispatcher to handle the command, then.

app.MapPost("/toDo", async (CreateToDoCommand command, ICommandDispatcher commandDispatcher, CancellationToken token) =>
{
 var result = await commandDispatcher.SendAsync(command,token);
 return Results.Ok(result);
});

But where are the decorators I mentioned?! My apologies, let’screate a simple decorator that will log information about the type of command being processed: 

internal class LoggingCommandDecorator : ICommandHandler where TCommand : ICommand
{
 private readonly ICommandHandler _commandHandler;
 private readonly ILogger<LoggingCommandDecorator> _logger;

 public LoggingCommandDecorator(ICommandHandler commandHandler,
 ILogger<LoggingCommandDecorator> logger)
 {
  _commandHandler = commandHandler;
  _logger = logger;
 }

 public async Task HandleAsync(TCommand command, CancellationToken token)
 {
  _logger.LogInformation("Handling command {CommandType}", typeof(TCommand));

  var result = await _commandHandler.HandleAsync(command, token);
  _logger.LogInformation("Handled command {CommandType}", typeof(TCommand));

  return result;
 }
}

Do you remember the comparison of decorators to Matryoshka dolls? This is how it looks from a code perspective and requires two conventions: 

  • Decorators must implement the same interface as the Handler – just like Matryoshka dolls, decorators must be the same as the Handler. 
  • Decorators must have the Handler in their constructor – this will be our next doll to open. 

In this way, we have a universal way to implement decorators. The decorator we created will be executed for every command type – such decorators are called “OpenType.” If you want to create a decorator that runs only for a specific command, then you need to use a “ClosedType” decorator. 

After implementing the decorator, do not forget to register it:

services.AddPipeline()
 .AddInput(typeof(ICommand))
 .AddHandler(typeof(ICommandHandler<>), commandsAssembly)
 .AddDispatcher(infrastructureAssembly)
 .WithDecorator(typeof(LoggingCommandDecorator<>))
 .Build();

So, by using the “WithDecorator” method, we’ve added a decorator to our command mediator. 

You can find more information about decorators and how to register them here: Pipelines Main Concepts 

How do members of various fractions view this? Is Pipelines a magical ingredient capable of bridging the gaps between opposing camps, or is it a fourth party trying to solve a problem that does not exist? 

For us, the creators of the Pipelines library, this journey has been both a challenge and a passion. Through Pipelines, we’ve come to understand that there’s always an innovative approach to solving programming problems. This tool makes developers’ lives easier and also opens doors to creativity and experimentation. 

Let Pipelines become your ally on the path to outstanding projects. Visit our GitHub repository (DumplingsDevs – Pipelines), where you’ll find not only the library itself, but also comprehensive documentation to help you get started with our tool in an easy and understandable way. 

We’re open to your feedback, suggestions and comments you may have. Together, we can continue to evolve Pipelines to meet your expectations. After all, our goal is to ensure that code is clean, efficient and enjoyable to create.  

To get in touch with experts who are passionate about developing software, fill out the contact form. 

Resources used in article:

https://refactoring.guru/pl/design-patterns/mediator 

https://refactoring.guru/design-patterns/decorator 

https://trends.google.pl/trends/explore?date=all&q=Clean%20architecture&hl=pl 

 

About the authorMateusz Wróblewski

Senior Software Engineer

Mateusz has been working as a full-stack developer for 7 years but has been mostly focused on the backend part of programming. He discovered his love for coding during college, and right from the start, he knew that .NET was the technology he wanted to work with. A software architecture enthusiast, Mateusz is a member of Software Mind’s .NET Guild, a community of experts who share experiences, explore new technologies and present insights at industry events and universities. A big fan of Clean Architecture, which he's used in several projects, Mateusz has recently become involved in open-source projects and is currently working on the Pipelines library.

Subscribe to our newsletter

Sign up for our newsletter

Most popular posts