Найти в Дзене

Сравнение AutoMapper и Mapster

Когда мы читаем/записываем/обрабатываем данные в приложении, то часто нужно переместить информацию между разными слоями приложения (прочитать из БД entity, преобразовать её в модель для api и отдать пользователю) или преобразовать данные в формат системы (при интеграциях). Всё это сводится к преобразованию объектов одного (исходного) типа в объекты другого (целевого) типа. Для этого нужно сопоставлять наборы свойств, а часто и сложных объектов, содержащих другие объекты в качестве свойств. По мере роста приложения таких типов и преобразований становится всё больше, а код конвертации растекается по всему проекту и становится сложным в поддержке. Использование автоматизированных инструментов преобразования объектов (object-object mapping) может помочь в организации кода и отделении ответственности за преобразования в отдельный изолированный уровень приложения. AutoMapper — самая популярная библиотека для маппинга объектов в dotnet — NuGet-пакет скачали больше 313 миллионов раз за 11 лет
Оглавление

Когда мы читаем/записываем/обрабатываем данные в приложении, то часто нужно переместить информацию между разными слоями приложения (прочитать из БД entity, преобразовать её в модель для api и отдать пользователю) или преобразовать данные в формат системы (при интеграциях). Всё это сводится к преобразованию объектов одного (исходного) типа в объекты другого (целевого) типа.

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

Использование автоматизированных инструментов преобразования объектов (object-object mapping) может помочь в организации кода и отделении ответственности за преобразования в отдельный изолированный уровень приложения.

AutoMapper — самая популярная библиотека для маппинга объектов в dotnet — NuGet-пакет скачали больше 313 миллионов раз за 11 лет существования библиотеки.

Mapster появился на 4 года позже AutoMapper и имеет 8.2 миллионов загрузок на nuget.org. Популярность отличается больше, чем на порядок, так зачем бы вообще смотреть на альтернативу AutoMapper? Дело в том, что Mapster обещает лучшую производительность и меньший объем памяти по сравнению с другими библиотеками маппинга объектов, поэтому стоит по крайней мере рассмотреть использование этой библиотеки и понять возможности для замены автомаппера на мапстер.

Маппинг простых моделей

Давайте проверим сценарий, когда исходный и целевой типы имеют одинаковый набор свойств с одинаковыми именами, но различаются по типу свойств. Исходный тип User:

-2

Тип, в который мы будем преобразовывать юзера — UserDto:

-3

Мы видим, что единственное различие типов свойств в том, что CreatedAt для User имеет тип DateTime, а для UserDto тип string. В подобных ситуациях библиотеки маппинга выполняют неявное приведение типов.

Чтобы использовать AutoMapper, нам сначала нужно создать объект IMapper. Существует несколько способов его создания, например, можно использовать класс MapperConfiguration. Для этого достатчно указать исходный и целевой типы в качестве generic-параметров в методе CreateMap<TSource, TDestination>(). В нашем случае это User и UserDto:

-4
В продакшен-коде, скорее всего, вы будете использовать профили Автомаппера чтобы лучше организовать код и разделить ответственности, но для примера нам достаточно упрощенного механизма создания.

Создадим объект исхоного типа и преобразуем его в объект целевого типа с помощью IMapper:

-5

Чтобы смаппить объект в новый тип в AutoMapper нам нужно передать исходный объект в качестве параметра метода Map, а generic-параметром указать целевой тип для преобразования.

Если мы проверим значения переменной destination, то все значения свойств исходного объекта будут совпадать с значениями свойств объекта целевого типа и значение свойства CreatedAt будет неявно преобразовано к типу string:

-6

В Mapster всё ещё проще. Для типов с совпадающими свойствами, где возможно неявное преобразование типов свойств, мы можем напрямую вызвать extension-метод Adapt<TDestination>(this object source) для объекта исходного типа с generic-параметром целевого типа:

-7

Ещё с помощью Mapster мы можем заполнить поля существующего объекта целевого типа из объекта исходного типа:

