Pull to refresh

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.

Ну не каждый же день новые сущности на сто свойств появляются)
Но вообще да, лучше генерить код, чем писать его самому
UFO just landed and posted this here
Вывод в большей степени основан на результатах бенчмарков из Mapster.
Там использовался Automapper 10 версии, а сейчас уже 11, так что сейчас действительно может лучше.

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

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

UFO just landed and posted this here

У меня почему-то нет такого метода, ну да ладно.

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

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


|                 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.

Если использовать source generators, то видно

UFO just landed and posted this here
Если докрутить source generator, то и Automapper не понадобится в принципе)
UFO just landed and posted this here
Типа атрибуты Ignore, или TypeConverter чтобы какой-нибудь string в enum переделать.

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

А то что сам Automapper протестирован, по хорошему, не избавляет от необходимости покрывать маппинг тестами (чтобы подтвердить, что эта вся автоматика и профили делают именно то что нужно).
UFO just landed and posted this here
Немного не понял с EF Entities.
Если говорить про маппинг из таблиц в объекты, то в этом и Automapper не участвует — этим сам EF занимается, и у него есть аналогичные атрибуты.
UFO just landed and posted this here
Я бы в таком специфичном кейсе наоборот тобил бы за то чтобы руками эти правила преобразования описать
UFO just landed and posted this here
UFO just landed and posted this here

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

UFO just landed and posted this here

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

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

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

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

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

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

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

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

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

Я сделал интерфейсы

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, крайне удобная штука.

Все остальное очень и очень плохо. Богард и его подручные не выносят критики или другого мнения. Захотели - добавили фичу, потом захотели убрали фичу, да еще и без всякого способа вернуть прежнее поведение, а вы господа делайте что хотите со своими мапингами (вы юзали не так и не то!). Самое интересное, что "тру вей" использования автомапера тоже никуда не годится - в большом проекте важно видеть где что и как используется, когда добавлено, магии должен быть минимум, но автомапер будет мапить как посчитает нужным. Я гарантирую, что в любом достаточно большом проекте с автомапером (где пользуются рекомендациями Богарда) есть баг в мапинге и не один.

Sign up to leave a comment.

Articles