From b4b454075e8aee968f8df76cd4f60106d8be1b66 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Sun, 22 Mar 2026 09:07:04 +0500 Subject: [PATCH 1/5] feat: add new config param DestinationAsRecord add test for #883 --- .../WhenMappingRecordRegression.cs | 36 +++++++++++++++++++ src/Mapster/Adapters/RecordTypeAdapter.cs | 7 +++- src/Mapster/Compile/PreCompileArgument.cs | 1 + src/Mapster/Models/TypeTuple.cs | 10 ++++++ src/Mapster/TypeAdapterConfig.cs | 5 +-- src/Mapster/TypeAdapterSetter.cs | 8 +++++ src/Mapster/TypeAdapterSettings.cs | 6 ++++ 7 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/Mapster.Tests/WhenMappingRecordRegression.cs b/src/Mapster.Tests/WhenMappingRecordRegression.cs index bdb9b833..a25d7db6 100644 --- a/src/Mapster.Tests/WhenMappingRecordRegression.cs +++ b/src/Mapster.Tests/WhenMappingRecordRegression.cs @@ -537,6 +537,24 @@ public void ClassUpdateAutoPropertyWitoutSetterWorking() result.X.ShouldBe(200); } + /// + /// https://github.com/MapsterMapper/Mapster/issues/883 + /// + [TestMethod] + public void ClassCtorActivateDefaultValue() + { + var source = new Source833 + { + Value1 = "123", + }; + + Should.NotThrow(() => + { + var target = source.Adapt(); + target.Value1.ShouldBe("123"); + target.Value2.ShouldBe(default); + }); + } #region NowNotWorking @@ -974,5 +992,23 @@ class InsiderWithCtorDestYx public AutoCtorDestYx X { set; get; } } + public class Source833 + { + public required string Value1 { get; init; } + } + + public class Target833 + { + public Target833(string value1, string value2) + { + Value1 = value1; + Value2 = value2; + } + + public string Value1 { get; } + + public string Value2 { get; } + } + #endregion TestClasses } diff --git a/src/Mapster/Adapters/RecordTypeAdapter.cs b/src/Mapster/Adapters/RecordTypeAdapter.cs index a4057111..e70acbd3 100644 --- a/src/Mapster/Adapters/RecordTypeAdapter.cs +++ b/src/Mapster/Adapters/RecordTypeAdapter.cs @@ -19,7 +19,12 @@ internal class RecordTypeAdapter : ClassAdapter protected override bool CanMap(PreCompileArgument arg) { - return arg.DestinationType.IsRecordType() && arg.MapType != MapType.Projection; + if(arg.MapType == MapType.Projection) + return false; + if (arg.CustomRecordType) + return true; + + return arg.DestinationType.IsRecordType(); } protected override bool CanInline(Expression source, Expression? destination, CompileArgument arg) diff --git a/src/Mapster/Compile/PreCompileArgument.cs b/src/Mapster/Compile/PreCompileArgument.cs index a907d267..df5c975e 100644 --- a/src/Mapster/Compile/PreCompileArgument.cs +++ b/src/Mapster/Compile/PreCompileArgument.cs @@ -9,5 +9,6 @@ public class PreCompileArgument public Type DestinationType; public MapType MapType; public bool ExplicitMapping; + public bool CustomRecordType; } } diff --git a/src/Mapster/Models/TypeTuple.cs b/src/Mapster/Models/TypeTuple.cs index e25a71fd..32f5cd04 100644 --- a/src/Mapster/Models/TypeTuple.cs +++ b/src/Mapster/Models/TypeTuple.cs @@ -39,5 +39,15 @@ public TypeTuple(Type source, Type destination) Source = source; Destination = destination; } + + public static TypeTuple ForDestinationType(Type destination) + { + return new TypeTuple(typeof(void), destination); + } + + public static TypeTuple ForDestinationType(TypeTuple tuple) + { + return new TypeTuple(typeof(void), tuple.Destination); + } } } diff --git a/src/Mapster/TypeAdapterConfig.cs b/src/Mapster/TypeAdapterConfig.cs index 1e6abade..3a71e05a 100644 --- a/src/Mapster/TypeAdapterConfig.cs +++ b/src/Mapster/TypeAdapterConfig.cs @@ -201,7 +201,7 @@ public TypeAdapterSetter ForType(Type sourceType, Type destinationType) /// public TypeAdapterSetter ForDestinationType() { - var key = new TypeTuple(typeof(void), typeof(TDestination)); + var key = TypeTuple.ForDestinationType(typeof(TDestination)); var settings = GetSettings(key); return new TypeAdapterSetter(settings, this); } @@ -214,7 +214,7 @@ public TypeAdapterSetter ForDestinationType() /// public TypeAdapterSetter ForDestinationType(Type destinationType) { - var key = new TypeTuple(typeof(void), destinationType); + var key = TypeTuple.ForDestinationType(destinationType); var settings = GetSettings(key); return new TypeAdapterSetter(settings, this); } @@ -593,6 +593,7 @@ internal TypeAdapterSettings GetMergedSettings(TypeTuple tuple, MapType mapType) DestinationType = tuple.Destination, MapType = mapType, ExplicitMapping = RuleMap.ContainsKey(tuple), + CustomRecordType = GetSettings(TypeTuple.ForDestinationType(tuple)).DestinationAsRecord.GetValueOrDefault(), }; //auto add setting if there is attr setting diff --git a/src/Mapster/TypeAdapterSetter.cs b/src/Mapster/TypeAdapterSetter.cs index e08ef312..c5c6aa67 100644 --- a/src/Mapster/TypeAdapterSetter.cs +++ b/src/Mapster/TypeAdapterSetter.cs @@ -510,6 +510,14 @@ public TypeAdapterSetter AfterMappingInline(Expression lambda); return this; } + + public TypeAdapterSetter DestinationAsRecord(bool value) + { + this.CheckCompiled(); + + Settings.DestinationAsRecord = value; + return this; + } } [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S4136:Method overloads should be grouped together", Justification = "")] diff --git a/src/Mapster/TypeAdapterSettings.cs b/src/Mapster/TypeAdapterSettings.cs index 15c666a2..1986fd25 100644 --- a/src/Mapster/TypeAdapterSettings.cs +++ b/src/Mapster/TypeAdapterSettings.cs @@ -185,6 +185,12 @@ public Action? Fork set => Set(nameof(Fork), value); } + public bool? DestinationAsRecord + { + get => Get(nameof(PreserveReference)); + set => Set(nameof(PreserveReference), value); + } + internal bool Compiled { get; set; } public TypeAdapterSettings Clone() From dfb3954f3d1a27cf1730b5518614079443170744 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Sun, 22 Mar 2026 17:39:54 +0500 Subject: [PATCH 2/5] feat: add UseDestinationValue for member in config --- src/Mapster/Adapters/BaseClassAdapter.cs | 22 ++++++++++++++++------ src/Mapster/TypeAdapterSetter.cs | 22 ++++++++++++++++++++++ src/Mapster/TypeAdapterSettings.cs | 5 +++++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index 41208035..2b7e2f8c 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -40,8 +40,8 @@ src is LambdaExpression lambda if (arg.Settings.IgnoreNonMapped == true) resolvers = resolvers.Where(ValueAccessingStrategy.CustomResolvers.Contains); var getter = (from fn in resolvers - from src in sources - select fn(src, destinationMember, arg)) + from src in sources + select fn(src, destinationMember, arg)) .FirstOrDefault(result => result != null); if (arg.MapType == MapType.Projection && getter != null) @@ -111,9 +111,9 @@ select fn(src, destinationMember, arg)) NextIgnore = nextIgnore, Source = (ParameterExpression)source, Destination = (ParameterExpression?)destination, - UseDestinationValue = arg.MapType != MapType.Projection && destinationMember.UseDestinationValue(arg), + UseDestinationValue = IsCanUsingDestinationValue(arg, destinationMember), }; - if(getter == null && !arg.DestinationType.IsRecordType() + if (getter == null && !arg.DestinationType.IsRecordType() && destinationMember.Info is PropertyInfo propinfo) { if (propinfo.GetCustomAttributes() @@ -129,8 +129,8 @@ select fn(src, destinationMember, arg)) } if (getter != null) { - propertyModel.Getter = arg.MapType == MapType.Projection - ? getter + propertyModel.Getter = arg.MapType == MapType.Projection + ? getter : getter.ApplyNullPropagation(); properties.Add(propertyModel); } @@ -189,6 +189,16 @@ select fn(src, destinationMember, arg)) }; } + protected static bool IsCanUsingDestinationValue(CompileArgument arg, IMemberModelEx destinationMember) + { + if (arg.MapType == MapType.Projection) + return false; + if(destinationMember.UseDestinationValue(arg) || arg.Settings.UseDestinationMember.Contains(destinationMember.Name)) + return true; + + return false; + } + protected static bool ProcessIgnores( CompileArgument arg, IMemberModel destinationMember, diff --git a/src/Mapster/TypeAdapterSetter.cs b/src/Mapster/TypeAdapterSetter.cs index c5c6aa67..677c46cf 100644 --- a/src/Mapster/TypeAdapterSetter.cs +++ b/src/Mapster/TypeAdapterSetter.cs @@ -518,6 +518,28 @@ public TypeAdapterSetter DestinationAsRecord(bool value) Settings.DestinationAsRecord = value; return this; } + + public TypeAdapterSetter UseDestinationValue (Expression> destinationMember) + { + this.CheckCompiled(); + var memberName = destinationMember.GetMemberPath()!; + + if (memberName != null) + { + Settings.UseDestinationMember.Add(memberName); + } + + return this; + } + + public TypeAdapterSetter UseDestinationValue(string destinationMemberName) + { + this.CheckCompiled(); + + Settings.UseDestinationMember.Add(destinationMemberName); + + return this; + } } [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S4136:Method overloads should be grouped together", Justification = "")] diff --git a/src/Mapster/TypeAdapterSettings.cs b/src/Mapster/TypeAdapterSettings.cs index 1986fd25..d9f75d3a 100644 --- a/src/Mapster/TypeAdapterSettings.cs +++ b/src/Mapster/TypeAdapterSettings.cs @@ -191,6 +191,11 @@ public bool? DestinationAsRecord set => Set(nameof(PreserveReference), value); } + public List UseDestinationMember + { + get => Get(nameof(UseDestinationMember), () => new List()); + } + internal bool Compiled { get; set; } public TypeAdapterSettings Clone() From 6886d3e907e7ace5715abfad7748e216c66e9639 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Sun, 22 Mar 2026 17:41:16 +0500 Subject: [PATCH 3/5] feat: fix #883 activate ctor param using default value --- src/Mapster/Adapters/ClassAdapter.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs index 9e44105d..f3fe2720 100644 --- a/src/Mapster/Adapters/ClassAdapter.cs +++ b/src/Mapster/Adapters/ClassAdapter.cs @@ -66,11 +66,16 @@ protected override Expression CreateInstantiationExpression(Expression source, E : arg.DestinationType; if (destType == null) return base.CreateInstantiationExpression(source, destination, arg); - classConverter = destType.GetConstructors() + + var constructors = destType.GetConstructors(); + classConverter = constructors .OrderByDescending(it => it.GetParameters().Length) .Select(it => GetConstructorModel(it, true)) .Select(it => CreateClassConverter(source, it, arg, ctorMapping:true)) .FirstOrDefault(it => it != null); + + if(classConverter == null && constructors.Length > 0) + classConverter = CreateClassConverter(source, GetConstructorModel(constructors[0], false), arg, ctorMapping: true); } else { From b9f32b730ef3367f7f6519ec4235078b1a8f6a61 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Sun, 22 Mar 2026 17:47:50 +0500 Subject: [PATCH 4/5] fix(test): ignore after implementation fix #883 --- src/Mapster.Tests/WhenMappingRecordRegression.cs | 1 + src/Mapster.Tests/WhenUsingNonDefaultConstructor.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Mapster.Tests/WhenMappingRecordRegression.cs b/src/Mapster.Tests/WhenMappingRecordRegression.cs index a25d7db6..fe79b47b 100644 --- a/src/Mapster.Tests/WhenMappingRecordRegression.cs +++ b/src/Mapster.Tests/WhenMappingRecordRegression.cs @@ -477,6 +477,7 @@ public void ClassCtorAutomapingWorking() /// /// https://github.com/MapsterMapper/Mapster/issues/842 /// + [Ignore] // after fix https://github.com/MapsterMapper/Mapster/issues/883 [TestMethod] public void ClassCustomCtorWitoutMapNotWorking() { diff --git a/src/Mapster.Tests/WhenUsingNonDefaultConstructor.cs b/src/Mapster.Tests/WhenUsingNonDefaultConstructor.cs index ac41e5fc..45bea2d5 100644 --- a/src/Mapster.Tests/WhenUsingNonDefaultConstructor.cs +++ b/src/Mapster.Tests/WhenUsingNonDefaultConstructor.cs @@ -72,6 +72,7 @@ public void Map_To_Existing_Destination_Instance_Should_Pass() dto.Unmapped.ShouldBe("unmapped"); } + [Ignore] // after https://github.com/MapsterMapper/Mapster/issues/883 [TestMethod] public void Map_To_Destination_Type_Without_Default_Constructor_Shoud_Throw_Exception() { From eb27050395d8a4854c68ceb6dd25513d63c8c08c Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 23 Mar 2026 09:27:56 +0500 Subject: [PATCH 5/5] fix: record detector --- src/Mapster/Adapters/BaseClassAdapter.cs | 8 ++++---- src/Mapster/Adapters/RecordTypeAdapter.cs | 8 ++++---- src/Mapster/Settings/ValueAccessingStrategy.cs | 4 ++-- src/Mapster/Utils/ReflectionUtils.cs | 12 +++++++++++- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index 2b7e2f8c..b0155ba8 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -113,7 +113,7 @@ select fn(src, destinationMember, arg)) Destination = (ParameterExpression?)destination, UseDestinationValue = IsCanUsingDestinationValue(arg, destinationMember), }; - if (getter == null && !arg.DestinationType.IsRecordType() + if (getter == null && !arg.DestinationType.IsRecordType(arg) && destinationMember.Info is PropertyInfo propinfo) { if (propinfo.GetCustomAttributes() @@ -123,7 +123,7 @@ select fn(src, destinationMember, arg)) } } - if (arg.MapType == MapType.MapToTarget && getter == null && arg.DestinationType.IsRecordType()) + if (arg.MapType == MapType.MapToTarget && getter == null && arg.DestinationType.IsRecordType(arg)) { getter = TryRestoreRecordMember(destinationMember, recordRestorMemberModel, destination) ?? getter; } @@ -229,7 +229,7 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi { getter = defaultConst; - if (arg.MapType == MapType.MapToTarget && arg.DestinationType.IsRecordType()) + if (arg.MapType == MapType.MapToTarget && arg.DestinationType.IsRecordType(arg)) getter = TryRestoreRecordMember(member.DestinationMember,recordRestorParamModel,destination) ?? getter; } else @@ -259,7 +259,7 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi { getter = defaultConst; - if (arg.MapType == MapType.MapToTarget && arg.DestinationType.IsRecordType()) + if (arg.MapType == MapType.MapToTarget && arg.DestinationType.IsRecordType(arg)) getter = TryRestoreRecordMember(member.DestinationMember, recordRestorParamModel, destination) ?? getter; } } diff --git a/src/Mapster/Adapters/RecordTypeAdapter.cs b/src/Mapster/Adapters/RecordTypeAdapter.cs index e70acbd3..6ae30b18 100644 --- a/src/Mapster/Adapters/RecordTypeAdapter.cs +++ b/src/Mapster/Adapters/RecordTypeAdapter.cs @@ -24,7 +24,7 @@ protected override bool CanMap(PreCompileArgument arg) if (arg.CustomRecordType) return true; - return arg.DestinationType.IsRecordType(); + return arg.DestinationType.IsRecordType(arg); } protected override bool CanInline(Expression source, Expression? destination, CompileArgument arg) @@ -204,11 +204,11 @@ protected override Expression CreateBlockExpression(Expression source, Expressio if (member.DestinationMember is PropertyModel && member.DestinationMember.Type.IsValueType || member.DestinationMember.Type.IsMapsterPrimitive() - || member.DestinationMember.Type.IsRecordType()) + || member.DestinationMember.Type.IsRecordType(arg)) { Expression adapt; - if (member.DestinationMember.Type.IsRecordType()) + if (member.DestinationMember.Type.IsRecordType(arg)) adapt = arg.Context.Config.CreateMapInvokeExpressionBody(member.Getter.Type, member.DestinationMember.Type, member.Getter); else adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member, result); @@ -232,7 +232,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio Expression destMemberVar2 = var2Param.DestinationMember.GetExpression(var2Param.Destination); var ParamLambdaVar2 = destMemberVar2; - if(member.DestinationMember.Type.IsRecordType()) + if(member.DestinationMember.Type.IsRecordType(arg)) ParamLambdaVar2 = arg.Context.Config.CreateMapInvokeExpressionBody(member.Getter.Type, member.DestinationMember.Type, destMemberVar2); var blocksVar2 = Expression.Block(SetValueTypeAutoPropertyByReflection(member, ParamLambdaVar2, classModel)); diff --git a/src/Mapster/Settings/ValueAccessingStrategy.cs b/src/Mapster/Settings/ValueAccessingStrategy.cs index fd13407d..df02b158 100644 --- a/src/Mapster/Settings/ValueAccessingStrategy.cs +++ b/src/Mapster/Settings/ValueAccessingStrategy.cs @@ -117,7 +117,7 @@ public static class ValueAccessingStrategy var propertyType = member.Type; if (propertyName.StartsWith(sourceMemberName) && - (propertyType.IsPoco() || propertyType.IsRecordType())) + (propertyType.IsPoco() || propertyType.IsRecordType(arg))) { var exp = member.GetExpression(source); var ifTrue = GetDeepFlattening(exp, propertyName.Substring(sourceMemberName.Length).TrimStart('_'), arg); @@ -168,7 +168,7 @@ private static IEnumerable GetDeepUnflattening(IMemberModel destinationM yield return member.Name; } else if (propertyName.StartsWith(destMemberName) && - (propertyType.IsPoco() || propertyType.IsRecordType())) + (propertyType.IsPoco() || propertyType.IsRecordType(arg))) { foreach (var prop in GetDeepUnflattening(member, propertyName.Substring(destMemberName.Length).TrimStart('_'), arg)) { diff --git a/src/Mapster/Utils/ReflectionUtils.cs b/src/Mapster/Utils/ReflectionUtils.cs index 3b9b1a1b..377f5938 100644 --- a/src/Mapster/Utils/ReflectionUtils.cs +++ b/src/Mapster/Utils/ReflectionUtils.cs @@ -193,7 +193,17 @@ public static Type UnwrapNullable(this Type type) return type.IsNullable() ? type.GetGenericArguments()[0] : type; } - public static bool IsRecordType(this Type type) + public static bool IsRecordType(this Type type, CompileArgument arg) + { + return arg.Settings.DestinationAsRecord.GetValueOrDefault() || type.IsRecordType(); + } + + public static bool IsRecordType(this Type type, PreCompileArgument arg) + { + return arg.CustomRecordType || type.IsRecordType(); + } + + private static bool IsRecordType(this Type type) { //not nullable if (type.IsNullable())