diff --git a/.gitignore b/.gitignore index 434416f4..e6d5ee03 100644 --- a/.gitignore +++ b/.gitignore @@ -188,3 +188,4 @@ src/.idea # VS Code settings .vscode/launch.json .vscode/tasks.json +.DS_Store diff --git a/src/Mapster.Tests/WhenMappingNullablePrimitives.cs b/src/Mapster.Tests/WhenMappingNullablePrimitives.cs index 47aa080d..bd7e3fcd 100644 --- a/src/Mapster.Tests/WhenMappingNullablePrimitives.cs +++ b/src/Mapster.Tests/WhenMappingNullablePrimitives.cs @@ -172,9 +172,95 @@ public void CustomConverterWorkWithNullablePrimitiveTypes() } + [TestMethod] + public void NullablePrimitiveTypesCorrectUsageNonNullableSettings() + { + TypeAdapterConfig config = new TypeAdapterConfig(); + config.NewConfig().MapWith(src => Convert.ToInt32(src)); + + long? value = null; + + Should.NotThrow(() => value.Adapt(config) ); + + } + + [TestMethod] + public void MapNullableValueTypePropertryToNonNullableCorrect() + { + TypeAdapterConfig config = new TypeAdapterConfig(); + config.NewConfig().MapWith(src => Convert.ToInt32(src)); + config.NewConfig() + .Map(dest => dest.IntNonNullable, src => src.LongNullable); + + var src = new PropSrc996 { LongNullable = 42 }; + var result = src.Adapt(config); + + var srcnull = new PropSrc996 { LongNullable = null }; + var resultnull = srcnull.Adapt(config); + + result.IntNonNullable.ShouldBe(42); + resultnull.IntNonNullable.ShouldBe(0); + + Should.Throw(() => new PropSrc996 { LongNullable = long.MaxValue }.Adapt(config)); + + } + + [TestMethod] + public void CustomNullConverterisWorked() + { + TypeAdapterConfig config = new TypeAdapterConfig(); + config.NewConfig().MapWith(src => src == null ? 996 : Convert.ToInt32(src.Value)); + config.NewConfig() + .Map(dest => dest.IntNonNullable, src => src.LongNullable); + + var src = new PropSrc996 { LongNullable = 42 }; + var result = src.Adapt(config); + + var srcnull = new PropSrc996 { LongNullable = null }; + var resultnull = srcnull.Adapt(config); + + result.IntNonNullable.ShouldBe(42); + resultnull.IntNonNullable.ShouldBe(996); + + Should.Throw(() => new PropSrc996 { LongNullable = long.MaxValue }.Adapt(config)); + + } + + [TestMethod] + public void ApplyNullPropagationToCustomConverterWorked() + { + TypeAdapterConfig config = new TypeAdapterConfig(); + config.ActivateCustomConvertersSrcNullPropagation = true; + config.ForType().MapWith(src => (long)Helper992.ToSecondsTimestamp(src)); // work only ParseToNullableBool() return not null + + var src = new NullableDateTimeInsaider(){ CreatedAt = null}; + Should.NotThrow(()=> src.Adapt(config)); + } + #region TestClasses + public class NullableDateTimeInsaider + { + public DateTime? CreatedAt { get; set; } + } + + public class LongInsaider + { + public long CreatedAt { get; set; } + } + + public class PropSrc996 + { + public long? LongNullable { get; set; } + } + + public class PropDest996 + { + public int IntNonNullable { get; set; } + } + + static class Helper992 { public static bool? ParseToNullableBool(string? value) @@ -189,13 +275,26 @@ static class Helper992 _ => null }; } + + public static long? ToSecondsTimestamp( DateTime? dateTime) + { + if (dateTime is null) + return null; + + return 42; + } + } public class Source992 { public string? Prop1 { get; set; } } - + public class Destination992NotNull + { + public bool Prop1 { get; set; } + } + public class Destination992 { public bool? Prop1 { get; set; } diff --git a/src/Mapster/Adapters/NullableAdapter.cs b/src/Mapster/Adapters/NullableAdapter.cs index 598a2192..178c3266 100644 --- a/src/Mapster/Adapters/NullableAdapter.cs +++ b/src/Mapster/Adapters/NullableAdapter.cs @@ -11,6 +11,8 @@ internal class NullableAdapter : BaseAdapter protected override bool CanMap(PreCompileArgument arg) { + if(arg.ExplicitMapping) + return false; return arg.SourceType.IsNullable() || arg.DestinationType.IsNullable(); } protected override bool CanInline(Expression source, Expression? destination, CompileArgument arg) @@ -20,22 +22,6 @@ protected override bool CanInline(Expression source, Expression? destination, Co protected override Expression? CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { - if (arg.ExplicitMapping) - { - LambdaExpression? Convert = null; - - TypeAdapterRule? getsettings; - arg.Context.Config.RuleMap.TryGetValue(new TypeTuple(arg.SourceType, arg.DestinationType), out getsettings); - - if (getsettings != null) - if (arg.MapType == MapType.MapToTarget) - Convert = getsettings.Settings.ConverterToTargetFactory(arg); - else - Convert = getsettings.Settings.ConverterFactory(arg); - if (Convert != null) - return Convert.Apply(arg.MapType, source); - } - var _source = source.Type.IsNullable() ? Expression.Convert(source, source.Type.GetGenericArguments()[0]) : source; diff --git a/src/Mapster/TypeAdapterConfig.cs b/src/Mapster/TypeAdapterConfig.cs index 9463072b..08db194a 100644 --- a/src/Mapster/TypeAdapterConfig.cs +++ b/src/Mapster/TypeAdapterConfig.cs @@ -89,6 +89,7 @@ private static List CreateRuleTemplate() public bool AllowImplicitDestinationInheritance { get; set; } public bool AllowImplicitSourceInheritance { get; set; } = true; public bool SelfContainedCodeGeneration { get; set; } + public bool ActivateCustomConvertersSrcNullPropagation { get; set; } = false; public Func Compiler { get; set; } = lambda => lambda.Compile(); @@ -269,7 +270,7 @@ private static TypeAdapterRule CreateDestinationTypeRule(TypeTuple key) private static int? GetSubclassDistance(Type type1, Type type2, bool allowInheritance) { //Support for using ValueType mapping configurations of types, for mapping cases on Nulllable ValueType values - if (type1.IsNullable() && !type1.ContainsGenericParameters) + if (type2.IsInterface && type1.IsNullable() && !type1.ContainsGenericParameters) type1 = type1.GetGenericArguments().FirstOrDefault(); if (type1 == type2) @@ -447,6 +448,10 @@ private static LambdaExpression CreateMapExpression(CompileArgument arg) private static LambdaExpression AdjustInheritedConverterReturnType(LambdaExpression lambda, CompileArgument arg) { + + if(arg.Settings.ApplyCustomConverterFactoryNullPropagation.GetValueOrDefault()) + lambda = Expression.Lambda(lambda.Parameters[0].NotNullReturn(lambda.Body),lambda.Parameters); + var destinationType = arg.DestinationType; var returnType = lambda.ReturnType; var lamdaBody = lambda.Body; @@ -501,7 +506,6 @@ private static LambdaExpression AdjustInheritedConverterReturnType(LambdaExpress } - return Expression.Lambda(body, lambda.Parameters); } diff --git a/src/Mapster/TypeAdapterSetter.cs b/src/Mapster/TypeAdapterSetter.cs index 03c41eeb..7a78af01 100644 --- a/src/Mapster/TypeAdapterSetter.cs +++ b/src/Mapster/TypeAdapterSetter.cs @@ -714,7 +714,8 @@ public TypeAdapterSetter ConstructUsing(Expression MapWith(Expression> converterFactory, bool applySettings = false) + public TypeAdapterSetter MapWith(Expression> converterFactory, bool applySettings = false, + bool disableCustomConvertersSrcNullPropagation = false) { this.CheckCompiled(); @@ -734,10 +735,22 @@ public TypeAdapterSetter MapWith(Expression MapToTargetWith(Expression> converterFactory, bool applySettings = false) + public TypeAdapterSetter MapToTargetWith(Expression> converterFactory, bool applySettings = false, + bool disableCustomConvertersSrcNullPropagation = false) { this.CheckCompiled(); @@ -753,6 +766,18 @@ public TypeAdapterSetter MapToTargetWith(Expression converterFactory; + + if (converterFactory.Parameters[0].Type.CanBeNull() + && Config.ActivateCustomConvertersSrcNullPropagation + && !disableCustomConvertersSrcNullPropagation) + { + var check = new NullCheckFinder(converterFactory.Parameters[0]); + check.Visit(converterFactory.Body); + + if (!check.FoundNullCheck) + Settings.ApplyCustomConverterFactoryNullPropagation = true; + } + return this; } diff --git a/src/Mapster/TypeAdapterSettings.cs b/src/Mapster/TypeAdapterSettings.cs index 38e3a53c..65f8b6e4 100644 --- a/src/Mapster/TypeAdapterSettings.cs +++ b/src/Mapster/TypeAdapterSettings.cs @@ -181,6 +181,11 @@ public Func? ConverterToTargetFactory get => Get>(nameof(ConverterToTargetFactory)); set => Set(nameof(ConverterToTargetFactory), value); } + public bool? ApplyCustomConverterFactoryNullPropagation + { + get => Get(nameof(ApplyCustomConverterFactoryNullPropagation)); + set => Set(nameof(ApplyCustomConverterFactoryNullPropagation), value); + } public object? MapToConstructor { get => Get(nameof(MapToConstructor)); diff --git a/src/Mapster/Utils/NullCheckFinder.cs b/src/Mapster/Utils/NullCheckFinder.cs new file mode 100644 index 00000000..ae1d302f --- /dev/null +++ b/src/Mapster/Utils/NullCheckFinder.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq.Expressions; + +public class NullCheckFinder : ExpressionVisitor +{ + private readonly ParameterExpression _targetParameter; + public bool FoundNullCheck { get; private set; } + + public NullCheckFinder(ParameterExpression targetParameter) + { + _targetParameter = targetParameter ?? throw new ArgumentNullException(nameof(targetParameter)); + FoundNullCheck = false; + } + + protected override Expression VisitBinary(BinaryExpression node) + { + + if (node.NodeType == ExpressionType.Equal || node.NodeType == ExpressionType.NotEqual) + { + + var isLeftTarget = IsSameParameter(node.Left); + var isRightTarget = IsSameParameter(node.Right); + + + var otherSideIsNull = + (!isLeftTarget && IsNullConstant(node.Left)) || + (!isRightTarget && IsNullConstant(node.Right)); + + if ((isLeftTarget || isRightTarget) && otherSideIsNull) + { + FoundNullCheck = true; + + return node; + } + } + + return base.VisitBinary(node); + } + + private bool IsSameParameter(Expression exp) + { + return exp is ParameterExpression param && param == _targetParameter; + } + + private static bool IsNullConstant(Expression exp) + { + return exp is ConstantExpression c && c.Value == null; + } +} \ No newline at end of file