From 9c78414d33d04034decdd86fc7022d1ed079fdf4 Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Mon, 25 May 2026 15:52:30 +0800 Subject: [PATCH 1/2] fix: apply EmptyCollectionIfNull to record constructor args (#943) Apply destination transforms when null-propagation is skipped for ctor parameters and when building record constructor arguments. Co-authored-by: Cursor --- src/Mapster/Adapters/BaseClassAdapter.cs | 2 +- src/Mapster/Utils/ExpressionEx.cs | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index 6d12a934..f308f953 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -283,7 +283,7 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi getter = TryRestoreRecordMember(member.DestinationMember, recordRestorParamModel, destination) ?? getter; } } - arguments.Add(getter); + arguments.Add(ExpressionEx.ApplyDestinationTransform(getter, arg)); } return Expression.New(classConverter.ConstructorInfo!, arguments); diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index b7ffc365..80012ac0 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -481,7 +481,7 @@ public static Expression ApplyNullPropagationFromCtor(this Expression getter, Ex } if (condition == null) - return adapt; + return ApplyDestinationTransform(adapt, arg); // add supporting DestinationTransforms var transform = arg.Settings.DestinationTransforms.Find(it => it.Condition(adapt.Type)); @@ -491,6 +491,15 @@ public static Expression ApplyNullPropagationFromCtor(this Expression getter, Ex return Expression.Condition(condition, adapt, Expression.Default(adapt.Type)); } + public static Expression ApplyDestinationTransform(Expression exp, CompileArgument arg) + { + var transform = arg.Settings.DestinationTransforms.Find(it => it.Condition(exp.Type)); + if (transform == null) + return exp; + + return transform.TransformFunc(exp.Type).Apply(arg.MapType, exp); + } + public static string? GetMemberPath(this LambdaExpression lambda, bool firstLevelOnly = false, bool noError = false) { var props = new List(); From e0e82e48a1d1f62ff89a3989de9e9a5e04f1853a Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Mon, 25 May 2026 15:53:42 +0800 Subject: [PATCH 2/2] fix: skip destination transforms for explicit ctor member maps (#943, #952) Do not apply EmptyCollectionIfNull when a member has an explicit Map resolver, including record constructor parameters. Co-authored-by: Cursor --- ...licitNullDestinationTransformRegression.cs | 38 +++++++++++++++++++ src/Mapster/Adapters/BaseClassAdapter.cs | 4 +- src/Mapster/Utils/ExpressionEx.cs | 23 +++++++++-- 3 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 src/Mapster.Tests/WhenExplicitNullDestinationTransformRegression.cs diff --git a/src/Mapster.Tests/WhenExplicitNullDestinationTransformRegression.cs b/src/Mapster.Tests/WhenExplicitNullDestinationTransformRegression.cs new file mode 100644 index 00000000..c8264bd2 --- /dev/null +++ b/src/Mapster.Tests/WhenExplicitNullDestinationTransformRegression.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; + +namespace Mapster.Tests +{ + /// + /// https://github.com/MapsterMapper/Mapster/issues/952 + /// + [TestClass] + public class WhenExplicitNullDestinationTransformRegression + { + [TestMethod] + public void ExplicitNullMapping_ShouldNotBeOverridden_ByDefaultEmptyCollectionTransform() + { + var config = new TypeAdapterConfig(); + + config.Default + .AddDestinationTransform(DestinationTransform.EmptyCollectionIfNull); + + config.NewConfig() + .Map(d => d.Strings, _ => (string[]?)null); + + var foo = new Foo952([]); + + var dto = foo.Adapt(config); + + dto.Strings.ShouldBeNull(); + } + + record Foo952(string?[] Strings); + + class FooDto952 + { + public string[]? Strings { get; set; } + } + } +} diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index f308f953..bc267d6e 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -263,7 +263,7 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi } else getter = member.Getter - .ApplyNullPropagationFromCtor(CreateAdaptExpressionCore(member.Getter, member.DestinationMember.Type, arg, member), arg); + .ApplyNullPropagationFromCtor(CreateAdaptExpressionCore(member.Getter, member.DestinationMember.Type, arg, member), arg, member); if (member.Ignore.Condition != null) @@ -283,7 +283,7 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi getter = TryRestoreRecordMember(member.DestinationMember, recordRestorParamModel, destination) ?? getter; } } - arguments.Add(ExpressionEx.ApplyDestinationTransform(getter, arg)); + arguments.Add(ExpressionEx.ApplyDestinationTransform(getter, arg, member)); } return Expression.New(classConverter.ConstructorInfo!, arguments); diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index 80012ac0..ffed8cd4 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -445,7 +445,7 @@ public static Expression ApplyPropertyNullPropagation(this Expression getter) return getter; } - public static Expression ApplyNullPropagationFromCtor(this Expression getter, Expression adapt, CompileArgument arg) + public static Expression ApplyNullPropagationFromCtor(this Expression getter, Expression adapt, CompileArgument arg, MemberMapping? member = null) { if (getter == null) return adapt; @@ -481,9 +481,12 @@ public static Expression ApplyNullPropagationFromCtor(this Expression getter, Ex } if (condition == null) - return ApplyDestinationTransform(adapt, arg); + return ApplyDestinationTransform(adapt, arg, member); // add supporting DestinationTransforms + if (HasExplicitMemberMap(member, arg)) + return Expression.Condition(condition, adapt, Expression.Default(adapt.Type)); + var transform = arg.Settings.DestinationTransforms.Find(it => it.Condition(adapt.Type)); if (transform != null) return transform.TransformFunc(adapt.Type).Apply(arg.MapType, Expression.Condition(condition, adapt, Expression.Default(adapt.Type))); @@ -491,8 +494,11 @@ public static Expression ApplyNullPropagationFromCtor(this Expression getter, Ex return Expression.Condition(condition, adapt, Expression.Default(adapt.Type)); } - public static Expression ApplyDestinationTransform(Expression exp, CompileArgument arg) + public static Expression ApplyDestinationTransform(Expression exp, CompileArgument arg, MemberMapping? mapping = null) { + if (HasExplicitMemberMap(mapping, arg)) + return exp; + var transform = arg.Settings.DestinationTransforms.Find(it => it.Condition(exp.Type)); if (transform == null) return exp; @@ -500,6 +506,17 @@ public static Expression ApplyDestinationTransform(Expression exp, CompileArgume return transform.TransformFunc(exp.Type).Apply(arg.MapType, exp); } + static bool HasExplicitMemberMap(MemberMapping? mapping, CompileArgument arg) + { + if (mapping?.DestinationMember == null) + return false; + + var memberName = mapping.DestinationMember.Name; + return arg.Settings.Resolvers.Any(resolver => + !resolver.IsChildPath && + resolver.DestinationMemberName.Equals(memberName, StringComparison.InvariantCultureIgnoreCase)); + } + public static string? GetMemberPath(this LambdaExpression lambda, bool firstLevelOnly = false, bool noError = false) { var props = new List();