From 5848f8536972289e8f668df2e9faca0c7995fd90 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 30 Mar 2026 13:26:35 +0500 Subject: [PATCH 1/4] fix: fix bug in #903. Drop collect ctor param with Getter == null --- src/Mapster/Adapters/BaseClassAdapter.cs | 2 +- src/Mapster/Utils/ExpressionEx.cs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index e36ad7a8..9d9a2f6a 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -220,7 +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))); + arg.Context.NullChecks.UnionWith(members.Where(x=>x.Getter != null).Select(x=>(x.Getter,arg))); var parameterInfo = (ParameterInfo)member.DestinationMember.Info!; var defaultConst = parameterInfo.IsOptional ? Expression.Constant(parameterInfo.DefaultValue, member.DestinationMember.Type) diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index bca07c31..e442e7a4 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -436,11 +436,14 @@ public static Expression ApplyNullPropagation(this Expression getter) public static Expression ApplyNullPropagationFromCtor(this Expression getter, Expression adapt, CompileArgument arg) { + if (getter == null) + return adapt; + Expression? condition = null; var current = getter; var checks = arg.Context.NullChecks .Where(x=> !object.ReferenceEquals(x.arg,arg)) - .Select(x=>x.param.Type); + .Select(x=>x.param?.Type); while (current != null) { From f9a44f6f35fdd86b8e3fcb321a0001db1df860c7 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 30 Mar 2026 14:57:32 +0500 Subject: [PATCH 2/4] feat(test): add test for new behavior Property NullablePropagation --- ...enPropertyNullablePropagationRegression.cs | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs diff --git a/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs b/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs new file mode 100644 index 00000000..d2239a18 --- /dev/null +++ b/src/Mapster.Tests/WhenPropertyNullablePropagationRegression.cs @@ -0,0 +1,113 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using System.Threading.Tasks; + +namespace Mapster.Tests; + +[TestClass] +public class WhenPropertyNullablePropagationRegression +{ + /// + /// https://github.com/MapsterMapper/Mapster/issues/858 + /// + /// + [TestMethod] + public async Task NotNullableStructMapToNotNullableCorrect() + { + TypeAdapterConfig + .NewConfig() + .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, + } + }; + + // Act + var bar = foo.Adapt(); + // Assert + bar.InnerAmount.Amount.ShouldBe(10m); + } + + [TestMethod] + public async Task NotNullableStructMapToNullableCorrect() + { + TypeAdapterConfig + .NewConfig() + .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, + } + }; + + // Act + var bar = foo.Adapt(); + // Assert + bar.InnerAmount?.Amount.ShouldBe(10m); + } + +} + +#region TestClasses +public enum Currency858 +{ + Eur, + Usd, + Ron +} + +file class Foo858 +{ + public required Money858 Amount { get; set; } + public required FooInner858 Inner { get; set; } +} + +file class FooInner858 +{ + public required Money858 Amount { get; set; } + public int Int { get; set; } +} + +file class Bar858 +{ + public Money858 Amount { get; set; } + public Money858 InnerAmount { get; set; } + +} + +file class Bar858Nullable +{ + public Money858? Amount { get; set; } + public Money858? InnerAmount { get; set; } + +} + +public struct Money858 +{ + public decimal? Amount { get; set; } + + public Currency858 Currency { get; set; } = Currency858.Ron; + + public Money858(decimal? amount, Currency858 currency = Currency858.Eur) + { + Amount = amount; + Currency = currency; + } +} + +#endregion TestClasses \ No newline at end of file From d88d346652a7535995f3e48dc91378f3c094ca7e Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 30 Mar 2026 15:13:06 +0500 Subject: [PATCH 3/4] fix: add new Property NullablePropagation --- src/Mapster/Adapters/BaseClassAdapter.cs | 2 +- src/Mapster/Utils/ExpressionEx.cs | 25 +++++++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index 9d9a2f6a..22314a5b 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.ApplyNullPropagation(); + : getter.ApplyPropertyNullPropagation(propertyModel); properties.Add(propertyModel); } else diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index e442e7a4..c00c5391 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -407,25 +407,36 @@ public static Expression NullableEnumExtractor(this Expression param) return param; } - public static Expression ApplyNullPropagation(this Expression getter) + public static Expression ApplyPropertyNullPropagation(this Expression getter, MemberMapping property) { var current = getter; var result = getter; + Expression? condition = null; + while (current.NodeType == ExpressionType.MemberAccess) { var memEx = (MemberExpression) current; var expr = memEx.Expression; if (expr == null) break; - if (expr.NodeType == ExpressionType.Parameter) - return result; + if (expr.NodeType == ExpressionType.Parameter && condition != null) + { + 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()); + } if (expr.CanBeNull()) { - var compareNull = Expression.Equal(expr, Expression.Constant(null, expr.Type)); - if (!result.Type.CanBeNull()) - result = Expression.Convert(result, typeof(Nullable<>).MakeGenericType(result.Type)); - result = Expression.Condition(compareNull, result.Type.CreateDefault(), result); + var compareNull = Expression.NotEqual(expr, Expression.Constant(null, expr.Type)); + if (condition == null) + condition = compareNull; + else + condition = Expression.AndAlso(compareNull, condition); } current = expr; From d27d46662bb3eee0c9e1466dbc54fcaaf1a23b93 Mon Sep 17 00:00:00 2001 From: DocSvartz Date: Mon, 30 Mar 2026 15:15:10 +0500 Subject: [PATCH 4/4] chore: Bump version to 10.0.7-pre02 --- src/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index f73ad693..8ce13048 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ false - 10.0.7-pre01 + 10.0.7-pre02 netstandard2.0;net10.0;net9.0;net8.0 netstandard2.0;net10.0;net9.0;net8.0 net10.0;net9.0;net8.0