diff --git a/src/Mapster.Tests/WhenMappingRecordRegression.cs b/src/Mapster.Tests/WhenMappingRecordRegression.cs index bdb9b833..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() { @@ -537,6 +538,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 +993,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.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() { diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index 41208035..b0155ba8 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(arg) && destinationMember.Info is PropertyInfo propinfo) { if (propinfo.GetCustomAttributes() @@ -123,14 +123,14 @@ 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; } 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, @@ -219,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 @@ -249,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/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 { diff --git a/src/Mapster/Adapters/RecordTypeAdapter.cs b/src/Mapster/Adapters/RecordTypeAdapter.cs index a4057111..6ae30b18 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(arg); } protected override bool CanInline(Expression source, Expression? destination, CompileArgument arg) @@ -199,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); @@ -227,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/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/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/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..677c46cf 100644 --- a/src/Mapster/TypeAdapterSetter.cs +++ b/src/Mapster/TypeAdapterSetter.cs @@ -510,6 +510,36 @@ public TypeAdapterSetter AfterMappingInline(Expression lambda); return this; } + + public TypeAdapterSetter DestinationAsRecord(bool value) + { + this.CheckCompiled(); + + 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 15c666a2..d9f75d3a 100644 --- a/src/Mapster/TypeAdapterSettings.cs +++ b/src/Mapster/TypeAdapterSettings.cs @@ -185,6 +185,17 @@ public Action? Fork set => Set(nameof(Fork), value); } + public bool? DestinationAsRecord + { + get => Get(nameof(PreserveReference)); + set => Set(nameof(PreserveReference), value); + } + + public List UseDestinationMember + { + get => Get(nameof(UseDestinationMember), () => new List()); + } + internal bool Compiled { get; set; } public TypeAdapterSettings Clone() 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())