-8
Mapster предоставляет и другие способы для преобразования: collection.ProjectToType() для преобразования IQueryable коллекций, экземпляр IMapper для DI, source generator для явной реализации мапперов.

В этом примере значения объекта целевого типа будут совпадать с теми, что получены с помощью AutoMapper.

Маппинг сложных моделей

Чаще всего польза маппинга раскрывается, когда мы начинаем преобразовывать сложные объекты, свойства которых содержат другие объекты, которые тоже нужно преобразовывать. Чтобы проиллюстрировать такой пример добавим ещё один тип для адреса:

-9

И добавим свойство с типом Address в тип User:

-10

Для простоты примера типы UserDto и AddessDto будут содержать аналогичный набор свойств.

Библиотеки маппинга должны преобразовывать все типы на любом уровне на своем пути (в том числе приводить типы неявно, если такое преобразование существует). В нашем случае необходимо сопоставить и User, и Address с соответствующими целевыми типами.

В AutoMapper нам понадобится при создании MapperConfiguration задать преобразование для всех типов, которые будут участвовать в маппинге:

-11

Код самого преобразования останется без изменений:

-12

В Mapster предварительная конфигурация всё ещё не нужна — поскольку исходный и целевой типы имеют одинаковые свойства, а свойство Address в целевом типе имеет тип AddressDto, который имеет набор свойств аналогичный (или неявно преобразуемый) типу Address. Поэтому всё ещё достаточно вызывать метод-расширение Adapt:

-13

Маппинг коллекций

Часто нам нужно маппить список или массив одного типа в другой. Для этого нам просто нужно указать в качестве generic-параметра метода маппинга List<TDestination>, TDestination[] или что-то подобное. Каких-то других специальных настроек для работы этой функции не требуется.

В AutoMapper нам все еще нужно указать конфигурацию маппинга для типов элеметов коллекций:

-14

В Mapster мы можем напрямую вызвать метод Adapt, указав в качестве generic-параметра List<TDestination>:

-15

Настройка маппинга свойств

Перейдём к более сложным сценариям — например, когда набор свойств исходного и целевого типов не совпадают и нам нужно настроить пользовательское преобразование. Для примера сделаем тип пользователя с именем и фамилией и целевой тип с одним свойством полного имени:

-16

Библиотеки маппинга не могут неявно вывести правила для отображения свойств, поэтому нам нужно задать это отображение явно.

Для этого в AutoMapper метод CreateMap позволяет с помощью fluent interface задать дополнительные настройки маппинга свойств методом ForMember:

-17

В Mapster можно использовать статический класс TypeAdapterConfig для задания правил маппинга свойств. Указываем исходный и целевой типы generic-параметрами, создаем новую конфигурацию с помощью метода NewConfig() и используем похожий на Автомаппер fluent-интерфейс для задания маппинга свойств:

-18

Разворачиваем сложные модели (object flattening)

Мы можем сопоставить свойства вложенных объектов со свойствами верхнего уровня, используя простое соглашение об именовании. Например, вложенное свойство Address.ZipCode из исходного типа User может быть отображено на свойство AddressZipCode в целевом типе UserDto. Это позволит сделать плоскую модель из сложного исходного объекта.

-19

Такой же результат можно получить, если в исходном типе есть метод с именем Get(DestinationPropertyName). Например, метод GetFullName() будет сопоставлен со свойством FullName:

-20

Такие соглашения об именовании по-умолчанию работают и в AutoMapper, и в Mapster.

Двухсторонний маппинг в сложные модели (unflattening)

Библиотеки маппинга имеют функциональность настройки двухстороннего маппинга, когда из объекта целевого типа мы хотим получить объект исходного. В случае прямого преобразования из сложного объекта в плоский (flattening), обратное преобразование (Reverse Mapping) подразумевает получение сложной модели из плоского представления.

В AutoMapper это можно сделать с помощью метода ReverseMap() в конфигурации маппера:

-21

