Mapster для C# (.NET): полное руководство с примерами
Mapster — это высокопроизводительная библиотека для маппинга объектов (object-to-object mapping) в экосистеме .NET. Она позволяет автоматически преобразовывать объекты одного типа в другой, избавляя разработчика от написания рутинного кода по копированию свойств. В отличие от таких монстров, как AutoMapper, Mapster делает упор на скорость работы и минимальное потребление памяти, что подтверждается многочисленными бенчмарками.
Зачем нужен маппинг объектов? В реальных проектах мы постоянно сталкиваемся с необходимостью преобразования данных: DTO (Data Transfer Object) в Entity, ViewModel в Model, Model в Response и так далее. Ручное копирование каждого поля — это скучно, чревато ошибками и усложняет поддержку кода. Mapster решает эту проблему, предоставляя простой, гибкий и быстрый API для автоматического маппинга.
Mapster генерирует код маппинга на этапе компиляции (через Roslyn-анализаторы) или использует динамическую компиляцию выражений. Это делает его одним из самых быстрых мапперов в .NET, часто обгоняя даже ручное копирование в некоторых сценариях. Библиотека поддерживает сложные сценарии: вложенные объекты, коллекции, условный маппинг, игнорирование полей, адаптеры для разных типов и многое другое.
Установка
Установка Mapster производится через NuGet Package Manager. Выполните одну из следующих команд в консоли диспетчера пакетов:
dotnet add package Mapster
Или через Package Manager Console:
Install-Package Mapster
Для работы с ASP.NET Core и внедрением зависимостей (DI) рекомендуется установить дополнительный пакет:
dotnet add package Mapster.DependencyInjection
После установки добавьте using Mapster; в начало вашего файла.
Быстрый старт
Рассмотрим простейший пример: у нас есть класс User (модель из базы данных) и класс UserDto (для передачи на клиент).
using Mapster;
// Исходный класс
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
}
// Целевой класс
public class UserDto
{
public int Id { get; set; }
public string FullName { get; set; }
public string Email { get; set; }
}
// Пример использования
public class Program
{
public static void Main()
{
var user = new User
{
Id = 1,
Name = "Иван Петров",
Email = "ivan@example.com",
CreatedAt = DateTime.Now
};
// Маппинг с помощью Mapster
var dto = user.Adapt<UserDto>();
Console.WriteLine($"Id: {dto.Id}, Name: {dto.FullName}, Email: {dto.Email}");
// Вывод: Id: 1, Name: Иван Петров, Email: ivan@example.com
}
}
Обратите внимание: свойство Name было автоматически сопоставлено с FullName, так как Mapster по умолчанию пытается найти соответствия по имени (регистронезависимо). Если бы в UserDto было свойство Name, оно бы тоже сопоставилось.
Основные методы и классы
Рассмотрим ключевые методы Mapster. Для каждого приведена сигнатура, описание и пример.
1. Adapt<TDestination>(object source)
Сигнатура: public static TDestination Adapt<TDestination>(this object source)
Описание: Базовый метод расширения для маппинга одного объекта в другой. Создает новый экземпляр TDestination и копирует значения свойств из source.
var user = new User { Id = 1, Name = "Анна" };
var dto = user.Adapt<UserDto>();
Console.WriteLine(dto.FullName); // Анна
2. Adapt(object source, Type sourceType, Type destinationType)
Сигнатура: public static object Adapt(this object source, Type sourceType, Type destinationType)
Описание: Позволяет выполнить маппинг, когда типы известны только во время выполнения (runtime). Возвращает object, который нужно привести к нужному типу.
object user = new User { Id = 2, Name = "Борис" };
Type sourceType = typeof(User);
Type destType = typeof(UserDto);
var result = user.Adapt(sourceType, destType) as UserDto;
Console.WriteLine(result.FullName); // Борис
3. TypeAdapterConfig<TSource, TDestination>.NewConfig()
Сигнатура: public static TypeAdapterConfig<TSource, TDestination> NewConfig()
Описание: Создает новую конфигурацию маппинга для пары типов. Позволяет задавать правила: игнорирование полей, переименование, условный маппинг, форматирование.
TypeAdapterConfig<User, UserDto>.NewConfig()
.Ignore(dest => dest.Email) // Не копировать Email
.Map(dest => dest.FullName, src => src.Name.ToUpper()); // Преобразовать имя в верхний регистр
var user = new User { Id = 3, Name = "Виктор", Email = "v@mail.ru" };
var dto = user.Adapt<UserDto>();
Console.WriteLine($"{dto.FullName}, Email: {dto.Email}"); // ВИКТОР, Email: (пусто)
4. TypeAdapterConfig.GlobalSettings.Default.IgnoreNonMapped()
Сигнатура: public TypeAdapterSettings IgnoreNonMapped(bool ignore = true)
Описание: Глобальная настройка, которая заставляет Mapster игнорировать все свойства, для которых нет соответствия в целевом типе. По умолчанию Mapster выбрасывает исключение при несовпадении имен.
TypeAdapterConfig.GlobalSettings.Default.IgnoreNonMapped(true);
var user = new User { Id = 4, Name = "Галина", Email = "g@mail.ru", CreatedAt = DateTime.Now };
var dto = user.Adapt<UserDto>(); // CreatedAt будет проигнорирован, ошибки нет
Console.WriteLine(dto.FullName); // Галина
5. TypeAdapterConfig<TSource, TDestination>.Map(destMember, sourceMember)
Сигнатура: public TypeAdapterConfig<TSource, TDestination> Map(Expression<Func<TDestination, object>> destMember, Expression<Func<TSource, object>> sourceMember)
Описание: Явно указывает, какое свойство источника должно быть сопоставлено с каким свойством цели. Используется для переименования или вычисления значений.
TypeAdapterConfig<User, UserDto>.NewConfig()
.Map(dest => dest.FullName, src => $"{src.Name} (ID: {src.Id})");
var user = new User { Id = 5, Name = "Дмитрий" };
var dto = user.Adapt<UserDto>();
Console.WriteLine(dto.FullName); // Дмитрий (ID: 5)
6. TypeAdapterConfig<TSource, TDestination>.Ignore(destMember)
Сигнатура: public TypeAdapterConfig<TSource, TDestination> Ignore(Expression<Func<TDestination, object>> destMember)
Описание: Указывает, что определенное свойство целевого объекта должно быть проигнорировано при маппинге (не будет изменено).
TypeAdapterConfig<User, UserDto>.NewConfig()
.Ignore(dest => dest.Id); // Id не будет скопирован
var user = new User { Id = 6, Name = "Елена" };
var dto = user.Adapt<UserDto>();
Console.WriteLine(dto.Id); // 0 (значение по умолчанию для int)
7. TypeAdapterConfig<TSource, TDestination>.IgnoreAttribute<TAttribute>()
Сигнатура: public TypeAdapterConfig<TSource, TDestination> IgnoreAttribute<TAttribute>(bool inherit = true) where TAttribute : Attribute
Описание: Позволяет игнорировать все свойства, помеченные определенным атрибутом. Например, если у вас есть кастомный атрибут [SensitiveData], вы можете исключить такие поля из маппинга.
public class User
{
public int Id { get; set; }
public string Name { get; set; }
[SensitiveData]
public string Password { get; set; }
}
TypeAdapterConfig.GlobalSettings.Default.IgnoreAttribute<SensitiveData>();
var user = new User { Id = 7, Name = "Жанна", Password = "secret123" };
var dto = user.Adapt<UserDto>(); // Password будет проигнорирован
8. QueryableExtensions.ProjectToType<TDestination>(IQueryable source)
Сигнатура: public static IQueryable<TDestination> ProjectToType<TDestination>(this IQueryable source)
Описание: Расширение для IQueryable, которое позволяет выполнять проекцию прямо в SQL-запросе (через Entity Framework Core или другую ORM). Это самый производительный способ маппинга, так как данные преобразуются на стороне базы данных.
using (var context = new AppDbContext())
{
var dtos = context.Users
.Where(u => u.IsActive)
.ProjectToType<UserDto>()
.ToList();
// SQL: SELECT Id, Name AS FullName, Email FROM Users WHERE IsActive = 1
}
9. TypeAdapterConfig<TSource, TDestination>.ConstructUsing(Func<TSource, TDestination> factory)
Сигнатура: public TypeAdapterConfig<TSource, TDestination> ConstructUsing(Func<TSource, TDestination> factory)
Описание: Позволяет задать кастомный способ создания целевого объекта. Например, если у вас есть конструктор с параметрами или нужно использовать фабрику.
TypeAdapterConfig<User, UserDto>.NewConfig()
.ConstructUsing(src => new UserDto
{
Id = src.Id,
FullName = $"Mr./Ms. {src.Name}"
});
var user = new User { Id = 8, Name = "Зоя" };
var dto = user.Adapt<UserDto>();
Console.WriteLine(dto.FullName); // Mr./Ms. Зоя
10. TypeAdapterConfig<TSource, TDestination>.AfterMapping(Action<TSource, TDestination> action)
Сигнатура: public TypeAdapterConfig<TSource, TDestination> AfterMapping(Action<TSource, TDestination> action)
Описание: Выполняет дополнительное действие после завершения маппинга. Полезно для пост-обработки: валидации, логирования, вычисления дополнительных полей.
TypeAdapterConfig<User, UserDto>.NewConfig()
.AfterMapping((src, dest) =>
{
dest.FullName = dest.FullName?.Trim();
Console.WriteLine($"Mapped user {src.Id} to DTO");
});
var user = new User { Id = 9, Name = "Игорь " };
var dto = user.Adapt<UserDto>();
Console.WriteLine($"'{dto.FullName}'"); // 'Игорь' (пробел удален)
Реальный практический пример
Рассмотрим более сложный сценарий: веб-API для управления заказами. У нас есть сущности базы данных, DTO для запросов и ответов, а также необходимость вложенного маппинга.
using Mapster;
using System;
using System.Collections.Generic;
using System.Linq;
// ===== Модели данных (Entity) =====
public class Customer
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public DateTime RegistrationDate { get; set; }
public List<Order> Orders { get; set; } = new();
}
public class Order
{
public int Id { get; set; }
public DateTime OrderDate { get; set; }
public decimal TotalAmount { get; set; }
public int CustomerId { get; set; }
public Customer Customer { get; set; }
public List<OrderItem> Items { get; set; } = new();
}
public class OrderItem
{
public int Id { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}
// ===== DTO =====
public class CustomerResponse
{
public int Id { get; set; }
public string FullName { get; set; }
public string Email { get; set; }
public int OrderCount { get; set; }
public List<OrderResponse> Orders { get; set; }
}
public class OrderResponse
{
public int Id { get; set; }
public string DateFormatted { get; set; }
public decimal Total { get; set; }
public int ItemCount { get; set; }
}
public class CreateOrderRequest
{
public int CustomerId { get; set; }
public List<CreateOrderItemRequest> Items { get; set; }
}
public class CreateOrderItemRequest
{
public string ProductName {