Привет, Хабр! Хочу поделиться библиотекой, которую написал как открытую альтернативу 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 Requestum
NuGet пакет: Requestum
Посмотреть сам код можно тут: Requestum
Лицензия: MIT (и останется MIT)
Заключение
Requestum создавался как ответ на коммерциализацию MediatR и как личный челлендж - попробовать сделать CQRS-библиотеку по-своему. В процессе получилась попытка сделать код чуть более явным и быстрым.
Если вы:
Ищете бесплатную альтернативу MediatR
Хотите больше выразительности в коде через систему типов
Нуждаетесь в настоящих синхронных обработчиках
Цените производительность
...то Requestum может вам подойти.
Буду рад обратной связи и предложениям по улучшению!
