При работе с данными (и не только) нам часто приходится сталкиваться с необходимостью копирования (маппинга) значений свойств одного объекта в новый объект другого типа.
Например предположим, что запрос к базе данных возвращает нам запись в виде объекта, представленного классом Person:
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
}
Далее нам необходимо создать новый объект, представленный классом Student:
public class Student
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
public DateTime AdmissionDate { get; set; }
}
и скопировать в его свойства данные из свойств полученного из БД объекта.
Без помощи сторонней библиотеки нам пришлось бы сделать это самим:
// получаем запись из БД
var person = _dbRepository.GetPerson(1);
// копируем значения свойств (осуществляем маппинг)
var student = new Student
{
FirstName = person.FirstName,
LastName = person.LastName,
BirthDate = person.BirthDate
};
А с использованием библиотеки AutoMapper, маппинг производится всего одной строкой кода:
var student = _mapper.Map<Student>(person);
1. Подключение библиотеки AutoMapper к проекту и ее использование
Шаг 1
Добавление в проект NuGet пакетов:
· AutoMapper
· AutoMapper.Extensions.Microsoft.DependencyInjection
Для этого в SolutionExplorer (Обозреватель решений) правой кнопкой мыши жмем по названию рабочего проекта и выбираем Manage NuGet Packages… (Управление пакетами Nuget).
Далее переходим на крайнюю левую вкладку Browse, и в строку поиска вводим название устанавливаемого пакета NuGet.
В левом окне выбираем нужный нам пакет, а в правом жмем кнопку Install.
Дожидаемся окончания установки.
Проделываем эти действия для обоих пакетов.
Шаг 2
Добавляем в проект класс AppMappingProfile:
public class AppMappingProfile : Profile
{
public AppMappingProfile()
{
CreateMap<Person, Student>();
}
}
В generics метода CreateMap первым передаем тип-источник значений, вторым – тип-приемник.
Т.е. в данном примере мы задаем маппинг из объекта Person в объект Student.
Если мы хотим, чтобы маппинг работал в обоих направлениях, добавляем вызов метода-расширения ReverseMap():
CreateMap<Person, Student>().ReverseMap();
Теперь мы можем также маппить объект student в объект person:
var person = _mapper.Map<Person>(student);
Шаг 3
Добавляем AutoMapper в DI контейнер. Для этого в метод ConfigureServices класса Startup.cs добавляем строку:
public void ConfigureServices(IServiceCollection services)
{
// другой код
services.AddAutoMapper(typeof(AppMappingProfile));
// другой код
}
Классов маппинг-профайлов может быть создано несколько. В таком случае, передаем их в параметры метода AddAutoMapper через запятую:
services.AddAutoMapper(typeof(AppMappingProfile), typeof(MappingProfile2));
Шаг 4
Используем маппер.
Теперь маппинг нашего объекта Person в Student происходит в одну строку кода:
var student = _mapper.Map<Student>(person);
Ниже приведен полный код класса, использующего маппинг:
using AutoMapper;
using AutoMapperInAspNetCore.Db;
using AutoMapperInAspNetCore.Models;
namespace AutoMapperInAspNetCore.Mapping
{
public class MappingHelper
{
private readonly IMapper _mapper;
private readonly IDbRepository _dbRepository;
public MappingHelper(IMapper mapper, IDbRepository dbRepository)
{
_mapper = mapper;
_dbRepository = dbRepository;
}
public void DoSomething()
{
// получаем запись из БД
var person = _dbRepository.GetPerson(1);
// Создаем новый объект типа Student и копируем в его свойства
// значения свойств объекта person (осуществляем маппинг)
var student = _mapper.Map<Student>(person);
}
}
}
Здесь в качестве generic в метод Map объекта _mapper передаем тип-приемник (Student), а в пераметре передаем объект-источник (person).
Теперь мы имеем объект student типа Student, со значениями полей из объекта person.
При этом мапятся только те поля, названия которых полностью совпадают у обоих типов.
В данном случае – это поля:
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
2. Маппинг объектов с не совпадающими наименованиями свойств
Для наглядности немного видоизменим класс Student, переименовав его свойства:
public class Student
{
public string Fio { get; set; }
public DateTime Birthday { get; set; }
public DateTime AdmissionDate { get; set; }
}
Теперь наименования свойств объектов person и student не совпадают.
Для того, чтобы маппинг заработал, нам придется дописать в маппинг-профайле явные правила:
public AppMappingProfile()
{
CreateMap<Person, Student>()
.ForMember(dest => dest.Fio, opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}"))
.ForMember(dest => dest.Birthday, opt => opt.MapFrom(src => src.BirthDate));
}
где параметр dest представляет собой объект-приемник, а src – объект-источник.
В методе MapFrom для поля Fio объекта student мы применили интерполяцию строки –
$"{src.FirstName} {src.LastName}"
результатом которой будет строка вида «Имя Отчество», которая и присвоится свойству Fio объекта student.
Комментарии (33)
Korobei
04.02.2022 01:04+1Можно сделать статический экземпляр маппера и extension метод, тогда вместо кишков типа
можно использовать что-то типа_mapper.Map<Person>(student);
а если докрутить source generator, то и что-то типаstudent.To<Person>();
student.ToPerson();
vabka
04.02.2022 01:06+1Если докрутить source generator, то и Automapper не понадобится в принципе)
Korobei
04.02.2022 01:12-1Ну можно и не source generator, а любой другой генератор по вкусу.
При любом мало-мальски полезном маппинге, всё равно придётся реализовывать фичи которые присутствуют в автомаппере. Типа атрибуты Ignore, или TypeConverter чтобы какой-нибудь string в enum переделать. Так что если у вас не high-load, то лучше уже надёжный и протестированный вариант взять, который большинство и так уже знает.vabka
04.02.2022 01:24Типа атрибуты Ignore, или TypeConverter чтобы какой-нибудь string в enum переделать.
А можно пример, где это может понадобится?
Просто в большинстве приложений (а большинство приложений — это бэкенд на asp net core), поток данных заканчивается на JsonSerializer.Serialize где-то в недрах аспнета.
А у JsonSerializer и так есть и Ignore, и конвертация енамов, и прочее. А значит в случаях с маппингом без изменений имён и без изменения данных — сериализатор уже и так делает работу автомаппера.
А то что сам Automapper протестирован, по хорошему, не избавляет от необходимости покрывать маппинг тестами (чтобы подтвердить, что эта вся автоматика и профили делают именно то что нужно).Korobei
04.02.2022 01:27При маппинге каких-нибудь EF entities например, тогда JsonSerializer.Serialize в этом не участвует.
Я занимался поддержкой legacy системы, каких интересных вещей там можно увидеть. Даже свой код годовалой давности вызывает движенье бровью (хотя изредка бывает даже с удивлением, что хорошо получилось).vabka
04.02.2022 01:34Немного не понял с EF Entities.
Если говорить про маппинг из таблиц в объекты, то в этом и Automapper не участвует — этим сам EF занимается, и у него есть аналогичные атрибуты.Korobei
04.02.2022 01:40Проблема в том что есть так как оно должно быть, а есть реальный мир.
Например у нас была таблица которая импортировалась из внешних данных, данные из этой таблицы надо замапить на четыре разных объекта, с разными правилами преобразования. Там ни EF ни JsonSerializer не вывезут, а автомапер скрипя справлялся. Можно сказать конечно ССЗБ, ну это и так понятно.vabka
04.02.2022 02:02Я бы в таком специфичном кейсе наоборот тобил бы за то чтобы руками эти правила преобразования описать
Korobei
04.02.2022 02:26Ну руками это не очень вариант, вот (source) generator позволит чтобы можно было на этапе компиляции упасть, если не соблюдается некоторое правило.
К примеру вводим правило, что все свойства (кроме помеченных ignore) на destination должны быть назначенны из source объекта, и что лишних свойств не было в source (которые отсутсвуют в destination). При генерации просто добавляем директиву #error в код, с описанием ошибки какие свойства не нашли напарника.
Тогда при изменении типа как destination, так и source будет на компиляции падать, если не удалось замапить свойство.
Руками это отследить сильно муторно. В автомапере можно сделать, но будет в рантайм падать, а не на компиляции.
Можно static analyzer под это заточить или в унит тестах покрыть, но в тестах тоже можно продолбать.
А если уж делать проверку, то до и генератора уже рукой подать, совместить приятное с полезным. Тем более недавно читал что генераторы обновили, стали получше с точки зрения эффекта на производительность среды разработки.
sebasww
04.02.2022 08:39Это неудобно с di.
Korobei
04.02.2022 18:09Интересно зачем для автомаперов di, на тестах свойства будут подругому мапиться? Не стёб, реально интересно понять. Единообразие, чтобы всё везде через di?
Ну и сервис локатор можно поиспользовать, если уж так подгорело.sebasww
05.02.2022 14:14+1Пробрасывать шифровальщики и компрессоры. Иногда и сериализаторы.
Korobei
05.02.2022 20:05А насколько это распростронённый сценарий чтобы шифровалось и копрессовалось на уровне свойств, а не на уровне транспорта? Ну а сермализаторами, тот же вопрос, смысл их через di прокидывать, реализация же не отличается от прод/тест и там же нету стандартизации, какой-нибудь newtonsoft не заменишь на JsonSerializer
sebasww
05.02.2022 20:16+1У меня есть один проект с полем, которое жмётся. Упаковывать сериализатор в свой класс много где распространено.
Я для маппинга всегда используй свой класс. Один на одну интеграцию.
sebasww
04.02.2022 08:38+1Разные поля, проброс аргумента в метод, инжекция шифровальщиков или компрессоров. Всё это неудобно в автомаппер. Удобнее делать свой маппер на каждую интеграцию с перегруженными методами From To.
mjr27
04.02.2022 10:12+2В документации automapper прямо сказано: если вам в профиле приходится прописывать более чем несколько правил или, упаси боже, какую-нибудь бизнес логику, вы его используете неправильно. Даже переименование полей лучше решать на уровне [JsonProperty].
Это отличная штука для избавления от рутины копирования одноименных полей между ДТОшками. Для всего остального -- это не самый изящный костыль
mjr27
04.02.2022 10:17+1Вообще Jimmy Bogard делает очень... Ммм... oppinionated вещи. Тот же Mediatr - очень прагматичный, очень удобный инструмент. Местами и точечно. А в большинстве случаев - рак, заражающий весь проект
DrPass
04.02.2022 11:40+2Мы использовали автомаппер на одном крупном проекте, потом как-то сели и посчитали, что в какой-то момент сопровождение настроек маппинга стали занимать дохрена времени, куда больше, чем просто ручное присваивание полей. И в значительной мере из-за того, что присваивание — оно очевидное. Вот ты смотришь на код, и перед тобой лежат все необходимые преобразования, если они есть. Маппер же является чёрным ящиком, и что там на самом деле происходит, лежит совсем в другом месте. Когда проект мелкий, а маппинг тривиальный, это не проблема. Но по мере роста проекта это жутко усложняет поиск ошибок или просто доработки, особенно когда сопровождаешь код, который писал не ты. В общем, в итоге мы посовещались, сели и выпилили его из проекта. Конечно, это частный опыт, у других может быть всё хорошо, но у нас вышло вот так.
Лично я бы вместо него предпочёл какой-нибудь кодогенератор, который по двум объектам просто генерил бы сниппет с присваиванием их полей.
Visier
04.02.2022 17:52+1Если честно, то я немного недрлюбливаю Автомаппер, потому что он скрывает учтановку пропертей от Решарпера. Бывает ищешь что устанавливает пропертю (в Решарпере есть кнопка - показать все места, где пропертя устанавливается), и не находишь. Тратишь час или больше - и находишь Автомаппер. Без него нашлось бы сразу. Конечно, не раз ловил себя на том, что писать копирование большого объекта в другой лень, но, блин, оно того стоит.
sebasww
05.02.2022 14:17Решарпер же умеет при создании объекта предложить собрать его из другого. В два клика маппинг пропертей кодится.
onets
04.02.2022 21:00Я сделал интерфейсы
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. И они автоматически регистрируются.
kawena54
05.02.2022 18:14надеюсь когда нибудь .net сделает нативную эту хрень , хотябы для начала это полный каст из ребенка в родители .
Прям сегодня с этим столкнулся при записи в монго
av_in
06.02.2022 22:51+1Автомаппер никогда не должен был существовать. Именно ему мы обязаны лучшим примерам "слоистой архитектуры" с десятком слоев DTOшек. Не будь автомаппера, люди бы поленились перегонять раз за разом одни и те же данные из одних дто в другие. А так пожалуйста, порождают монстров
vabka
По поводу автомаппера из моей личной практики (и капелька непопулярного мнения):
1. Со временем, его «авто» отваливается и приходится всё описывать в профиле, который пишется сложнее, чем ручной маппинг.
2. Сам Automapper очень тормозной и если прям принципиально не хочется самому писать методы для маппинга — можно взять Mapster или использовать любую другую генерацию кода перед компиляцией. (а лучше писать самому — это в любом случае контролируется лучше)
Опять же если на примере с Person -> Student из статьи, то чем принципиально лучше вариант с Automapper перед этим?
0 лишних зависимостей, никакой магии с Expressions, 0 оверхед, 0 дополнительных байт на хранение маппера в памяти, ещё и кода меньше нужно писать.
Ещё и во время компиляции проверяется, возможно ли в принципе смаппить таким образом Person к Student.
Помню, когда был на проекте с автомаппером — у каждого хотя бы иногда была ошибка в рантайме из-за того что профиль какой-то не тот, или в _mapper.Map передан какой-то не тот тип или объект.
Один раз даже на проде такое случилось в каком-то дальнем, редко используемом куске системы.
xFFFF
На текущем проекте встречаю классы с десятками свойств. Максимум было в районе 70. Замучаешься писать ручной маппинг.) И порой от одного класса нужно смапить несколько DTO.
vabka
Ну не каждый же день новые сущности на сто свойств появляются)
Но вообще да, лучше генерить код, чем писать его самому
VolodjaT
С чего вы взяли что он тормозной? Он ведь даже может компилировать мапинги в IL коде
https://docs.automapper.org/en/stable/Configuration.html#configuration-compilation
vabka
Вывод в большей степени основан на результатах бенчмарков из Mapster.
Там использовался Automapper 10 версии, а сейчас уже 11, так что сейчас действительно может лучше.
Свои бенчмарки я не делал.
Но мне кажется вполне логичным, что вызов кода, который скомпилирован заранее, будет работать быстрее, чем вызов аналогичного кода, который генерируется в реальном времени через emit.
VolodjaT
Так emit по идее он делает только раз на старте. А compilemapping вызывали?
vabka
Если скажете, где это надо вызывать — вызову и сделаю новый бенчмарк :)
Ещё хотел попробовать хвалёный кодген от мапстера, но он что-то не хочетработать на .NET 6.
VolodjaT
Because expression compilation can be a bit resource intensive, AutoMapper lazily compiles the type map plans on first map
Не уверен что у вас не происходила компиляция на каждом прогоне.
Надо в коде инициализации бенчмарка вызвать
vabka
Теперь у меня есть бенчмарк, который подтверждает мои слова:
MapShortBySourceMapper — это вариант с первым попавшимся маппером, который использует Source Generators.
Маппил классы с одним int32 свойством.
AlexDevFx
Mapster умеет генерировать классы во время сборки. Есть ещё Mapping Generator, он создает код прям студии.