Start Debugging

AutoMapper против Mapperly и написанного вручную маппинга в 2026 году

Mapperly - выбор по умолчанию для нового кода .NET: он не уступает по скорости ручному маппингу, переживает Native AOT и находит незамапленные члены во время компиляции. AutoMapper по-прежнему выигрывает в ProjectTo. С бенчмарками и лицензионными порогами.

Для нового кода .NET в 2026 году используйте Mapperly. Он генерирует обычный C# во время компиляции, работает в пределах 3% от написанного вручную маппинга, чисто публикуется под Native AOT и превращает забытое свойство в диагностику компилятора вместо молчаливо пустой строки. Пишите маппинг вручную, когда в проекте меньше примерно двадцати отображений или когда формы источника и приёмника действительно расходятся. Оставайтесь на AutoMapper только тогда, когда ProjectTo несёт основную нагрузку в большой кодовой базе на EF Core и вы попадаете в бесплатный уровень Community, потому что выше 5 000 000 USD годовой выручки лицензия превращает решение в заявку на закупку.

Все числа ниже измерены на Apple M4 (10 ядер) с .NET SDK 10.0.302 и целевой платформой net10.0, с AutoMapper 16.2.0 (выпущен 2026-07-02), Riok.Mapperly 4.3.1 (выпущен 2025-12-22) и BenchmarkDotNet 0.15.8.

Матрица

AutoMapper 16.2.0Mapperly 4.3.1Вручную
Лицензиякопилефт RPL-1.5 или платная коммерческаяApache 2.0никакой
Стоимость выше 5 000 000 USD выручкиот 799 до 6 399 USD в годбесплатнобесплатно
Как создаётся маппингрефлексия плюс скомпилированные деревья выражений при первом вызовегенератор исходного кода Roslyn во время компиляциивы
Незамапленный член приёмникамолча, ловит только AssertConfigurationIsValid()предупреждение RMG012, повышается до ошибкикомпилятор тоже ничего не скажет
Незамапленный член источникане сообщается вовсепредупреждение RMG020не сообщается
Публикация с Native AOTIL2104 плюс IL3053, падает при запускеноль предупреждений, работаетноль предупреждений, работает
Холодная стоимость первого маппинга~33 мс для 3 отображений~1 мс0
Маппинг одного объекта105.79 ns60.44 ns58.48 ns
Проекция в EF CoreProjectTo с явным раскрытием, параметрами и глубиной рекурсиисгенерированная проекция IQueryable, несколько возможностей не поддерживаютсянапишите Select
Map(object, type) во время выполненияданетнет
Отлаживаемый выводскомпилированное дерево выраженийчитаемый .g.cs, в который можно зайти отладчикомваш собственный код

Лицензия - это ось, на которой держится всё остальное

2025-07-02 Джимми Богард передал AutoMapper и MediatR компании Lucky Penny Software и перелицензировал оба проекта. AutoMapper 15.0.0 и новее поставляются по двойной модели: Reciprocal Public License 1.5 для открытого использования или платная коммерческая лицензия. Версия 14.x и старше навсегда остаются под MIT.

RPL-1.5 - это не MIT с дополнительными шагами. Это сильный реципрокный копилефт, который распространяется на развёрнутое, а не только на распространяемое программное обеспечение, поэтому коммерческие продукты с закрытым кодом реально не могут поставляться на сборке RPL. Остаётся коммерческое соглашение, бесплатный уровень Community которого покрывает организации с валовой годовой выручкой менее 5 000 000 USD, привлёкшие менее 10 000 000 USD внешнего капитала и не относящиеся к государственным, квазигосударственным структурам или высшему образованию. Выше этой границы опубликованные уровни такие: Standard за 799 USD в год на 1-10 разработчиков, Professional за 1 499 USD в год на 11-50 и Enterprise за 6 399 USD в год без ограничения по числу разработчиков. Считаются только разработчики, которые активно пишут или сопровождают код, вызывающий библиотеку, то есть без QA, дизайнеров и фронтенда.

