Возможно вам не нужен AutoMapper

d0b4838c344185429563558f51632046.png

Вы знали, что AutoMapper и MediatR создал один и тот же человек?

Джимми Богард создал две крайне обсуждаемые и спорные темы в .NET разработке. Если с MediatR уже разобрались, то c AutoMapper также хотелось бы расставить все точки над «ё».

В этой статье хочу поговорить об истории возникновения библиотеки. О том какую задачу она была призвана решать изначально. И уделить внимание её недостаткам.

В интернете часто можно встретить такое мнение:

He got a point!He got a point!

Или такие восклицания о помощи:

Так начинается типичный тред про AutoMapper на RedditТак начинается типичный тред про AutoMapper на Reddit

Начнём с ответа на вопрос:

Что хотел сказать автор?

Стоит уделить внимание тому, в какие времена родилась библиотека. На дворе был конец нулевых. Только начиналась эпоха MVC фреймворков. На свет уже появились:

  • Ruby on Rails;

  • Django;

  • ASP.NET MVC.

При этом, в отличие от первых двух, для детища Microsoft не было ни советов, ни руководств по поводу того, что обозначает буква «M». Разработчики были вольны проектировать модели как угодно, от того было сложно выбирать. Сделать модель сущностью? А может объектом доступа к данным (data access object)? Или DTO подойдёт?

Поэтому, каждая команда разработки самостоятельно определяла правила по созданию моделей. Команда Джимми решила делать модели с привязкой к представлениям, получив некий ViewModel (не путать с МVVM). Поэтому их правила были следующими:

  1. Все View строго типизированы.

  2. Соответствие ViewModel-View — один к одному (для каждого View свой ViewModel).

  3. View определяет структуру ViewModel. Во ViewModel передаётся только то, что должно быть отображено на View.

  4. ViewModel содержит данные и поведение относящиеся только к своему View.

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

То и дело стреляли NRE, было невыносимо писать тесты, а в планах тем временем ещё больше экранов. Возможно, даже до тысячи. Так и появился на свет AutoMapper — как решение задачи по расчистке этого бардака.

Философия Automapper

Появившийся инструмент сделал следующие вещи:

  • Обязал типы, в которые происходит отображение, соблюдать некоторую конвенцию;

  • Спрятал возникновение NRE так сильно, насколько это возможно;

  • Предоставил возможность простого тестирования подобного функционала.

На чём это базируется? Лучше автора библиотеки не скажет никто:

AutoMapper works because it enforces a convention. It assumes that your destination types are a subset of the source type. It assumes that everything on your destination type is meant to be mapped. It assumes that the destination member names follow the exact name of the source type. It assumes that you want to flatten complex models into simple ones.

All of these assumptions come from our original use case — view models for MVC, where all of those assumptions are in line with our view model design. With AutoMapper, we could enforce our view model design philosophy.

And this is why our usage of AutoMapper has stayed so steady over the years — because our design philosophy for view models hasn’t changed.

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

Соответственно, возникает логичный вопрос:

Вашему проекту это подходит?

Мне кажется, что причина большинства негативных постов про библиотеку, заключается в том, что ответом на этот вопрос было «нет». Но его никто не задавал.

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

Думаю, такой пример кода не стоит предъявлять в текстовом виде. А то, не дай Бог, скопируютДумаю, такой пример кода не стоит предъявлять в текстовом виде. А то, не дай Бог, скопируют

Есть специальный чек-лист прямиком от Джимми Богарда, с помощью которого можно проверить, правильно ли вы используете библиотеку.

Однако, даже если всё делается по назначению, то, стоит упомянуть, что

AutoMapper не лишён недостатков

Давайте детально, по пунктам, аргументируем этот тезис.

Обманчивый статический анализ

Статический анализатор сообщает только о том, что некоторые поля модели не используются вообще. Можно пометить их атрибутом [UsedImplicitly], но это лишь попытка уйти от проблемы.

Код, который не задействован ни в какой бизнес-логике, а лишь декларирует данные, гоняющиеся туда-сюда, нельзя удалить по подсказке IDE. Мы сломаем приложение и узнаем об этом лишь после запуска.

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

Беспомощность навигации кода

Невозможно выяснить какое поле сущности отображается в какое-то другое поле DTO. Дело в том, что кнопка "show usages" не покажет ничего, кроме объявления поля.

AutoMapper ведь работает неявно.

Решением проблемы могло бы быть написание явных конфигураций отображения. Однако, нужен ли весь этот комплексный механизм, основанный на рефлексии, в таком случае?

Сложность отладки

Или практически невозможность.

Понятно, что в случае неявного маппинга что-то фурычит под капотом библиотеки, и разработчику это никак не отследить. Он может только взглянуть на результат и сравнить с описанным ожидаемым поведением.