В Mapster из-за конфигурации по-умолчанию при совпадении свойств типов задавать возможность обратного преобразования не нужно. Если свойства типов отличаются и нужно задать двухстороннее преобразование явно, то можно использовать в конфигурации метод TwoWay(). Этот метод так же включает преобразование плоских моделей в сложные, которое не работает по-умолчанию:

-22

Конфигурация с помощью атрибутов

До сих пор мы рассматривали fluent-конфигурацию в коде для разных сценариев. Чаще всего используется именно этот способ, он позволяет отделить логику маппинга между типами от самих типов. Но AutoMapper и Mapster предоставляют ещё один способ конфигурирования преобразований — атрибуты для задания параметров маппинга.

В AutoMapper есть целый набор атрибутов для разных сценариев:  AutoMap,  Ignore,  ReverseMap,  SourceMember и другие:

-23

Кроме разметки типов атрибутами AutoMapper требует явно добавить маппинги из атрибутов с помощью метода AddMaps(), который принимает на вход сборку, в которой будет идти поиск типов с атрибутами для маппинга:

-24

Mapster предоставляет свой набор аналогичных атрибутов: AdaptTo,  AdaptFrom,  AdaptTwoWays,  AdaptMember и другие. Аналогичный предыдущему примеру код будет выгядеть так:

-25

Dependency Injection

AutoMapper предоставляет NuGet-пакет AutoMapper.Extensions.Microsoft.DependencyInjection, который позволяет добавить IMapper в IServiceCollection и сконфигурировать его с помощью метода AddAutoMapper. Есть много перегрузок, позволяющих добавить конфигурацию явно или подтянуть все настройки по сборкам.

-26

У Mapster похожий способ подключения — нужно добавить NuGet-пакет Mapster.DependencyInjection и зарегистрировать в контейнере типы TypeAdapterConfig и ServiceMapper:

-27

После этого мы можем использовать интерфейс IMapper в качестве зависимости:

-28

Сравнение производительности AutoMapper и Mapster

Для тестов производительности будем использовать BenchmarkDotNet. Исходный код бенчмарка и результаты есть на github.

Для подготовки данных используется NuGet-пакет Bogus, который умеет генерить данные подходящих типов:

-29

Почти все бенчмарки устроены одинаково — для подготовленного списка из 1000 элементов исходного типа они поэлементно маппят объекты на целевой тип:

-30

Есть тесты на простые модели, маппинг списков, сложные модели с вложенностью, flattening и unflattening, модели с настройкой маппера для определенных свойств, модели с разметкой атрибутами. Результаты бенчмарка для dotnet 7:

-31

Mapster превосходит AutoMapper по скорости работы во всех сценариях, на большинстве сценариев на 30-50%, а в некоторых сценариях больше, чем в 2 раза. При этом потребление памяти или не изменяется, или уменьшается.

Бонус. Сравнение производительности с ручным маппингом

На самом деле в стороннем бенчмарке из статьи есть сравнение как раз с ручным маппингом через явную реализацию метода. Правда, это сравнение кажется не совсем честным — например, там повсеместно используется LINQ, что очевидно будет замедлять код.

Без LINQ и на моих модельках результаты немного другие, Mapster вдвое медленнее на комплексных типах и чуть медленнее на простых на примерах из этой статьи:

-32

Выводы

Mapster — зрелая библиотека, которая имеет функциональность сопоставимую с AutoMapper. Во многих сценариях Mapster проще в настройке из-за того, что нет необходимости явно задавать исходный и целевой типы для моделей до тех пор, пока не требуется дополнительных настроек маппинга свойств. По производительности Mapster превосходит AutoMapper во всех сценариях, а в некоторых дает и выигрыш в количестве потребляемой памяти. Библиотека существует уже 7 лет, имеет много пользователей и продолжает развиваться. В разных бенчмарках Mapster занимает первое место по производительности среди библиотек object-object маппинга.

Это не значит, что нужно обязательно заменять AutoMapper на Mapster во всех своих проектах. Mapster — хорошая альтернатива, о которой полезно знать и уметь с ней работать.