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

  1. Полностью open-source - MIT лицензия, никаких планов на коммерциализацию

  2. Явная типизация - команды, запросы и события разделены на уровне интерфейсов

  3. Говорящие методы - ExecuteHandlePublish вместо универсального Send

  4. Настоящая синхронность - не Task.FromResult(), а реальные синхронные методы

  5. Производительность - на 20-50% быстрее и на 30-60% меньше аллокаций памяти

  6. Простота - минимум зависимостей, понятный 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 может вам подойти.

Буду рад обратной связи и предложениям по улучшению!

Комментарии (4)


  1. ArtZilla
    31.10.2025 08:44

    Думаю, имеет смысл сделать сравнение с Mediator - открытая лицензия и производительность выше, чем у MediatR.


    1. GigSter2017 Автор
      31.10.2025 08:44

      По производительности с SourceGenerator тягаться не получится, только когда найду силы сделать свою реализацию на SourceGenerator'ах)


  1. tsvettsih
    31.10.2025 08:44

    Эта либа позволяет одну и ту же middleware добавить и на команду, и на запрос?


    1. GigSter2017 Автор
      31.10.2025 08:44

      Да, в случае команды в TResponse будет CommandResponse заглушка, по аналогии с Unit