Start Debugging

MediatR против простых сервисных классов в 2026: должна ли смена лицензии вас сдвинуть?

Для нового кода простые сервисные классы - лучший выбор по умолчанию. Смена лицензии MediatR в июле 2025 важна, только если вы выше порога Community в 5 млн долларов или отвергаете copyleft RPL-1.5. Сохраняйте MediatR, когда pipeline behaviors несут реальную нагрузку.

Краткая версия: для нового кода на .NET 11 в 2026 году по умолчанию выбирайте простые сервисные классы и обращайтесь к MediatR только тогда, когда вы действительно опираетесь на его pipeline behaviors. Смена лицензии в июле 2025 - реальная причина для переоценки, но это не та пропасть, какой её изобразили некоторые. Если ваша компания имеет годовой валовой доход менее 5 000 000 долларов, вы можете и дальше бесплатно использовать новейшую версию MediatR в рамках редакции Community, так что вопрос лицензирования вынуждает к решению только более крупные команды или тех, кто не хочет принимать copyleft RPL-1.5 в бесплатной лицензии с открытым исходным кодом. Технический вопрос (нужен ли вам медиатор вообще?) - именно тот, что должен на самом деле определять выбор, и для большинства случаев диспетчеризации запросов вы можете убрать именно косвенность, а не библиотеку.

Версии, упоминаемые по всему тексту: MediatR 12.5.0 - последний выпуск под чистой лицензией Apache 2.0; MediatR 13.0 (выпущен 2025-07-02) и последующая линейка 14.x поставляются под двойной моделью Reciprocal Public License 1.5 / коммерческая лицензия от Lucky Penny Software. Код нацелен на <TargetFramework>net11.0</TargetFramework> с SDK .NET 11 и C# 14.

Что на самом деле изменилось в июле 2025

MediatR и AutoMapper годами поддерживались Джимми Богардом под разрешительной лицензией с открытым исходным кодом. 2025-07-02 он передал обе библиотеки новой компании, Lucky Penny Software, и сменил модель лицензирования. Механизмы, важные для вашего решения:

Итак, практическая развилка такова. Небольшая мастерская с доходом ниже 5 млн? Вы можете бесплатно остаться на текущей версии MediatR, и единственное трение - это предупреждение в журнале, пока вы не зарегистрируете ключ Community. Крупнее или хорошо профинансированы? Вы либо платите, либо принимаете взаимные обязательства RPL-1.5, либо уходите. Эта третья группа - именно те, кому стоит читать сравнение “против простых сервисных классов”.

Матрица возможностей с первого взгляда

АспектMediatR 13+Простые сервисные классы
Диспетчеризация запросовКосвенность через ISender.SendПрямой вызов метода внедрённого интерфейса
Сквозная функциональностьIPipelineBehavior<,> как первоклассный механизмДекораторы (Scrutor) или явные вызовы
Переход к определению из вызывающего кодаПопадает в Send, а не в handlerПопадает в реализацию
Безопасность связывания на этапе компиляцииРазрешение во время выполнения, может упасть при первом вызовеВнедрение через конструктор, падает при запуске
Уведомления / fan-outВстроенная публикация INotificationСписок handler-ов, написанный вручную
Стоимость запускаСканирование сборок для регистрации handler-овНикакой сверх обычной регистрации DI
Накладные расходы на вызовАллокация wrapper + поиск в словаре + виртуальная диспетчеризацияПочти ноль, JIT может девиртуализировать
Native AOT / trimmingТребует осторожности, регистрация на основе рефлексииЧисто
Лицензия (выше 5 млн дохода)Коммерческая покупка или RPL-1.5Никакой, это ваш собственный код
Новый код в 2026Только если behaviors несут реальную нагрузкуВыбор по умолчанию

Строка, которая решает большинство споров, - “сквозная функциональность”. Почти всё остальное в этой таблице говорит в пользу простых сервисных классов. Подлинная и труднозаменимая ценность MediatR - это конвейер: единое место, чтобы обернуть каждый запрос валидацией, журналированием, транзакциями и кешированием. Если вы не используете этот конвейер, вы платите за косвенность и лицензию ради приукрашенного локатора служб.

Как выглядит MediatR и простой эквивалент

Вот каноническая форма MediatR: запрос, handler и вызывающий код, который диспетчеризует через ISender.

// .NET 11, C# 14, MediatR 13+ - request + handler
using MediatR;

public record GetOrderById(int OrderId) : IRequest<OrderDto>;

public sealed class GetOrderByIdHandler(AppDbContext db)
    : IRequestHandler<GetOrderById, OrderDto>
{
    public async Task<OrderDto> Handle(GetOrderById request, CancellationToken ct)
    {
        var order = await db.Orders.FindAsync([request.OrderId], ct)
            ?? throw new OrderNotFoundException(request.OrderId);
        return order.ToDto();
    }
}

