diff --git a/src/Mapster.Tests/WhenAddCtorNullablePropagation.cs b/src/Mapster.Tests/WhenAddCtorNullablePropagation.cs new file mode 100644 index 00000000..b13eefa6 --- /dev/null +++ b/src/Mapster.Tests/WhenAddCtorNullablePropagation.cs @@ -0,0 +1,51 @@ +using Mapster.Tests.Classes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; +using System.Linq; + +namespace Mapster.Tests +{ + [TestClass] + public class WhenAddCtorNullablePropagation + { + + /// + /// https://github.com/MapsterMapper/Mapster/issues/898 + /// + [TestMethod] + public void NullablePropagationFromCtorWorking() + { + var source = new List(); + + source.Add(new OrderEntity898() { Id = 1, Cod = new OrderCodEntity898 { Value = 42L } }); + source.Add(new OrderEntity898() { Id = 2, Cod = null }); + + var str = new OrderEntity898() { Id = 1, Cod = new OrderCodEntity898 { Value = 42L } }.BuildAdapter().CreateProjectionExpression(); + + var result = source.AsQueryable().ProjectToType().ToList(); + } + + } + + #region TestClasses + + public record OrderDto898(int Id, OrderCodDto898? Cod); + public record OrderCodDto898(long Value); + + + public class OrderEntity898 + { + public int Id { get; set; } + public int? CodId { get; set; } + public OrderCodEntity898? Cod { get; set; } + } + + public class OrderCodEntity898 + { + public int Id { get; set; } + public long Value { get; set; } + } + + + #endregion TestClasses +} diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index f092c3ab..e36ad7a8 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -220,6 +220,7 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi var arguments = new List(); foreach (var member in members) { + arg.Context.NullChecks.UnionWith(members.Select(x=>(x.Getter,arg))); var parameterInfo = (ParameterInfo)member.DestinationMember.Info!; var defaultConst = parameterInfo.IsOptional ? Expression.Constant(parameterInfo.DefaultValue, member.DestinationMember.Type) @@ -245,7 +246,9 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi defaultConst); } else - getter = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); + getter = member.Getter + .ApplyNullPropagationFromCtor(CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member), arg); + if (member.Ignore.Condition != null) { diff --git a/src/Mapster/Compile/CompileContext.cs b/src/Mapster/Compile/CompileContext.cs index f0188fb4..72d640c8 100644 --- a/src/Mapster/Compile/CompileContext.cs +++ b/src/Mapster/Compile/CompileContext.cs @@ -12,6 +12,7 @@ public class CompileContext public int? MaxDepth { get; set; } public int Depth { get; set; } public HashSet ExtraParameters { get; } = new(); + public HashSet<(Expression param, CompileArgument arg)> NullChecks { get; } = new(); internal bool IsSubFunction() { diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index 090d81e1..bca07c31 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -1,8 +1,9 @@ -using System.Linq.Expressions; +using Mapster.Models; using System; using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Reflection; namespace Mapster.Utils @@ -433,6 +434,44 @@ public static Expression ApplyNullPropagation(this Expression getter) return getter; } + public static Expression ApplyNullPropagationFromCtor(this Expression getter, Expression adapt, CompileArgument arg) + { + Expression? condition = null; + var current = getter; + var checks = arg.Context.NullChecks + .Where(x=> !object.ReferenceEquals(x.arg,arg)) + .Select(x=>x.param.Type); + + while (current != null) + { + Expression? compareNull = null; + + if (current.CanBeNull() && current is not ParameterExpression) + compareNull = Expression.NotEqual(current, Expression.Constant(null, current.Type)); + else if (current.CanBeNull() && current is ParameterExpression + && !checks.Contains(current.Type)) + compareNull = Expression.NotEqual(current, Expression.Constant(null, current.Type)); + + if (compareNull != null) + { + if (condition == null) + condition = compareNull; + else + condition = Expression.AndAlso(compareNull, condition); + } + + if (current is MemberExpression member) + current = member.Expression; + else + current = null; + } + + if (condition == null) + return adapt; + + return Expression.Condition(condition, adapt, adapt.Type.CreateDefault()); + } + public static string? GetMemberPath(this LambdaExpression lambda, bool firstLevelOnly = false, bool noError = false) { var props = new List();