При работе с данными (и не только) нам часто приходится сталкиваться с необходимостью копирования (маппинга) значений свойств одного объекта в новый объект другого типа.

Например предположим, что запрос к базе данных возвращает нам запись в виде объекта, представленного классом 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)


  1. vabka
    04.02.2022 01:00
    +12

    По поводу автомаппера из моей личной практики (и капелька непопулярного мнения):
    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 передан какой-то не тот тип или объект.
    Один раз даже на проде такое случилось в каком-то дальнем, редко используемом куске системы.


    1. xFFFF
      04.02.2022 01:19
      +2

      На текущем проекте встречаю классы с десятками свойств. Максимум было в районе 70. Замучаешься писать ручной маппинг.) И порой от одного класса нужно смапить несколько DTO.


      1. vabka
        04.02.2022 01:31

        Ну не каждый же день новые сущности на сто свойств появляются)
        Но вообще да, лучше генерить код, чем писать его самому


    1. VolodjaT
      04.02.2022 12:42
      +1

      С чего вы взяли что он тормозной? Он ведь даже может компилировать мапинги в IL коде
      https://docs.automapper.org/en/stable/Configuration.html#configuration-compilation


      1. vabka
        05.02.2022 06:29

        Вывод в большей степени основан на результатах бенчмарков из Mapster.
        Там использовался Automapper 10 версии, а сейчас уже 11, так что сейчас действительно может лучше.

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


        1. VolodjaT
          05.02.2022 15:43

          Так emit по идее он делает только раз на старте. А compilemapping вызывали?


          1. vabka
            05.02.2022 16:19

            Если скажете, где это надо вызывать — вызову и сделаю новый бенчмарк :)
            Ещё хотел попробовать хвалёный кодген от мапстера, но он что-то не хочетработать на .NET 6.


            1. VolodjaT
              06.02.2022 08:11

              var configuration = new MapperConfiguration(cfg => {});
              configuration.CompileMappings();

              Because expression compilation can be a bit resource intensive, AutoMapper lazily compiles the type map plans on first map

              Не уверен что у вас не происходила компиляция на каждом прогоне.

              Надо в коде инициализации бенчмарка вызвать


      1. vabka
        05.02.2022 06:56
        +1

        Теперь у меня есть бенчмарк, который подтверждает мои слова:


        |                 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 свойством.


    1. AlexDevFx
      04.02.2022 16:26

      Mapster умеет генерировать классы во время сборки. Есть ещё Mapping Generator, он создает код прям студии.


  1. Korobei
    04.02.2022 01:04
    +1

    Можно сделать статический экземпляр маппера и extension метод, тогда вместо кишков типа

    _mapper.Map<Person>(student);
    можно использовать что-то типа
    student.To<Person>();
    а если докрутить source generator, то и что-то типа
    student.ToPerson();


    1. vabka
      04.02.2022 01:06
      +1

      Если докрутить source generator, то и Automapper не понадобится в принципе)


      1. Korobei
        04.02.2022 01:12
        -1

        Ну можно и не source generator, а любой другой генератор по вкусу.

        При любом мало-мальски полезном маппинге, всё равно придётся реализовывать фичи которые присутствуют в автомаппере. Типа атрибуты Ignore, или TypeConverter чтобы какой-нибудь string в enum переделать. Так что если у вас не high-load, то лучше уже надёжный и протестированный вариант взять, который большинство и так уже знает.


        1. vabka
          04.02.2022 01:24

          Типа атрибуты Ignore, или TypeConverter чтобы какой-нибудь string в enum переделать.

          А можно пример, где это может понадобится?
          Просто в большинстве приложений (а большинство приложений — это бэкенд на asp net core), поток данных заканчивается на JsonSerializer.Serialize где-то в недрах аспнета.
          А у JsonSerializer и так есть и Ignore, и конвертация енамов, и прочее. А значит в случаях с маппингом без изменений имён и без изменения данных — сериализатор уже и так делает работу автомаппера.

          А то что сам Automapper протестирован, по хорошему, не избавляет от необходимости покрывать маппинг тестами (чтобы подтвердить, что эта вся автоматика и профили делают именно то что нужно).


          1. Korobei
            04.02.2022 01:27

            При маппинге каких-нибудь EF entities например, тогда JsonSerializer.Serialize в этом не участвует.

            Я занимался поддержкой legacy системы, каких интересных вещей там можно увидеть. Даже свой код годовалой давности вызывает движенье бровью (хотя изредка бывает даже с удивлением, что хорошо получилось).


            1. vabka
              04.02.2022 01:34

              Немного не понял с EF Entities.
              Если говорить про маппинг из таблиц в объекты, то в этом и Automapper не участвует — этим сам EF занимается, и у него есть аналогичные атрибуты.


              1. Korobei
                04.02.2022 01:40

                Проблема в том что есть так как оно должно быть, а есть реальный мир.

                Например у нас была таблица которая импортировалась из внешних данных, данные из этой таблицы надо замапить на четыре разных объекта, с разными правилами преобразования. Там ни EF ни JsonSerializer не вывезут, а автомапер скрипя справлялся. Можно сказать конечно ССЗБ, ну это и так понятно.


                1. vabka
                  04.02.2022 02:02

                  Я бы в таком специфичном кейсе наоборот тобил бы за то чтобы руками эти правила преобразования описать


                  1. Korobei
                    04.02.2022 02:26

                    Ну руками это не очень вариант, вот (source) generator позволит чтобы можно было на этапе компиляции упасть, если не соблюдается некоторое правило.

                    К примеру вводим правило, что все свойства (кроме помеченных ignore) на destination должны быть назначенны из source объекта, и что лишних свойств не было в source (которые отсутсвуют в destination). При генерации просто добавляем директиву #error в код, с описанием ошибки какие свойства не нашли напарника.

                    Тогда при изменении типа как destination, так и source будет на компиляции падать, если не удалось замапить свойство.

                    Руками это отследить сильно муторно. В автомапере можно сделать, но будет в рантайм падать, а не на компиляции.

                    Можно static analyzer под это заточить или в унит тестах покрыть, но в тестах тоже можно продолбать.

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


    1. sebasww
      04.02.2022 08:39

      Это неудобно с di.


      1. Korobei
        04.02.2022 18:09

        Интересно зачем для автомаперов di, на тестах свойства будут подругому мапиться? Не стёб, реально интересно понять. Единообразие, чтобы всё везде через di?

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


        1. sebasww
          05.02.2022 14:14
          +1

          Пробрасывать шифровальщики и компрессоры. Иногда и сериализаторы.


          1. Korobei
            05.02.2022 20:05

            А насколько это распростронённый сценарий чтобы шифровалось и копрессовалось на уровне свойств, а не на уровне транспорта? Ну а сермализаторами, тот же вопрос, смысл их через di прокидывать, реализация же не отличается от прод/тест и там же нету стандартизации, какой-нибудь newtonsoft не заменишь на JsonSerializer


            1. sebasww
              05.02.2022 20:16
              +1

              У меня есть один проект с полем, которое жмётся. Упаковывать сериализатор в свой класс много где распространено.

              Я для маппинга всегда используй свой класс. Один на одну интеграцию.


  1. sebasww
    04.02.2022 08:38
    +1

    Разные поля, проброс аргумента в метод, инжекция шифровальщиков или компрессоров. Всё это неудобно в автомаппер. Удобнее делать свой маппер на каждую интеграцию с перегруженными методами From To.


  1. mjr27
    04.02.2022 10:12
    +2

    В документации automapper прямо сказано: если вам в профиле приходится прописывать более чем несколько правил или, упаси боже, какую-нибудь бизнес логику, вы его используете неправильно. Даже переименование полей лучше решать на уровне [JsonProperty].

    Это отличная штука для избавления от рутины копирования одноименных полей между ДТОшками. Для всего остального -- это не самый изящный костыль


    1. mjr27
      04.02.2022 10:17
      +1

      Вообще Jimmy Bogard делает очень... Ммм... oppinionated вещи. Тот же Mediatr - очень прагматичный, очень удобный инструмент. Местами и точечно. А в большинстве случаев - рак, заражающий весь проект


  1. DrPass
    04.02.2022 11:40
    +2

    Мы использовали автомаппер на одном крупном проекте, потом как-то сели и посчитали, что в какой-то момент сопровождение настроек маппинга стали занимать дохрена времени, куда больше, чем просто ручное присваивание полей. И в значительной мере из-за того, что присваивание — оно очевидное. Вот ты смотришь на код, и перед тобой лежат все необходимые преобразования, если они есть. Маппер же является чёрным ящиком, и что там на самом деле происходит, лежит совсем в другом месте. Когда проект мелкий, а маппинг тривиальный, это не проблема. Но по мере роста проекта это жутко усложняет поиск ошибок или просто доработки, особенно когда сопровождаешь код, который писал не ты. В общем, в итоге мы посовещались, сели и выпилили его из проекта. Конечно, это частный опыт, у других может быть всё хорошо, но у нас вышло вот так.
    Лично я бы вместо него предпочёл какой-нибудь кодогенератор, который по двум объектам просто генерил бы сниппет с присваиванием их полей.


  1. Visier
    04.02.2022 17:52
    +1

    Если честно, то я немного недрлюбливаю Автомаппер, потому что он скрывает учтановку пропертей от Решарпера. Бывает ищешь что устанавливает пропертю (в Решарпере есть кнопка - показать все места, где пропертя устанавливается), и не находишь. Тратишь час или больше - и находишь Автомаппер. Без него нашлось бы сразу. Конечно, не раз ловил себя на том, что писать копирование большого объекта в другой лень, но, блин, оно того стоит.


    1. sebasww
      05.02.2022 14:17

      Решарпер же умеет при создании объекта предложить собрать его из другого. В два клика маппинг пропертей кодится.


  1. 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. И они автоматически регистрируются.


  1. kawena54
    05.02.2022 18:14

    надеюсь когда нибудь .net сделает нативную эту хрень , хотябы для начала это полный каст из ребенка в родители .

    Прям сегодня с этим столкнулся при записи в монго


  1. av_in
    06.02.2022 22:51
    +1

    Автомаппер никогда не должен был существовать. Именно ему мы обязаны лучшим примерам "слоистой архитектуры" с десятком слоев DTOшек. Не будь автомаппера, люди бы поленились перегонять раз за разом одни и те же данные из одних дто в другие. А так пожалуйста, порождают монстров