第十四章:对象映射

一、对象映射概述

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 优化数据库查询性能。


上一章: 第13章-远程请求与HTTP客户端 返回目录 下一章: 第15章-多租户架构