Привет, Хабр! Хочу поделиться библиотекой, которую написал как открытую альтернативу 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 может вам подойти.
Буду рад обратной связи и предложениям по улучшению!
