From e5bab1c19215263bb7f26f2fc87767039a69f552 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Tue, 31 Mar 2026 17:25:17 +0500 Subject: [PATCH 1/4] fix: IgnoreNullValues regression from MapToTarget cases in #905 --- ...enPropertyNullablePropagationRegression.cs | 53 +++++++++++++++++-- src/Mapster/Adapters/BaseClassAdapter.cs | 2 +- src/Mapster/Utils/ExpressionEx.cs | 24 +++++++-- 3 files changed, 68 insertions(+), 11 deletions(-) diff --git a/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs b/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs index d2239a18..09e9716e 100644 --- a/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs +++ b/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs @@ -12,12 +12,17 @@ public class WhenPropertyNullablePropagationRegression /// /// [TestMethod] - public async Task NotNullableStructMapToNotNullableCorrect() + public void NotNullableStructMapToNotNullableCorrect() { - TypeAdapterConfig + TypeAdapterConfig + .NewConfig() + .IgnoreNullValues(true) + .Map(dest => dest.Amount, src => src.Amount) + .Map(dest => dest.InnerAmount, src => src.Inner.Amount); + + TypeAdapterConfig .NewConfig() - .Map(dest => dest.Amount, src => src.Amount) - .Map(dest => dest.InnerAmount, src => src.Inner.Amount); + .IgnoreNullValues(true); Foo858 foo = new() @@ -30,14 +35,21 @@ public async Task NotNullableStructMapToNotNullableCorrect() } }; + var updateBar = new Bar858 { Amount = new(10, Currency858.Eur), InnerAmount = new(10, Currency858.Eur) }; + var snull = new Foo858() { Amount = new(1, Currency858.Usd), Inner = null }; + // Act var bar = foo.Adapt(); + + var str = snull.BuildAdapter().CreateMapToTargetExpression(); + + snull.Adapt(updateBar); // Assert bar.InnerAmount.Amount.ShouldBe(10m); } [TestMethod] - public async Task NotNullableStructMapToNullableCorrect() + public void NotNullableStructMapToNullableCorrect() { TypeAdapterConfig .NewConfig() @@ -61,6 +73,37 @@ public async Task NotNullableStructMapToNullableCorrect() bar.InnerAmount?.Amount.ShouldBe(10m); } + [TestMethod] + public void IgnoreNullValueWorkCorrect() + { + TypeAdapterConfig + .NewConfig() + .IgnoreNullValues(true) + .Map(dest => dest.Amount, src => src.Amount) + .Map(dest => dest.InnerAmount, src => src.Inner.Amount); + + Foo858 foo = new() + { + Amount = new(1, Currency858.Usd), + Inner = new() + { + Amount = new(10, Currency858.Eur), + Int = 100, + } + }; + + var nullFoo = new Foo858() { Amount = new(2, Currency858.Ron), Inner = null }; + + // Act + var bar = foo.Adapt(); + nullFoo.Adapt(bar); + + // Assert + bar.InnerAmount.Amount.ShouldBe(10m); + bar.Amount.Amount.ShouldBe(2m); + bar.Amount.Currency.ShouldBe(Currency858.Ron); + } + } #region TestClasses diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index 22314a5b..fcb05b94 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -132,7 +132,7 @@ select fn(src, destinationMember, arg)) { propertyModel.Getter = arg.MapType == MapType.Projection ? getter - : getter.ApplyPropertyNullPropagation(propertyModel); + : getter.ApplyPropertyNullPropagation(propertyModel, arg.MapType); properties.Add(propertyModel); } else diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index c00c5391..48fbc2c3 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -407,7 +407,7 @@ public static Expression NullableEnumExtractor(this Expression param) return param; } - public static Expression ApplyPropertyNullPropagation(this Expression getter, MemberMapping property) + public static Expression ApplyPropertyNullPropagation(this Expression getter, MemberMapping property, MapType mapType) { var current = getter; var result = getter; @@ -421,13 +421,27 @@ public static Expression ApplyPropertyNullPropagation(this Expression getter, Me break; if (expr.NodeType == ExpressionType.Parameter && condition != null) { - if (property.DestinationMember.Type.CanBeNull() && !getter.CanBeNull()) + if(mapType != MapType.MapToTarget) { - var transform = Expression.Convert(getter, typeof(Nullable<>).MakeGenericType(getter.Type)); - return Expression.Condition(condition, transform, transform.Type.CreateDefault()); + if (property.DestinationMember.Type.CanBeNull() && !getter.CanBeNull()) + { + var transform = Expression.Convert(getter, typeof(Nullable<>).MakeGenericType(getter.Type)); + return Expression.Condition(condition, transform, transform.Type.CreateDefault()); + } + else + return Expression.Condition(condition, getter, getter.Type.CreateDefault()); } else - return Expression.Condition(condition, getter, getter.Type.CreateDefault()); + { + if (!getter.CanBeNull()) + { + var transform = Expression.Convert(getter, typeof(Nullable<>).MakeGenericType(getter.Type)); + return Expression.Condition(condition, transform, transform.Type.CreateDefault()); + } + else + return Expression.Condition(condition, getter, getter.Type.CreateDefault()); + } + } if (expr.CanBeNull()) From 8edf70ee718c3e8704cc2d96993cd72ae6e54752 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Tue, 31 Mar 2026 20:32:37 +0500 Subject: [PATCH 2/4] fIx: fix Property ApplyNullablePropagation not null MapToTarget case --- ...enPropertyNullablePropagationRegression.cs | 44 ++++++++++++++++--- src/Mapster/Adapters/ClassAdapter.cs | 13 ++++++ src/Mapster/Utils/ExpressionEx.cs | 5 +++ 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs b/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs index 09e9716e..1e1e27d9 100644 --- a/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs +++ b/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs @@ -16,7 +16,6 @@ public void NotNullableStructMapToNotNullableCorrect() { TypeAdapterConfig .NewConfig() - .IgnoreNullValues(true) .Map(dest => dest.Amount, src => src.Amount) .Map(dest => dest.InnerAmount, src => src.Inner.Amount); @@ -35,15 +34,9 @@ public void NotNullableStructMapToNotNullableCorrect() } }; - var updateBar = new Bar858 { Amount = new(10, Currency858.Eur), InnerAmount = new(10, Currency858.Eur) }; - var snull = new Foo858() { Amount = new(1, Currency858.Usd), Inner = null }; - // Act var bar = foo.Adapt(); - var str = snull.BuildAdapter().CreateMapToTargetExpression(); - - snull.Adapt(updateBar); // Assert bar.InnerAmount.Amount.ShouldBe(10m); } @@ -104,6 +97,43 @@ public void IgnoreNullValueWorkCorrect() bar.Amount.Currency.ShouldBe(Currency858.Ron); } + [TestMethod] + public void MapToTargetWorkCorrect() + { + TypeAdapterConfig + .NewConfig() + .IgnoreNullValues(true) + .Map(dest => dest.Amount, src => src.Amount) + .Map(dest => dest.InnerAmount, src => src.Inner.Amount); + + Foo858 foo = new() + { + Amount = new(1, Currency858.Usd), + Inner = new() + { + Amount = new(10, Currency858.Eur), + Int = 100, + } + }; + + var nullFoo = new Foo858() { Amount = new(2, Currency858.Ron), Inner = new() + { + Amount = new(20, Currency858.Eur), + Int = 100, + } + }; + + // Act + var bar = foo.Adapt(); + nullFoo.Adapt(bar); + + // Assert + bar.InnerAmount.Amount.ShouldBe(20m); + bar.Amount.Amount.ShouldBe(2m); + bar.Amount.Currency.ShouldBe(Currency858.Ron); + } + + } #region TestClasses diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs index 78cfacc1..ef1361c5 100644 --- a/src/Mapster/Adapters/ClassAdapter.cs +++ b/src/Mapster/Adapters/ClassAdapter.cs @@ -270,5 +270,18 @@ private static Expression SetValueByReflection(MemberMapping member, MemberExpre return Expression.MemberInit(newInstance, lines); } + + protected override Expression TransformSource(Expression source) + { + if (source.Type.IsNullableType()) + { + var getValueOrDefaultMethod = source.Type.GetMethod("GetValueOrDefault", Type.EmptyTypes); + var getValue = Expression.Call(source, getValueOrDefaultMethod); + + return getValue; + } + + return source; + } } } diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index 48fbc2c3..4ec2e8bc 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -546,5 +546,10 @@ internal static Expression GetNameConverterExpression(Func conve return Expression.Constant(converter); } + public static bool IsNullableType(this Type type) + { + return Nullable.GetUnderlyingType(type) != null; + } + } } From acb617aeba5f37ea9b2aadb633a76b44ecd76241 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 1 Apr 2026 13:52:56 +0500 Subject: [PATCH 3/4] fix: Final Fix --- src/Mapster/Adapters/ClassAdapter.cs | 37 +++++++++++++++----------- src/Mapster/Utils/ExpressionEx.cs | 39 ++++++++++++++-------------- 2 files changed, 41 insertions(+), 35 deletions(-) diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs index ef1361c5..a17b079a 100644 --- a/src/Mapster/Adapters/ClassAdapter.cs +++ b/src/Mapster/Adapters/ClassAdapter.cs @@ -114,7 +114,17 @@ protected override Expression CreateBlockExpression(Expression source, Expressio ? member.DestinationMember.GetExpression(destination) : null; - var adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member, destMember); + Expression adapt; + + // convert ApplyNullable Propagation for NotPrimitive Nullable types + if (member.Getter is ConditionalExpression cond && member.Getter.Type.IsNotPrimitiveNullableType() + && !member.DestinationMember.Type.IsNullable()) + { + var value = CreateAdaptExpression(cond.IfTrue.GetNotPrimitiveNullableValue(), member.DestinationMember.Type, arg, member, destMember); + adapt = Expression.Condition(cond.Test, value, member.DestinationMember.Type.CreateDefault()); + } + else + adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member, destMember); if (member.UseDestinationValue && member.DestinationMember.Type.IsMapsterImmutable() @@ -251,7 +261,17 @@ private static Expression SetValueByReflection(MemberMapping member, MemberExpre if (member.DestinationMember.SetterModifier == AccessModifier.None) continue; - var value = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); + Expression value; + + // convert ApplyNullable Propagation for NotPrimitive Nullable types + if (member.Getter is ConditionalExpression cond && member.Getter.Type.IsNotPrimitiveNullableType() + && !member.DestinationMember.Type.IsNullable()) + { + var adapt = CreateAdaptExpression(cond.IfTrue.GetNotPrimitiveNullableValue(), member.DestinationMember.Type, arg, member); + value = Expression.Condition(cond.Test, adapt, member.DestinationMember.Type.CreateDefault()); + } + else + value = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); //special null property check for projection //if we don't set null to property, EF will create empty object @@ -270,18 +290,5 @@ private static Expression SetValueByReflection(MemberMapping member, MemberExpre return Expression.MemberInit(newInstance, lines); } - - protected override Expression TransformSource(Expression source) - { - if (source.Type.IsNullableType()) - { - var getValueOrDefaultMethod = source.Type.GetMethod("GetValueOrDefault", Type.EmptyTypes); - var getValue = Expression.Call(source, getValueOrDefaultMethod); - - return getValue; - } - - return source; - } } } diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index 4ec2e8bc..75fee84d 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -421,27 +421,13 @@ public static Expression ApplyPropertyNullPropagation(this Expression getter, Me break; if (expr.NodeType == ExpressionType.Parameter && condition != null) { - if(mapType != MapType.MapToTarget) + if (!getter.CanBeNull()) { - if (property.DestinationMember.Type.CanBeNull() && !getter.CanBeNull()) - { - var transform = Expression.Convert(getter, typeof(Nullable<>).MakeGenericType(getter.Type)); - return Expression.Condition(condition, transform, transform.Type.CreateDefault()); - } - else - return Expression.Condition(condition, getter, getter.Type.CreateDefault()); + var transform = Expression.Convert(getter, typeof(Nullable<>).MakeGenericType(getter.Type)); + return Expression.Condition(condition, transform, transform.Type.CreateDefault()); } else - { - if (!getter.CanBeNull()) - { - var transform = Expression.Convert(getter, typeof(Nullable<>).MakeGenericType(getter.Type)); - return Expression.Condition(condition, transform, transform.Type.CreateDefault()); - } - else - return Expression.Condition(condition, getter, getter.Type.CreateDefault()); - } - + return Expression.Condition(condition, getter, getter.Type.CreateDefault()); } if (expr.CanBeNull()) @@ -546,9 +532,22 @@ internal static Expression GetNameConverterExpression(Func conve return Expression.Constant(converter); } - public static bool IsNullableType(this Type type) + public static bool IsNotPrimitiveNullableType(this Type type) + { + return Nullable.GetUnderlyingType(type) != null && !type.IsMapsterPrimitive(); + } + + public static Expression GetNotPrimitiveNullableValue(this Expression exp) { - return Nullable.GetUnderlyingType(type) != null; + if (exp.Type.IsNotPrimitiveNullableType()) + { + var getValueOrDefaultMethod = exp.Type.GetMethod("GetValueOrDefault", Type.EmptyTypes); + var getValue = Expression.Call(exp, getValueOrDefaultMethod); + + return getValue; + } + + return exp; } } From fbf3ec42abe9d7bc1a3afcde244db612e6b791d5 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Wed, 1 Apr 2026 14:43:37 +0500 Subject: [PATCH 4/4] fix: Drop not using params --- src/Mapster/Adapters/BaseClassAdapter.cs | 2 +- src/Mapster/Utils/ExpressionEx.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index fcb05b94..a8b7a4c0 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -132,7 +132,7 @@ select fn(src, destinationMember, arg)) { propertyModel.Getter = arg.MapType == MapType.Projection ? getter - : getter.ApplyPropertyNullPropagation(propertyModel, arg.MapType); + : getter.ApplyPropertyNullPropagation(); properties.Add(propertyModel); } else diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index 75fee84d..4e6af8a7 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -407,7 +407,7 @@ public static Expression NullableEnumExtractor(this Expression param) return param; } - public static Expression ApplyPropertyNullPropagation(this Expression getter, MemberMapping property, MapType mapType) + public static Expression ApplyPropertyNullPropagation(this Expression getter) { var current = getter; var result = getter;