Comments 38
1. Со временем, его «авто» отваливается и приходится всё описывать в профиле, который пишется сложнее, чем ручной маппинг.
2. Сам Automapper очень тормозной и если прям принципиально не хочется самому писать методы для маппинга — можно взять Mapster или использовать любую другую генерацию кода перед компиляцией. (а лучше писать самому — это в любом случае контролируется лучше)
Опять же если на примере с Person -> Student из статьи, то чем принципиально лучше вариант с Automapper перед этим?
public static class PersonExtensions {
public static Student ToStudent(this Person person) => new Student {
Fio = $"{person.FirstName} {person.LastName}",
Birthday = person.BirthDate
// ...
}
}
0 лишних зависимостей, никакой магии с Expressions, 0 оверхед, 0 дополнительных байт на хранение маппера в памяти, ещё и кода меньше нужно писать.
Ещё и во время компиляции проверяется, возможно ли в принципе смаппить таким образом Person к Student.
Помню, когда был на проекте с автомаппером — у каждого хотя бы иногда была ошибка в рантайме из-за того что профиль какой-то не тот, или в _mapper.Map передан какой-то не тот тип или объект.
Один раз даже на проде такое случилось в каком-то дальнем, редко используемом куске системы.
На текущем проекте встречаю классы с десятками свойств. Максимум было в районе 70. Замучаешься писать ручной маппинг.) И порой от одного класса нужно смапить несколько DTO.
Там использовался Automapper 10 версии, а сейчас уже 11, так что сейчас действительно может лучше.
Свои бенчмарки я не делал.
Но мне кажется вполне логичным, что вызов кода, который скомпилирован заранее, будет работать быстрее, чем вызов аналогичного кода, который генерируется в реальном времени через emit.
Если скажете, где это надо вызывать — вызову и сделаю новый бенчмарк :)
Ещё хотел попробовать хвалёный кодген от мапстера, но он что-то не хочетработать на .NET 6.
Теперь у меня есть бенчмарк, который подтверждает мои слова:
| Method | Mean | Error | StdDev |
|----------------------- |----------:|----------:|----------:|
| MapShortWithAutomapper | 93.737 ns | 1.1254 ns | 0.9397 ns |
| MapShortByHand | 5.744 ns | 0.0977 ns | 0.0913 ns |
| MapShortWithMapster | 38.592 ns | 0.3471 ns | 0.2898 ns |
| MapShortBySourceMapper | 6.629 ns | 0.1265 ns | 0.1457 ns |
MapShortBySourceMapper — это вариант с первым попавшимся маппером, который использует Source Generators.
Маппил классы с одним int32 свойством.
Mapster умеет генерировать классы во время сборки. Есть ещё Mapping Generator, он создает код прям студии.
Все ещё хуже: все эти мапперы делают свою магию под капотом, и то что какое-то поле используется совсем не видно find usage ide.
Типа атрибуты Ignore, или TypeConverter чтобы какой-нибудь string в enum переделать.
А можно пример, где это может понадобится?
Просто в большинстве приложений (а большинство приложений — это бэкенд на asp net core), поток данных заканчивается на JsonSerializer.Serialize где-то в недрах аспнета.
А у JsonSerializer и так есть и Ignore, и конвертация енамов, и прочее. А значит в случаях с маппингом без изменений имён и без изменения данных — сериализатор уже и так делает работу автомаппера.
А то что сам Automapper протестирован, по хорошему, не избавляет от необходимости покрывать маппинг тестами (чтобы подтвердить, что эта вся автоматика и профили делают именно то что нужно).
Если говорить про маппинг из таблиц в объекты, то в этом и Automapper не участвует — этим сам EF занимается, и у него есть аналогичные атрибуты.
Это неудобно с di.
Разные поля, проброс аргумента в метод, инжекция шифровальщиков или компрессоров. Всё это неудобно в автомаппер. Удобнее делать свой маппер на каждую интеграцию с перегруженными методами From To.
В документации automapper прямо сказано: если вам в профиле приходится прописывать более чем несколько правил или, упаси боже, какую-нибудь бизнес логику, вы его используете неправильно. Даже переименование полей лучше решать на уровне [JsonProperty].
Это отличная штука для избавления от рутины копирования одноименных полей между ДТОшками. Для всего остального -- это не самый изящный костыль
Лично я бы вместо него предпочёл какой-нибудь кодогенератор, который по двум объектам просто генерил бы сниппет с присваиванием их полей.
Если честно, то я немного недрлюбливаю Автомаппер, потому что он скрывает учтановку пропертей от Решарпера. Бывает ищешь что устанавливает пропертю (в Решарпере есть кнопка - показать все места, где пропертя устанавливается), и не находишь. Тратишь час или больше - и находишь Автомаппер. Без него нашлось бы сразу. Конечно, не раз ловил себя на том, что писать копирование большого объекта в другой лень, но, блин, оно того стоит.
Я сделал интерфейсы
public interface ICustomMap<TSource, TDestination>
{
void ConfigureMap(IMappingExpression<TSource, TDestination> mapping);
}
public interface IMapTo<TDestination>
{
}
public interface IMapFrom<TSource>
{
}
Использую их в моделях вот так
public class RoleAddEditModel :
ICustomMap<Role, RoleAddEditModel>,
ICustomMap<RoleAddEditModel, Role>
{
public string Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public void ConfigureMap(IMappingExpression<Role, RoleAddEditModel> mapping)
{
}
public void ConfigureMap(IMappingExpression<RoleAddEditModel, Role> mapping)
{
mapping.ForMember(e => e.Id, o => o.Ignore());
}
}
Сделал Setup метод, который пробегает по сборке и ищет все классы наследники этих трех интерфейсов и регистрирует маппинги. Метод вызываю в Startup.ConfigureServices.
public static class Setup
{
public static IServiceCollection SetupMapping(this IServiceCollection services, params Assembly[] assemblies)
{
var mappingTypeInfo = GetMappingTypeInfo(assemblies);
var mapperConfiguration = new MapperConfiguration(cfg =>
{
foreach (var item in mappingTypeInfo)
{
if (item.Type == MappingType.Default)
{
cfg.CreateMap(item.Source, item.Destination);
}
if (item.Type == MappingType.Custom)
{
var createMapMethod = typeof(IProfileExpression).GetMethod(nameof(IProfileExpression.CreateMap), new Type[0]);
var createMapMethodGeneric = createMapMethod.MakeGenericMethod(item.Source, item.Destination);
var configureMapMethod = typeof(ICustomMap<,>)
.MakeGenericType(item.Source, item.Destination)
.GetMethod(nameof(ICustomMap<object, object>.ConfigureMap));
var mapConfiguration = createMapMethodGeneric.Invoke(cfg, null);
configureMapMethod.Invoke(item.Instance, new[] { mapConfiguration });
}
}
});
services.AddSingleton<IMapper>(serviceProvider => new Mapper(mapperConfiguration));
return services;
}
private static List<MappingTypeInfo> GetMappingTypeInfo(Assembly[] assemblies)
{
var result = new List<MappingTypeInfo>();
foreach (var assembly in assemblies)
{
var types = assembly.GetExportedTypes();
var mappingTypes = from t in types
from i in t.GetInterfaces()
where i.IsGenericType && t.IsAbstract == false && t.IsInterface == false && (
i.GetGenericTypeDefinition() == typeof(IMapFrom<>) ||
i.GetGenericTypeDefinition() == typeof(IMapTo<>) ||
i.GetGenericTypeDefinition() == typeof(ICustomMap<,>))
select new { Type = t, Interface = i };
foreach (var item in mappingTypes)
{
if (item.Interface.GetGenericTypeDefinition() == typeof(IMapFrom<>))
{
result.Add(new MappingTypeInfo
{
Type = MappingType.Default,
Source = item.Interface.GetGenericArguments()[0],
Destination = item.Type,
});
continue;
}
if (item.Interface.GetGenericTypeDefinition() == typeof(IMapTo<>))
{
result.Add(new MappingTypeInfo
{
Type = MappingType.Default,
Destination = item.Interface.GetGenericArguments()[0],
Source = item.Type
});
continue;
}
if (item.Interface.GetGenericTypeDefinition() == typeof(ICustomMap<,>))
{
result.Add(new MappingTypeInfo
{
Type = MappingType.Custom,
Instance = Activator.CreateInstance(item.Type),
Source = item.Interface.GetGenericArguments()[0],
Destination = item.Interface.GetGenericArguments()[1]
});
continue;
}
}
}
return result;
}
}
И потом в классах бизнес-логики использую IMapper.
В итоге маппинги хранятся в модели. Хотя их можно хранить и в отдельных классах при необходимости, главное чтобы метод наследовал интерфейс ICustomMap / IMapTo / IMapFrom. И они автоматически регистрируются.
надеюсь когда нибудь .net сделает нативную эту хрень , хотябы для начала это полный каст из ребенка в родители .
Прям сегодня с этим столкнулся при записи в монго
Автомаппер никогда не должен был существовать. Именно ему мы обязаны лучшим примерам "слоистой архитектуры" с десятком слоев DTOшек. Не будь автомаппера, люди бы поленились перегонять раз за разом одни и те же данные из одних дто в другие. А так пожалуйста, порождают монстров
https://youtu.be/UIslFVEHkzA Хорошее видео на эту тему, пусть название вас не смущает. Тут довольно подробно обсуждается тема ручного vs автоматического маппинга и сравниваются бенчмарки всех популярных на рынке мапперов включая ручной. Лично у меня после этого видео открытых вопросов на эту тему не осталось.
У автомапера есть один плюс - ProjectTo
, крайне удобная штука.
Все остальное очень и очень плохо. Богард и его подручные не выносят критики или другого мнения. Захотели - добавили фичу, потом захотели убрали фичу, да еще и без всякого способа вернуть прежнее поведение, а вы господа делайте что хотите со своими мапингами (вы юзали не так и не то!). Самое интересное, что "тру вей" использования автомапера тоже никуда не годится - в большом проекте важно видеть где что и как используется, когда добавлено, магии должен быть минимум, но автомапер будет мапить как посчитает нужным. Я гарантирую, что в любом достаточно большом проекте с автомапером (где пользуются рекомендациями Богарда) есть баг в мапинге и не один.
AutoMapper: добавление и использование в проекте ASP.Net Core