// In an endpoint:
public async Task<OrderDto> Get(int id, ISender sender, CancellationToken ct)
    => await sender.Send(new GetOrderById(id), ct);

Простая версия сворачивает запрос и handler в один метод внедрённого сервиса. Вызывающий код зависит напрямую от интерфейса.

// .NET 11, C# 14 - plain service class, no MediatR
public interface IOrderService
{
    Task<OrderDto> GetByIdAsync(int orderId, CancellationToken ct);
}

public sealed class OrderService(AppDbContext db) : IOrderService
{
    public async Task<OrderDto> GetByIdAsync(int orderId, CancellationToken ct)
    {
        var order = await db.Orders.FindAsync([orderId], ct)
            ?? throw new OrderNotFoundException(orderId);
        return order.ToDto();
    }
}

// In an endpoint:
public async Task<OrderDto> Get(int id, IOrderService orders, CancellationToken ct)
    => await orders.GetByIdAsync(id, ct);

Простая версия короче, и, что критично, нажатие “перейти к определению” на orders.GetByIdAsync приводит вас к методу, который выполняется. С sender.Send(new GetOrderById(id)) вы попадаете в Send от MediatR и переходите к handler-у угадыванием или окольным путём через “найти реализации”. В небольшой команде эта разница незначительна. В большой кодовой базе с сотнями handler-ов потеря прямой навигации - реальный ежедневный налог, который модель MediatR взимает в обмен на развязку вызывающего кода от типа handler-а. Даёт ли вам что-то эта развязка, зависит от того, заменяет ли кто-нибудь когда-либо handler, не трогая вызывающий код, что на практике редкость.

Регистрация тоже заслуживает сравнения. MediatR сканирует сборки при запуске; простые сервисные классы регистрируются явно, и вы получаете сбой во время запуска (а не при первом запросе), если забудете какой-то из них, что напрямую связано с теми ошибками, что стоят за Unable to resolve service for type while attempting to activate.

// .NET 11, C# 14 - registration, side by side
// MediatR: scan an assembly, register every handler reflectively
builder.Services.AddMediatR(cfg =>
    cfg.RegisterServicesFromAssemblyContaining<GetOrderById>());

// Plain: explicit, trim-friendly, fails fast at startup if a dep is missing
builder.Services.AddScoped<IOrderService, OrderService>();

Pipeline behavior и как жить без него

Вот где MediatR оправдывает своё место. Pipeline behavior оборачивает каждый запрос, что даёт вам ровно одно место для добавления валидации, журналирования, измерения времени или транзакции.

// .NET 11, C# 14, MediatR 13+ - cross-cutting validation for ALL requests
using MediatR;

public sealed class ValidationBehavior<TRequest, TResponse>(
    IEnumerable<IValidator<TRequest>> validators)
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : notnull
{
    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken ct)
    {
        foreach (var validator in validators)
            await validator.ValidateAndThrowAsync(request, ct);
        return await next(ct);
    }
}

Зарегистрированный один раз, этот behavior выполняется перед каждым handler-ом. Заменить это скопированными и вставленными вызовами валидации в каждом простом сервисе было бы регрессом, и это лучший аргумент, чтобы сохранить медиатор. Но вы можете получить то же свойство “обернуть всё” в простом DI с помощью паттерна декоратора, используя Scrutor (под лицензией MIT) для регистрации декоратора вокруг интерфейса:

// .NET 11, C# 14, Scrutor 6.x - a decorator gives you the same cross-cutting hook
public sealed class LoggingOrderService(
    IOrderService inner,
    ILogger<LoggingOrderService> logger) : IOrderService
{
    public async Task<OrderDto> GetByIdAsync(int orderId, CancellationToken ct)
    {
        logger.LogInformation("Fetching order {OrderId}", orderId);
        return await inner.GetByIdAsync(orderId, ct);
    }
}

// Registration: decorate the real implementation
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.Decorate<IOrderService, LoggingOrderService>();

Компромисс честен: behavior MediatR универсален для каждого запроса при одной регистрации, тогда как декоратор действует на отдельный интерфейс. Если у вас одна-две сквозные функции и горстка сервисных интерфейсов, декораторы выигрывают в ясности. Если у вас десять behaviors, которые должны единообразно применяться к двумстам типам запросов, универсальный конвейер MediatR действительно требует меньше кода, и это тот сценарий, в котором остаться (и платить или квалифицироваться для Community) оправданно. Для запросных функций, которые на самом деле являются функциями HTTP, учтите, что часть того, что люди помещают в behaviors, относится к middleware или фильтрам, к той же области, что охватывает добавление глобального фильтра исключений в ASP.NET Core 11.

Сколько на самом деле стоит диспетчеризация