Принуждение к лицензии намеренно мягкое. Нет ни сервера лицензий, ни сетевых вызовов, ни блокировки возможностей. Отсутствующий или просроченный ключ даёт сообщение в логе и больше ничего, а начиная с 16.2.0 ключ можно задать переменными окружения AUTOMAPPER_LICENSE_KEY или LUCKYPENNY_LICENSE_KEY вместо cfg.LicenseKey. Но мягкое принуждение - это не то же самое, что разрешение, и “мы не заметили предупреждения в логах” не та лицензионная позиция, которую кто-то захочет защищать на закупочной проверке.

Это та же самая развилка, что и с библиотеками-медиаторами, и рассуждение переносится напрямую: см. MediatR против простых сервисных классов в 2026 году для полного разбора уровня Community и обязательств RPL-1.5.

Когда выбирать Mapperly

Когда писать маппинг вручную

Когда оставаться на AutoMapper

Если вы решите уходить, механика расписана по шагам в статье миграция с AutoMapper на маппинг с генерацией исходного кода через Mapperly.

Бенчмарк

Модель - это Order с пятью скалярными членами, вложенным Customer, пятью дочерними OrderLine и перечислением, отображаемым в строковое имя. [MemoryDiagnoser], job по умолчанию, компиляция выражений AutoMapper прогрета в [GlobalSetup], чтобы измерялась пропускная способность в установившемся режиме, а не стоимость первого вызова.

// .NET SDK 10.0.302, net10.0, C# 14
// AutoMapper 16.2.0, Riok.Mapperly 4.3.1, BenchmarkDotNet 0.15.8
[MemoryDiagnoser]
public class MappingBenchmarks
{
    private Order _order = null!;
    private List<Order> _orders = null!;
    private IMapper _autoMapper = null!;
    private OrderMapper _mapperly = null!;

    [GlobalSetup]
    public void Setup()
    {
        _order = MakeOrder(1);
        _orders = Enumerable.Range(1, 1000).Select(MakeOrder).ToList();

        var config = new MapperConfiguration(
            cfg => cfg.AddProfile<OrderProfile>(),
            NullLoggerFactory.Instance);
        _autoMapper = config.CreateMapper();
        _mapperly = new OrderMapper();

        _autoMapper.Map<OrderDto>(_order); // warm the expression compilation
    }

    [Benchmark(Baseline = true)]
    public OrderDto HandWritten_Single() => HandMapper.ToDto(_order);

    [Benchmark]
    public OrderDto Mapperly_Single() => _mapperly.ToDto(_order);

    [Benchmark]
    public OrderDto AutoMapper_Single() => _autoMapper.Map<OrderDto>(_order);
}

Результаты на Apple M4, 10 физических ядер, .NET 10.0.10 Arm64 RyuJIT:

МетодСреднееRatioВыделеноОтношение выделений
HandWritten_Single58.48 ns1.00624 B1.00
Mapperly_Single60.44 ns1.03624 B1.00
AutoMapper_Single105.79 ns1.81704 B1.13
HandWritten_100072,696 ns1.00632,091 B1.00
Mapperly_100077,334 ns1.06672,093 B1.06
AutoMapper_1000103,376 ns1.42720,640 B1.14

Прочтите это честно: 45 наносекунд на объект - не причина переходить. На запросе, который маппит 1 000 заказов, вся разница составляет 31 микросекунду, что не будет заметно рядом с одним обращением к базе данных. Аргумент производительности начинает что-то значить только при очень больших количествах объектов, и это самая слабая из трёх причин предпочесть Mapperly.

Разрыв в 40 000 байт между Mapperly и ручным кодом в случае с 1 000 объектов - реальный артефакт, который стоит понять. Mapperly расширяет параметр сгенерированного маппера вложенной коллекции до IReadOnlyCollection<T>:

// Riok.Mapperly 4.3.1 generated output, trimmed
private List<OrderLineDto> MapToListOfOrderLineDto(IReadOnlyCollection<OrderLine> source)
{
    var target = new List<OrderLineDto>(source.Count);
    foreach (var item in source)
        target.Add(MapToOrderLineDto(item));
    return target;
}

Перебор List<T> через интерфейс упаковывает её структурный энумератор: 40 байт на заказ, 40 000 байт на всю партию. Если объявить маппер вложенной коллекции самостоятельно с конкретным параметром List<OrderLine>, это исчезает. Именно такие вещи можно найти и исправить, потому что сгенерированный код лежит на диске, и в этом практическая разница между генератором исходного кода и скомпилированным деревом выражений.

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

