diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
index 9b5911227f..c13ead6284 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
@@ -16,8 +16,6 @@ namespace Riok.Mapperly.Descriptors;
public class MappingBuilderContext : SimpleMappingBuilderContext
{
private readonly FormatProviderCollection _formatProviders;
- private CollectionInfos? _collectionInfos;
- private DictionaryInfos? _dictionaryInfos;
public MappingBuilderContext(
SimpleMappingBuilderContext parentCtx,
@@ -57,9 +55,9 @@ bool ignoreDerivedTypes
public ITypeSymbol Target => MappingKey.Target;
- public CollectionInfos? CollectionInfos => _collectionInfos ??= CollectionInfoBuilder.Build(Types, SymbolAccessor, Source, Target);
+ public CollectionInfos? CollectionInfos => field ??= CollectionInfoBuilder.Build(Types, SymbolAccessor, Source, Target);
- public DictionaryInfos? DictionaryInfos => _dictionaryInfos ??= DictionaryInfoBuilder.Build(Types, CollectionInfos);
+ public DictionaryInfos? DictionaryInfos => field ??= DictionaryInfoBuilder.Build(Types, CollectionInfos);
public IUserMapping? UserMapping { get; }
@@ -162,7 +160,7 @@ bool ignoreDerivedTypes
///
/// Finds or builds a mapping ().
/// Before a new mapping is built existing mappings are tried to be found by the following priorities:
- /// 1. exact match
+ /// 1. user mapping with exact type match
/// 2. ignoring the nullability of the source and the target (needs to be handled by the caller of this method)
/// If no mapping can be found a new mapping is built with the source and the target as non-nullables.
///
@@ -176,7 +174,7 @@ bool ignoreDerivedTypes
Location? diagnosticLocation = null
)
{
- if (FindMapping(key) is { } mapping)
+ if (FindMapping(key) is INewInstanceUserMapping mapping)
return mapping;
// if a user mapping is referenced
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs
index 6c04d8adeb..c5c7cef44f 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/MemberMappings/SourceValue/NullMappedMemberSourceValue.cs
@@ -26,9 +26,17 @@ bool useNullConditionalAccess
public ExpressionSyntax Build(TypeMappingBuildContext ctx)
{
- // the source type of the delegate mapping is nullable or the source path is not nullable
- // build mapping with null conditional access
- if (_delegateMapping.SourceType.IsNullable() || !_sourceGetter.MemberPath.IsAnyNullable())
+ // if the source is not nullable, return it directly.
+ if (!_sourceGetter.MemberPath.IsAnyNullable())
+ {
+ ctx = ctx.WithSource(_sourceGetter.BuildAccess(ctx.Source));
+ return _delegateMapping.Build(ctx);
+ }
+
+ // if null conditional is allowed and the delegate mapping allows null source types,
+ // use null conditional access.
+ // => Map(source?.Value)
+ if (_delegateMapping.SourceType.IsNullable() && useNullConditionalAccess)
{
ctx = ctx.WithSource(_sourceGetter.BuildAccess(ctx.Source, nullConditional: true));
return _delegateMapping.Build(ctx);
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs
index e5a5d24756..695202a02d 100644
--- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs
+++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserImplementedInlinedExpressionMapping.cs
@@ -17,8 +17,12 @@ public class UserImplementedInlinedExpressionMapping(
ParameterSyntax sourceParameter,
IReadOnlyDictionary mappingInvocations,
ExpressionSyntax mappingBody
-) : NewInstanceMapping(userMapping.SourceType, userMapping.TargetType), INewInstanceMapping
+) : NewInstanceMapping(userMapping.SourceType, userMapping.TargetType), INewInstanceUserMapping
{
+ public IMethodSymbol Method => userMapping.Method;
+ public bool? Default => userMapping.Default;
+ public bool IsExternal => userMapping.IsExternal;
+
public override ExpressionSyntax Build(TypeMappingBuildContext ctx)
{
var body = InlineUserMappings(ctx, mappingBody);
diff --git a/src/Riok.Mapperly/Symbols/Members/MemberPath.cs b/src/Riok.Mapperly/Symbols/Members/MemberPath.cs
index 5f8662bd3b..daf03f76db 100644
--- a/src/Riok.Mapperly/Symbols/Members/MemberPath.cs
+++ b/src/Riok.Mapperly/Symbols/Members/MemberPath.cs
@@ -10,7 +10,7 @@ namespace Riok.Mapperly.Symbols.Members;
/// Represents a (possibly empty) list of members to access a certain member.
/// E.g. A.B.C
///
-[DebuggerDisplay("{ToDebugString}")]
+[DebuggerDisplay("{ToDebugString()}")]
public abstract class MemberPath(ITypeSymbol rootType, IReadOnlyList path)
{
protected const string MemberAccessSeparator = ".";
diff --git a/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs b/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs
index 528ec72b61..c88b98419a 100644
--- a/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs
+++ b/src/Riok.Mapperly/Symbols/Members/NonEmptyMemberPath.cs
@@ -1,10 +1,8 @@
-using System.Diagnostics;
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Descriptors;
namespace Riok.Mapperly.Symbols.Members;
-[DebuggerDisplay("{FullName}")]
public class NonEmptyMemberPath : MemberPath
{
public NonEmptyMemberPath(ITypeSymbol rootType, IReadOnlyList path)
diff --git a/src/Riok.Mapperly/Symbols/Members/SourceMemberPath.cs b/src/Riok.Mapperly/Symbols/Members/SourceMemberPath.cs
index 6941303bfc..d1002526dc 100644
--- a/src/Riok.Mapperly/Symbols/Members/SourceMemberPath.cs
+++ b/src/Riok.Mapperly/Symbols/Members/SourceMemberPath.cs
@@ -1,3 +1,6 @@
+using System.Diagnostics;
+
namespace Riok.Mapperly.Symbols.Members;
+[DebuggerDisplay("{MemberPath} ({Type})")]
public record SourceMemberPath(MemberPath MemberPath, SourceMemberType Type);
diff --git a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionNullableTest.cs b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionNullableTest.cs
index adfba4e959..71e118291c 100644
--- a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionNullableTest.cs
+++ b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionNullableTest.cs
@@ -112,6 +112,26 @@ public Task ClassToClassNullableSourcePathAutoFlatten()
return TestHelper.VerifyGenerator(source);
}
+ [Fact]
+ public Task NestedPropertyWithDeepCloneable()
+ {
+ // see https://github.com/riok/mapperly/issues/1710
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ public partial System.Linq.IQueryable Map(System.Linq.IQueryable source);
+
+ [MapNestedProperties("Nested")]
+ public partial B MapConfig(A source);
+ """,
+ TestSourceBuilderOptions.WithDeepCloning,
+ "class A { public C Nested { get; set; } }",
+ "class B { public string[] Value0 { get; set; } public string Value { get; set; } }",
+ "class C { public string[] Value0 { get; set; } public string Value { get; set; } }"
+ );
+
+ return TestHelper.VerifyGenerator(source, TestHelperOptions.DisabledNullable);
+ }
+
[Fact]
public Task ClassToClassNullableSourcePathAutoFlattenString()
{
diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.NestedPropertyWithDeepCloneable#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.NestedPropertyWithDeepCloneable#Mapper.g.verified.cs
new file mode 100644
index 0000000000..3bd8523ec4
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionNullableTest.NestedPropertyWithDeepCloneable#Mapper.g.verified.cs
@@ -0,0 +1,53 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+ public partial global::System.Linq.IQueryable? Map(global::System.Linq.IQueryable? source)
+ {
+ if (source == null)
+ return default;
+#nullable disable
+ return System.Linq.Queryable.Select(
+ source,
+ x => new global::B()
+ {
+ Value0 = x.Nested != null && x.Nested.Value0 != null ? global::System.Linq.Enumerable.ToArray(
+ global::System.Linq.Enumerable.Select(x.Nested.Value0, x1 => x1 == null ? default : x1)
+ ) : default,
+ Value = x.Nested != null && x.Nested.Value != null ? x.Nested.Value : default,
+ }
+ );
+#nullable enable
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+ public partial global::B? MapConfig(global::A? source)
+ {
+ if (source == null)
+ return default;
+ var target = new global::B();
+ if (source.Nested?.Value0 != null)
+ {
+ target.Value0 = MapToStringArray(source.Nested.Value0);
+ }
+ else
+ {
+ target.Value0 = null;
+ }
+ target.Value = source.Nested?.Value;
+ return target;
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")]
+ private string?[] MapToStringArray(string?[] source)
+ {
+ var target = new string?[source.Length];
+ for (var i = 0; i < source.Length; i++)
+ {
+ target[i] = source[i] == null ? default : source[i]!;
+ }
+ return target;
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.TwoQueryableMappingsWithNamedUsedMappingsAndAmbiguousName.verified.txt b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.TwoQueryableMappingsWithNamedUsedMappingsAndAmbiguousName.verified.txt
index 9adf3dc393..b85411a512 100644
--- a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.TwoQueryableMappingsWithNamedUsedMappingsAndAmbiguousName.verified.txt
+++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionUseNamedMappingTest.TwoQueryableMappingsWithNamedUsedMappingsAndAmbiguousName.verified.txt
@@ -37,6 +37,25 @@
DefaultSeverity: Error,
IsEnabledByDefault: true
}
+ },
+ {
+ Location: /*
+
+[MapProperty(nameof(A.StringValue1), nameof(B.StringValue1), Use = nameof(ModifyString)]
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+[MapProperty(nameof(A.StringValue2), nameof(B.StringValue2), Use = nameof(ModifyString2)]
+*/
+ : (18,1)-(18,87),
+ Message: The referenced mapping name ModifyString is ambiguous, use a unique name,
+ Severity: Error,
+ Descriptor: {
+ Id: RMG062,
+ Title: The referenced mapping name is ambiguous,
+ MessageFormat: The referenced mapping name {0} is ambiguous, use a unique name,
+ Category: Mapper,
+ DefaultSeverity: Error,
+ IsEnabledByDefault: true
+ }
}
]
}
\ No newline at end of file