А вдруг мы решим явно описать некую конфигурацию отображения? Грубо говоря, это просто жонглирование методами ForMember и MapFrom. Давайте взглянем на сигнатуру одного из них, она будет выглядеть так:

void MapFrom(Expression> sourceMember);

То есть, опять же, это не вычисляемый код, а код, описывающий поведение, потому что на вход ожидается выражение, а не делегат. Даже негде поставить точку останова. И исключение не получится отловить тоже. Например, у нас есть две модельки UserEntity и UserDTO:

public class UserEntity
{
    public string FirstName { get; set; }
    
    public string LastName { get; set; }
    
    public Address Address { get; set; }
}

public class Address
{
    public string City { get; set; }
}

public class UserDTO
{
    public string FullName { get; set; }
}

И вот мы предъявляем некую конфигурацию маппинга:

Mapper.Initialize(cfg =>
{
    cfg.CreateMap().ForMember(
        x => x.FullName,
        opt => opt.MapFrom(x =>
            $"{x.FirstName} {x.LastName} ({x.Address.City})")
    );
});

Тогда при предъявлении объекта, который ну точно должен вызвать NRE:

var userEntity = new UserEntity()
{
    FirstName = "Cezary",
    LastName = "Piątek",
    Address = null
};
var userDto = Mapper.Map(userEntity);
Console.WriteLine(JsonConvert.SerializeObject(userDto, Formatting.Indented));

мы получим не NRE:

{
    FullName : null
}

Из-за повсеместного использования выражений и рефлексии в библиотеке возникает и другой интересный кейс. Возьмём следующий кусок кода:

using AutoMapper;
using System;

var config = new MapperConfiguration(cfg =>
{
    cfg.CreateMap()
        .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Name.ToLower()));
});
var mapper = config.CreateMapper();

var source = new UserSource("VASYA");

var destination = mapper.Map(source);

Console.WriteLine(destination);

public record UserSource(string Name);
public record UserDestination(string Name);

При конструировании объекта destination поле Name смаппится, а функция ToLower применена не будет:

UserDestination { Name = VASYA }

Слом организации кода

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

И если раньше это была пара ручек, выполняющих SELECT'ы без JOIN'ов, то обязательно всё начнётся с реализации чего-то из следующего:

  1. Форматирование;

  2. Композиция одного большого объекта из нескольких маленьких;

  3. Бизнес-логика, влияющая на условия отображения данных;

  4. Ролевая модель.

Список можно продолжать и дальше, но суть в том, что пользователи AutoMapper склонны к расположению подобной логики внутри конфигурации маппинга, так как это самый быстрый способ достигнуть результата, при условии, что новые фичи влияют на этот процесс.

Впрочем, о том, что это неправильно говорил и автор библиотеки, о чём было упомянуто ранее в статье. Но, хаос в кодовых базах всё ещё продолжает появляться.

Низкая производительность

Чтобы не быть голословным, я написал простейший бенчмарк. Код доступен на GitHub.

Я сравнил быстродействие AutoMapper и другого, одного из альтернативных, способов создания конвертации объекта — метода расширения:

public static class UserModelExtensions
{
    public static User ToUser(this UserModel model) =>
        new(model.FirstName, model.LastName, model.BirthDate, model.Address.ToAddress());
}

public static class AddressModelExtensions
{
    public static Address ToAddress(this AddressModel model) =>
        new(model.Latitude, model.Longitude);
}

Сравнивал в двух кейсах:

  1. Маппинг объекта в объект;

  2. Маппинг списка размером в 10 000 элементов в другой список.

Результаты на картинке снизу:

M1, кстати, очень шустрый. На intel i5 8-го поколения (уже с hyperthreading) разрыв между AutoMapper и extension method, в кейсе List, был 5-кратным против 3-кратного здесь.M1, кстати, очень шустрый. На intel i5 8-го поколения (уже с hyperthreading) разрыв между AutoMapper и extension method, в кейсе List, был 5-кратным против 3-кратного здесь.

Влияние на размер сборки

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

У меня есть pet project, в котором я отказался от AutoMapper. При этом, размер релизной сборки уменьшился почти на мегабайт!

До удаления - 1.7 MBДо удаления — 1.7 MBПосле удаления - ~795 KBПосле удаления — ~795 KB

Выводы

Не зря говорят:»явное лучше неявного». Вот и рассмотрев AutoMapper под лупой, мы в очередной раз убедились в этом.

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

Оставив AutoMapper позади, как пережиток прошлого, можно обратить внимание на другие способы создания конвертеров. Ими могут быть как старые добрые нативные инструменты языка программирования C# вроде методов расширения, так и новые решения по кодогенерации, ставшие возможными благодаря развитию платформы.

Материалы

Ещё я веду telegram канал StepOne, где оставляю много интересных заметок про коммерческую разработку и мир IT глазами эксперта.

© Habrahabr.ru