Привет, Хабр! Хочу поделиться библиотекой, которую написал как открытую альтернативу MediatR после новостей о его планируемой коммерциализации.
Предыстория
2 апреля 2025 года Джимми Богард объявил о планах коммерциализации своих популярных библиотек AutoMapper и MediatR для "обеспечения долгосрочной устойчивости". Для многих это стало неожиданностью - библиотеки, на которых построены тысячи проектов, могут стать платными.
Я не против того, чтобы разработчики зарабатывали на своих проектах. Но зависимость от библиотеки, которая в любой момент может изменить лицензию или модель распространения - это риск. Особенно когда речь идёт о фундаментальных вещах вроде паттерна медиатор.
К тому же, я давно хотел попробовать свои силы в создании чего-то подобного - бросить вызов себе и попытаться сделать лучше. Так и родилась идея Requestum - полностью открытая библиотека с лицензией MIT, которая останется бесплатной всегда.
Почему не просто форк?
Раз уж пришлось делать альтернативу, я решил заодно переосмыслить несколько вещей:
1. Явная семантика через интерфейсы
В MediatR всё унифицировано через IRequest и Send(). Это универсально, но иногда хочется большей выразительности в коде.
В Requestum я сделал явное разделение через разные интерфейсы и методы:
// Это команда - она что-то делает
public record CreateUserCommand : ICommand
{
    public string Name { get; set; }
    public string Email { get; set; }
}
// Это запрос - он что-то возвращает
public record GetUserQuery : IQuery<UserDto>
{
    public int UserId { get; set; }
}
// Это событие - оно сообщает о чём-то случившемся
public record UserCreatedEvent : IEventMessage
{
    public int UserId { get; set; }
    public string Name { get; set; }
}
Три разных интерфейса для трёх разных целей. Да, это чуть больше кода при определении типов, но зато абсолютная ясность намерений на уровне системы типов.
2. Говорящие имена методов
В MediatR есть универсальный Send() для всего. В Requestum каждый тип операции имеет свой метод:
// Команды выполняются
await _requestum.ExecuteAsync(new CreateUserCommand());
// Запросы обрабатываются и возвращают результат
var user = await _requestum.HandleAsync<GetUserQuery, UserDto>(query);
// События публикуются
await _requestum.PublishAsync(new UserCreatedEvent());
Читаешь код и сразу понимаешь, что происходит. Метод сам документирует намерение.
3. Настоящая синхронность
Это, наверное, моя любимая особенность. Не всегда нужен async/await. Валидация данных, простые вычисления, операции в памяти - для всего этого асинхронность только добавляет оверхед.
В Requestum есть отдельные интерфейсы для синхронных и асинхронных операций:
// Синхронный обработчик - чистый код, без лишних накладных расходов
public class SumQueryHandler : IQueryHandler<SumQuery, SumQueryResponse>
{
    public SumQueryResponse Handle(SumQuery query)
    {
        if (query.B == 0) throw new Exception("B is 0");
        return new SumQueryResponse { C = query.A + query.B };
    }
}
// Асинхронный - когда действительно нужен I/O
public class CreateUserHandler : IAsyncCommandHandler<CreateUserCommand>
{
    public async Task ExecuteAsync(CreateUserCommand command, CancellationToken ct = default)
    {
        await _database.SaveAsync(command, ct);
    }
}
Выбирайте то, что подходит для конкретной задачи. Не нужно везде async ради async.
Множественные получатели событий
В Requestum события могут иметь несколько получателей, что позволяет реализовать чистый паттерн pub/sub:
// Три разных получателя одного события
public class SendWelcomeEmailReceiver : IAsyncEventMessageReceiver<UserCreatedEvent>
{
    public async Task ReceiveAsync(UserCreatedEvent message, CancellationToken ct = default)
    {
        await _emailService.SendWelcomeEmailAsync(message.Email);
    }
}
public class LogUserCreationReceiver : IEventMessageReceiver<UserCreatedEvent>
{
    public void Receive(UserCreatedEvent message)
    {
        _logger.LogInformation($"User created: {message.UserId}");
    }
}
public class UpdateAnalyticsReceiver : IAsyncEventMessageReceiver<UserCreatedEvent>
{
    public async Task ReceiveAsync(UserCreatedEvent message, CancellationToken ct = default)
    {
        await _analytics.TrackUserRegistrationAsync(message.UserId);
    }
}
Один вызов PublishAsync() - и все зарегистрированные получатели обработают событие. При необходимости можно настроить поведение, если получателей нет:
services.AddRequestum(cfg =>
{
    // По умолчанию требуется хотя бы один получатель
    // Можно разрешить публикацию событий без получателей
    cfg.RequireEventHandlers = false;
});
Производительность
Раз уж пришлось писать с нуля, я сразу позаботился о производительности. Вот результаты бенчмарков (BenchmarkDotNet, .NET 9):
Выполнение команд
| Метод | Среднее время | Выделено памяти | 
|---|---|---|
| MediatR_Command_ExecuteAsync | 70.87 ns | 192 B | 
| Requestum_Command_ExecuteAsync | 55.80 ns (79%) | 120 B (62%) | 
| Requestum_Command_ExecuteSync | 47.84 ns (68%) | 120 B (62%) | 
Обработка запросов
| Метод | Среднее время | Выделено памяти | 
|---|---|---|
| MediatR_Query_HandleAsync | 70.03 ns | 360 B | 
| Requestum_Query_HandleAsync | 64.64 ns (92%) | 288 B (80%) | 
| Requestum_Query_HandleSync | 59.00 ns (84%) | 216 B (60%) | 
Команды с middleware
| Метод | Среднее время | Выделено памяти | 
|---|---|---|
| MediatR_CommandWithMiddleware_ExecuteAsync | 229.7 ns | 1144 B | 
| Requestum_CommandWithMiddleware_ExecuteAsync | 189.3 ns (82%) | 1024 B (90%) | 
| Requestum_CommandWithMiddleware_ExecuteSync | 150.9 ns (66%) | 800 B (70%) | 
Публикация событий
| Метод | Среднее время | Выделено памяти | 
|---|---|---|
| MediatR_Notification_SingleHandler | 164.29 ns | 744 B | 
| Requestum_EventMessage_SingleHandler_Async | 54.44 ns (33%) | 88 B (12%) | 
| Requestum_EventMessage_SingleHandler_Sync | 36.45 ns (22%) | 88 B (12%) | 
| MediatR_Notification_MultipleHandlers | 161.07 ns | 744 B | 
| Requestum_EventMessage_MultipleHandlers_Async | 53.66 ns (33%) | 88 B (12%) | 
| Requestum_EventMessage_MultipleHandlers_Sync | 45.91 ns (28%) | 88 B (12%) | 
Ключевые выводы:
- Синхронные операции выполняются на 20-35% быстрее асинхронных версий MediatR 
- Публикация событий в 3-4.5 раза быстрее и выделяет в 8 раз меньше памяти 
- Даже с middleware pipeline остаётся заметный выигрыш в производительности 
- Для высоконагруженных систем экономия на каждой операции может быть существенной 
Полные результаты бенчмарков доступны в данном репозитории.
Middleware без сюрпризов
Pipeline для middleware работает так, как и ожидается. Можете использовать синхронные или асинхронные middleware:
// Синхронный middleware для логирования
public class LogMiddleware<TRequest, TResponse> : IRequestMiddleware<TRequest, TResponse>
{
    public TResponse Invoke(TRequest request, RequestNextDelegate<TRequest, TResponse> next)
    {
        Console.WriteLine($"Before: {request}");
        var response = next.Invoke(request);
        Console.WriteLine($"After: {response}");
        return response;
    }
}
// Асинхронный middleware для обработки исключений
public class ExceptionHandlerMiddleware<TRequest, TResponse> 
    : IAsyncRequestMiddleware<TRequest, TResponse>
{
    public async Task<TResponse> InvokeAsync(
        TRequest request, 
        AsyncRequestNextDelegate<TRequest, TResponse> next, 
        CancellationToken ct = default)
    {
        try
        {
            return await next.InvokeAsync(request);
        }
        catch (Exception ex)
        {
            // Обработка ошибки
            throw;
        }
    }
}
Простая регистрация
Интеграция с DI контейнером стандартная:
services.AddRequestum(cfg =>
{
    // Сканирование сборки - найдёт все обработчики и middleware
    cfg.Default(typeof(Program).Assembly);
  
    // Или по отдельности
    cfg.RegisterHandlers(typeof(Program).Assembly);
    cfg.RegisterMiddlewares(typeof(Program).Assembly);
    // Настройка времени жизни
    cfg.Lifetime = ServiceLifetime.Scoped;
    
    // События без получателей - можно, но по умолчанию будет исключение
    cfg.RequireEventHandlers = false;
});
Что получилось
В итоге Requestum - это:
- Полностью open-source - MIT лицензия, никаких планов на коммерциализацию 
- Явная типизация - команды, запросы и события разделены на уровне интерфейсов 
- Говорящие методы - - Execute,- Handle,- Publishвместо универсального- Send
- Настоящая синхронность - не - Task.FromResult(), а реальные синхронные методы
- Производительность - на 20-50% быстрее и на 30-60% меньше аллокаций памяти 
- Простота - минимум зависимостей, понятный API 
Миграция с MediatR
Если вы используете MediatR и хотите попробовать Requestum, основные изменения минимальны:
Было (MediatR):
public class CreateUserCommand : IRequest { }
public class CreateUserHandler : IRequestHandler<CreateUserCommand>
{
    public async Task Handle(CreateUserCommand request, CancellationToken ct)
    {
        // код
    }
}
await _mediator.Send(new CreateUserCommand());
Стало (Requestum):
public record CreateUserCommand : ICommand;
public class CreateUserHandler : IAsyncCommandHandler<CreateUserCommand>
{
    public async Task ExecuteAsync(CreateUserCommand command, CancellationToken ct = default)
    {
        // код
    }
}
await _requestum.ExecuteAsync(new CreateUserCommand());
Большинство паттернов один-в-один, так что миграция обычно занимает немного времени.
Установка и исходный код
Библиотека доступна через NuGet:
dotnet add package RequestumNuGet пакет: Requestum
Посмотреть сам код можно тут: Requestum
Лицензия: MIT (и останется MIT)
Заключение
Requestum создавался как ответ на коммерциализацию MediatR и как личный челлендж - попробовать сделать CQRS-библиотеку по-своему. В процессе получилась попытка сделать код чуть более явным и быстрым.
Если вы:
- Ищете бесплатную альтернативу MediatR 
- Хотите больше выразительности в коде через систему типов 
- Нуждаетесь в настоящих синхронных обработчиках 
- Цените производительность 
...то Requestum может вам подойти.
Буду рад обратной связи и предложениям по улучшению!
Комментарии (4)
 - tsvettsih31.10.2025 08:44- Эта либа позволяет одну и ту же middleware добавить и на команду, и на запрос?  - GigSter2017 Автор31.10.2025 08:44- Да, в случае команды в TResponse будет CommandResponse заглушка, по аналогии с Unit 
 
 
           
 
ArtZilla
Думаю, имеет смысл сделать сравнение с Mediator - открытая лицензия и производительность выше, чем у MediatR.
GigSter2017 Автор
По производительности с SourceGenerator тягаться не получится, только когда найду силы сделать свою реализацию на SourceGenerator'ах)