znlgis 博客

GIS开发与技术分享

第十四章:对象映射

一、对象映射概述

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