Опубликуйте консольное приложение, вызывающее AutoMapper 16.2.0, с <PublishAot>true</PublishAot> на net10.0, и сборка выдаст предупреждения:

AutoMapper.dll : warning IL2104: Assembly 'AutoMapper' produced trim warnings.
AutoMapper.dll : warning IL3053: Assembly 'AutoMapper' produced AOT analysis warnings.

Предупреждения легко проигнорировать. Полученный бинарник - нет:

Unhandled exception. System.TypeInitializationException: A type initializer threw an exception.
 ---> System.ArgumentNullException: Value cannot be null. (Parameter 'method')
   at System.Linq.Expressions.Expression.Call(MethodInfo, Expression)
   at AutoMapper.Execution.ExpressionBuilder..cctor()
   at AutoMapper.MapperConfiguration..ctor(MapperConfigurationExpression, ILoggerFactory)

Триммер удалил метод, который ExpressionBuilder ищет рефлексией, поэтому статический конструктор умирает до вашего первого маппинга. Эквивалентное приложение на Mapperly, опубликованное с теми же настройками, выдаёт ноль предупреждений IL, даёт нативный бинарник в 1.1 MB и работает. Это не проблема настройки, которую можно решить атрибутами DynamicDependency в месте вызова; это свойство построения отображений из деревьев выражений во время выполнения, та же ловушка, что описана в статье что такое trim-safe код и как его писать. Если Native AOT есть в вашей дорожной карте, решение уже принято.

Более мягкая версия того же эффекта - холодный старт. Построение конфигурации и первый маппинг для трёх типов заняли на этой машине 33 миллисекунды против 1 миллисекунды для new OrderMapper() с его первым вызовом. В долгоживущем веб-приложении это незаметно. В Lambda это измеримая доля холодного вызова, поэтому это всплывает в статье как сократить время холодного старта .NET-функции AWS Lambda.

Где разница в безопасности проявляется на деле

Добавьте свойство Slug в DTO и забудьте его замапить. AutoMapper 16.2.0 всё равно отобразит объект:

map ok: Id=1 Name=n Slug=''

AssertConfigurationIsValid() действительно это ловит, бросая AutoMapperConfigurationException с текстом “Unmapped members were found”, но только если вы вспомнили его вызвать, и только для незамапленных членов приёмника. Свойство источника, которое больше не попадает ни в один DTO, не сообщается вообще.

Mapperly сообщает об обоих направлениях во время компиляции, с реальным текстом сообщения:

warning RMG020: The member InternalNote on the mapping source type Diag.Source
                is not mapped to any member on the mapping target type Diag.Target
warning RMG012: The member Slug on the mapping target type Diag.Target
                was not found on the mapping source type Diag.Source

По умолчанию это предупреждения, а значит, они утонут в шумной сборке. Повысьте их в .editorconfig, и сборка упадёт целиком:

[*.cs]
dotnet_diagnostic.RMG012.severity = error
dotnet_diagnostic.RMG020.severity = error

Именно эта настройка превращает Mapperly из “более быстрого AutoMapper” в инструмент другой категории: ошибки маппинга перестают быть инцидентами в продакшене и становятся ошибками сборки. Это же самая наглядная иллюстрация того, почему генераторы исходного кода стоят зависимости во время сборки.

Написанный вручную маппинг, справедливости ради, никакой такой проверки не даёт. Забытое присваивание в методе ToDto ровно так же молчаливо, как и в AutoMapper. Его безопасность идёт от видимости на код-ревью, а не от инструментов.

Итоговое решение

По умолчанию берите Mapperly для нового кода и с первого дня повышайте RMG012 и RMG020 до ошибок, чтобы действительно получить выгоду. Пишите маппинг вручную, когда проект небольшой или формы нерегулярны, и примите, что вы меняете проверки инструментов на удобство ревью. Оставайтесь на AutoMapper, когда зрелая кодовая база, плотно построенная на ProjectTo, уже работает, вы ниже порога Community, а Native AOT нет в дорожной карте; и как только любое из этих трёх условий перестаёт выполняться, начинайте миграцию, а не закладывайте лицензию в бюджет. Таблица производительности - наименее интересная часть этого сравнения. Реально поведение кодовой базы меняют безопасность при trimming и диагностика во время компиляции.

Похожие статьи

Источники

Comments

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

< Назад