Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/Mapster.Tests/WhenMappingRecordRegression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@ public void ClassCtorAutomapingWorking()
/// <summary>
/// https://github.com/MapsterMapper/Mapster/issues/842
/// </summary>
[Ignore] // after fix https://github.com/MapsterMapper/Mapster/issues/883
[TestMethod]
public void ClassCustomCtorWitoutMapNotWorking()
{
Expand Down Expand Up @@ -537,6 +538,24 @@ public void ClassUpdateAutoPropertyWitoutSetterWorking()
result.X.ShouldBe(200);
}

/// <summary>
/// https://github.com/MapsterMapper/Mapster/issues/883
/// </summary>
[TestMethod]
public void ClassCtorActivateDefaultValue()
{
var source = new Source833
{
Value1 = "123",
};

Should.NotThrow(() =>
{
var target = source.Adapt<Target833>();
target.Value1.ShouldBe("123");
target.Value2.ShouldBe(default);
});
}

#region NowNotWorking

Expand Down Expand Up @@ -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
}
1 change: 1 addition & 0 deletions src/Mapster.Tests/WhenUsingNonDefaultConstructor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
28 changes: 19 additions & 9 deletions src/Mapster/Adapters/BaseClassAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

#region Build the Adapter Model

protected ClassMapping CreateClassConverter(Expression source, ClassModel classModel, CompileArgument arg, Expression? destination = null, bool ctorMapping = false, ClassModel recordRestorMemberModel = null)

Check warning on line 18 in src/Mapster/Adapters/BaseClassAdapter.cs

View workflow job for this annotation

GitHub Actions / build

Cannot convert null literal to non-nullable reference type.
{
var destinationMembers = classModel.Members;
var unmappedDestinationMembers = new List<string>();
Expand All @@ -40,8 +40,8 @@
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)
Expand Down Expand Up @@ -111,9 +111,9 @@
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()
Expand All @@ -123,14 +123,14 @@
}
}

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);
}
Expand Down Expand Up @@ -189,6 +189,16 @@
};
}

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,
Expand All @@ -202,7 +212,7 @@
&& ignore.Condition == null;
}

protected Expression CreateInstantiationExpression(Expression source, ClassMapping classConverter, CompileArgument arg, Expression? destination, ClassModel recordRestorParamModel = null)

Check warning on line 215 in src/Mapster/Adapters/BaseClassAdapter.cs

View workflow job for this annotation

GitHub Actions / build

Cannot convert null literal to non-nullable reference type.
{
var members = classConverter.Members;

Expand All @@ -219,7 +229,7 @@
{
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
Expand Down Expand Up @@ -249,7 +259,7 @@
{
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;
}
}
Expand Down
7 changes: 6 additions & 1 deletion src/Mapster/Adapters/ClassAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
13 changes: 9 additions & 4 deletions src/Mapster/Adapters/RecordTypeAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@

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)
Expand Down Expand Up @@ -51,7 +56,7 @@
}


return RecordInlineExpression(source, destination, arg, installExpr); // Activator field when not include in public ctor

Check warning on line 59 in src/Mapster/Adapters/RecordTypeAdapter.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference return.
}

private Expression? RecordInlineExpression(Expression source, Expression? destination, CompileArgument arg, Expression installExpr)
Expand Down Expand Up @@ -101,7 +106,7 @@
binEx.Right is ConstantExpression { Value: null })
adapt = condEx.IfFalse;
}
var destinationCompareNull = Expression.Equal(destination, Expression.Constant(null, destination.Type));

Check warning on line 109 in src/Mapster/Adapters/RecordTypeAdapter.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
var sourceCondition = Expression.NotEqual(member.Getter, Expression.Constant(null, member.Getter.Type));
var destinationCanbeNull = Expression.Condition(destinationCompareNull, member.DestinationMember.Type.CreateDefault(), member.DestinationMember.GetExpression(destination));
adapt = Expression.Condition(sourceCondition, adapt, destinationCanbeNull);
Expand Down Expand Up @@ -152,7 +157,7 @@
contructorMembers.Any(x => string.Equals(x.Name, member.Name, StringComparison.InvariantCultureIgnoreCase)))
continue;

lines.Add(Expression.Bind((MemberInfo)member.Info, Expression.MakeMemberAccess(destination, (MemberInfo)member.Info)));

Check warning on line 160 in src/Mapster/Adapters/RecordTypeAdapter.cs

View workflow job for this annotation

GitHub Actions / build

Converting null literal or possible null value to non-nullable type.

Check warning on line 160 in src/Mapster/Adapters/RecordTypeAdapter.cs

View workflow job for this annotation

GitHub Actions / build

Converting null literal or possible null value to non-nullable type.
}

return lines;
Expand Down Expand Up @@ -199,11 +204,11 @@

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);
Expand All @@ -227,7 +232,7 @@

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));
Expand Down
1 change: 1 addition & 0 deletions src/Mapster/Compile/PreCompileArgument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ public class PreCompileArgument
public Type DestinationType;
public MapType MapType;
public bool ExplicitMapping;
public bool CustomRecordType;
}
}
10 changes: 10 additions & 0 deletions src/Mapster/Models/TypeTuple.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
4 changes: 2 additions & 2 deletions src/Mapster/Settings/ValueAccessingStrategy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -168,7 +168,7 @@ private static IEnumerable<string> 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))
{
Expand Down
5 changes: 3 additions & 2 deletions src/Mapster/TypeAdapterConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ public TypeAdapterSetter ForType(Type sourceType, Type destinationType)
/// <returns></returns>
public TypeAdapterSetter<TDestination> ForDestinationType<TDestination>()
{
var key = new TypeTuple(typeof(void), typeof(TDestination));
var key = TypeTuple.ForDestinationType(typeof(TDestination));
var settings = GetSettings(key);
return new TypeAdapterSetter<TDestination>(settings, this);
}
Expand All @@ -214,7 +214,7 @@ public TypeAdapterSetter<TDestination> ForDestinationType<TDestination>()
/// <returns></returns>
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);
}
Expand Down Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions src/Mapster/TypeAdapterSetter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,36 @@ public TypeAdapterSetter<TDestination> AfterMappingInline(Expression<Action<TDes
Settings.AfterMappingFactories.Add(arg => lambda);
return this;
}

public TypeAdapterSetter<TDestination> DestinationAsRecord(bool value)
{
this.CheckCompiled();

Settings.DestinationAsRecord = value;
return this;
}

public TypeAdapterSetter<TDestination> UseDestinationValue<TDestinationMember> (Expression<Func<TDestination, TDestinationMember>> 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 = "<Pending>")]
Expand Down
11 changes: 11 additions & 0 deletions src/Mapster/TypeAdapterSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,17 @@ public Action<TypeAdapterConfig>? Fork
set => Set(nameof(Fork), value);
}

public bool? DestinationAsRecord
{
get => Get(nameof(PreserveReference));
set => Set(nameof(PreserveReference), value);
}

public List<string> UseDestinationMember
{
get => Get(nameof(UseDestinationMember), () => new List<string>());
}

internal bool Compiled { get; set; }

public TypeAdapterSettings Clone()
Expand Down
12 changes: 11 additions & 1 deletion src/Mapster/Utils/ReflectionUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Loading