From 4257f9f790da7e7cd6ca78917d43ca60aae9430f Mon Sep 17 00:00:00 2001 From: wuyangfan Date: Mon, 25 May 2026 14:05:50 +0800 Subject: [PATCH 1/2] fix: skip destination transforms for explicit member maps (#952) Explicit .Map(...) configuration should take precedence over global DestinationTransform rules such as EmptyCollectionIfNull. Co-authored-by: Cursor --- .../WhenPerformingDestinationTransforms.cs | 20 +++++++++++++++++++ src/Mapster/Adapters/BaseAdapter.cs | 13 +++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/Mapster.Tests/WhenPerformingDestinationTransforms.cs b/src/Mapster.Tests/WhenPerformingDestinationTransforms.cs index cc84b155..fe767b8f 100644 --- a/src/Mapster.Tests/WhenPerformingDestinationTransforms.cs +++ b/src/Mapster.Tests/WhenPerformingDestinationTransforms.cs @@ -95,6 +95,19 @@ public void Adapter_Destination_Transform_CreateNewIfNull() destination.Set.Count.ShouldBe(0); } + [TestMethod] + public void Explicit_Null_Mapping_Is_Not_Overridden_By_EmptyCollectionIfNull() + { + var config = new TypeAdapterConfig(); + config.Default.AddDestinationTransform(DestinationTransform.EmptyCollectionIfNull); + config.NewConfig() + .Map(d => d.Strings, _ => (string[]?)null); + + var destination = new ExplicitNullSource([]).Adapt(config); + + destination.Strings.ShouldBeNull(); + } + #region TestClasses public class SimplePoco @@ -145,6 +158,13 @@ public class CollectionDto public ISet Set { get; set; } } + public record ExplicitNullSource(string?[] Strings); + + public class ExplicitNullDestination + { + public string[]? Strings { get; set; } + } + #endregion } diff --git a/src/Mapster/Adapters/BaseAdapter.cs b/src/Mapster/Adapters/BaseAdapter.cs index b31a0dbe..fd4ba826 100644 --- a/src/Mapster/Adapters/BaseAdapter.cs +++ b/src/Mapster/Adapters/BaseAdapter.cs @@ -521,7 +521,7 @@ internal Expression CreateAdaptExpression(Expression source, Type destinationTyp exp = CreateAdaptExpressionCore(_source, destinationType, arg, mapping, destination); //transform(adapt(_source)); - if (notUsingDestinationValue) + if (notUsingDestinationValue && !HasExplicitMemberMap(mapping, arg)) { var transform = arg.Settings.DestinationTransforms.Find(it => it.Condition(exp.Type)); if (transform != null) @@ -545,5 +545,16 @@ internal Expression CreateAdaptExpression(Expression source, Type destinationTyp return exp.To(destinationType); } + + 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)); + } } } From 8b8898a83f674351887e0228165e2a15ed77c676 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 --- .../WhenPerformingDestinationTransforms.cs | 15 ++++++++++ src/Mapster/Adapters/BaseClassAdapter.cs | 4 +-- src/Mapster/Utils/ExpressionEx.cs | 30 +++++++++++++++++-- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/Mapster.Tests/WhenPerformingDestinationTransforms.cs b/src/Mapster.Tests/WhenPerformingDestinationTransforms.cs index fe767b8f..3e5558ad 100644 --- a/src/Mapster.Tests/WhenPerformingDestinationTransforms.cs +++ b/src/Mapster.Tests/WhenPerformingDestinationTransforms.cs @@ -108,6 +108,19 @@ public void Explicit_Null_Mapping_Is_Not_Overridden_By_EmptyCollectionIfNull() destination.Strings.ShouldBeNull(); } + [TestMethod] + public void Explicit_Null_Record_Ctor_Mapping_Is_Not_Overridden_By_EmptyCollectionIfNull() + { + var config = new TypeAdapterConfig(); + config.Default.AddDestinationTransform(DestinationTransform.EmptyCollectionIfNull); + config.NewConfig() + .Map(d => d.Strings, _ => (string[]?)null); + + var destination = new ExplicitNullSource([]).Adapt(config); + + destination.Strings.ShouldBeNull(); + } + #region TestClasses public class SimplePoco @@ -165,6 +178,8 @@ public class ExplicitNullDestination public string[]? Strings { get; set; } } + public record ExplicitNullRecordDestination(string[]? Strings); + #endregion } diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index 6d12a934..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(getter); + 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 b7ffc365..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 adapt; + 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,6 +494,29 @@ 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, 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; + + 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();