diff --git a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs index ee775e6b9d2..6eaea15569e 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBodyBuilders/BuilderContext/MembersContainerBuilderContext.cs @@ -1,5 +1,6 @@ using Riok.Mapperly.Descriptors.Mappings.MemberMappings; using Riok.Mapperly.Diagnostics; +using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; namespace Riok.Mapperly.Descriptors.MappingBodyBuilders.BuilderContext; @@ -22,6 +23,13 @@ public void AddNullDelegateMemberAssignmentMapping(IMemberAssignmentMapping memb var nullConditionSourcePath = new MemberPath(memberMapping.SourcePath.PathWithoutTrailingNonNullable().ToList()); var container = GetOrCreateNullDelegateMappingForPath(nullConditionSourcePath); AddMemberAssignmentMapping(container, memberMapping); + + // set target member to null if null assignments are allowed + // and the source is null + if (BuilderContext.MapperConfiguration.AllowNullPropertyAssignment && memberMapping.TargetPath.Member.Type.IsNullable()) + { + container.AddNullMemberAssignment(SetterMemberPath.Build(BuilderContext, memberMapping.TargetPath)); + } } private void AddMemberAssignmentMapping(IMemberAssignmentMappingContainer container, IMemberAssignmentMapping mapping) diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullDelegateAssignmentMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullDelegateAssignmentMapping.cs index efb88087784..97e1c9b391f 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullDelegateAssignmentMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/MemberNullDelegateAssignmentMapping.cs @@ -18,6 +18,7 @@ bool needsNullSafeAccess { private readonly GetterMemberPath _nullConditionalSourcePath = nullConditionalSourcePath; private readonly bool _throwInsteadOfConditionalNullMapping = throwInsteadOfConditionalNullMapping; + private readonly List _targetsToSetNull = new(); public override IEnumerable Build(TypeMappingBuildContext ctx, ExpressionSyntax targetAccess) { @@ -26,17 +27,16 @@ public override IEnumerable Build(TypeMappingBuildContext ctx, // else // throw ... var sourceNullConditionalAccess = _nullConditionalSourcePath.BuildAccess(ctx.Source, false, needsNullSafeAccess, true); - var nameofSourceAccess = _nullConditionalSourcePath.BuildAccess(ctx.Source, false, false, true); var condition = IsNotNull(sourceNullConditionalAccess); var conditionCtx = ctx.AddIndentation(); var trueClause = base.Build(conditionCtx, targetAccess); - var elseClause = _throwInsteadOfConditionalNullMapping - ? new[] { conditionCtx.SyntaxFactory.ExpressionStatement(ThrowArgumentNullException(nameofSourceAccess)) } - : null; + var elseClause = BuildElseClause(conditionCtx, targetAccess); var ifExpression = ctx.SyntaxFactory.If(condition, trueClause, elseClause); return new[] { ifExpression }; } + public void AddNullMemberAssignment(SetterMemberPath targetPath) => _targetsToSetNull.Add(targetPath); + public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) @@ -70,4 +70,19 @@ protected bool Equals(MemberNullDelegateAssignmentMapping other) return _nullConditionalSourcePath.Equals(other._nullConditionalSourcePath) && _throwInsteadOfConditionalNullMapping == other._throwInsteadOfConditionalNullMapping; } + + private IEnumerable? BuildElseClause(TypeMappingBuildContext ctx, ExpressionSyntax targetAccess) + { + if (_throwInsteadOfConditionalNullMapping) + { + // throw new ArgumentNullException + var nameofSourceAccess = _nullConditionalSourcePath.BuildAccess(ctx.Source, false, false, true); + return new[] { ctx.SyntaxFactory.ExpressionStatement(ThrowArgumentNullException(nameofSourceAccess)) }; + } + + // target.A = null; + return _targetsToSetNull.Count == 0 + ? null + : _targetsToSetNull.Select(x => ctx.SyntaxFactory.ExpressionStatement(x.BuildAssignment(targetAccess, NullLiteral()))); + } } diff --git a/src/Riok.Mapperly/Symbols/SetterMemberPath.cs b/src/Riok.Mapperly/Symbols/SetterMemberPath.cs index aed864cb9f1..dbf76a98f95 100644 --- a/src/Riok.Mapperly/Symbols/SetterMemberPath.cs +++ b/src/Riok.Mapperly/Symbols/SetterMemberPath.cs @@ -55,7 +55,7 @@ private static (IMappableMember, bool) BuildMemberSetter(MappingBuilderContext c return (new MethodAccessorMember(member, unsafeGetAccessor.MethodName, methodRequiresParameter: true), true); } - public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax sourceValue, bool coalesceAssignment = false) + public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, ExpressionSyntax valueToAssign, bool coalesceAssignment = false) { IEnumerable path = Path; @@ -73,16 +73,16 @@ public ExpressionSyntax BuildAssignment(ExpressionSyntax? baseAccess, Expression Debug.Assert(!IsMethod); // target.Value ??= mappedValue; - return CoalesceAssignment(memberPath, sourceValue); + return CoalesceAssignment(memberPath, valueToAssign); } if (IsMethod) { // target.SetValue(source.Value); - return Invocation(memberPath, sourceValue); + return Invocation(memberPath, valueToAssign); } // target.Value = source.Value; - return Assignment(memberPath, sourceValue); + return Assignment(memberPath, valueToAssign); } } diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFlatteningTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFlatteningTest.cs index ac9ae349dc9..533866fc277 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFlatteningTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyFlatteningTest.cs @@ -264,6 +264,10 @@ public void AutoFlattenedMultiplePropertiesPathDisabledNullable() { target.ValueId = source.Value.Id.ToString(); } + else + { + target.ValueId = null; + } target.ValueName = source.Value?.Name; return target; """ @@ -516,8 +520,17 @@ public void ManualNestedPropertyNullablePath() target.Value2.Value2.Id2 = source.Value1.Value1.Id1; target.Value2.Value2.Id20 = source.Value1.Value1.Id10; } + else + { + target.Value2.Value2.Id2 = null; + target.Value2.Value2.Id20 = null; + } target.Value2.Id200 = source.Value1.Id100; } + else + { + target.Value2.Id200 = null; + } return target; """ ); diff --git a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNullableTest.cs b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNullableTest.cs index fdfcba999c6..b120d9ffd99 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNullableTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ObjectPropertyNullableTest.cs @@ -319,6 +319,10 @@ public void NullableClassToNullableClassProperty() { target.Value = MapToD(source.Value); } + else + { + target.Value = null; + } return target; """ ); @@ -440,6 +444,39 @@ public void NullableClassPropertyToDisabledNullableProperty() { target.Value = MapToD(source.Value); } + else + { + target.Value = null; + } + return target; + """ + ); + } + + [Fact] + public void NullableValueTypeToOtherNullableValueType() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + "class A { public float? Value { get; set; } }", + "class B { public decimal? Value { get; set; } }" + ); + + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::B(); + if (source.Value != null) + { + target.Value = new decimal(source.Value.Value); + } + else + { + target.Value = null; + } return target; """ ); diff --git a/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ArrayToCollectionShouldUpgradeNullability#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ArrayToCollectionShouldUpgradeNullability#Mapper.g.verified.cs index 5267eb47d5f..c68eaab91b7 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ArrayToCollectionShouldUpgradeNullability#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ArrayToCollectionShouldUpgradeNullability#Mapper.g.verified.cs @@ -12,6 +12,10 @@ public partial class Mapper { target.Value = MapToICollection(source.Value); } + else + { + target.Value = null; + } return target; } diff --git a/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ArrayToReadOnlyCollectionShouldUpgradeNullability#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ArrayToReadOnlyCollectionShouldUpgradeNullability#Mapper.g.verified.cs index 0f146fd1c0b..ad980589c92 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ArrayToReadOnlyCollectionShouldUpgradeNullability#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ArrayToReadOnlyCollectionShouldUpgradeNullability#Mapper.g.verified.cs @@ -12,6 +12,10 @@ public partial class Mapper { target.Value = MapToIReadOnlyCollection(source.Value); } + else + { + target.Value = null; + } return target; } diff --git a/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.CollectionToReadOnlyCollectionShouldUpgradeNullability#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.CollectionToReadOnlyCollectionShouldUpgradeNullability#Mapper.g.verified.cs index 55e9a83148f..e4857907bc1 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.CollectionToReadOnlyCollectionShouldUpgradeNullability#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.CollectionToReadOnlyCollectionShouldUpgradeNullability#Mapper.g.verified.cs @@ -12,6 +12,10 @@ public partial class Mapper { target.Value = MapToIReadOnlyCollection(source.Value); } + else + { + target.Value = null; + } return target; } diff --git a/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ShouldUpgradeNullabilityInDisabledNullableContextInSelectClause#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ShouldUpgradeNullabilityInDisabledNullableContextInSelectClause#Mapper.g.verified.cs index e370d2585dd..e876283bd62 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ShouldUpgradeNullabilityInDisabledNullableContextInSelectClause#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/EnumerableTest.ShouldUpgradeNullabilityInDisabledNullableContextInSelectClause#Mapper.g.verified.cs @@ -12,6 +12,10 @@ public partial class Mapper { target.Value = MapToDArray(source.Value); } + else + { + target.Value = null; + } return target; } diff --git a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyNullableTest.ShouldUpgradeNullabilityInDisabledNullableContextInNestedProperty#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyNullableTest.ShouldUpgradeNullabilityInDisabledNullableContextInNestedProperty#Mapper.g.verified.cs index 1606f4b5915..074cd160dae 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyNullableTest.ShouldUpgradeNullabilityInDisabledNullableContextInNestedProperty#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/ObjectPropertyNullableTest.ShouldUpgradeNullabilityInDisabledNullableContextInNestedProperty#Mapper.g.verified.cs @@ -12,6 +12,10 @@ public partial class Mapper { target.Value = MapToD(source.Value); } + else + { + target.Value = null; + } return target; }