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 d419932f..7fd04fd3 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -226,7 +226,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 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) 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