Mapster 集合映射完全指南List、数组、集合与 Dictionary 的 Adapt 映射原理【免费下载链接】MapsterA fast, fun and stimulating object to object Mapper项目地址: https://gitcode.com/GitHub_Trending/ma/Mapster导读本篇指南围绕 Mapster 官方文档 Collections.md 展开深入讲解 Mapster 在列表、数组、各类集合与字典之间的一键映射能力从IListT、ICollectionT、IEnumerableT、ISetT到IDictionaryTKey, TValue等接口的自动适配。读完本文你将掌握Adapt扩展方法在集合场景下的全部用法、目标类型实例化与元素转换的底层适配器原理、字典与对象双向映射的进阶技巧以及集合映射在 EF Core 投影等场景中的最佳实践。一、集合映射的核心能力Mapster 的集合映射覆盖了 .NET 中最常见的全部集合形态包括列表与泛型列表ListT、IListT、IReadOnlyListT数组一维数组T[]与多维数组T[,]、T[,,]通用集合接口IEnumerableT、ICollectionT集合语义接口ISetT、HashSetT键值对容器DictionaryTKey, TValue、IDictionaryTKey, TValue非泛型集合ArrayList、IEnumerable、byte[]等官方文档给出的最典型用法就是从数据库实体列表映射到 DTO 列表var list db.Pocos.ToList(); var target list.AdaptIEnumerableDto();一行代码即可完成集合 → 集合的转换目标元素类型Poco会被逐项转换为Dto而外层集合类型由源类型ListPoco直接切换为IEnumerableDto。1.1 三种等价的调用方式Mapster 为集合映射提供了与普通对象映射一致的三种入口方式一扩展方法推荐var dest src.AdaptTSource, TDestination(); // 显式指定源类型避免值类型装箱 var dest2 src.AdaptTDestination(); // 简洁写法内部会将 src 转为 object如 Mappers.md 所述src.AdaptTDestination()会把src隐式转成object如果映射的是值类型集合如int[]→Listlong建议使用AdaptTSource, TDestination以避免装箱拆箱开销。方式二Mapper 实例IMapper mapper new Mapper(); var result mapper.MapListDto(pocos);Mapper类见 Mapper.cs内部通过Config.GetMapFunctionTSource, TDestination()获取编译后的委托再执行映射适合注入 DI 容器的场景。方式三Buildervar target pocos.BuildAdapter() .AddParameters(user, this.User.Identity.Name) .AdaptToTypeListDto();Builder 可以携带运行时参数常用于需要把上下文信息传入元素映射表达式的复杂场景。二、底层适配器如何接管集合映射Mapster 采用基于评分Score的适配器选择机制。当源类型与目标类型都是集合时BaseAdapter会根据每种适配器的Score分值决定由谁处理映射。从源码结构看各适配器的优先级如下适配器Score职责源码文件CollectionAdapter-125各类泛型/非泛型集合的常规映射CollectionAdapter.csDictionaryAdapter-124字符串键字典的映射对象 ⇄ 字典DictionaryAdapter.csArrayAdapter-123一维数组映射ArrayAdapter.csMultiDimensionalArrayAdapter-122多维数组映射MultiDimensionalArrayAdapter.cs分值越大优先级越高。CollectionAdapter.CanMap的判断逻辑是源类型和目标类型都是集合IsCollection()且目标类型与集合兼容或可解析出字典类型即进入集合映射通道。2.1 目标类型接口的实例化规则当目标类型是接口时CollectionAdapter.CreateInstantiationExpression会按以下规则自动选择具体实现类IDictionaryK,V→DictionaryK,VIListT、ICollectionT、IEnumerableT等可赋值给ListT的类型 →ListT其余如ISetT→HashSetT// 目标为接口Mapster 自动实例化为 ListDto IEnumerableDto target list.AdaptIEnumerableDto();同时CollectionAdapter会尝试读取源集合的Count若目标具体类型提供(int capacity)构造函数则直接以源长度预分配容量new ListT(count)避免扩容开销若源类型没有可预知的长度如纯IEnumerableT则退化为new ListT()。2.2 元素级转换CreateListSet 的循环填充集合映射的本质是逐元素调用一次元素类型的Adapt。CollectionAdapter.CreateListSet会为源类型生成for源为IListT或foreach源为IEnumerableT循环// 以 IListT 源为例生成的逻辑等价于 for (var i 0, len src.Count; i len; i) { var item src[i]; dest.Add(convert(item)); // convert 即元素级 Adapt }其中convert(item)由CreateAdaptExpression(item, destinationElementType, arg)生成这意味着元素类型同样会递归走 Mapster 的对象/集合/字典适配流程——例如ListPerson中嵌套的ICollectionXProject属性会在元素映射过程中再次触发集合适配。2.3 内联Inline映射与浅拷贝保护在支持内联的场景例如投影到IEnumerableT或同类型浅拷贝开启时CollectionAdapter.CreateInlineExpression会生成source.Select(item convert(item))表达式链。值得注意的一个安全细节当元素类型无需转换时若目标类型与源类型相同Mapster 不会直接复用源集合对象而是通过MapsterHelper.ToEnumerable包一层新的可枚举对象防止后续对目标集合的修改反向污染源集合。这一点有专门测试覆盖见 WhenMappingCollections.cs 的ShouldNotUsingTheSameEnumerable对Foo中ListFoo、Foo[]、IEnumerableint、int[]四个集合属性做同类型映射后逐一断言源与目标集合不是同一引用且集合内元素也不是同一引用。三、数组与多维数组映射3.1 一维数组ArrayAdapter负责目标为一维数组的场景。它同样会为每个元素生成转换表达式最终以src.Select(item convert(item)).ToArray()的形式完成映射。一个重要的性能优化分支当**源、目标元素类型完全相同且为基本类型primitive**时ArrayAdapter.CreateBlockExpression会直接退化为Array.Copy(src, 0, dest, 0, len)的块拷贝并且当使用MapToTarget带目标实例模式时会取源、目标长度的较小值Math.Min避免越界。这意味着int[] → int[]、byte[] → byte[]这类映射几乎零开销。3.2 多维数组MultiDimensionalArrayAdapter专门处理目标为T[,]、T[,,]等秩大于 1 的数组。从 MultiDimensionalArrayAdapter.cs 的GetArrayBounds可以看出目标数组各维度的长度取自源集合长度与源数组GetLength(i)源是数组时目标维度尺寸尽量沿用源数组各维长度不足的维度补齐为1源是普通集合时第 0 维用源Count其余维度补1。填充过程会为每个维度维护游标变量v0、v1…每写入一个元素便推进游标达到某维边界时归零并进位到下一维逻辑上等价于// 以二维数组为例生成的逻辑等价于 var v0 0, v1 0; for (var i 0, len src.Count; i len; i) { var item src[i]; dest[v0, v1] convert(item); v1; if (v1 vlen1) { v1 0; v0; } }3.3 只枚举一次的保证测试 WhenMappingCollections.cs 中MapToArrayEnumerateOnlyOnce、MapToListEnumerateOnlyOnce、MapToMultiRanksArrayEnumerateOnlyOnce三个用例专门验证了一个行为源IEnumerableT在整个映射过程中只会被枚举一次。这对延迟枚举 副作用的迭代器如yield return生成器尤为重要——Mapster 先通过一次枚举建立长度/计数信息再复用其结果完成填充不会因重复遍历引发副作用或性能问题。四、Dictionary 映射对象 ⇄ 字典DictionaryAdapter继承自ClassAdapter专门处理字符串键字典CanMap要求TKey string其优先级高于CollectionAdapter确保Dictionarystring, V走字典专属逻辑而不是普通集合通道。4.1 对象 → 字典把 POCO 映射成Dictionarystring, object是字典映射最常见的场景在 WhenMappingWithDictionary.cs 中有直接验证var poco new SimplePoco { Id Guid.NewGuid(), Name test }; var dict TypeAdapter.AdaptDictionarystring, object(poco); dict.Count.ShouldBe(2); dict[Id].ShouldBe(poco.Id); dict[Name].ShouldBe(poco.Name);其内联表达式的生成逻辑CreateInlineExpression等价于new Dictionarystring, object { { Id, convert(poco.Id) }, { Name, convert(poco.Name) }, }即属性的名字成为字典键属性值经元素级转换后成为字典值。对象上的每个可映射成员都会生成一个ElementInit最终组装成一个ListInitExpression。4.2 字典 → 对象反向映射同样成立字典键与目标对象属性名匹配时值会被反序列化回对应属性var dict new Dictionarystring, object { [Id] Guid.NewGuid(), [Foo] test, }; var poco TypeAdapter.AdaptSimplePoco(dict); // 仅匹配到的 Id 被填充未匹配的 Foo 被忽略 poco.Id.ShouldBe(dict[Id]); poco.Name.ShouldBeNull();更深层的用法是嵌套字典映射到嵌套对象。测试MapNestedDictionariesToClassesWhenMappingWithDictionary.cs展示了把Dictionarystring, object中的Pet键值对递归映射成Person.Pet对象属性的能力var pet new Dictionarystring, object {{Name, Fluffy}, {Type, Cat}}; var dictionary new Dictionarystring, object { {Name, Alice}, {Pet, pet} }; var person dictionary.AdaptPerson(); person.Name.ShouldBe(dictionary[Name]); person.Pet.Name.ShouldBe(pet[Name]); person.Pet.Type.ShouldBe(pet[Type]);4.3 字典键名的灵活匹配与转换字典映射的键名匹配同样受NameMatchingStrategy控制。DictionaryAdapter中大量使用了SourceMemberNameConverter/DestinationMemberNameConverter与MapsterHelper.FlexibleGet/FlexibleSet/GetValueOrDefault辅助方法使键名匹配支持大小写与分隔符的灵活换算。测试中的典型用例// 对象 → 字典键转 camelCase且支持双向TwoWays var config new TypeAdapterConfig(); config.NewConfigSimplePoco, IDictionarystring, object() .TwoWays() .NameMatchingStrategy(NameMatchingStrategy.ToCamelCase); var dict poco.AdaptSimplePoco, IDictionarystring, object(config); dict[id].ShouldBe(poco.Id); // 键名变为小驼峰 var poco2 dict.AdaptSimplePoco(config); // 反向也成立 poco2.Id.ShouldBe(dict[id]);以及通过NameMatchingStrategy.ConvertSourceMemberName自定义键名变换TypeAdapterConfigDictionarystring, int?, Dictionarystring, int.NewConfig() .Map(A, a) .Ignore(c) .IgnoreIf((src, dest) src.Count 3, d) .IgnoreNullValues(true) .NameMatchingStrategy(NameMatchingStrategy.ConvertSourceMemberName(s _ s));该配置在 WhenMappingWithDictionary.cs 中验证源键a被显式映射为Ab经名称转换器变成_bc被忽略d因条件不成立保留e的 null 值被IgnoreNullValues过滤。4.4 字典映射与常规配置项的组合从 DictionaryAdapter.cs 的实现可以看到字典映射对Ignore、IgnoreIf、IgnoreNullValues等设置是完整支持的忽略项被编译进一个switch表达式IgnoreNullValues会为可空值类型生成if (kvp.Value ! null)守卫MapToTarget模式下还会先Clear()再回填等价于foreach (var kvp in source) { if (kvp.Value ! null) dest[kvp.Key] convert(kvp.Value); }此外非字符串键的字典如Dictionaryint, T、DictionarySomePoco, T则走普通CollectionAdapter通道逐KeyValuePair转换键值测试 WhenMappingWithDictionary.cs 覆盖了Dictionaryint, SimplePoco及DictionarySimplePoco, intPOCO 键的映射。五、集合映射与 EF Core 投影集合映射与ProjectToType组合时具有独特优势。Mapster.EFCore提供了ProjectToType扩展见 Extensions.cs 与 MapsterQueryable.cs它把映射表达式翻译进 SQL而不是在内存中逐条转换var dtos db.Pocos.ProjectToTypeDto().ToList();底层通过自定义MapsterQueryableProvider包装IQueryProvider在执行查询时创建MapContextScope并把IAdapterBuilder注入表达式树使集合属性的映射以Select形式下推到数据库端。这对ListNavigation、ICollectionChild等导航集合属性尤其重要——可以在单次 SQL 查询中完成关联集合的投影。需要注意的约束见 CollectionAdapter.cs投影模式下目标集合类型必须是可赋值类型否则会抛出InvalidOperationException例如目标写IEnumerableT时会提示 not supported for projection, please consider usingList。因此做投影时应优先使用ListT作为集合属性的目标类型。六、集合映射的进阶配置与最佳实践6.1 用 ConstructUsing 自定义集合实例化集合适配器在CreateInstantiationExpression中会优先检查ConstructUsing若有自定义工厂则完全绕过默认的new ListT()/new HashSetT()逻辑。测试RespectConstructUsingWhenMappingCollections.cs给出了实际用法var result source .BuildAdapter() .ForkConfig(x x.ForDestinationTypeICollectionstring() .ConstructUsing(() new HashSetstring())) .AdaptToTypeICollectionstring(); result.ShouldBe(new Liststring { 1, 3 }); // 元素 int → string 被转换且去重这表示你可以控制目标集合的具体实现类型同时元素转换照常生效示例中int被转为string。6.2 保持元素类型安全只读集合与接口目标从 WhenMappingCollections.cs 的测试模型可以看到Person与PersonDTO之间的集合属性几乎全面错位对应int[] ⇄ HashSetint、Listint ⇄ int[]、ArrayList ⇄ ICollectionGuid、Liststring ⇄ IReadOnlyListstring、ListXProject ⇄ ListYProject、ICollectionstring ⇄ IEnumerablestring等全部能正确互转并保持元素数量与顺序MapCollectionProperty与MapCollection两个用例逐一断言。因此在实际项目中源与目标的集合具体类型可以任意不同Mapster 会自动适配目标声明为接口IEnumerableT、ICollectionT、ISetT时运行时实例按 2.1 节规则自动选择源集合内的元素类型不同如XProject→YProject时会逐元素触发对象映射。6.3 性能与使用建议值类型元素推荐使用src.AdaptTSource, TDestination()形式避免object装箱源为IEnumerableT如yield迭代器时Mapster 保证只枚举一次可放心传给Adapt相同基本类型数组之间的映射会走Array.Copy快路径性能开销极小EF Core 投影场景下把目标集合属性声明为ListT避免IEnumerableT触发的投影异常涉及MapToTarget更新已有目标集合时Mapster 会先Clear()再填充且UseDestinationValue语义对字典取值而非新建也有影响需按需配置。七、总结Mapster 的集合映射由CollectionAdapter、ArrayAdapter、MultiDimensionalArrayAdapter、DictionaryAdapter四个按评分优先级协作的适配器共同完成覆盖从ListT、IEnumerableT、ISetT、数组到DictionaryTKey, TValue的全部形态支持接口目标的自动实例化、元素级递归转换、多维数组游标填充以及字典键名的灵活匹配策略。无论是一行Adapt的快捷映射、带运行时参数的 Builder还是下推到 SQL 的ProjectToType集合映射都能在不写循环的前提下与对象映射规则保持完全一致的行为。如需继续深入可参考同一系列文档 Mappable-Objects.md、Primitive-types.md或直接阅读核心实现 CollectionAdapter.cs、DictionaryAdapter.cs 及其配套测试 WhenMappingCollections.cs、WhenMappingWithDictionary.cs。【免费下载链接】MapsterA fast, fun and stimulating object to object Mapper项目地址: https://gitcode.com/GitHub_Trending/ma/Mapster创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考