From 8586a4c8bde5c5de6474aba0c116264644517ad6 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 29 Sep 2025 23:01:13 +0500 Subject: [PATCH 1/2] add constructor param null checker --- .../WhenCtorNullableParamMapping.cs | 115 +++++++++++++ src/Mapster/Adapters/BaseClassAdapter.cs | 12 +- src/Mapster/Adapters/ClassAdapter.cs | 2 +- .../Utils/MemberExpressionExtractor.cs | 152 ++++++++++++++++++ 4 files changed, 279 insertions(+), 2 deletions(-) create mode 100644 src/Mapster.Tests/WhenCtorNullableParamMapping.cs create mode 100644 src/Mapster/Utils/MemberExpressionExtractor.cs diff --git a/src/Mapster.Tests/WhenCtorNullableParamMapping.cs b/src/Mapster.Tests/WhenCtorNullableParamMapping.cs new file mode 100644 index 00000000..bef0b16f --- /dev/null +++ b/src/Mapster.Tests/WhenCtorNullableParamMapping.cs @@ -0,0 +1,115 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; + +namespace Mapster.Tests +{ + [TestClass] + public class WhenCtorNullableParamMapping + { + [TestMethod] + public void Dto_To_Domain_MapsCorrectly() + { + var config = new TypeAdapterConfig(); + + config.Default.MapToConstructor(true); + config + .NewConfig() + .Include(); + + + var dtoDerived = new DerivedDtoTestClass + { + DerivedProperty = "DerivedValue", + AbstractProperty = "AbstractValue" + }; + + var dto = new DtoTestClass + { + AbstractType = dtoDerived + }; + + var domain = dto.Adapt(config); + + domain.AbstractType.ShouldNotBe(null); + domain.AbstractType.ShouldBeOfType(); + + var domainDerived = (DerivedDomainTestClass)domain.AbstractType; + domainDerived.DerivedProperty.ShouldBe(dtoDerived.DerivedProperty); + domainDerived.AbstractProperty.ShouldBe(dtoDerived.AbstractProperty); + + } + + [TestMethod] + public void Dto_To_Domain_AbstractClassNull_MapsCorrectly() + { + var config = new TypeAdapterConfig(); + + config.Default.MapToConstructor(true); + config + .NewConfig() + .Include(); + + var dto = new DtoTestClass + { + AbstractType = null + }; + + var domain = dto.Adapt(config); + + domain.AbstractType.ShouldBeNull(); + } + + + #region Immutable classes with private setters, map via ctors + private abstract class AbstractDomainTestClass + { + public string AbstractProperty { get; private set; } + + protected AbstractDomainTestClass(string abstractProperty) + { + AbstractProperty = abstractProperty; + } + } + + private class DerivedDomainTestClass : AbstractDomainTestClass + { + public string DerivedProperty { get; private set; } + + /// + public DerivedDomainTestClass(string abstractProperty, string derivedProperty) + : base(abstractProperty) + { + DerivedProperty = derivedProperty; + } + } + + private class DomainTestClass + { + public AbstractDomainTestClass? AbstractType { get; private set; } + + public DomainTestClass( + AbstractDomainTestClass? abstractType) + { + AbstractType = abstractType; + } + } + #endregion + + #region DTO classes + private abstract class AbstractDtoTestClass + { + public string AbstractProperty { get; set; } + } + + private class DerivedDtoTestClass : AbstractDtoTestClass + { + public string DerivedProperty { get; set; } + } + + private class DtoTestClass + { + public AbstractDtoTestClass? AbstractType { get; set; } + } + #endregion + } +} diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index faa490ec..0a18523d 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -147,7 +147,17 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi } else { - getter = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); + + if (member.Getter.CanBeNull() && member.Ignore.Condition == null) + { + var compareNull = Expression.Equal(member.Getter, Expression.Constant(null, member.Getter.Type)); + getter = Expression.Condition(ExpressionEx.Not(compareNull), + CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member), + defaultConst); + } + else + getter = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); + if (member.Ignore.Condition != null) { var body = member.Ignore.IsChildPath diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs index 407c5a5b..0b984f0a 100644 --- a/src/Mapster/Adapters/ClassAdapter.cs +++ b/src/Mapster/Adapters/ClassAdapter.cs @@ -201,7 +201,7 @@ private static Expression SetValueByReflection(MemberMapping member, MemberExpre var exp = CreateInstantiationExpression(source, arg); var memberInit = exp as MemberInitExpression; var newInstance = memberInit?.NewExpression ?? (NewExpression)exp; - var contructorMembers = newInstance.Arguments.OfType().Select(me => me.Member).ToArray(); + var contructorMembers = newInstance.GetAllMemberExpressionsMemberInfo().ToArray(); var classModel = GetSetterModel(arg); var classConverter = CreateClassConverter(source, classModel, arg); var members = classConverter.Members; diff --git a/src/Mapster/Utils/MemberExpressionExtractor.cs b/src/Mapster/Utils/MemberExpressionExtractor.cs new file mode 100644 index 00000000..f333ad79 --- /dev/null +++ b/src/Mapster/Utils/MemberExpressionExtractor.cs @@ -0,0 +1,152 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; + +public static class MemberExpressionExtractor +{ + public static IEnumerable GetAllMemberExpressionsMemberInfo(this Expression expression) + { + var result = new List(); + CollectMemberInfos(expression, result); + return result; + } + + private static void CollectMemberInfos(Expression expression, ICollection results) + { + if (expression == null) return; + + if (expression is MemberExpression memberExpression) + { + results.Add(memberExpression.Member); + } + else if (expression is BinaryExpression binaryExpression && binaryExpression.NodeType == ExpressionType.Assign) + { + ProcessBinaryAssign(binaryExpression, results); + } + + foreach (var subExpression in expression.GetSubExpressions()) + { + CollectMemberInfos(subExpression, results); + } + } + + private static void ProcessBinaryAssign(BinaryExpression assignExpression, ICollection results) + { + if (assignExpression == null) return; + + if (assignExpression.Left is MemberExpression leftMember) + { + results.Add(leftMember.Member); + } + + CollectMemberInfos(assignExpression.Right, results); + } + + private static IEnumerable GetSubExpressions(this Expression expression) + { + if (expression == null) yield break; + + switch (expression.NodeType) + { + case ExpressionType.MemberAccess: + yield return ((MemberExpression)expression).Expression; + break; + + case ExpressionType.Call: + foreach (var arg in ((MethodCallExpression)expression).Arguments) + yield return arg; + yield return ((MethodCallExpression)expression).Object; + break; + + case ExpressionType.Lambda: + yield return ((LambdaExpression)expression).Body; + break; + + case ExpressionType.Block: + foreach (var blockExpr in ((BlockExpression)expression).Expressions) + yield return blockExpr; + break; + + case ExpressionType.Conditional: + yield return ((ConditionalExpression)expression).Test; + yield return ((ConditionalExpression)expression).IfTrue; + yield return ((ConditionalExpression)expression).IfFalse; + break; + + case ExpressionType.NewArrayInit or ExpressionType.NewArrayBounds: + foreach (var arrayItem in ((NewArrayExpression)expression).Expressions) + yield return arrayItem; + break; + + case ExpressionType.New: + foreach (var arg in ((NewExpression)expression).Arguments) + yield return arg; + break; + + case ExpressionType.Invoke: + yield return ((InvocationExpression)expression).Expression; + break; + + case ExpressionType.Assign: + yield return ((BinaryExpression)expression).Left; + yield return ((BinaryExpression)expression).Right; + break; + + case ExpressionType.Add: + case ExpressionType.Subtract: + case ExpressionType.Multiply: + case ExpressionType.Divide: + case ExpressionType.Equal: + case ExpressionType.NotEqual: + case ExpressionType.GreaterThan: + case ExpressionType.LessThan: + case ExpressionType.AndAlso: + case ExpressionType.OrElse: + yield return ((BinaryExpression)expression).Left; + yield return ((BinaryExpression)expression).Right; + break; + + case ExpressionType.Not: + case ExpressionType.Negate: + case ExpressionType.Convert: + case ExpressionType.Increment: + case ExpressionType.Decrement: + case ExpressionType.Quote: + case ExpressionType.TypeAs: + case ExpressionType.OnesComplement: + yield return ((UnaryExpression)expression).Operand; + break; + + case ExpressionType.TypeIs: + yield return ((TypeBinaryExpression)expression).Expression; + break; + + case ExpressionType.Coalesce: + yield return ((BinaryExpression)expression).Left; + yield return ((BinaryExpression)expression).Conversion; + yield return ((BinaryExpression)expression).Right; + break; + + case ExpressionType.Index: + yield return ((IndexExpression)expression).Object; + foreach (var indexArg in ((IndexExpression)expression).Arguments) + yield return indexArg; + break; + + case ExpressionType.Loop: + yield return ((LoopExpression)expression).Body; + break; + + case ExpressionType.Try: + yield return ((TryExpression)expression).Body; + foreach (var handler in ((TryExpression)expression).Handlers) + yield return handler.Body; + yield return ((TryExpression)expression).Finally; + break; + + + default: + break; + } + } +} \ No newline at end of file From a2a19f1641114b0f2fe0ab68227f6bd0db4cba38 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Tue, 30 Sep 2025 22:24:43 +0500 Subject: [PATCH 2/2] Restore --- src/Mapster/Adapters/ClassAdapter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs index 5c99ddde..de39dec2 100644 --- a/src/Mapster/Adapters/ClassAdapter.cs +++ b/src/Mapster/Adapters/ClassAdapter.cs @@ -201,7 +201,7 @@ private static Expression SetValueByReflection(MemberMapping member, MemberExpre var exp = CreateInstantiationExpression(source, arg); var memberInit = exp as MemberInitExpression; var newInstance = memberInit?.NewExpression ?? (NewExpression)exp; - var contructorMembers = newInstance.Arguments.OfType().Select(me => me.Member).ToArray(); + var contructorMembers = newInstance.GetAllMemberExpressionsMemberInfo().ToArray(); ClassModel? classModel; ClassMapping? classConverter; if (IsRequiredOnly)