Build your own Mediator

Amr elshaer
4 min read1 day ago

--

MediatR, AutoMapper, and MassTransit — are moving to commercial licenses. Not so long ago, Fluent Assertions also announced its plans to move to a commercial license.

Using the MediatR library in .NET (especially in DDD/CQRS-style applications) brings several benefits when building scalable and maintainable systems. Here’s a breakdown of the main benefits:

1. Decouples Request Senders from Handlers

  • Without MediatR: The controller or service directly calls a method on another service/class.
  • With MediatR: The controller sends a request (IRequest<T>) through MediatR, and the handler (IRequestHandler<T>) processes it.
  • 🔄 Benefit: You can change the handler’s implementation without changing the caller. This improves loose coupling and testability.

2. Clean Separation of Concerns (SoC)

  • Each handler has a single responsibility.
  • You can organize your logic into Commands, Queries, Events, etc.
  • 🔄 Benefit: Promotes clean architecture and is easier to reason about and test.

3. Built-in Pipeline Behavior (Cross-Cutting Concerns)

  • You can inject behaviors like:
  • Logging
  • Validation
  • Performance tracking
  • Caching
  • 🔄 Benefit: Helps you centralize logic that would otherwise be duplicated across handlers.

✅ 4. Supports CQRS

  • MediatR makes it easy to implement the Command Query Responsibility Segregation pattern.
  • 🔄 Benefit: Improves scalability and clarity by separating read and write operations.

✅ 5. Reduces Dependencies Between Layers

  • The application layer doesn’t need to directly reference the infrastructure or presentation layers.
  • 🔄 Benefit: Encourages a more modular architecture.

6. Easy to Unit Test

  • Since handlers are just classes implementing IRequestHandler<T>, you can easily test them in isolation.
  • 🔄 Benefit: Better test coverage and faster unit testing.

7. Event Publishing (Notifications)

  • You can raise domain eventsINotification, and multiple handlers can react.
  • 🔄 Benefit: Helps build event-driven workflows or domain-driven designs.

Now let’s discuss how to build our mediator instead of using the mediator library. We will build the main functionality of the mediator
1- Request/Response(Commands, Queries)

2- Event Publishing (Notifications)

3- Pipeline Behavior (Cross-Cutting Concerns)

public interface IRequest<out TResponse> { }
public interface IRequestHandler<in TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
}
public interface INotification { }
public interface INotificationHandler<in TNotification>
where TNotification : INotification
{
Task Handle(TNotification notification, CancellationToken cancellationToken);
}

public interface IMediator
{
Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default);
Task Publish<TNotification>(TNotification notification, CancellationToken cancellationToken = default) where TNotification : INotification;
}
public delegate Task<TResponse> RequestHandlerDelegate<TResponse>();
public interface IPipelineBehavior<in TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken);
}
public class Mediator : IMediator
{
private readonly IServiceProvider _serviceProvider;

public Mediator(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}

// Send - For command/query operations
public async Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default)
{
var requestType = request.GetType();
var handlerType = typeof(IRequestHandler<,>).MakeGenericType(requestType, typeof(TResponse));

var handler = _serviceProvider.GetService(handlerType);
if (handler == null)
throw new InvalidOperationException($"No handler registered for {requestType.Name}");

// Get pipeline behaviors
var behaviors = _serviceProvider.GetServices<IPipelineBehavior<IRequest<TResponse>, TResponse>>();

// Create the request pipeline
RequestHandlerDelegate<TResponse> pipeline = () =>
{
var method = handlerType.GetMethod("Handle");
return (Task<TResponse>)method.Invoke(handler, new object[] { request, cancellationToken });
};

// Apply behaviors in reverse order (so first registered runs first)
foreach (var behavior in behaviors.Reverse())
{
var currentPipeline = pipeline;
pipeline = () => behavior.Handle(request, currentPipeline, cancellationToken);
}

return await pipeline();
}

// Publish - For notification operations
public async Task Publish<TNotification>(TNotification notification, CancellationToken cancellationToken = default)
where TNotification : INotification

