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.0 | Mapperly 4.3.1 | Вручную | |
|---|---|---|---|
| Лицензия | копилефт RPL-1.5 или платная коммерческая | Apache 2.0 | никакой |
| Стоимость выше 5 000 000 USD выручки | от 799 до 6 399 USD в год | бесплатно | бесплатно |
| Как создаётся маппинг | рефлексия плюс скомпилированные деревья выражений при первом вызове | генератор исходного кода Roslyn во время компиляции | вы |
| Незамапленный член приёмника | молча, ловит только AssertConfigurationIsValid() | предупреждение RMG012, повышается до ошибки | компилятор тоже ничего не скажет |
| Незамапленный член источника | не сообщается вовсе | предупреждение RMG020 | не сообщается |
| Публикация с Native AOT | IL2104 плюс IL3053, падает при запуске | ноль предупреждений, работает | ноль предупреждений, работает |
| Холодная стоимость первого маппинга | ~33 мс для 3 отображений | ~1 мс | 0 |
| Маппинг одного объекта | 105.79 ns | 60.44 ns | 58.48 ns |
| Проекция в EF Core | ProjectTo с явным раскрытием, параметрами и глубиной рекурсии | сгенерированная проекция 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
- Всё, что публикуется с trimming или Native AOT. Это не предпочтение, а жёсткий барьер. См. раздел про AOT ниже.
- Serverless и короткоживущие процессы. Mapperly ничего не стоит при запуске, потому что нет объекта конфигурации, который нужно построить.
- Кодовые базы, где расхождение DTO - реальный риск. Новая колонка в сущности, которую никто не добавил в DTO, даёт
RMG020во время компиляции. AutoMapper об этом вообще не упомянет. - Команды, которые хотят читать маппинг. Mapperly пишет файл
.g.cs, который можно открыть, сравнить и пройти в отладчике.
Когда писать маппинг вручную
- Маленькая поверхность. Меньше примерно двадцати отображений - и статический метод
ToDtoна тип оказывается меньшей машинерией, чем генератор со своим словарём атрибутов, и он никого не удивляет. - Формы, которые действительно различаются. Когда большинству членов нужны
MapFrom,IValueResolverили условная логика, обе библиотеки вырождаются в худшую запись того метода, который вы всё равно собирались написать. - Контракты публичного API. DTO, которые являются версионируемым форматом передачи, заслуживают явного, проверяемого маппинга, где каждое присваивание поля видно в диффе.
- Любой слой, где вам нужен ноль зависимостей сборки. Mapperly - генератор исходного кода, поэтому он участвует в вашей сборке; статический метод - нет.
Когда оставаться на AutoMapper
- Большая кодовая база на EF Core, построенная вокруг
ProjectTo. Расширения queryable в AutoMapper поддерживают явное раскрытие, параметризацию во время выполнения через анонимные объекты,RecursiveQueriesMaxDepthдля самоссылающихся моделей и полиморфный маппинг. Проекции Mapperly покрывают обычный случай, но явно не поддерживают object factories, стратегии перечисленийByName, обработку ссылок и глубокое клонирование, и сообщаютRMG068, когда не могут встроить пользовательский метод. - Вы ниже порога Community, и отображения уже работают. Переписывать 200 работающих отображений ради экономии 45 ns на вызов - это не бизнес-обоснование.
- Динамический, нетипизированный маппинг. У
mapper.Map(source, sourceType, destType)нет эквивалента, порождаемого во время компиляции. Если у вас система плагинов, которая находит типы во время выполнения, AutoMapper делает то, чего Mapperly структурно не может.
Если вы решите уходить, механика расписана по шагам в статье миграция с 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_Single | 58.48 ns | 1.00 | 624 B | 1.00 |
| Mapperly_Single | 60.44 ns | 1.03 | 624 B | 1.00 |
| AutoMapper_Single | 105.79 ns | 1.81 | 704 B | 1.13 |
| HandWritten_1000 | 72,696 ns | 1.00 | 632,091 B | 1.00 |
| Mapperly_1000 | 77,334 ns | 1.06 | 672,093 B | 1.06 |
| AutoMapper_1000 | 103,376 ns | 1.42 | 720,640 B | 1.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 и диагностика во время компиляции.
Похожие статьи
- Миграция с AutoMapper на маппинг с генерацией исходного кода через Mapperly
- Решение: ‘MapperConfiguration’ не содержит конструктор, принимающий 1 аргументов
- MediatR против простых сервисных классов в 2026 году
- Что такое генератор исходного кода и когда он нужен?
- Что такое Native AOT и во что он вам обходится?
Источники
- Licensing FAQ - Lucky Penny Software - граница на 15.0.0, пороги Community в 5 000 000 USD выручки и 10 000 000 USD капитала, и как считаются разработчики.
- AutoMapper LICENSE.md - текст двойной лицензии RPL-1.5 или коммерческой.
- Документация по настройке лицензии AutoMapper - обнаружение
AUTOMAPPER_LICENSE_KEYиLUCKYPENNY_LICENSE_KEYи модель принуждения только через логи. - AutoMapper Queryable Extensions - явное раскрытие в
ProjectTo, параметризация и ограничение “должен быть последним вызовом в цепочке”. - Проекции queryable в Mapperly - список неподдерживаемых возможностей и диагностика встраивания
RMG068. - Диагностики анализатора Mapperly -
RMG012,RMG020и повышение уровня в.editorconfig. - Riok.Mapperly в NuGet - дата выпуска 4.3.1 и лицензия Apache 2.0.
- AutoMapper в NuGet - дата выпуска 16.2.0 и история версий.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.