From a0aee31392aa010116334d5988334fca21582170 Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Mon, 25 May 2026 14:37:05 +0800 Subject: [PATCH] fix: guard nullable navigation in ProjectToType record ctor mapping (#898) When a nullable navigation maps to a record DTO constructor parameter, skip nested mapping when the source is null instead of evaluating record ctor args against a null navigation. Also track NullChecks by expression identity so nested projection parameters are not skipped solely because they share a nullable type with a parent getter. Co-authored-by: Cursor --- .../WhenAddCtorNullablePropagation.cs | 25 +++++++++++++------ src/Mapster/Adapters/BaseClassAdapter.cs | 9 ++++--- src/Mapster/Utils/ExpressionEx.cs | 10 ++++---- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/Mapster.Tests/WhenAddCtorNullablePropagation.cs b/src/Mapster.Tests/WhenAddCtorNullablePropagation.cs index b13eefa6..d321f971 100644 --- a/src/Mapster.Tests/WhenAddCtorNullablePropagation.cs +++ b/src/Mapster.Tests/WhenAddCtorNullablePropagation.cs @@ -1,5 +1,5 @@ -using Mapster.Tests.Classes; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; using System.Collections.Generic; using System.Linq; @@ -15,14 +15,25 @@ public class WhenAddCtorNullablePropagation [TestMethod] public void NullablePropagationFromCtorWorking() { - var source = new List(); + var source = new List + { + new() { Id = 1, Cod = new OrderCodEntity898 { Value = 42L } }, + new() { Id = 2, Cod = null }, + }; - 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(); + Should.NotThrow(() => + { + source.AsQueryable().BuildAdapter().CreateProjectionExpression(); + }); var result = source.AsQueryable().ProjectToType().ToList(); + + result.Count.ShouldBe(2); + result[0].Id.ShouldBe(1); + result[0].Cod.ShouldNotBeNull(); + result[0].Cod!.Value.ShouldBe(42L); + result[1].Id.ShouldBe(2); + result[1].Cod.ShouldBeNull(); } } diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index 6d12a934..140f8b18 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -218,9 +218,9 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi var members = classConverter.Members; var arguments = new List(); + arg.Context.NullChecks.UnionWith(members.Where(x => x.Getter != null).Select(x => (x.Getter, arg))); foreach (var member in members) { - arg.Context.NullChecks.UnionWith(members.Where(x=>x.Getter != null).Select(x=>(x.Getter,arg))); var parameterInfo = (ParameterInfo)member.DestinationMember.Info!; Expression defaultConst; Expression getter; @@ -253,12 +253,13 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi else { - if (member.Getter.CanBeNull() && member.DestinationMember.Type.IsAbstractOrNotPublicCtor() - && member.Ignore.Condition == null) + if (member.Getter.CanBeNull() && member.Ignore.Condition == null + && (member.DestinationMember.Type.IsAbstractOrNotPublicCtor() + || member.DestinationMember.Type.UnwrapNullable().IsRecordType())) { 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), + CreateAdaptExpressionCore(member.Getter, member.DestinationMember.Type, arg, member), defaultConst); } else diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index b7ffc365..bffdefde 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -453,8 +453,8 @@ public static Expression ApplyNullPropagationFromCtor(this Expression getter, Ex Expression? condition = null; var current = getter; var checks = arg.Context.NullChecks - .Where(x=> !object.ReferenceEquals(x.arg,arg)) - .Select(x=>x.param?.Type); + .Where(x => !object.ReferenceEquals(x.arg, arg)) + .Select(x => x.param); while (current != null) { @@ -462,9 +462,9 @@ public static Expression ApplyNullPropagationFromCtor(this Expression getter, Ex 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)); + else if (current.CanBeNull() && current is ParameterExpression param + && !checks.Contains(param)) + compareNull = Expression.NotEqual(param, Expression.Constant(null, param.Type)); if (compareNull != null) {