Введение в AutoMapper для C# (.NET)
В современной разработке на C# (.NET) часто возникает задача преобразования одного объекта в другой. Это может быть необходимо при передаче данных между слоями приложения (например, из базы данных в DTO для API), при маппинге ViewModel в Entity, или при работе с различными версиями API. Ручное написание кода для каждого такого преобразования — это утомительная, подверженная ошибкам и трудно поддерживаемая работа. Именно здесь на помощь приходит AutoMapper.
AutoMapper — это популярная библиотека с открытым исходным кодом для .NET, которая автоматизирует процесс маппинга (сопоставления) свойств одного объекта с другим. Она основана на принципе «соглашение по конфигурации» (convention over configuration). Это означает, что если имена и типы свойств в исходном и целевом объектах совпадают (или следуют определенным правилам), AutoMapper может выполнить маппинг без явного указания правил. Если же правила отличаются, вы можете легко настроить их с помощью гибкого API.
Библиотека значительно сокращает объем шаблонного кода, делает код чище, уменьшает количество ошибок, связанных с ручным присваиванием, и упрощает рефакторинг. AutoMapper широко используется в проектах на ASP.NET Core, WPF, Xamarin, и других .NET-приложениях.
Установка AutoMapper
AutoMapper распространяется через NuGet — официальный менеджер пакетов для .NET. Установить его можно несколькими способами:
1. Через консоль диспетчера пакетов (Package Manager Console)
Install-Package AutoMapper
2. Через .NET CLI
dotnet add package AutoMapper
3. Через интерфейс Visual Studio (Manage NuGet Packages)
Откройте проект -> ПКМ по проекту -> "Manage NuGet Packages" -> найдите "AutoMapper" -> установите последнюю стабильную версию.
Для работы с ASP.NET Core также рекомендуется установить пакет AutoMapper.Extensions.Microsoft.DependencyInjection, который упрощает интеграцию с DI-контейнером:
dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
Быстрый старт: первый маппинг
Давайте создадим простейший пример. У нас есть класс UserEntity (из базы данных) и UserDto (для передачи на клиент).
using AutoMapper;
// 1. Исходный и целевой классы
public class UserEntity
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
public class UserDto
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
// 2. Создание конфигурации и Mapper
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<UserEntity, UserDto>();
});
var mapper = config.CreateMapper();
// 3. Использование
var userEntity = new UserEntity
{
Id = 1,
FirstName = "Иван",
LastName = "Петров",
Email = "ivan@example.com"
};
var userDto = mapper.Map<UserDto>(userEntity);
Console.WriteLine(userDto.FirstName); // Выведет: Иван
В этом примере мы создали карту (Map) между UserEntity и UserDto. Поскольку имена свойств совпадают, AutoMapper автоматически сопоставил их. Метод Map<TDestination>(source) выполняет маппинг и возвращает новый объект целевого типа.
Основные методы и функции AutoMapper
AutoMapper предоставляет богатый API для настройки маппинга. Ниже приведены 10 ключевых методов и функций, которые вы будете использовать чаще всего.
1. CreateMap<TSource, TDestination>()
Что делает: Создает конфигурацию маппинга между двумя типами. Это основной метод для объявления карты.
Параметры: Два generic-параметра: TSource (исходный тип) и TDestination (целевой тип).
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Order, OrderDto>();
});
2. ForMember()
Что делает: Позволяет настроить маппинг для конкретного свойства целевого объекта. Используется, когда имя свойства не совпадает или требуется преобразование.
Параметры: ForMember(dest => dest.PropertyName, options => ...). В options можно использовать MapFrom, Ignore, UseValue и др.
cfg.CreateMap<UserEntity, UserDto>()
.ForMember(dest => dest.FullName, opt => opt.MapFrom(src => $"{src.FirstName} {src.LastName}"))
.ForMember(dest => dest.Password, opt => opt.Ignore()); // Игнорируем пароль
3. MapFrom()
Что делает: Указывает, из какого свойства или выражения исходного объекта брать значение для целевого свойства. Используется внутри ForMember.
Параметры: Лямбда-выражение, которое получает исходный объект и возвращает значение.
.ForMember(dest => dest.CreatedDateFormatted, opt => opt.MapFrom(src => src.CreatedDate.ToString("yyyy-MM-dd")))
4. Ignore()
Что делает: Указывает, что свойство целевого объекта должно быть пропущено при маппинге (останется со значением по умолчанию).
Параметры: Не принимает дополнительных параметров, вызывается на объекте IMemberConfigurationExpression.
.ForMember(dest => dest.InternalId, opt => opt.Ignore())
5. ReverseMap()
Что делает: Создает обратную карту (из TDestination в TSource) на основе уже определенной прямой карты. Экономит время, если маппинг симметричный.
Параметры: Не принимает.
cfg.CreateMap<UserEntity, UserDto>()
.ForMember(dest => dest.FullName, opt => opt.Ignore())
.ReverseMap(); // Теперь можно маппить и UserDto в UserEntity
6. ConstructUsing()
Что делает: Позволяет указать пользовательскую логику создания целевого объекта. Это полезно, если конструктор целевого класса требует параметры.
Параметры: Функция, которая принимает исходный объект и возвращает новый экземпляр целевого типа.
cfg.CreateMap<Order, OrderDto>()
.ConstructUsing(src => new OrderDto(src.Id, src.TotalPrice * 1.2m)); // Пример с наценкой
7. AfterMap() / BeforeMap()
Что делает: Позволяют выполнить произвольный код до или после маппинга. Полезно для валидации, логирования или пост-обработки.
Параметры: Action<TSource, TDestination>.
cfg.CreateMap<UserEntity, UserDto>()
.BeforeMap((src, dest) => Console.WriteLine($"Маппинг пользователя {src.Id}"))
.AfterMap((src, dest) => dest.CreatedAt = DateTime.UtcNow);
8. ForAllOtherMembers()
Что делает: Позволяет применить общее правило ко всем свойствам, которые не были явно настроены. Используется редко, но может быть полезно для глобальных настроек.
Параметры: Action<IMemberConfigurationExpression>.
cfg.CreateMap<Source, Dest>()
.ForAllOtherMembers(opt => opt.Condition((src, dest, srcMember) => srcMember != null)); // Не маппить null
9. ValueTransformers
Что делает: Позволяет применить глобальное преобразование ко всем значениям определенного типа. Например, обрезать строки или преобразовывать даты.
Параметры: AddTransform<TType>(Func<TType, TType>).
var config = new MapperConfiguration(cfg =>
{
cfg.ValueTransformers.Add<string>(val => val?.Trim()); // Обрезать все строки
cfg.CreateMap<UserEntity, UserDto>();
});
10. ProjectTo<TDestination>()
Что делает: Ключевой метод для работы с Entity Framework. Он не выполняет маппинг в памяти, а транслирует его в SQL-запрос. Это позволяет загружать из базы данных только те поля, которые нужны в DTO, что повышает производительность.
Параметры: Generic-параметр TDestination. Вызывается на IQueryable.
using (var context = new AppDbContext())
{
var usersDto = context.Users
.ProjectTo<UserDto>(mapper.ConfigurationProvider)
.ToList();
// SQL запрос будет содержать только поля из UserDto
}
Реальный практический пример: API для интернет-магазина
Рассмотрим более сложный пример. Представим, что мы разрабатываем API для интернет-магазина. У нас есть сущность Product и DTO ProductResponse для ответа клиенту. Также нам нужно маппить запрос на создание продукта CreateProductRequest в сущность Product.
using AutoMapper;
// --- Модели ---
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public DateTime CreatedAt { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; }
public bool IsDeleted { get; set; } // Софт-удаление
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
}
public class ProductResponse
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public string CategoryName { get; set; } // Имя категории, а не ID
public string CreatedAtFormatted { get; set; } // Отформатированная дата
}
public class CreateProductRequest
{
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public int CategoryId { get; set; }
}
// --- Профиль маппинга (рекомендуемый способ организации) ---
public class ProductProfile : Profile
{
public ProductProfile()
{
// Маппинг Product -> ProductResponse
CreateMap<Product, ProductResponse>()
.ForMember(dest => dest.CategoryName, opt => opt.MapFrom(src => src.Category.Name))
.ForMember(dest => dest.CreatedAtFormatted, opt => opt.MapFrom(src => src.CreatedAt.ToString("dd.MM.yyyy HH:mm")))
.ForMember(dest => dest.Description, opt => opt.Condition(src => !string.IsNullOrEmpty(src.Description))); // Не маппить пустое описание
// Маппинг CreateProductRequest -> Product
CreateMap<CreateProductRequest, Product>()
.ForMember(dest => dest.Id, opt => opt.Ignore()) // ID генерируется БД
.ForMember(dest => dest.CreatedAt, opt => opt.MapFrom(src => DateTime.UtcNow))
.ForMember(dest => dest.IsDeleted, opt => opt.MapFrom(src => false))
.ForMember(dest => dest.Category, opt => opt.Ignore()); // Катего