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 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 diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index e36ad7a8..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 @@ -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..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; @@ -436,11 +447,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) {