对象映射 MapTo / MMapper 完整指南
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) == 用 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(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 或更小 超过深度会抛 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. 类型转换的内置规则
5. IQueryable.ProjectTo<T>()(兼容 EF 用户老代码) using AutoMapper.QueryableExtensions; IQueryable<Src> query = ...; var dtoList = query.ProjectTo<Src, Dest>(config).ToList(); 注意:NCore 用 Dapper, var srcList = ORM.Build<Src>().Get(...).ToList(); var dtoList = srcList.MapTo<Src, Dest>(); == 九、安全建议 ==
|