资讯动态

Mapster 中 C Record 类型的映射:不可变语义、构造函数选择与 v7.4.0 / v10.0 行为对比

发布时间:2026/9/18 9:24:57 来源:尧图企业网站定制
Mapster 中 C# Record 类型的映射不可变语义、构造函数选择与 v7.4.0 / v10.0 行为对比【免费下载链接】MapsterA fast, fun and stimulating object to object Mapper项目地址: https://gitcode.com/GitHub_Trending/ma/MapsterMapster 将 C# Record 视为不可变类型处理映射时不会原地修改目标对象而是执行非破坏性变更Nondestructive mutation——即通过构造函数创建一个带有修改属性值的新对象。本文以 Record-types.md 为核心结合 Mapster 源码中的RecordTypeAdapter、RecordTypeIdentityHelper与测试用例完整讲解 Record 类型的映射原理、构造函数参数默认值规则、多构造函数选择策略以及 v7.4.0 与 v10.0 两个版本的行为差异和可用的扩展配置MapToConstructor、Ignore、IgnoreNullValues。Record 类型映射的不可变语义[!IMPORTANT] Mapster 将 Record 类型视为不可变类型immutable type。 映射时只进行非破坏性变更Nondestructive mutation——即创建一个带有修改属性值的新对象而不是修改原对象。对于目标类型是 Record 的映射var result source.adapt(data) // 等价于 var result data with { X source.X.Adapt(), ...}也就是说Adapt的结果是一个新实例源对象中与目标属性同名的成员会被依次转换并传入目标 Record 的构造函数。这一点与普通 class 的就地赋值行为有本质区别普通 class 映射会通过属性 setter 修改目标对象而 Record 映射则始终走构造新对象的路径。这一语义在源码中得到了验证。在 RecordTypeAdapter.cs 的CreateInstantiationExpression中Record 的实例化表达式直接构建为new TDestination(src.Prop1, src.Prop2)形式随后RecordInlineExpression再把剩余的可写成员通过MemberInit绑定到新实例上最终返回一个全新的对象表达式。测试 WhenMappingRecordRegression.cs 中的AdaptRecordToRecord也印证了总是创建新对象的行为var _result _source.Adapt(_destination); object.ReferenceEquals(_result, _destination).ShouldBeFalse(); // 结果一定不是原目标对象即使是从 Record 到 Record 的更新式映射MapToTargetMapster 也不会复用目标实例而是创建新对象只有构造函数中无法覆盖的成员或被Ignore的成员才会从目标对象恢复原值。Mapster 如何识别 Record 类型RecordTypeAdapter的CanMap判断目标类型是否为 Recordprotected override bool CanMap(PreCompileArgument arg) { return arg.DestinationType.IsRecordType(); }而IsRecordType扩展方法定义在 ReflectionUtils.cs 中其判断逻辑为可空类型NullableT不算 Record实现了IConvertible的原始类型不算 Record通过RecordTypeIdentityHelper.IsDirectiveTagret检查是否带有[AdaptWith(AdaptDirectives.DestinationAsRecord)]指令支持通过自定义特性强制把某类型当作 Record 处理KeyValuePair,泛型类型被视为 Record为了兼容 Config Clone 和 Fork 的既有行为最终调用RecordTypeIdentityHelper.IsRecordType(type)做标准检测。核心检测逻辑位于 RecordTypeIdentityHelper.cs它依据 C# 规范中 Record 的两个身份特征来判断存在复制构造函数类型拥有两个及以上构造函数其中一个是受保护的IsFamily或 sealed record 中私有的IsPrivate且参数类型为自身类型即record编译器生成的protected R(R original)复制构造函数存在Clone$方法类型包含一个带IL方法实现标志的Clone$方法。只有同时满足这两个条件类型才会被认定为真正的 C# Record。这也解释了测试DetectFakeRecordWhenMappingRecordRegression.cs中的伪 Record带protected复制构造函数的普通 class不会被当作 Record 映射而是走普通 class 的修改原对象路径。v10.0 与 v7.4.0 的行为差异原文档按版本区分了两套行为写作时请以当前仓库版本对应的 v10.0 行为为准。v10.0全量 Record 支持[!NOTE] 默认情况下所有 C# Record 都被视为 record 类型进行映射。 Mapster 7.4.0 中关于构造函数数量与构造函数参数的限制不再适用。换句话说v10.0 起位置 Recordpositional record与普通 Record 声明均可直接映射目标 Record 可以拥有多个构造函数构造函数参数与属性名不必完全一致Mapster 会按映射规则自动匹配源成员构造函数参数可以带默认值源中没有对应成员时回退到默认值详见下文。v7.4.0严格限制历史行为[!NOTE] Record 类型不能有 setter且只能有一个非空构造函数并且所有构造函数参数名必须与属性名完全匹配。如果不满足上述条件就需要显式添加MapToConstructor配置例如class Person { public string Name { get; } public int Age { get; } public Person(string name, int age) { this.Name name; this.Age age; } } var src new { Name Mapster, Age 3 }; var target src.AdaptPerson();在 v7.4.0 中这类只读属性 唯一构造函数的类型可以自动通过构造函数完成映射。而一旦出现多个构造函数或 setter就必须借助MapToConstructor手动指定。这些限制在 v10.0 中已全部移除。构造函数参数的默认值处理原文档指出如果源类型中不存在可以作为构造函数参数使用的成员那么将使用该参数类型的默认值default。示例class SourceData { public string MyString {get; set;} } record RecordDestination(int myInt, string myString); var result source.AdaptRecordDestination() // 等价于 var result new RecordDestination(default(int), source.myString)也就是说源对象SourceData中没有myInt对应的成员Mapster 会给它填入default(int)即 0而myString能从源中找到同名成员则使用源值。这一行为与位置 Record 的参数默认值相辅相成。测试 WhenMappingRecordTypes.cs 的Map_RecordType给出了带默认参数值的完整验证public record RecordType { public RecordType(Guid id, DayOfWeek day, string name foo, int age 10) { this.Id id; this.Day day; this.Name name; this.Age age; } public Guid Id { get; } public string Name { get; } public int Age { get; } public DayOfWeek Day { get; } } var source new SimplePoco {Id Guid.NewGuid(), Name bar}; var dest source.AdaptRecordType(); dest.Id.ShouldBe(source.Id); // 源中有 Id使用源值 dest.Name.ShouldBe(source.Name); // 源中有 Name使用源值 dest.Day.ShouldBe(default(DayOfWeek)); // 源中无 Day使用默认值 default(DayOfWeek) dest.Age.ShouldBe(10); // 源中无 Age但构造函数声明了默认值 10注意这里两条规则并行生效构造函数参数带默认值name foo、age 10时若源中无对应成员Mapster 会优先使用声明的默认值构造函数参数不带默认值如int myInt时若源中无对应成员则回退到default(T)。多构造函数 Record自动选择参数最多的构造函数原文档规定如果 Record 有多个构造函数默认使用参数数量最多的那个构造函数进行映射。示例record MultiCtorRecord { public MultiCtorRecord(int myInt) { MyInt myInt; } public MultiCtorRecord(int myInt, string myString) // 此构造函数将被使用 : this(myInt) { MyString myString; } }源码中该策略的实现位于 RecordTypeAdapter.csvar ctor arg.DestinationType.GetConstructors() .OrderByDescending(it it.GetParameters().Length).ToArray().FirstOrDefault(); // 使用参数数量最多的公共构造函数对应测试MultyCtorRecordWorkedWhenMappingRecordRegression.cs验证了两个构造函数的 Record 可以同时完成新建映射与更新映射。补充说明该选参数最多构造函数的默认逻辑仅在未显式指定构造函数时生效。源码中判断条件是arg.GetConstructUsing() ! null || arg.Settings.MapToConstructor ! null时直接走base.CreateInstantiationExpression即使用用户显式配置否则才执行自动选择逻辑RecordTypeAdapter.cs当构造函数参数多于源可用成员时无法匹配的参数按上节规则回退到默认值构造完成后Record 中未被构造函数覆盖的可写成员如init属性、带 setter 的属性会通过成员初始化器MemberInit一并赋值源码见RecordInlineExpressionRecordTypeAdapter.cs。支持的其他映射特性对比原文档以表格形式给出了 v7.4.0 与 v10.0 对三个附加映射特性的支持情况映射特性v7.4.0v10.0自定义构造函数映射MapToConstructor-✅Ignore忽略成员-✅IgnoreNullValues忽略空值-✅自定义构造函数映射MapToConstructor当需要手动指定目标构造函数而不是依赖参数最多的自动选择时使用MapToConstructor配置参见 Constructor-mapping.md// 全局生效 TypeAdapterConfig.GlobalSettings.Default.MapToConstructor(true); // 针对某一对类型 TypeAdapterConfigPoco, Dto.NewConfig().MapToConstructor(true); // 显式传入 ConstructorInfo var ctor typeof(Dto).GetConstructor(new[] { typeof(int), typeof(int) }); TypeAdapterConfigPoco, Dto.NewConfig() .MapToConstructor(ctor);配置MapToConstructor后自定义成员映射需要使用 PascalCase 命名TypeAdapterConfigPoco, Dto.NewConfig() .MapToConstructor(true) .Map(Code, Id); // 使用 PascalCaseIgnore忽略成员Ignore可用于在 Record 映射时跳过某个目标成员。对 Record 而言被忽略的构造函数参数在新建映射AdaptT()中会被置为默认值在更新映射Adapt(destination)中则会保留目标对象中的原值。测试WhenRecordReceivedIgnoreCtorParamProcessingWhenMappingRecordRegression.cs验证了这一点TypeAdapterConfigUserDto456, UserRecord456.NewConfig() .Ignore(dest dest.Name); var map userDto.AdaptUserRecord456(); // map.Name 为默认值空 var maptoTarget userDto.Adapt(user); // 被忽略成员保留目标原值 John源码中MapToTarget模式下被Ignore且未在构造函数中的成员会通过RecordIngnoredWithoutConditonRestore从目标对象恢复原值RecordTypeAdapter.cs从而保证忽略语义在不可变类型上依然成立。IgnoreNullValues忽略空值IgnoreNullValues(true)配合 Record 映射可实现在更新场景下只覆盖源中非 null 的成员TypeAdapterConfigUpdateUser, UserAccount .NewConfig() .IgnoreNullValues(true);测试UpdateNullableWhenMappingRecordRegression.cs展示了其效果源对象中为 null 的Email、Modified字段不会覆盖目标 Record 中的既有值而源中非 null 的Id会被正常映射。源码中RecordInlineExpression对IgnoreNullValues的处理见 RecordTypeAdapter.csMap 模式下直接跳过可空的 null 源成员MapToTarget 模式下则生成条件表达式源成员非 null 时映射、为 null 时保留目标值。小结与使用建议针对 Record 类型的目标对象Mapster 的处理可以总结为以下几点不可变语义Adapt永远返回新实例等价于with表达式的非破坏性变更Record 识别通过复制构造函数 Clone$方法识别真正的 C# Record也可用[AdaptWith]特性强制指定构造函数默认值源无对应成员时优先使用构造函数参数的声明默认值否则使用default(T)多构造函数未显式配置时自动选择参数数量最多的公共构造函数版本差异v7.4.0 要求 Record 无 setter、单一非空构造函数且参数名与属性名完全匹配否则需MapToConstructorv10.0 已取消这些限制扩展配置MapToConstructor、Ignore、IgnoreNullValues均可在 Record 映射上正常使用相关源码位于 RecordTypeAdapter.cs完整测试用例可参考 WhenMappingRecordTypes.cs 与 WhenMappingRecordRegression.cs。在实际项目中如果目标 DTO 是不可变 Record 且需要增量更新推荐组合使用MapToTargetAdapt(destination)与IgnoreNullValues(true)既能享受 Record 的不可变与线程安全优势又能避免空值覆盖已有数据。【免费下载链接】MapsterA fast, fun and stimulating object to object Mapper项目地址: https://gitcode.com/GitHub_Trending/ma/Mapster创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价