# 对象映射 (04_Mapper)

最后更新: 2024-09-20


# 📚 概述

DarkM 对象映射模块基于 AutoMapper 构建,提供对象间自动映射功能,支持模块自动发现和配置注册。

源代码位置: DarkM/src/Framework/Mapper


# 🏗️ 模块架构

# 完整目录结构

Mapper/
└── Mapper.AutoMapper/
    ├── IMapperConfig.cs                   # 映射配置接口
    ├── MapperConfigurationExpression.cs   # 映射配置表达式
    └── ServiceCollectionExtensions.cs     # DI 扩展
1
2
3
4
5

# 🔧 核心接口

# 1. IMapperConfig(映射配置接口)

文件位置: Mapper.AutoMapper/IMapperConfig.cs

namespace DarkM.Lib.Mapper.AutoMapper
{
    /// <summary>
    /// 对象映射绑定
    /// </summary>
    public interface IMapperConfig
    {
        /// <summary>
        /// 绑定映射配置
        /// </summary>
        /// <param name="cfg">Mapper 配置表达式</param>
        void Bind(IMapperConfigurationExpression cfg);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

说明: 所有映射配置类都必须实现此接口,用于注册映射关系。


# 2. IMapper(映射器接口)

AutoMapper 提供的核心接口,用于执行对象映射:

public interface IMapper
{
    /// <summary>
    /// 映射对象
    /// </summary>
    TDestination Map<TDestination>(object source);
    
    /// <summary>
    /// 映射对象(指定源类型和目标类型)
    /// </summary>
    TDestination Map<TSource, TDestination>(TSource source);
    
    /// <summary>
    /// 映射对象(到现有实例)
    /// </summary>
    TDestination Map<TSource, TDestination>(TSource source, TDestination destination);
    
    /// <summary>
    /// 投影到 IQueryable
    /// </summary>
    IQueryable<TDestination> ProjectTo<TDestination>(IQueryable source);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 🏗️ 依赖注入

# ServiceCollectionExtensions

文件位置: Mapper.AutoMapper/ServiceCollectionExtensions.cs

public static class ServiceCollectionExtensions
{
    /// <summary>
    /// 添加对象映射
    /// </summary>
    public static IServiceCollection AddMappers(
        this IServiceCollection services, 
        IModuleCollection modules)
    {
        var config = new MapperConfiguration(cfg =>
        {
            foreach (var moduleInfo in modules)
            {
                // 查找所有 IMapperConfig 实现
                var types = moduleInfo.AssemblyDescriptor.Application
                    .GetTypes()
                    .Where(t => typeof(IMapperConfig).IsAssignableFrom(t));

                foreach (var type in types)
                {
                    // 创建实例并绑定配置
                    ((IMapperConfig)Activator.CreateInstance(type)).Bind(cfg);
                }
            }
        });

        // 验证配置
        config.AssertConfigurationIsValid();

        // 注册 IMapper(单例)
        services.AddSingleton(config.CreateMapper());

        return services;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

说明:

  • 自动遍历所有模块
  • 自动发现所有 IMapperConfig 实现
  • 自动注册 IMapper 为单例服务

# 💡 配置说明

# 1. 定义映射配置类

在每个 Service 目录中创建 _MapperConfig.cs 文件:

文件位置: Admin.Application/RoleService/_MapperConfig.cs

using AutoMapper;
using DarkM.Lib.Mapper.AutoMapper;
using DarkM.Module.Admin.Application.RoleService.ViewModels;
using DarkM.Module.Admin.Domain.RoleModule.Models;

namespace DarkM.Module.Admin.Application.RoleService
{
    /// <summary>
    /// 角色映射配置
    /// </summary>
    public class _MapperConfig : IMapperConfig
    {
        public void Bind(IMapperConfigurationExpression cfg)
        {
            // Entity <-> AddModel
            cfg.CreateMap<RoleAddModel, RoleEntity>();
            
            // Entity <-> UpdateModel
            cfg.CreateMap<RoleEntity, RoleUpdateModel>();
            cfg.CreateMap<RoleUpdateModel, RoleEntity>();
            
            // Entity <-> ViewModel
            cfg.CreateMap<RoleEntity, RoleViewModel>()
                .ForMember(dest => dest.CreatorName, 
                    opt => opt.MapFrom(src => src.Creator.Name));
            
            // Entity <-> QueryModel
            cfg.CreateMap<RoleQueryModel, RoleEntity>()
                .ForMember(dest => dest.Name, 
                    opt => opt.Condition(src => src.Name != null));
            
            // Entity <-> BindModel
            cfg.CreateMap<RoleMenuButtonBindModel, RoleMenuButtonEntity>();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

命名约定:

  • 文件名为 _MapperConfig.cs(下划线开头,始终位于文件列表顶部)
  • 每个 Service 目录一个映射配置文件
  • 集中管理该 Service 的所有映射关系

# 💡 使用示例

# 1. 注入 IMapper

在需要使用映射的地方,注入 IMapper 即可:

using AutoMapper;

public class RoleService : IRoleService
{
    // 注入 IMapper
    private readonly IMapper _mapper;
    private readonly IRoleRepository _repository;

    public RoleService(IMapper mapper, IRoleRepository repository)
    {
        _mapper = mapper;
        _repository = repository;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 2. 新增操作

public async Task<IResultModel> Add(RoleAddModel model)
{
    if (await _repository.Exists(model.Name))
        return ResultModel.HasExists;

    // 映射:AddModel -> Entity
    var entity = _mapper.Map<RoleEntity>(model);

    var result = await _repository.AddAsync(entity);

    return ResultModel.Result(result);
}
1
2
3
4
5
6
7
8
9
10
11
12

# 3. 更新操作

public async Task<IResultModel> Update(RoleUpdateModel model)
{
    var entity = await _repository.GetAsync(model.Id);
    if (entity == null)
        return ResultModel.NotFound;
    
    // 映射:UpdateModel -> Entity(到现有实例)
    _mapper.Map(model, entity);
    
    var result = await _repository.UpdateAsync(entity);
    
    return ResultModel.Result(result);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 4. 查询操作

public async Task<IResultModel<RoleViewModel>> Get(int id)
{
    var entity = await _repository.GetAsync(id);
    if (entity == null)
        return ResultModel<RoleViewModel>.NotFound;
    
    // 映射:Entity -> ViewModel
    var viewModel = _mapper.Map<RoleViewModel>(entity);
    
    return ResultModel.Success(viewModel);
}
1
2
3
4
5
6
7
8
9
10
11

# 5. 分页查询

public async Task<IResultModel<PageModel<RoleViewModel>>> Query(RoleQueryModel model)
{
    // 映射:QueryModel -> Entity(用于构建查询条件)
    var queryEntity = _mapper.Map<RoleEntity>(model);
    
    var page = await _repository.QueryAsync(queryEntity, model.Page, model.PageSize);
    
    // 映射:Entity List -> ViewModel List
    var viewModels = _mapper.Map<List<RoleViewModel>>(page.Data);
    
    return ResultModel.Success(new PageModel<RoleViewModel>
    {
        Data = viewModels,
        Total = page.Total
    });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 🔧 高级用法

# 1. 条件映射

cfg.CreateMap<UserAddModel, UserEntity>()
    .ForMember(dest => dest.CreateTime, opt => opt.Ignore())  // 忽略
    .ForMember(dest => dest.CreatorId, opt => opt.Ignore())
    .ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.Enabled ? 1 : 0));  // 条件映射
1
2
3
4

# 2. 自定义值解析器

public class FullNameResolver : IValueResolver<UserEntity, UserViewModel, string>
{
    public string Resolve(UserEntity source, UserViewModel destination, string destMember, ResolutionContext context)
    {
        return $"{source.FirstName} {source.LastName}";
    }
}

// 配置
cfg.CreateMap<UserEntity, UserViewModel>()
    .ForMember(dest => dest.FullName, opt => opt.ResolveUsing<FullNameResolver>());
1
2
3
4
5
6
7
8
9
10
11

# 3. 嵌套对象映射

cfg.CreateMap<OrderEntity, OrderViewModel>()
    .ForMember(dest => dest.CustomerName, opt => opt.MapFrom(src => src.Customer.Name))
    .ForMember(dest => dest.CustomerEmail, opt => opt.MapFrom(src => src.Customer.Email));
1
2
3

# 4. 集合映射

cfg.CreateMap<OrderEntity, OrderViewModel>()
    .ForMember(dest => dest.Items, opt => opt.MapFrom(src => src.OrderItems));

// 使用
var orderViewModel = _mapper.Map<OrderViewModel>(orderEntity);
// orderViewModel.Items 会自动映射
1
2
3
4
5
6

# 5. 投影到 IQueryable

// 使用 ProjectTo 进行数据库层面的投影
var query = _dbContext.Users
    .ProjectTo<UserViewModel>(_mapper.ConfigurationProvider);

// 生成的 SQL 只会查询需要的字段
1
2
3
4
5

# 6. 反向映射

cfg.CreateMap<UserEntity, UserViewModel>()
    .ReverseMap();  // 自动创建反向映射

// 现在可以双向映射
var viewModel = _mapper.Map<UserViewModel>(entity);
var entity = _mapper.Map<UserEntity>(viewModel);
1
2
3
4
5
6

# 7. ForPath 映射嵌套属性

cfg.CreateMap<UserAddModel, UserEntity>()
    .ForPath(dest => dest.Address.Province, opt => opt.MapFrom(src => src.Province))
    .ForPath(dest => dest.Address.City, opt => opt.MapFrom(src => src.City));
1
2
3

# 📚 最佳实践

# 1. 使用 _MapperConfig 命名

将映射配置文件命名为 _MapperConfig.cs,这样会始终位于文件列表顶部,方便查找。

RoleService/
├── _MapperConfig.cs      # 映射配置(顶部)
├── RoleService.cs
├── RoleViewModel.cs
└── ViewModels/
1
2
3
4
5

# 2. 按 Service 组织映射

每个 Service 目录中放置对应的 _MapperConfig.cs 文件,保持映射关系与业务逻辑在一起。

Admin.Application/
├── RoleService/
│   └── _MapperConfig.cs
├── UserService/
│   └── _MapperConfig.cs
└── MenuService/
    └── _MapperConfig.cs
1
2
3
4
5
6
7

# 3. 避免复杂映射

如果映射关系过于复杂,考虑:

  • 使用自定义值解析器
  • 手动映射部分属性
  • 重新设计 ViewModel

# 4. 使用 Ignore 忽略不需要的属性

cfg.CreateMap<UserAddModel, UserEntity>()
    .ForMember(dest => dest.Id, opt => opt.Ignore())
    .ForMember(dest => dest.CreateTime, opt => opt.Ignore());
1
2
3

# 5. 使用 MaxDepth 限制循环引用

cfg.CreateMap<UserEntity, UserViewModel>()
    .MaxDepth(1);  // 限制深度为 1
1
2

# 🔍 常见问题

# Q1: 映射不生效?

检查清单:

  1. ✅ 是否正确实现了 IMapperConfig 接口
  2. ✅ 是否在 Bind 方法中配置了映射关系
  3. ✅ 是否正确注入了 IMapper
  4. ✅ 是否调用了 AddMappers 注册映射

解决方案:

// 确保在 Startup.cs 或 Program.cs 中调用
services.AddMappers(modules);
1
2

# Q2: 如何处理循环引用?

解决方案:

cfg.CreateMap<UserEntity, UserViewModel>()
    .MaxDepth(1);  // 限制映射深度
1
2

或者使用 Ignore 忽略循环引用属性:

cfg.CreateMap<UserEntity, UserViewModel>()
    .ForMember(dest => dest.Orders, opt => opt.Ignore());
1
2

# Q3: 性能问题如何优化?

优化方案:

  1. 使用 ProjectTo 进行数据库层面投影
var query = _dbContext.Users
    .ProjectTo<UserViewModel>(_mapper.ConfigurationProvider);
1
2
  1. 避免重复创建 MapperConfiguration
// ✅ 推荐:单例注册
services.AddSingleton(config.CreateMapper());

// ❌ 不推荐:每次请求都创建
var mapper = config.CreateMapper();
1
2
3
4
5
  1. 使用并行映射
var viewModels = entities
    .AsParallel()
    .Select(e => _mapper.Map<ViewModel>(e))
    .ToList();
1
2
3
4

# Q4: 如何映射枚举?

AutoMapper 自动支持枚举映射,无需额外配置:

// Entity
public class UserEntity
{
    public Status Status { get; set; }
}

// ViewModel
public class UserViewModel
{
    public Status Status { get; set; }
}

// 自动映射,无需配置
cfg.CreateMap<UserEntity, UserViewModel>();
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# Q5: 如何处理 null 值映射?

解决方案:

cfg.CreateMap<UserEntity, UserViewModel>()
    .ForMember(dest => dest.Name, opt => opt.Condition(src => src.Name != null));
1
2

或者使用 AllowNullDestinationValues

var config = new MapperConfiguration(cfg =>
{
    cfg.AllowNullDestinationValues = true;
});
1
2
3
4

# 📚 相关文档


# 🔗 参考链接


最后更新: 2024-09-20