{
var handlerType = typeof(INotificationHandler<>).MakeGenericType(notification.GetType());
var handlers = _serviceProvider.GetServices(handlerType);

var tasks = new List<Task>();
foreach (var handler in handlers)
{
var method = handlerType.GetMethod("Handle");
tasks.Add((Task)method.Invoke(handler, new object[] { notification, cancellationToken }));
}

await Task.WhenAll(tasks);
}
}
public static class MediatorExtensions
{
public static IServiceCollection AddMediator(this IServiceCollection services, Assembly assembly)
{
// Register mediator
services.AddScoped<IMediator, Mediator>();

// Register handlers
RegisterHandlers(services, assembly);

return services;
}

private static void RegisterHandlers(IServiceCollection services, Assembly assembly)
{
// Register request handlers
var requestHandlerTypes = assembly.GetTypes()
.Where(t => t.GetInterfaces().Any(i =>
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IRequestHandler<,>)))
.ToList();

foreach (var handlerType in requestHandlerTypes)
{
var handlerInterface = handlerType.GetInterfaces()
.First(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IRequestHandler<,>));

services.AddTransient(handlerInterface, handlerType);
}

// Register notification handlers
var notificationHandlerTypes = assembly.GetTypes()
.Where(t => t.GetInterfaces().Any(i =>
i.IsGenericType && i.GetGenericTypeDefinition() == typeof(INotificationHandler<>)))
.ToList();

foreach (var handlerType in notificationHandlerTypes)
{
var handlerInterfaces = handlerType.GetInterfaces()
.Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(INotificationHandler<>));

foreach (var handlerInterface in handlerInterfaces)
{
services.AddTransient(handlerInterface, handlerType);
}
}
}
}

Let’s use our own code to implement a basic scenario where creating a user from the command, logging, and validation behaviour, and send a notification.


public interface IUserRepository
{
Task<int> AddAsync(User user, CancellationToken cancellationToken);
}

public class UserRepository:IUserRepository
{
public Task<int> AddAsync(User user, CancellationToken cancellationToken)
{
return Task.FromResult(1);
}
}
public class CreateUserCommand : IRequest<int>
{
public string Name { get; set; }
public string Email { get; set; }

public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
public CreateUserCommandValidator()
{
RuleFor(command => command.Name)
.NotEmpty().WithMessage("Name is required")
.MaximumLength(100).WithMessage("Name cannot exceed 100 characters");

RuleFor(command => command.Email)
.NotEmpty().WithMessage("Email is required")
.EmailAddress().WithMessage("A valid email address is required")
.MaximumLength(255).WithMessage("Email cannot exceed 255 characters");
}
}
}

public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, int>
{
private readonly IUserRepository _repository;
private readonly IMediator _mediator;

public CreateUserCommandHandler(IUserRepository repository,IMediator mediator)
{
_repository = repository;
_mediator = mediator;
}

public async Task<int> Handle(CreateUserCommand request, CancellationToken cancellationToken)
{
var user = new User { Id= Guid.NewGuid(), Name = request.Name, Email = request.Email };
var userCreatedNotification = new UserCreatedNotification()
{
Name = user.Name,
UserId = user.Id,
};
await _mediator.Publish(userCreatedNotification,cancellationToken);
return await _repository.AddAsync(user, cancellationToken);
}
}

public interface IEmailService
{
Task SendWelcomeEmailAsync(Guid notificationUserId, string notificationName);
}

public class EmailService : IEmailService
{
public Task SendWelcomeEmailAsync(Guid notificationUserId, string notificationName)
{
return Task.CompletedTask;
}
}
public class UserCreatedNotification : INotification
{
public Guid UserId { get; set; }
public string Name { get; set; }
}

public class EmailNotificationHandler : INotificationHandler<UserCreatedNotification>
{
private readonly IEmailService _emailService;

public EmailNotificationHandler(IEmailService emailService)
{
_emailService = emailService;
}

public async Task Handle(UserCreatedNotification notification, CancellationToken cancellationToken)
{
await _emailService.SendWelcomeEmailAsync(notification.UserId, notification.Name);
}
}
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
// Add mediator with the assembly containing handlers
builder.Services.AddMediator(typeof(Program).Assembly);
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddTransient<IEmailService, EmailService>();
app.MapPost("/create-user", async (CreateUserCommand command,IMediator mediator) =>
{

var result = await mediator.Send(command);
return result;
})
.WithName("Create user")
.WithOpenApi();

All source code is in GitHub.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Amr elshaer
Amr elshaer

Written by Amr elshaer

Software engineer | .Net ,C# ,Angular, Javascript

No responses yet

Write a response