Производительность - самый слабый аргумент в любом направлении, и не стоит выбирать только по нему, но полезно знать, где живёт стоимость, чтобы никто не отделывался общими словами. Простой вызов интерфейса - это одна виртуальная диспетчеризация, которую JIT часто может девиртуализировать и встроить; она ничего не аллоцирует и стоит порядка одной наносекунды. Send от MediatR делает больше работы на вызов: он ищет тип запроса в кеше handler-ов, конструирует или извлекает RequestHandlerWrapper, проходит цепочку behaviors через делегаты и разрешает handler из контейнера. Это порядка десятков наносекунд плюс небольшая аллокация на отправку, ещё до того как выполнится тело вашего handler-а.

АспектSend от MediatRПростой вызов интерфейса
Разрешение типа в handlerПоиск в словаре на вызовНикакого, привязано при внедрении
Цепочка wrapper / делегатовАллоцируется на отправкуНикакой
Девиртуализация со стороны JITНетЧасто да
Сканирование сборок при запускеДа, растёт с числом handler-овНикакого
Порядок величины на вызовДесятки нс + небольшая аллокация~1 нс, ноль аллокаций

Методологическая честность здесь: на фоне handler-а, который обращается к базе данных или сети, десятки наносекунд незаметны, и вам стоит измерять собственные формы объектов с помощью BenchmarkDotNet, а не доверять обобщённой цифре. Стоимость, которая действительно проявляется на практике, - это запуск. Сканирование сборок для регистрации сотен handler-ов добавляет измеримые миллисекунды к холодному старту, что важно для serverless и относится к тому, с чем вы боретесь при сокращении времени холодного старта AWS Lambda на .NET 11. Явная простая регистрация полностью обходит это и дружелюбнее к trimming и Native AOT, потому что нет рефлексивного обнаружения, которое нужно держать безопасным для trimming.

Деталь, которая решает за вас

Несколько ограничений улаживают этот вопрос ещё до того, как в дело вступает архитектурный вкус.

RPL-1.5 - это настоящая решающая сила, а не цена. Бесплатный вариант с открытым исходным кодом в MediatR 13+ - это Reciprocal Public License 1.5, которая несёт взаимные обязательства copyleft: она спроектирована, чтобы закрыть лазейку SaaS, так что развёртывание сетевой службы, построенной на коде под лицензией RPL, может обязать вас сделать ваш исходный код доступным. Если вы поставляете закрытое коммерческое ПО и находитесь выше порога Community, бесплатная OSS-лицензия для вас на деле непригодна, и “MediatR всё ещё с открытым исходным кодом” в вашем случае вводит в заблуждение. Вы либо покупаете коммерческую лицензию, либо уходите. Для тонкого диспетчера, которым вы на самом деле не пользовались, уйти легко.

Граница в 5 млн дохода и 10 млн капитала щедра. Большинство небольших продуктовых команд, консалтинговых компаний и стартапов оказываются ниже неё и могут и дальше бесплатно использовать новейшую версию MediatR в рамках редакции Community. Если это про вас, лицензия - не повод что-либо вырывать; зарегистрируйте ключ Community, заглушите предупреждение и двигайтесь дальше. Потратить спринт на удаление MediatR, чтобы избежать счёта, который вы не должны, - неверный размен.

Зафиксировать 12.5.0 - реальный вариант с реальной ценой. Apache 2.0 на последней бесплатной версии не истекает. Но вы замораживаетесь на версии, которая не получает патчей безопасности, и наследуете риск сопровождения сами. Это нормально для стабильного внутреннего приложения и опасно для всего, что выставлено в интернет.

Если вы только когда-либо вызываете Send, медиатор вам не нужен. Честная проверка: откройте своё решение и поищите IPipelineBehavior и INotification. Если behaviors ноль и вы не публикуете уведомления, MediatR работает как слой косвенности поверх вызовов методов, и его удаление - механический рефакторинг, который делает код более навигируемым, убирает зависимость и стирает вопрос лицензии одним движением. Это тот же инстинкт рационализации библиотек, что стоит за выбором встроенного варианта в System.Text.Json против Newtonsoft.Json в 2026.

Решение, в одну строку

Для нового кода на .NET 11 в 2026 году пишите простые сервисные классы и внедряйте интерфейсы напрямую: вы получаете переход к определению, связывание при запуске, которое падает быстро, отсутствие накладных расходов на вызов, дружелюбие к trimming и нулевую лицензионную уязвимость. Сохраняйте MediatR только тогда, когда его pipeline behaviors выполняют реальную единообразную работу по множеству типов запросов, и в этом случае решайте осознанно: оставайтесь бесплатно в рамках редакции Community, если вы ниже 5 млн, платите, если вы выше и конвейер оправдывает счёт, и фиксируйте 12.5.0 только как временную меру. Ошибка - относиться к “MediatR стал коммерческим” либо как к не-событию, либо как к пожару пяти тревог. Это ни то ни другое. Это повод спросить себя, нужен ли вам был медиатор вообще, и для большинства кода диспетчеризации запросов ответ всегда был - нет.

Связанное

Источники

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад