OrangeCloud.NCore 内置自研对象映射器 MMapper,从 v3.3.2.5 起替代 AutoMapper(消除 CVE-2026-32933),保留完整的 AutoMapper 兼容 shim,老代码无需改动直接编译。

本文覆盖 MapTo 的全部用法。


== 一、三种调用风格 ==

① 扩展方法风格(最简洁,最常用)

// 单对象
var dest = src.MapTo<Src, Dest>();

// 列表
var destList = srcList.MapTo<Src, Dest>();

// 一次性配置
var dest = src.MapTo<Src, Dest>(cfg => cfg.CreateMap<Src, Dest>()
    .ForMember(d => d.Name, opt => opt.MapFrom(s => s.FullName)));

② AutoMapper DI 风格(适合需要全局配置 / IMapper 注入)

var config = new MapperConfiguration(cfg => {
    cfg.CreateMap<Src, Dest>();
});
IMapper mapper = config.CreateMapper();
var dest = mapper.Map<Src, Dest>(src);

③ 引擎直调(无 AutoMapper 命名空间依赖)

// 一次性配置
var dest = MMapper.Map<Src, Dest>(src, cfg => cfg.Map(d => d.Name, s => s.FullName));

// 全局注册一次,到处用
MMapper.Register<Src, Dest>(cfg => cfg.Map(d => d.Name, s => s.FullName));
var dest = MMapper.Map<Src, Dest>(src);

== 二、基础用法 ==

同名属性自动映射:源对象与目标对象同名公开属性自动赋值,无需任何配置。

var src = new SrcUser { Id = 1, Name = "Alice" };
var dest = src.MapTo<SrcUser, DestUser>();
// dest.Id == 1, dest.Name == "Alice"

列表映射

List<SrcUser> list = ...;
List<DestUser> result = list.MapTo<SrcUser, DestUser>();

null 与空集合处理

  • 源对象为 null → MapTo 返回 null(不抛异常)
  • 源列表为 null → 返回空 List<TDest>
  • 列表中含 null 元素 → 目标对应位置为 null,保留位置
SrcUser? src = null;
var dest = src.MapTo<SrcUser, DestUser>(); // dest == null

List<SrcUser?> list = new() { new SrcUser{ Id = 1 }, null };
var result = list.MapTo<SrcUser, DestUser>();
// result[0] != null,result[1] == null

== 三、字段级配置(ForMember) ==

cfg => cfg.CreateMap<Src, Dest>().ForMember(...) 链式语法做字段级配置,与 AutoMapper 完全一致。

Ignore - 跳过指定目标字段

.ForMember(d => d.Password, opt => opt.Ignore())

MapFrom - 显式指定源字段(表达式 / 字符串 / 计算)

// 表达式
.ForMember(d => d.Name, opt => opt.MapFrom(s => s.FullName))
// 字符串名(动态场景)
.ForMember(d => d.Name, opt => opt.MapFrom("FullName"))
// 计算字段
.ForMember(d => d.DisplayName, opt => opt.MapFrom(s => s.FirstName + " " + s.LastName))

UseValue - 强制赋常量

.ForMember(d => d.Source, opt => opt.UseValue("SYSTEM"))

NullSubstitute - 源为 null 时用替代值

.ForMember(d => d.Name, opt => opt.NullSubstitute("匿名"))

Condition - 仅当条件为真时才映射

.ForMember(d => d.Name, opt => opt.Condition(s => s.Id > 0))

ForAllOtherMembers - 批量忽略,只保留显式声明字段

.ForMember(d => d.Id, opt => { /* 保持映射 */ })
.ForAllOtherMembers(opt => opt.Ignore());
// 只有 Id 被映射,其它字段全部跳过

== 四、类型级配置 ==

ConvertUsing - 整体接管映射逻辑,跳过默认成员映射

// 函数版
.ConvertUsing(s => new Dest { Id = s.Id, Tag = "X" })

// 类版(需实现 ITypeConverter)
.ConvertUsing<MyConverter>()

public class MyConverter : ITypeConverter<Src, Dest> {
    public Dest Convert(Src source, ResolutionContext context)
        => new Dest { Id = source.Id * 10 };
}

ConstructUsing - 自定义目标对象的构造方式(之后仍走默认成员映射)

.ConstructUsing(s => new Dest(specialCtorArg: s.Id))

// 注意:默认映射会覆盖同名属性。如要保护构造里设的值,加 Ignore
.ConstructUsing(s => new Dest { Price = 999 })
.ForMember(d => d.Price, opt => opt.Ignore())

BeforeMap / AfterMap - 钩子,在映射前/后做副作用(日志/补字段/初始化)

.BeforeMap((s, d) => logger.Info($"映射开始 {s.Id}"))
.AfterMap((s, d) => d.FullName = d.FirstName + " " + d.LastName)

ReverseMap - 反向映射,自动注册 Dest -> Src

cfg.CreateMap<Src, Dest>().ReverseMap();
// 之后 dest.MapTo<Dest, Src>() 也能用

== 五、自定义解析器 ==

当字段映射逻辑很复杂或要复用时,把它抽成独立的解析器类。

IValueResolver - 单字段解析器

public class NameUpperResolver : IValueResolver<Src, Dest, string> {
    public string Resolve(Src source, Dest destination, string destMember, ResolutionContext context)
        => source.Name?.ToUpper();
}

// 使用
.ForMember(d => d.Name, opt => opt.ResolveUsing<NameUpperResolver>())

ITypeConverter - 整类型转换器(与 ConvertUsing<TConverter> 等价)

public class SrcToDestConverter : ITypeConverter<Src, Dest> {
    public Dest Convert(Src source, ResolutionContext context)
        => new Dest { Id = source.Id, Name = source.FullName };
}

// 使用
.ConvertUsing<SrcToDestConverter>()

== 六、Profile 与全局注册 ==

项目级别有大量映射时,建议把它们集中放到 Profile 类里,启动时一次注册。

方式 A:MMapperProfile(引擎原生,推荐)

public class UserMappingProfile : MMapperProfile {
    public override void Configure() {
        CreateMap<SrcUser, DestUser>()
            .Map(d => d.Name, s => s.FullName);

        CreateMap<SrcOrder, DestOrder>()
            .Ignore(d => d.InternalNote);
    }
}

// 启动时
MMapper.AddProfile<UserMappingProfile>();

方式 B:AutoMapper Profile(兼容 shim)

public class UserProfile : Profile {
    public UserProfile() {
        CreateMap<SrcUser, DestUser>()
            .ForMember(d => d.Name, opt => opt.MapFrom(s => s.FullName));
    }
}

// 通过 MapperConfiguration
new MapperConfiguration(cfg => cfg.AddProfile<UserProfile>());

方式 C:程序集自动发现,扫描所有 Profile 子类

var config = new MapperConfiguration(cfg => {
    cfg.AddMaps(typeof(Startup).Assembly);
    // 或
    cfg.AddProfiles(AppDomain.CurrentDomain.GetAssemblies());
});

方式 D:直接全局注册(最简单)

MMapper.Register<SrcUser, DestUser>(cfg => {
    cfg.Map(d => d.Name, s => s.FullName);
});
// 之后任何地方都能用 src.MapTo<SrcUser, DestUser>() 享受这个配置

== 七、IMapper 与 MapperConfiguration 工厂 ==

NCore 完整支持 AutoMapper 9.x+ 的 IMapper / MapperConfiguration 工厂模式,原 AutoMapper 项目零改动迁移。

1. 基本工厂用法

using AutoMapper;

var config = new MapperConfiguration(cfg => {
    cfg.CreateMap<Src, Dest>()
       .ForMember(d => d.Tag, opt => opt.MapFrom(s => "T_" + s.Id));
});
IMapper mapper = config.CreateMapper();

var dest = mapper.Map<Src, Dest>(src);

2. IMapper 多种 Map 重载

// 双泛型
mapper.Map<Src, Dest>(src);
// 单泛型(运行时推断源类型)
mapper.Map<Dest>(srcAsObject);
// 非泛型
mapper.Map(src, typeof(Src), typeof(Dest));
// 合并到已有目标对象(保留目标已有字段)
mapper.Map(src, existingDest);
mapper.Map(src, existingDest, typeof(Src), typeof(Dest));

3. DI 注入(ASP.NET Core)

// Program.cs / Startup.cs
var mapperConfig = new MapperConfiguration(cfg => {
    cfg.AddProfile<UserProfile>();
    cfg.AddProfile<OrderProfile>();
});
services.AddSingleton(mapperConfig);
services.AddSingleton<IMapper>(sp => sp.GetRequiredService<MapperConfiguration>().CreateMapper());

// Controller 中
public class UsersController : ControllerBase {
    private readonly IMapper _mapper;
    public UsersController(IMapper mapper) => _mapper = mapper;

    [HttpGet]
    public DestUser Get(int id) {
        var entity = ORM.Get<SrcUser>(id.ToString());
        return _mapper.Map<SrcUser, DestUser>(entity);
    }
}

4. 6.x 老代码兼容(MapperLegacy)

AutoMapper 6.x 用 Mapper.Initialize / Mapper.Map 静态门面。NCore 因 C# 语法限制(同一类不能同时是静态与实例)把它拆到 MapperLegacy

// 老代码
Mapper.Initialize(cfg => cfg.CreateMap<Src, Dest>());
var dest = Mapper.Map<Dest>(src);

// NCore 改为
MapperLegacy.Initialize(cfg => cfg.CreateMap<Src, Dest>());
var dest = MapperLegacy.Map<Dest>(src);

== 八、高级特性 ==

1. MaxDepth - 限制递归深度(防御 StackOverflow,CVE-2026-32933 同款防护)

// 单次配置
src.MapTo<Src, Dest>(cfg => cfg.CreateMap<Src, Dest>().MaxDepth(10));

// 全局默认
MMapper.DefaultMaxDepth = 32;

// 0 表示不限制(默认值),生产环境建议设为 32 或更小

超过深度会抛 MMapperException

2. PreserveReferences - 处理循环引用

// 模型 a.Next = b; b.Next = a;
var dest = src.MapTo<Node, NodeDto>(cfg =>
    cfg.CreateMap<Node, NodeDto>()
       .MaxDepth(50)         // 兜底
       .PreserveReferences() // 同一源对象映射出同一目标实例
);

3. IgnoreUnmappedMembers - 默认全部跳过,只映射显式声明的字段

var dest = src.MapTo<Src, Dest>(cfg => {
    cfg.CreateMap<Src, Dest>();
    cfg.IgnoreUnmappedMembers();   // 引擎风格 API
    cfg.Map(d => d.Id, s => s.Id); // 只有 Id 会映射
});

4. 类型转换的内置规则

  • 同名属性 + 同类型:直接赋值
  • 数值之间:int -> long / long -> int / int? -> int 自动转换
  • 字符串:所有类型 ToString() 转字符串;字符串可解析为 int/bool/DateTime 等
  • 枚举:同底层值的枚举互转 / 字符串名 → 枚举
  • 嵌套对象:自动递归(保留字段名匹配 + 类型映射规则)
  • 集合:List<T> / T[] / HashSet<T> 自动逐元素映射

5. IQueryable.ProjectTo<T>()(兼容 EF 用户老代码)

using AutoMapper.QueryableExtensions;

IQueryable<Src> query = ...;
var dtoList = query.ProjectTo<Src, Dest>(config).ToList();

注意:NCore 用 Dapper,ProjectTo 不会生成 SQL 投影,会枚举到内存后再映射,等价但失去 SQL 投影优化。Dapper 项目更推荐:

var srcList = ORM.Build<Src>().Get(...).ToList();
var dtoList = srcList.MapTo<Src, Dest>();

== 九、安全建议 ==

  • 生产环境务必设置 MMapper.DefaultMaxDepth = 32(或更小),防御深图 StackOverflow
  • 明知有循环引用的对象,显式开 PreserveReferences() 并设 MaxDepth 兜底
  • 外部输入的 JSON / DTO 反序列化后再走 MapTo,深度同样适用此原则
  • 大列表映射(>1 万元素)建议用 MapList,不要逐元素 MapTo(前者复用类型缓存)