第十四章:对象映射
一、对象映射概述
1.1 什么是对象映射
对象映射(Object Mapping)是将一个对象的属性值复制到另一个对象的过程。在分层架构中,不同层之间的数据传递通常需要进行对象转换,例如将数据库实体(Entity)转换为数据传输对象(DTO),或将前端请求模型转换为领域对象。
1.2 为什么需要对象映射
| 场景 | 说明 |
|---|---|
| Entity → DTO | 数据库实体转换为接口返回对象,隐藏敏感字段 |
| DTO → Entity | 前端提交数据转换为数据库实体用于持久化 |
| ViewModel → DTO | 视图模型转换为数据传输对象 |
| Entity → Entity | 数据库实体之间的数据复制 |
| 集合转换 | 列表数据的批量转换 |
1.3 手动映射 vs 自动映射
// ❌ 手动映射:代码冗长、维护困难
var userDto = new UserDto
{
Id = user.Id,
UserName = user.UserName,
Email = user.Email,
RealName = user.RealName,
Phone = user.Phone,
Avatar = user.Avatar,
CreatedTime = user.CreatedTime
// ... 更多属性
};
// ✅ 自动映射:简洁高效
var userDto = user.Adapt<UserDto>();
1.4 Furion 对象映射方案
Furion 默认集成了 Mapster 作为对象映射引擎,同时提供了统一的映射接口,支持切换为其他映射库(如 AutoMapper)。
| 特性 | Mapster(默认) | AutoMapper |
|---|---|---|
| 性能 | 极高(编译时映射) | 一般(反射映射) |
| 配置方式 | 约定优先,可选配置 | 需要 Profile 配置 |
| 学习成本 | 低 | 中 |
| 内存占用 | 低 | 较高 |
| 嵌套映射 | 自动支持 | 需配置 |
| NuGet 依赖 | Furion 内置 | 需额外安装 |
二、Furion 内置映射器
2.1 注册映射服务
var builder = WebApplication.CreateBuilder(args);
// 注册对象映射服务(默认使用 Mapster)
builder.Services.AddObjectMapper();
var app = builder.Build();
app.Run();
2.2 IObjectMapper 接口
Furion 提供了统一的 IObjectMapper 接口:
public interface IObjectMapper
{
/// <summary>
/// 将源对象映射为目标类型
/// </summary>
TDestination Map<TDestination>(object source);
/// <summary>
/// 将源对象映射到已存在的目标对象
/// </summary>
TDestination Map<TSource, TDestination>(TSource source, TDestination destination);
}
2.3 使用 IObjectMapper
[ApiDescriptionSettings("用户管理")]
public class UserAppService : IDynamicApiController
{
private readonly IRepository<User> _userRepository;
private readonly IObjectMapper _objectMapper;
public UserAppService(
IRepository<User> userRepository,
IObjectMapper objectMapper)
{
_userRepository = userRepository;
_objectMapper = objectMapper;
}
/// <summary>
/// 获取用户详情
/// </summary>
public async Task<UserDto> GetUser(long id)
{
var user = await _userRepository.FindAsync(id);
// 使用 IObjectMapper 映射
return _objectMapper.Map<UserDto>(user);
}
/// <summary>
/// 更新用户信息
/// </summary>
public async Task UpdateUser(UpdateUserInput input)
{
var user = await _userRepository.FindAsync(input.Id);
// 映射到已存在的对象(保留未映射的属性)
_objectMapper.Map(input, user);
await _userRepository.UpdateAsync(user);
}
}
三、Adapt 扩展方法
3.1 基本映射
Furion(通过 Mapster)提供了 Adapt 扩展方法,是最常用的映射方式:
// 单个对象映射
var userDto = user.Adapt<UserDto>();
// 映射到已有对象
var existingDto = new UserDto();
user.Adapt(existingDto);
// 链式调用
var result = await _userRepository.FindAsync(id);
return result.Adapt<UserDto>();
3.2 各种映射场景
/// <summary>
/// 用户实体
/// </summary>
public class User
{
public long Id { get; set; }
public string UserName { get; set; }
public string Password { get; set; } // 敏感字段
public string Email { get; set; }
public string RealName { get; set; }
public string Phone { get; set; }
public string Avatar { get; set; }
public int Status { get; set; }
public DateTime CreatedTime { get; set; }
public DateTime? UpdatedTime { get; set; }
public long? DepartmentId { get; set; }
public Department Department { get; set; } // 导航属性
}
/// <summary>
/// 用户输出 DTO(不包含敏感信息)
/// </summary>
public class UserDto
{
public long Id { get; set; }
public string UserName { get; set; }
// 注意:没有 Password 属性,自动忽略
public string Email { get; set; }
public string RealName { get; set; }
public string Phone { get; set; }
public string Avatar { get; set; }
public string StatusText { get; set; } // 名称不同,需要配置
public DateTime CreatedTime { get; set; }
public string DepartmentName { get; set; } // 扁平化映射
}
/// <summary>
/// 创建用户输入
/// </summary>
public class CreateUserInput
{
public string UserName { get; set; }
public string Password { get; set; }
public string Email { get; set; }
public string RealName { get; set; }
public string Phone { get; set; }
}
四、DTO 与实体映射
4.1 Entity 转 DTO
[ApiDescriptionSettings("用户管理")]
public class UserAppService : IDynamicApiController
{
private readonly IRepository<User> _userRepository;
public UserAppService(IRepository<User> userRepository)
{
_userRepository = userRepository;
}
/// <summary>
/// 获取用户列表
/// </summary>
public async Task<List<UserDto>> GetUserList()
{
var users = await _userRepository.AsQueryable().ToListAsync();
return users.Adapt<List<UserDto>>();
}
/// <summary>
/// 获取用户详情
/// </summary>
public async Task<UserDto> GetUserDetail(long id)
{
var user = await _userRepository
.Include(u => u.Department)
.FirstOrDefaultAsync(u => u.Id == id);
return user.Adapt<UserDto>();
}
}
4.2 DTO 转 Entity
/// <summary>
/// 创建用户
/// </summary>
public async Task<long> CreateUser(CreateUserInput input)
{
// DTO 转 Entity
var user = input.Adapt<User>();
user.CreatedTime = DateTime.Now;
user.Status = 1;
var result = await _userRepository.InsertNowAsync(user);
return result.Entity.Id;
}
/// <summary>
/// 更新用户
/// </summary>
public async Task UpdateUser(UpdateUserInput input)
{
var user = await _userRepository.FindAsync(input.Id);
// 将 input 的值映射到已有 entity(只覆盖有值的属性)
input.Adapt(user);
user.UpdatedTime = DateTime.Now;
await _userRepository.UpdateAsync(user);
}
4.3 分页查询映射
/// <summary>
/// 分页查询用户
/// </summary>
public async Task<PagedResult<UserDto>> GetUserPage(UserPageInput input)
{
var query = _userRepository.AsQueryable();
// 条件筛选
if (!string.IsNullOrEmpty(input.Keyword))
{
query = query.Where(u =>
u.UserName.Contains(input.Keyword) ||
u.RealName.Contains(input.Keyword));
}
if (input.Status.HasValue)
{
query = query.Where(u => u.Status == input.Status.Value);
}
// 分页查询
var total = await query.CountAsync();
var users = await query
.OrderByDescending(u => u.CreatedTime)
.Skip((input.Page - 1) * input.PageSize)
.Take(input.PageSize)
.ToListAsync();
// 映射为 DTO
return new PagedResult<UserDto>
{
Total = total,
Items = users.Adapt<List<UserDto>>()
};
}
五、集合映射
5.1 列表映射
// List 映射
var userDtos = users.Adapt<List<UserDto>>();
// Array 映射
var userArray = users.Adapt<UserDto[]>();
// IEnumerable 映射
var userEnumerable = users.Adapt<IEnumerable<UserDto>>();
// 带 LINQ 查询的映射
var activeDtos = users
.Where(u => u.Status == 1)
.Select(u => u.Adapt<UserDto>())
.ToList();
// 或者先过滤后批量映射(推荐,性能更好)
var activeUsers = users.Where(u => u.Status == 1).ToList();
var activeDtos2 = activeUsers.Adapt<List<UserDto>>();
5.2 字典映射
// 字典映射
var dict = new Dictionary<string, object>
{
{ "Id", 1L },
{ "UserName", "张三" },
{ "Email", "zhangsan@example.com" }
};
var userDto = dict.Adapt<UserDto>();
5.3 元组映射
// 从元组映射
var tuple = (Id: 1L, UserName: "张三", Email: "zhangsan@example.com");
// Mapster 支持通过配置将元组映射到对象
六、嵌套对象映射
6.1 自动嵌套映射
Mapster 自动处理嵌套对象的映射:
/// <summary>
/// 订单实体
/// </summary>
public class Order
{
public long Id { get; set; }
public string OrderNo { get; set; }
public decimal TotalAmount { get; set; }
public long UserId { get; set; }
public User User { get; set; } // 嵌套对象
public List<OrderItem> Items { get; set; } // 嵌套集合
public Address ShippingAddress { get; set; } // 嵌套对象
}
public class OrderItem
{
public long Id { get; set; }
public long ProductId { get; set; }
public string ProductName { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
}
public class Address
{
public string Province { get; set; }
public string City { get; set; }
public string District { get; set; }
public string Detail { get; set; }
}
/// <summary>
/// 订单 DTO
/// </summary>
public class OrderDto
{
public long Id { get; set; }
public string OrderNo { get; set; }
public decimal TotalAmount { get; set; }
public string UserName { get; set; } // 扁平化:User.UserName
public List<OrderItemDto> Items { get; set; } // 嵌套集合映射
public AddressDto ShippingAddress { get; set; } // 嵌套对象映射
}
public class OrderItemDto
{
public string ProductName { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
public decimal SubTotal => Price * Quantity; // 计算属性
}
public class AddressDto
{
public string Province { get; set; }
public string City { get; set; }
public string FullAddress { get; set; } // 需要自定义映射
}
// 使用映射(嵌套对象自动映射)
var order = await _orderRepository
.Include(o => o.User)
.Include(o => o.Items)
.Include(o => o.ShippingAddress)
.FirstOrDefaultAsync(o => o.Id == orderId);
var orderDto = order.Adapt<OrderDto>();
// Items 和 ShippingAddress 会自动递归映射
6.2 扁平化映射
Mapster 支持自动扁平化嵌套属性:
/// <summary>
/// 扁平化 DTO
/// </summary>
public class OrderFlatDto
{
public long Id { get; set; }
public string OrderNo { get; set; }
// 自动映射 User.UserName(命名约定)
public string UserUserName { get; set; }
// 自动映射 User.Email
public string UserEmail { get; set; }
// 自动映射 ShippingAddress.Province
public string ShippingAddressProvince { get; set; }
public string ShippingAddressCity { get; set; }
}
七、自定义映射规则
7.1 使用 TypeAdapterConfig
/// <summary>
/// 映射配置注册
/// </summary>
public class MapsterConfig : IRegister
{
public void Register(TypeAdapterConfig config)
{
// User -> UserDto 映射配置
config.NewConfig<User, UserDto>()
.Map(dest => dest.StatusText, src => src.Status == 1 ? "启用" : "禁用")
.Map(dest => dest.DepartmentName, src => src.Department.Name)
.Ignore(dest => dest.Phone) // 忽略某个属性
.AfterMapping((src, dest) =>
{
// 映射后处理
dest.Phone = MaskPhone(src.Phone);
});
// Order -> OrderDto 映射配置
config.NewConfig<Order, OrderDto>()
.Map(dest => dest.UserName, src => src.User.UserName);
// Address -> AddressDto 映射配置
config.NewConfig<Address, AddressDto>()
.Map(dest => dest.FullAddress,
src => $"{src.Province}{src.City}{src.District}{src.Detail}");
}
private static string MaskPhone(string phone)
{
if (string.IsNullOrEmpty(phone) || phone.Length < 7) return phone;
return phone[..3] + "****" + phone[^4..];
}
}
7.2 条件映射
config.NewConfig<User, UserDto>()
// 条件映射:仅当源值不为 null 时才映射
.Map(dest => dest.Email, src => src.Email, srcCond => srcCond.Email != null)
// 条件映射:根据条件选择映射值
.Map(dest => dest.Avatar, src =>
string.IsNullOrEmpty(src.Avatar)
? "/default-avatar.png"
: src.Avatar);
7.3 自定义转换器
/// <summary>
/// 全局类型转换配置
/// </summary>
public class GlobalMapperConfig : IRegister
{
public void Register(TypeAdapterConfig config)
{
// DateTime -> string 全局转换
config.Default.Settings.Resolvers.Add(new DateTimeResolver());
// 枚举 -> 字符串描述
config.NewConfig<OrderStatus, string>()
.MapWith(src => src switch
{
OrderStatus.Pending => "待支付",
OrderStatus.Paid => "已支付",
OrderStatus.Shipped => "已发货",
OrderStatus.Completed => "已完成",
OrderStatus.Cancelled => "已取消",
_ => "未知"
});
}
}
7.4 映射前后钩子
config.NewConfig<CreateOrderInput, Order>()
.BeforeMapping((src, dest) =>
{
// 映射前处理
})
.AfterMapping((src, dest) =>
{
// 映射后处理
dest.OrderNo = GenerateOrderNo();
dest.CreatedTime = DateTime.Now;
dest.Status = OrderStatus.Pending;
});
private static string GenerateOrderNo()
{
return $"ORD{DateTime.Now:yyyyMMddHHmmss}{new Random().Next(1000, 9999)}";
}
八、Mapster 集成
8.1 Mapster 高级特性
// 安装 Mapster NuGet 包(Furion 已内置)
// dotnet add package Mapster
// 使用 ProjectToType 进行查询投影(直接在数据库层面映射)
var userDtos = await _dbContext.Users
.Where(u => u.Status == 1)
.ProjectToType<UserDto>()
.ToListAsync();
8.2 查询投影优势
// ❌ 不推荐:先查询所有字段,再映射
var users = await _dbContext.Users.ToListAsync();
var dtos = users.Adapt<List<UserDto>>(); // 从数据库加载了所有字段
// ✅ 推荐:使用 ProjectToType 直接投影
var dtos = await _dbContext.Users
.ProjectToType<UserDto>() // SQL 只查询 DTO 需要的字段
.ToListAsync();
8.3 Mapster 代码生成
Mapster 支持编译时代码生成,进一步提升性能:
// 安装代码生成包
// dotnet add package Mapster.Tool
// 定义映射接口
[AdaptFrom(typeof(User))]
[GenerateMapper]
public partial class UserDto
{
public long Id { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
}
// 编译后自动生成映射代码,无需反射
8.4 注册全局配置
// Program.cs 中注册 Mapster 配置
var config = TypeAdapterConfig.GlobalSettings;
// 扫描并注册所有 IRegister 实现
config.Scan(typeof(Program).Assembly);
// 全局配置
config.Default.Settings.PreserveReference = true; // 保留引用关系
config.Default.Settings.MaxDepth = 5; // 最大嵌套深度
config.Default.Settings.ShallowCopyForSameType = true; // 相同类型浅拷贝
// 注册到 DI
builder.Services.AddSingleton(config);
builder.Services.AddScoped<IMapper, ServiceMapper>();
九、AutoMapper 集成(可选)
9.1 安装和配置 AutoMapper
// 安装 AutoMapper
// dotnet add package AutoMapper
// dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection
// 注册 AutoMapper
builder.Services.AddAutoMapper(typeof(Program).Assembly);
9.2 定义 Profile
/// <summary>
/// AutoMapper 映射配置
/// </summary>
public class UserMappingProfile : Profile
{
public UserMappingProfile()
{
// User -> UserDto
CreateMap<User, UserDto>()
.ForMember(dest => dest.StatusText,
opt => opt.MapFrom(src => src.Status == 1 ? "启用" : "禁用"))
.ForMember(dest => dest.DepartmentName,
opt => opt.MapFrom(src => src.Department.Name))
.ForMember(dest => dest.Phone,
opt => opt.MapFrom(src => MaskPhone(src.Phone)));
// CreateUserInput -> User
CreateMap<CreateUserInput, User>()
.ForMember(dest => dest.CreatedTime, opt => opt.Ignore())
.ForMember(dest => dest.Status, opt => opt.Ignore());
// 双向映射
CreateMap<Address, AddressDto>()
.ForMember(dest => dest.FullAddress,
opt => opt.MapFrom(src =>
$"{src.Province}{src.City}{src.District}{src.Detail}"))
.ReverseMap();
}
private static string MaskPhone(string phone)
{
if (string.IsNullOrEmpty(phone) || phone.Length < 7) return phone;
return phone[..3] + "****" + phone[^4..];
}
}
9.3 使用 AutoMapper
public class UserAppService : IDynamicApiController
{
private readonly IMapper _mapper; // AutoMapper 的 IMapper
public UserAppService(IMapper mapper) => _mapper = mapper;
public async Task<UserDto> GetUser(long id)
{
var user = await _userRepository.FindAsync(id);
return _mapper.Map<UserDto>(user);
}
public async Task<List<UserDto>> GetUsers()
{
var users = await _userRepository.ToListAsync();
return _mapper.Map<List<UserDto>>(users);
}
}
十、性能对比
10.1 映射方式性能比较
| 映射方式 | 1000次映射耗时 | 内存占用 | 说明 |
|---|---|---|---|
| 手动映射 | ~0.5ms | 最低 | 最快但代码最多 |
| Mapster Adapt | ~0.8ms | 低 | 接近手动映射性能 |
| Mapster CodeGen | ~0.6ms | 低 | 编译时生成,近似手动 |
| AutoMapper Map | ~2.5ms | 中 | 基于反射和表达式树 |
| 反射映射 | ~15ms | 高 | 最慢,不推荐 |
10.2 性能测试代码
/// <summary>
/// 映射性能测试
/// </summary>
public class MappingBenchmark
{
private readonly List<User> _users;
public MappingBenchmark()
{
_users = Enumerable.Range(1, 1000).Select(i => new User
{
Id = i,
UserName = $"User_{i}",
Email = $"user{i}@example.com",
RealName = $"用户{i}",
Phone = "13800138000",
Status = 1,
CreatedTime = DateTime.Now
}).ToList();
}
/// <summary>
/// Mapster Adapt 测试
/// </summary>
public void MapsterAdaptTest()
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var dtos = _users.Adapt<List<UserDto>>();
stopwatch.Stop();
Console.WriteLine($"Mapster Adapt: {stopwatch.ElapsedMilliseconds}ms, Count: {dtos.Count}");
}
/// <summary>
/// 手动映射测试
/// </summary>
public void ManualMappingTest()
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var dtos = _users.Select(u => new UserDto
{
Id = u.Id,
UserName = u.UserName,
Email = u.Email,
RealName = u.RealName,
CreatedTime = u.CreatedTime
}).ToList();
stopwatch.Stop();
Console.WriteLine($"Manual Mapping: {stopwatch.ElapsedMilliseconds}ms, Count: {dtos.Count}");
}
}
十一、映射最佳实践
11.1 设计原则
| 原则 | 说明 |
|---|---|
| 单一职责 | 每个 DTO 只服务于特定场景 |
| 命名约定 | 保持源和目标属性名一致 |
| 集中配置 | 映射规则集中管理,使用 IRegister |
| 避免过度映射 | 只映射需要的属性 |
| 使用投影 | 查询时使用 ProjectToType 优化性能 |
| 不可变 DTO | DTO 尽量设计为不可变对象 |
11.2 DTO 设计规范
/// <summary>
/// 用户列表 DTO(用于列表展示,字段精简)
/// </summary>
public class UserListDto
{
public long Id { get; set; }
public string UserName { get; set; }
public string RealName { get; set; }
public string StatusText { get; set; }
public DateTime CreatedTime { get; set; }
}
/// <summary>
/// 用户详情 DTO(用于详情展示,字段完整)
/// </summary>
public class UserDetailDto
{
public long Id { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
public string RealName { get; set; }
public string Phone { get; set; }
public string Avatar { get; set; }
public string StatusText { get; set; }
public DateTime CreatedTime { get; set; }
public string DepartmentName { get; set; }
public List<RoleDto> Roles { get; set; }
}
/// <summary>
/// 创建用户输入 DTO
/// </summary>
public class CreateUserInput
{
public string UserName { get; set; }
public string Password { get; set; }
public string Email { get; set; }
public string RealName { get; set; }
public string Phone { get; set; }
public long? DepartmentId { get; set; }
public List<long> RoleIds { get; set; }
}
/// <summary>
/// 更新用户输入 DTO
/// </summary>
public class UpdateUserInput
{
public long Id { get; set; }
public string Email { get; set; }
public string RealName { get; set; }
public string Phone { get; set; }
public string Avatar { get; set; }
}
11.3 完整映射配置示例
/// <summary>
/// 全局映射配置
/// </summary>
public class GlobalMapperRegister : IRegister
{
public void Register(TypeAdapterConfig config)
{
// 全局设置
config.Default.Settings.PreserveReference = true;
// User 映射组
config.NewConfig<User, UserListDto>()
.Map(d => d.StatusText, s => s.Status == 1 ? "启用" : "禁用");
config.NewConfig<User, UserDetailDto>()
.Map(d => d.StatusText, s => s.Status == 1 ? "启用" : "禁用")
.Map(d => d.DepartmentName, s => s.Department != null ? s.Department.Name : "")
.Map(d => d.Phone, s => MaskPhone(s.Phone));
config.NewConfig<CreateUserInput, User>()
.Ignore(d => d.Id)
.Ignore(d => d.CreatedTime)
.Ignore(d => d.Status)
.AfterMapping((src, dest) =>
{
dest.CreatedTime = DateTime.Now;
dest.Status = 1;
});
// Order 映射组
config.NewConfig<Order, OrderDto>()
.Map(d => d.UserName, s => s.User.UserName)
.Map(d => d.Items, s => s.Items);
config.NewConfig<Address, AddressDto>()
.Map(d => d.FullAddress,
s => $"{s.Province}{s.City}{s.District}{s.Detail}");
}
private static string MaskPhone(string phone)
{
if (string.IsNullOrEmpty(phone) || phone.Length < 7) return phone;
return phone[..3] + "****" + phone[^4..];
}
}
通过合理使用 Furion 的对象映射功能,可以大幅减少重复的属性赋值代码,提高开发效率和代码可维护性。推荐优先使用 Mapster 的 Adapt 扩展方法进行日常映射,对于复杂场景通过 IRegister 集中配置映射规则,并善用 ProjectToType 优化数据库查询性能。