Mapster 自定义转换逻辑完全指南:MapWith、MapToTargetWith 与 AfterMapping 深度实战 Mapster 自定义转换逻辑完全指南MapWith、MapToTargetWith 与 AfterMapping 深度实战【免费下载链接】MapsterA fast, fun and stimulating object to object Mapper项目地址: https://gitcode.com/GitHub_Trending/ma/MapsterMapster 作为一款高性能的对象映射库虽然默认的自动映射已经覆盖了绝大多数场景但在某些边界情况下如类型转换规则特殊、需要复用已有实例、映射后需要补充业务逻辑仍然需要开发者完全接管映射过程。本文以 Mapster 仓库中的 Custom-conversion-logic.md 为核心骨架结合src/Mapster下的源码实现与src/Mapster.Tests中的测试用例系统讲解MapWith、MapToTargetWith、AfterMapping三种自定义转换机制的使用方式、底层原理与组合技巧。读完本文你将能够为任意类型对注册完全自定义的转换逻辑并理解这些配置在表达式树编译阶段是如何被消费的。一、MapWith完全接管类型转换逻辑默认情况下Mapster 会按照成员名匹配、构造函数推断等规则自动生成映射表达式。但在某些场景下你希望完全掌控源对象如何变成目标对象此时可以使用MapWith方法注册一个自定义的转换委托converter。1.1 基本用法注册自定义转换工厂MapWith接收一个ExpressionFuncTSource, TDestination类型的转换表达式Mapster 在编译该配置时会把整个表达式树作为映射主体而不再走默认的逐成员拷贝流程//Example of transforming string to char[]. TypeAdapterConfigstring, char[].NewConfig() .MapWith(str str.ToCharArray());注册之后调用TypeAdapter.Adaptchar[](Hello)即可得到{H,e,l,l,o}。这一点在测试用例 WhenUsingConverterFactory.cs 中有完整验证[TestMethod] public void Custom_Mapping_From_String_To_Char_Array() { TypeAdapterConfigstring, char[].NewConfig() .MapWith(str str.ToCharArray()); var chars TypeAdapter.Adaptchar[](Hello); chars.Length.ShouldBe(5); chars[0].ShouldBe(H); chars[1].ShouldBe(e); chars[2].ShouldBe(l); chars[3].ShouldBe(o); }MapWith同样适用于无法通过默认规则映射的不可变类型。测试 WhenMappingPrimitives.cs 展示了如何使用MapWith手动调用不可变类的构造函数config.NewConfigMutableA, ImmutableB() .MapWith(src new ImmutableB(src.Name));1.2 深层原理设置被编译进 ConverterFactory从源码看MapWith的核心实现位于 TypeAdapterSetter.cspublic TypeAdapterSetterTSource, TDestination MapWith(ExpressionFuncTSource, TDestination converterFactory, bool applySettings false) { this.CheckCompiled(); if (applySettings) { var adapter new DelegateAdapter(converterFactory); Settings.ConverterFactory adapter.CreateAdaptFunc; Settings.ConverterToTargetFactory ?? adapter.CreateAdaptToTargetFunc; } else { Settings.ConverterFactory arg converterFactory; if (Settings.ConverterToTargetFactory null) { var dest Expression.Parameter(typeof(TDestination)); Settings.ConverterToTargetFactory arg Expression.Lambda(converterFactory.Body, converterFactory.Parameters[0], dest); } } return this; }可以看到ConverterFactory负责生成源 → 新目标实例的表达式当未显式设置MapToTargetWith时ConverterToTargetFactory会被自动填充为忽略目标参数、复用MapWith主体的表达式——这正是原文档中若只设置MapWith而未设置MapToTargetWithMapster 将沿用MapWith逻辑这一规则在代码层面的体现CheckCompiled()保证配置一旦编译Compile()/BuildAdapter()之后便不可再修改防止表达式树不一致。applySettings: false默认值时转换表达式直接被整体注册PreserveReference、Include、AfterMapping等其它设置不会参与处理若需要组合使用必须将applySettings置为true此时会走DelegateAdapter分支。1.3 将引用类型当作原样返回来使用MapWith另一个非常实用的场景是某些对象应当被当作**原始值primitive**处理而不是当作 POCO 被深拷贝。例如Newtonsoft.Json.Linq.JObject、System.Data.Entity.Spatial.DbGeography这类自带复杂内部结构、深拷贝毫无意义甚至会产生副作用的对象TypeAdapterConfigJObject, JObject.NewConfig() .MapWith(json json);这段配置让 JObject 到 JObject 的映射直接返回源引用避免了 Mapster 尝试枚举其内部属性逐层拷贝。从适配器体系看这相当于把该类型对的映射降级为原语语义——DelegateAdapter见 DelegateAdapter.cs将整个转换表达式作为CreateInstantiationExpression的返回体且CreateBlockExpression返回空表达式意味着映射过程不产生任何额外的成员赋值块protected override Expression CreateInstantiationExpression(Expression source, Expression? destination, CompileArgument arg) { if (destination null) return _lambda.Apply(arg.MapType, source); else return _lambda.Apply(arg.MapType, source, destination); } protected override Expression CreateBlockExpression(Expression source, Expression destination, CompileArgument arg) { return Expression.Empty(); }1.4 与其它设置组合applySettings 参数原文档明确指出当你希望MapWith与PreserveReference、Include、AfterMapping等其它配置同时生效时必须传入applySettings: trueTypeAdapterConfigComplexPoco, ComplexDto.NewConfig() .PreserveReference(true) .MapWith(poco poco.ToDto(), applySettings: true);这背后的原因是applySettings: true时Mapster 会以DelegateAdapter生成带完整设置的适配规则ConverterFactory与ConverterToTargetFactory均来自适配器参见 BaseAdapter.cs 的CreateRule从而保留对引用保留、包含派生类型、映射前后回调等设置的处理能力。而默认的applySettings: false是纯函数式的快捷路径适合无需组合的简单转换。提示PreserveReference、Include等设置的详细用法可参考 Object-references.md 与 Custom-mapping.md。二、MapToTargetWith自定义映射到已存在对象MapWith解决的是创建新实例的转换而Adapt(destination)这类映射到已有目标对象MapToTarget的场景则由MapToTargetWith接管。其委托签名为ExpressionFuncTSource, TDestination, TDestination即同时接收源对象与目标对象返回处理后的目标对象。2.1 基本用法拷贝到已有数组原文档给出的示例是将源数组拷贝进已存在的目标数组TypeAdapterConfigstring[], string[].NewConfig() .MapToTargetWith((src, dest) Array.Copy(src, dest, src.Length));这样当执行srcArray.Adapt(destArray)时映射逻辑不再逐个元素赋值而是直接调用Array.Copy完成整块拷贝。2.2 底层实现与回退规则MapToTargetWith的实现位于 TypeAdapterSetter.cspublic TypeAdapterSetterTSource, TDestination MapToTargetWith(ExpressionFuncTSource, TDestination, TDestination converterFactory, bool applySettings false) { this.CheckCompiled(); if (typeof(TSource).IsMapsterPrimitive() || typeof(TDestination).IsMapsterPrimitive()) { this.Settings.MapToTargetPrimitive true; } if (applySettings) { var adapter new DelegateAdapter(converterFactory); Settings.ConverterToTargetFactory adapter.CreateAdaptToTargetFunc; } else Settings.ConverterToTargetFactory arg converterFactory; return this; }需要注意的两点实现细节若源类型或目标类型是 Mapster 认定的原始类型primitive会同时标记MapToTargetPrimitive true这会影响适配器的选择与内联策略回退规则如果只注册了MapWith而没有注册MapToTargetWith那么执行Adapt(destination)时ConverterToTargetFactory会退化使用MapWith的表达式仅消费源参数忽略目标对象即目标实例会被MapWith的返回值覆盖。测试用例 WhenSettingMapToTargetWith.cs 从三个角度验证了这一行为// 1) MapToTargetWith 生效于 Adapt(destination) config.NewConfigdouble, double().MapToTargetWith((x, y) 5); a.Adapt(b, config); b.A.ShouldBe(5); // 2) 只配置 MapWith 时Adapt(destination) 使用 MapWith 逻辑 config.NewConfigdouble, double().MapWith(_ 5); var b a.AdaptListdouble(config); b.ShouldBe(new Listdouble{ 5, 5, 5}); // 3) 集合类型同理 config.NewConfigListdouble, Listdouble().MapWith(_ new Listdouble{ 5, 5, 5}); var b a.AdaptListdouble(config); b.ShouldBe(new Listdouble{ 5, 5, 5});2.3 与 ShallowCopyForSameType 等全局设置的协作在 WhenForkingConfig.cs 中还有一处典型用法对同类型配置浅拷贝时用MapToTargetWith控制源为空字符串则保留目标原值的合并语义config.NewConfigstring, string() .MapToTargetWith((src, dest) string.IsNullOrEmpty(src) ? dest : src);这说明MapToTargetWith非常适合实现合并/覆盖类业务逻辑与浅合并Shallow-merge.md配合可以构造出灵活的目标值决策规则。三、AfterMapping映射完成后的自定义后处理很多时候你并不需要完全接管映射而只是希望在 Mapster 完成常规映射之后再补充一些框架无法覆盖的逻辑例如计算派生字段、填充审计信息、调用外部服务修正数据。此时使用AfterMapping。3.1 基本用法TypeAdapterConfigPoco, Dto.NewConfig() .AfterMapping((src, dest) SpecialSetFn(src, dest));AfterMapping的委托接收(TSource source, TDestination destination)两个参数映射完成后立即执行。由于它是回调Action而非纯表达式可以方便地访问外部方法、捕获闭包变量。3.2 三种重载与源码实现TypeAdapterSetter提供了多个重载见 TypeAdapterSetter.cs重载签名语义AfterMapping(ActionTDestination action)只接收目标对象单参版本定义于基类 TypeAdapterSetter.csAfterMapping(ActionTSource, TDestination action)同时接收源与目标对象最常用AfterMapping(ActionTSource, TDestination, TDestination? action)三参版本额外接收destination即Adapt(destination)传入的目标实例可能为 null三参版本尤其适合区分新建对象与更新已有对象两种映射路径——result是最终返回的对象destination是可选的原始目标引用。源码将委托封装为表达式树常量并追加到Settings.AfterMappingFactoriesSettings.AfterMappingFactories.Add(arg { var p1 Expression.Parameter(arg.SourceType, SourceParameterName); var p2 Expression.Parameter(arg.DestinationType, ResultParameterName); var p3 Expression.Parameter(arg.DestinationType, DestinationParameterName); var actionType action.GetType(); var actionExp Expression.Constant(action, actionType); var invoke Expression.Call(actionExp, Invoke, null, p1, p2, p3); return Expression.Lambda(invoke, p1, p2, p3); });在 BaseAdapter.cs 的表达式构建阶段这些工厂生成的调用会按顺序并Reverse逆序处理嵌套场景拼接到映射块之后执行。3.3 对内联inline映射的影响有一个值得注意的底层行为AfterMappingFactories数量大于 0 时CanInline会返回false见 BaseAdapter.cs即该类映射不允许被内联到外层映射表达式中而是作为一个独立的适配器调用。这与PreserveReference、BeforeMapping、Includes等设置的行为一致——都是副作用型配置不能安全地内联进纯表达式。关联阅读AfterMapping与BeforeMapping的完整用法对比见 Before-after-mapping.md若你的后处理逻辑可以写成纯表达式也可使用AfterMappingInline以获得更好的内联优化空间。四、三种机制的选型建议场景推荐机制说明完全自定义源 → 新对象的转换MapWith整个映射由你的表达式接管适用于类型转换、不可变类构造引用类型按原样返回JObject、DbGeography 等MapWith(json json)避免无意义的深拷贝控制映射到已有目标的行为MapToTargetWith例如数组整块拷贝、字符串合并决策映射完成后补充业务逻辑AfterMapping委托可捕获闭包、调用外部方法不影响默认成员映射需要同时使用上述机制与 PreserveReference / Include 等设置applySettings: true否则其它设置不会生效五、注意事项小结配置必须在编译前完成MapWith、MapToTargetWith、AfterMapping都会调用CheckCompiled()配置一经Compile()后再次修改会抛出异常MapWith与MapToTargetWith的回退关系只设置MapWith时Adapt(destination)会复用MapWith逻辑并忽略目标实例务必明确这一覆盖语义副作用回调会禁用内联注册了AfterMapping/BeforeMapping的类型对将无法内联进外层映射表达式这在追求极致性能时需要权衡applySettings是组合开关需要与PreserveReference、Include、AfterMapping等设置共存时务必显式传入true相关示例可对照 Object-references.md 与 Setting-values.md 理解组合效果。通过合理运用这三种自定义转换机制你可以在享受 Mapster 默认映射能力的同时对边界场景保持完全的控制力——这正是 Mapster 在约定优于配置与显式接管之间保持平衡的设计精髓。【免费下载链接】MapsterA fast, fun and stimulating object to object Mapper项目地址: https://gitcode.com/GitHub_Trending/ma/Mapster创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考