HomeNikola Knezevic

In this article

Banner

Mediator Pattern Without MediatR in ASP.NET Core

18 Sept 2025
5 min

Sponsor Newsletter

In software development, one of the biggest challenges is keeping business logic clean and decoupled from infrastructure concerns.

As applications grow, controllers or services can easily become bloated with validation, logging, caching and transaction-handling code.

The mediator pattern offers a way to centralize request handling, making applications easier to maintain and extend.

In .NET, the most popular way to implement it is with the MediatR library, however since MediatR became commercial people started seeking for alternatives.

Recently, we explored alternatives to AutoMapper. Today, let’s take a look at whether and how we might replace MediatR.

Mediator Pattern

The Mediator Pattern defines an object that coordinates how requests and responses are exchanged across the system. Instead of components communicating directly, they interact through a mediator.

Basically, the mediator receives a request, routes it to the appropriate handler and returns the response.

Mediator Pattern Diagram

This approach has a few advantages:

  • Loose coupling - Callers don't need to know the handler and the handler doesnt need to know who is the caller
  • Single entry point - All requests flow through the mediator
  • Pipeline flexibility - Cross-cutting concerns can be added around the main logic.

This makes the pattern especially appealing when paired with CQRS.

MediatR

Before we dive into alternatives, let’s quickly recap what MediatR offers.

MediatR isn’t just a library for implementing the Mediator Pattern, it provides a rich set of features:

  • Request/Response messaging
  • Notifications
  • Streams
  • Pipeline behaviors and more

The key advantage is reliability, you can trust it to handle the underlying mechanics while working solely with its abstractions.

Manual Approach

Implementing the Mediator Pattern manually, similar to what MediatR does, requires defining a few key abstractions:

csharp
public interface IRequest<TResponse>;

public interface IRequestHandler<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
    Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken = default);
}
    
public interface ISender
{
    Task<TResponse> Send<TResponse>(
        IRequest<TResponse> request,
        CancellationToken cancellationToken = default);
}
        
public struct Unit;

Out of these, the main piece to implement is the ISender interface:

csharp
public class Sender(IServiceProvider serviceProvider) : ISender
{
    public async Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default)
    {
        var handlerType = typeof(IRequestHandler<,>).MakeGenericType(request.GetType(), typeof(TResponse));
        dynamic handler = serviceProvider.GetRequiredService(handlerType);

        return await handler.Handle((dynamic)request, cancellationToken);
    }
}

The trickiest part is registering the mediator and all handlers, especially if you plan to extend it with pipelines or other features:

csharp
public static class MediatorExtensions
{
    public static IServiceCollection AddMediator(this IServiceCollection services, params Assembly[] assemblies)
    {
        services.AddScoped(ISender, Sender);

        assemblies = assemblies.Any() ? assemblies : [Assembly.GetCallingAssembly()];

        RegisterHandlers(services, assemblies);

        return services;
    }

    private static void RegisterHandlers(IServiceCollection services, Assembly[] assemblies)
    {
        var handlerTypes = assemblies
            .SelectMany(assembly => assembly.GetTypes())
            .Where(type =>
                type.IsClass &&
                !type.IsAbstract &&
                type.GetInterfaces().Any(
                    i =>
                        i.IsGenericType &&
                        i.GetGenericTypeDefinition() == typeof(IRequestHandler<,>)))
            .Select(t => new {
                Implementation = t,
                Interface = t.GetInterfaces()
                    .First(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IRequestHandler<,>))
            });

        foreach (var handler in handlerTypes)
        {
            services.AddTransient(handler.Interface, handler.Implementation);
        }
    }
}

This extension method keeps registration clean and simple. Then, register everything in Program.cs:

csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddMediator();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.Run();

Minimal API Example:

csharp
app.MapGet("/weatherforecast", async (ISender sender, CancellationToken cancellationToken) =>
{
    var request = new GetWeatherForecastsRequest();

    var response = await sender.Send(request, cancellationToken);
   
    return Results.Ok(response);
});

Request definition:

csharp
public record GetWeatherForecastsRequest : IRequest<IEnumerable<WeatherForecast>>;

Handler example:

csharp
public class GetWeatherForecastsRequestHandler : IRequestHandler<GetWeatherForecastsRequest, IEnumerable<WeatherForecast>>
{
    private readonly string[] _summaries =
    [
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    ];

    public async Task<IEnumerable<WeatherForecast>> Handle(GetWeatherForecastsRequest request, CancellationToken cancellationToken = default)
    {
        var forecast = Enumerable.Range(1, 5)
            .Select(index =>
                new WeatherForecast
                (
                    DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                    Random.Shared.Next(-20, 55),
                    _summaries[Random.Shared.Next(_summaries.Length)]
                ))
            .ToArray();

        return forecast;
    }
}

This approach provides a clear, practical way to implement the mediator pattern without relying on MediatR, while keeping your architecture flexible and maintainable.

Conclusion

Building mediator pattern is easy and can be a fun and educational exercise, however, I don't think it's worth it especially if you need all of the features that MediatR offers.

If you only need mediator pattern, something very specific or simply want to have full control I can see why you would skip on paying for commercial licence and building your own.

Additionally, could also try Wolverine, an interesting and free alternative to MediatR.

If you want to check out examples I created, you can find the source code here:

Source Code

I hope you enjoyed it, subscribe and get a notification when a new blog is up!

Subscribe

Stay tuned for valuable insights every Thursday morning.