Skip to content
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,4 @@ src/.idea
# VS Code settings
.vscode/launch.json
.vscode/tasks.json
.DS_Store
101 changes: 100 additions & 1 deletion src/Mapster.Tests/WhenMappingNullablePrimitives.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,95 @@ public void CustomConverterWorkWithNullablePrimitiveTypes()

}

[TestMethod]
public void NullablePrimitiveTypesCorrectUsageNonNullableSettings()
{
TypeAdapterConfig config = new TypeAdapterConfig();
config.NewConfig<long, int>().MapWith(src => Convert.ToInt32(src));

long? value = null;

Should.NotThrow(() => value.Adapt<long?,int?>(config) );

}

[TestMethod]
public void MapNullableValueTypePropertryToNonNullableCorrect()
{
TypeAdapterConfig config = new TypeAdapterConfig();
config.NewConfig<long, int>().MapWith(src => Convert.ToInt32(src));
config.NewConfig<PropSrc996, PropDest996>()
.Map(dest => dest.IntNonNullable, src => src.LongNullable);

var src = new PropSrc996 { LongNullable = 42 };
var result = src.Adapt<PropDest996>(config);

var srcnull = new PropSrc996 { LongNullable = null };
var resultnull = srcnull.Adapt<PropDest996>(config);

result.IntNonNullable.ShouldBe(42);
resultnull.IntNonNullable.ShouldBe(0);

Should.Throw<OverflowException>(() => new PropSrc996 { LongNullable = long.MaxValue }.Adapt<PropDest996>(config));

}

[TestMethod]
public void CustomNullConverterisWorked()
{
TypeAdapterConfig config = new TypeAdapterConfig();
config.NewConfig<long?, int>().MapWith(src => src == null ? 996 : Convert.ToInt32(src.Value));
config.NewConfig<PropSrc996, PropDest996>()
.Map(dest => dest.IntNonNullable, src => src.LongNullable);

var src = new PropSrc996 { LongNullable = 42 };
var result = src.Adapt<PropDest996>(config);

var srcnull = new PropSrc996 { LongNullable = null };
var resultnull = srcnull.Adapt<PropDest996>(config);

result.IntNonNullable.ShouldBe(42);
resultnull.IntNonNullable.ShouldBe(996);

Should.Throw<OverflowException>(() => new PropSrc996 { LongNullable = long.MaxValue }.Adapt<PropDest996>(config));

}

[TestMethod]
public void ApplyNullPropagationToCustomConverterWorked()
{
TypeAdapterConfig config = new TypeAdapterConfig();
config.ActivateCustomConvertersSrcNullPropagation = true;
config.ForType<DateTime?, long>().MapWith(src => (long)Helper992.ToSecondsTimestamp(src)); // work only ParseToNullableBool() return not null

var src = new NullableDateTimeInsaider(){ CreatedAt = null};
Should.NotThrow(()=> src.Adapt<LongInsaider>(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)
Expand All @@ -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; }
Expand Down
18 changes: 2 additions & 16 deletions src/Mapster/Adapters/NullableAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
Expand Down
8 changes: 6 additions & 2 deletions src/Mapster/TypeAdapterConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ private static List<TypeAdapterRule> 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<LambdaExpression, Delegate> Compiler { get; set; } = lambda => lambda.Compile();

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -501,7 +506,6 @@ private static LambdaExpression AdjustInheritedConverterReturnType(LambdaExpress

}


return Expression.Lambda(body, lambda.Parameters);
}

Expand Down
29 changes: 27 additions & 2 deletions src/Mapster/TypeAdapterSetter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -714,7 +714,8 @@ public TypeAdapterSetter<TSource, TDestination> ConstructUsing(Expression<Func<T
return this;
}

public TypeAdapterSetter<TSource, TDestination> MapWith(Expression<Func<TSource, TDestination>> converterFactory, bool applySettings = false)
public TypeAdapterSetter<TSource, TDestination> MapWith(Expression<Func<TSource, TDestination>> converterFactory, bool applySettings = false,
bool disableCustomConvertersSrcNullPropagation = false)
{
this.CheckCompiled();

Expand All @@ -734,10 +735,22 @@ public TypeAdapterSetter<TSource, TDestination> MapWith(Expression<Func<TSource,
}
}

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;
}

public TypeAdapterSetter<TSource, TDestination> MapToTargetWith(Expression<Func<TSource, TDestination, TDestination>> converterFactory, bool applySettings = false)
public TypeAdapterSetter<TSource, TDestination> MapToTargetWith(Expression<Func<TSource, TDestination, TDestination>> converterFactory, bool applySettings = false,
bool disableCustomConvertersSrcNullPropagation = false)
{
this.CheckCompiled();

Expand All @@ -753,6 +766,18 @@ public TypeAdapterSetter<TSource, TDestination> MapToTargetWith(Expression<Func<
}
else
Settings.ConverterToTargetFactory = arg => 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;
}

Expand Down
5 changes: 5 additions & 0 deletions src/Mapster/TypeAdapterSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ public Func<CompileArgument, LambdaExpression>? ConverterToTargetFactory
get => Get<Func<CompileArgument, LambdaExpression>>(nameof(ConverterToTargetFactory));
set => Set(nameof(ConverterToTargetFactory), value);
}
public bool? ApplyCustomConverterFactoryNullPropagation
{
get => Get(nameof(ApplyCustomConverterFactoryNullPropagation));
set => Set(nameof(ApplyCustomConverterFactoryNullPropagation), value);
}
public object? MapToConstructor
{
get => Get<object>(nameof(MapToConstructor));
Expand Down
49 changes: 49 additions & 0 deletions src/Mapster/Utils/NullCheckFinder.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading