diff --git a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs
index c93e11d5090..d5b1e63f94d 100644
--- a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs
+++ b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs
@@ -106,4 +106,9 @@ public class MapperAttribute : Attribute
/// When false, accessible constructors are ordered in descending order by their parameter count.
///
public bool PreferParameterlessConstructors { get; set; } = true;
+
+ ///
+ /// Defines the maximum recursion depth that an IQueryable mapping will use.
+ ///
+ public int MaxRecursionDepth { get; set; } = 8;
}
diff --git a/src/Riok.Mapperly.Abstractions/MapperMaxRecursionDepthAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperMaxRecursionDepthAttribute.cs
new file mode 100644
index 00000000000..ceb419e9cc3
--- /dev/null
+++ b/src/Riok.Mapperly.Abstractions/MapperMaxRecursionDepthAttribute.cs
@@ -0,0 +1,22 @@
+namespace Riok.Mapperly.Abstractions;
+
+///
+/// Defines the maximum recursion depth that an IQueryable mapping will use.
+///
+[AttributeUsage(AttributeTargets.Method)]
+public sealed class MapperMaxRecursionDepthAttribute : Attribute
+{
+ ///
+ /// Defines the maximum recursion depth that an IQueryable mapping will use.
+ ///
+ /// The maximum recursion depth used when mapping IQueryable members.
+ public MapperMaxRecursionDepthAttribute(int maxRecursionDepth)
+ {
+ MaxRecursionDepth = maxRecursionDepth;
+ }
+
+ ///
+ /// The maximum recursion depth used when mapping IQueryable members.
+ ///
+ public int MaxRecursionDepth { get; }
+}
diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
index b40cc119902..4482358b6d0 100644
--- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
+++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
@@ -143,3 +143,8 @@ Riok.Mapperly.Abstractions.ReferenceHandling.PreserveReferenceHandler.SetReferen
Riok.Mapperly.Abstractions.ReferenceHandling.PreserveReferenceHandler.TryGetReference(TSource source, out TTarget? target) -> bool
Riok.Mapperly.Abstractions.MapperAttribute.PreferParameterlessConstructors.get -> bool
Riok.Mapperly.Abstractions.MapperAttribute.PreferParameterlessConstructors.set -> void
+Riok.Mapperly.Abstractions.MapperAttribute.MaxRecursionDepth.get -> int
+Riok.Mapperly.Abstractions.MapperAttribute.MaxRecursionDepth.set -> void
+Riok.Mapperly.Abstractions.MapperMaxRecursionDepthAttribute
+Riok.Mapperly.Abstractions.MapperMaxRecursionDepthAttribute.MapperMaxRecursionDepthAttribute(int maxRecursionDepth) -> void
+Riok.Mapperly.Abstractions.MapperMaxRecursionDepthAttribute.MaxRecursionDepth.get -> int
diff --git a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs
index 60859f4487e..7d990945a60 100644
--- a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs
+++ b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs
@@ -110,4 +110,9 @@ public record MapperConfiguration
/// When false, accessible constructors are ordered in descending order by their parameter count.
///
public bool? PreferParameterlessConstructors { get; init; }
+
+ ///
+ /// Defines the maximum recursion depth that an IQueryable mapping will use.
+ ///
+ public int? MaxRecursionDepth { get; init; }
}
diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs
index 22f86b7f28e..7173bf05674 100644
--- a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs
+++ b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs
@@ -59,6 +59,9 @@ public static MapperAttribute Merge(MapperConfiguration mapperConfiguration, Map
?? defaultMapperConfiguration.PreferParameterlessConstructors
?? mapper.PreferParameterlessConstructors;
+ mapper.MaxRecursionDepth =
+ mapperConfiguration.MaxRecursionDepth ?? defaultMapperConfiguration.MaxRecursionDepth ?? mapper.MaxRecursionDepth;
+
return mapper;
}
}
diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs
index cbc0e86859e..069802bdb98 100644
--- a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs
+++ b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs
@@ -33,7 +33,8 @@ MapperConfiguration defaultMapperConfiguration
Array.Empty(),
Array.Empty(),
Mapper.IgnoreObsoleteMembersStrategy,
- Mapper.RequiredMappingStrategy
+ Mapper.RequiredMappingStrategy,
+ Mapper.MaxRecursionDepth
),
Array.Empty()
);
@@ -79,13 +80,18 @@ private PropertiesMappingConfiguration BuildPropertiesConfig(IMethodSymbol metho
var requiredMapping = _dataAccessor.Access(method).FirstOrDefault() is not { } methodWarnUnmapped
? _defaultConfiguration.Properties.RequiredMappingStrategy
: methodWarnUnmapped.RequiredMappingStrategy;
+ var maxRecursionDepth = _dataAccessor.Access(method).FirstOrDefault()
+ is not { } methodMaxRecursionDepth
+ ? _defaultConfiguration.Properties.MaxRecursionDepth
+ : methodMaxRecursionDepth.MaxRecursionDepth;
return new PropertiesMappingConfiguration(
ignoredSourceProperties,
ignoredTargetProperties,
propertyConfigurations,
ignoreObsolete,
- requiredMapping
+ requiredMapping,
+ maxRecursionDepth
);
}
diff --git a/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs b/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs
index 9d65612eb8f..658c4332fcc 100644
--- a/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs
+++ b/src/Riok.Mapperly/Configuration/PropertiesMappingConfiguration.cs
@@ -7,5 +7,6 @@ public record PropertiesMappingConfiguration(
IReadOnlyCollection IgnoredTargets,
IReadOnlyCollection ExplicitMappings,
IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy,
- RequiredMappingStrategy RequiredMappingStrategy
+ RequiredMappingStrategy RequiredMappingStrategy,
+ int MaxRecursionDepth
);
diff --git a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs
index 63baeae0c23..b123bd3c3ed 100644
--- a/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs
+++ b/src/Riok.Mapperly/Descriptors/InlineExpressionMappingBuilderContext.cs
@@ -54,6 +54,15 @@ conversionType is not MappingConversionType.EnumToString and not MappingConversi
/// The if a mapping was found or null if none was found.
public override INewInstanceMapping? FindMapping(TypeMappingKey mappingKey)
{
+ // check for recursion loop returning null to prevent a loop or default when recursion limit is reached.
+ var count = _parentTypes.GetDepth(mappingKey);
+ if (count >= 1)
+ {
+ return count >= Configuration.Properties.MaxRecursionDepth + 2
+ ? new DefaultMemberMapping(mappingKey.Source, mappingKey.Target)
+ : null;
+ }
+
if (_inlineExpressionMappings.Find(mappingKey) is { } mapping)
return mapping;
diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
index a32585e6608..594078f8fbb 100644
--- a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
+++ b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
@@ -17,6 +17,7 @@ public class MappingBuilderContext : SimpleMappingBuilderContext
{
private readonly FormatProviderCollection _formatProviders;
private CollectionInfos? _collectionInfos;
+ internal readonly MappingRecursionDepthTracker _parentTypes;
public MappingBuilderContext(
SimpleMappingBuilderContext parentCtx,
@@ -30,6 +31,10 @@ public MappingBuilderContext(
{
ObjectFactories = objectFactories;
_formatProviders = formatProviders;
+ _parentTypes = parentCtx is MappingBuilderContext inlineCtx
+ ? inlineCtx._parentTypes.AddOrIncrement(mappingKey)
+ : MappingRecursionDepthTracker.Create(mappingKey);
+
UserSymbol = userSymbol;
MappingKey = mappingKey;
Configuration = ReadConfiguration(new MappingConfigurationReference(UserSymbol, mappingKey.Source, mappingKey.Target));
diff --git a/src/Riok.Mapperly/Descriptors/MappingRecursionDepthTracker.cs b/src/Riok.Mapperly/Descriptors/MappingRecursionDepthTracker.cs
new file mode 100644
index 00000000000..371220ae545
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/MappingRecursionDepthTracker.cs
@@ -0,0 +1,42 @@
+using System.Collections.Immutable;
+
+namespace Riok.Mapperly.Descriptors;
+
+///
+/// Immutable wrapper for which tracks the parent types for a mapping.
+/// Used to detect self referential loops.
+///
+/// Dictionary tracking how many times a type has been seen.
+public readonly struct MappingRecursionDepthTracker(ImmutableDictionary parentTypes)
+{
+ ///
+ /// Increments how many times a has been mapped.
+ /// Used to track how many times a parent context has mapped a type.
+ ///
+ /// The mapped type.
+ /// A new with the updated key.
+ public MappingRecursionDepthTracker AddOrIncrement(TypeMappingKey typeMappingKey)
+ {
+ var mappingRecursionCount = parentTypes.GetValueOrDefault(typeMappingKey);
+ var newParentTypes = parentTypes.SetItem(typeMappingKey, mappingRecursionCount + 1);
+ return new(newParentTypes);
+ }
+
+ ///
+ /// Gets the number of times a has been mapped by the parent contexts.
+ ///
+ /// The candidate mapping.
+ /// The number of times the has been mapped.
+ public int GetDepth(TypeMappingKey typeMappingKey) => parentTypes.GetValueOrDefault(typeMappingKey);
+
+ ///
+ /// Creates a new containing the initial type mapping.
+ ///
+ /// Initial value.
+ /// A containing the initial type mapping.
+ public static MappingRecursionDepthTracker Create(TypeMappingKey mappingKey)
+ {
+ var dict = ImmutableDictionary.Empty;
+ return new(dict.Add(mappingKey, 1));
+ }
+}
diff --git a/src/Riok.Mapperly/Descriptors/Mappings/DefaultMemberMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/DefaultMemberMapping.cs
new file mode 100644
index 00000000000..1fe857a8065
--- /dev/null
+++ b/src/Riok.Mapperly/Descriptors/Mappings/DefaultMemberMapping.cs
@@ -0,0 +1,16 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;
+
+namespace Riok.Mapperly.Descriptors.Mappings;
+
+///
+/// Represents a mapping that returns default.
+///
+/// target = default;
+///
+///
+public class DefaultMemberMapping(ITypeSymbol sourceType, ITypeSymbol targetType) : NewInstanceMapping(sourceType, targetType)
+{
+ public override ExpressionSyntax Build(TypeMappingBuildContext ctx) => DefaultLiteral();
+}
diff --git a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
index 9d3af22a497..dd66298261c 100644
--- a/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
+++ b/src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
@@ -509,4 +509,13 @@ public static class DiagnosticDescriptors
DiagnosticSeverity.Error,
true
);
+
+ public static readonly DiagnosticDescriptor MaxRecursionDepthMustBeZeroOrMore = new DiagnosticDescriptor(
+ "RMG056",
+ $"The value of MaxRecursionDepth cannot be less than zero",
+ $"The value of MaxRecursionDepth cannot be less than zero",
+ DiagnosticCategories.Mapper,
+ DiagnosticSeverity.Error,
+ true
+ );
}
diff --git a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionLoopTest.cs b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionLoopTest.cs
new file mode 100644
index 00000000000..17c368f97ec
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionLoopTest.cs
@@ -0,0 +1,197 @@
+using Riok.Mapperly.Diagnostics;
+
+namespace Riok.Mapperly.Tests.Mapping;
+
+[UsesVerify]
+public class QueryableProjectionLoopTest
+{
+ [Fact]
+ public Task ReferenceLoopInitProperty()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "System.Linq.IQueryable",
+ "System.Linq.IQueryable",
+ "class A { public A? Parent { get; set; } }",
+ "class B { public B? Parent { get; set; } }"
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task SetRecursionDepthToZero()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "System.Linq.IQueryable",
+ "System.Linq.IQueryable",
+ TestSourceBuilderOptions.Default with
+ {
+ MaxRecursionDepth = 0
+ },
+ "class A { public A? Parent { get; set; } }",
+ "class B { public B? Parent { get; set; } }"
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task SetRecursionDepthToOne()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "System.Linq.IQueryable",
+ "System.Linq.IQueryable",
+ TestSourceBuilderOptions.Default with
+ {
+ MaxRecursionDepth = 1
+ },
+ "class A { public A? Parent { get; set; } }",
+ "class B { public B? Parent { get; set; } }"
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task SetRecursionDepthToTwo()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "System.Linq.IQueryable",
+ "System.Linq.IQueryable",
+ TestSourceBuilderOptions.Default with
+ {
+ MaxRecursionDepth = 2
+ },
+ "class A { public A? Parent { get; set; } }",
+ "class B { public B? Parent { get; set; } }"
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task MethodAttributeSetRecursionDepthForLoop()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ partial System.Linq.IQueryable Map(System.Linq.IQueryable src);
+
+ [MapperMaxRecursionDepth(2)]
+ partial B Map(A src);
+ """,
+ "class A { public A? Parent { get; set; } }",
+ "class B { public B? Parent { get; set; } }"
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task MethodAttributeOverridesClassSetRecursionDepthForLoop()
+ {
+ var source = TestSourceBuilder.MapperWithBodyAndTypes(
+ """
+ partial System.Linq.IQueryable Map(System.Linq.IQueryable src);
+ [MapperMaxRecursionDepth(2)] partial B Map(A src);
+ """,
+ TestSourceBuilderOptions.Default with
+ {
+ MaxRecursionDepth = 4
+ },
+ "class A { public A? Parent { get; set; } }",
+ "class B { public B? Parent { get; set; } }"
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task AssemblyDefaultShouldWork()
+ {
+ var source = TestSourceBuilder.CSharp(
+ """
+ using Riok.Mapperly.Abstractions;
+
+ [assembly: MapperDefaultsAttribute(MaxRecursionDepth = 2)]
+ [Mapper()]
+ public partial class MyMapper
+ {
+ partial System.Linq.IQueryable Map(System.Linq.IQueryable src);
+ }
+
+ class A { public A? Parent { get; set; } }
+
+ class B { public B? Parent { get; set; } }
+ """
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task AttributeShouldOverrideAssemblyDefault()
+ {
+ var source = TestSourceBuilder.CSharp(
+ """
+ using Riok.Mapperly.Abstractions;
+
+ [assembly: MapperDefaultsAttribute(MaxRecursionDepth = 2)]
+ [Mapper(MaxRecursionDepth = 4)]
+ public partial class MyMapper
+ {
+ partial System.Linq.IQueryable Map(System.Linq.IQueryable src);
+ }
+
+ class A { public A? Parent { get; set; } }
+
+ class B { public B? Parent { get; set; } }
+ """
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task ReferenceLoopCtor()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "System.Linq.IQueryable",
+ "System.Linq.IQueryable",
+ "class A { public A? Parent { get; set; } }",
+ "class B { public B(B? parent) {} }"
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public Task IndirectReferenceLoop()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "System.Linq.IQueryable",
+ "System.Linq.IQueryable",
+ "class A { public string StringValue { get; set; } public C Parent { get; set; } }",
+ "class B { public string StringValue { get; set; } public D Parent { get; set; } }",
+ "class C { public A Parent { get; set; } }",
+ "class D { public B Parent { get; set; } }"
+ );
+
+ return TestHelper.VerifyGenerator(source);
+ }
+
+ [Fact]
+ public void WithReferenceHandlingShouldDiagnostic()
+ {
+ var source = TestSourceBuilder.Mapping(
+ "System.Linq.IQueryable",
+ "System.Linq.IQueryable",
+ TestSourceBuilderOptions.WithReferenceHandling
+ );
+
+ TestHelper
+ .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
+ .Should()
+ .HaveDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingsDoNotSupportReferenceHandling)
+ .HaveAssertedAllDiagnostics();
+ }
+}
diff --git a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionTest.cs b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionTest.cs
index 77b52668c4f..0ae05f642a0 100644
--- a/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionTest.cs
+++ b/test/Riok.Mapperly.Tests/Mapping/QueryableProjectionTest.cs
@@ -1,5 +1,3 @@
-using Riok.Mapperly.Diagnostics;
-
namespace Riok.Mapperly.Tests.Mapping;
[UsesVerify]
@@ -137,32 +135,6 @@ public Task ClassToClassWithUserImplemented()
return TestHelper.VerifyGenerator(source);
}
- [Fact]
- public Task ReferenceLoopInitProperty()
- {
- var source = TestSourceBuilder.Mapping(
- "System.Linq.IQueryable",
- "System.Linq.IQueryable",
- "class A { public A? Parent { get; set; } }",
- "class B { public B? Parent { get; set; } }"
- );
-
- return TestHelper.VerifyGenerator(source);
- }
-
- [Fact]
- public Task ReferenceLoopCtor()
- {
- var source = TestSourceBuilder.Mapping(
- "System.Linq.IQueryable",
- "System.Linq.IQueryable",
- "class A { public A? Parent { get; set; } }",
- "class B { public B(B? parent) {} }"
- );
-
- return TestHelper.VerifyGenerator(source);
- }
-
[Fact]
public Task CtorShouldSkipUnmatchedOptionalParameters()
{
@@ -175,20 +147,4 @@ public Task CtorShouldSkipUnmatchedOptionalParameters()
return TestHelper.VerifyGenerator(source);
}
-
- [Fact]
- public void WithReferenceHandlingShouldDiagnostic()
- {
- var source = TestSourceBuilder.Mapping(
- "System.Linq.IQueryable",
- "System.Linq.IQueryable",
- TestSourceBuilderOptions.WithReferenceHandling
- );
-
- TestHelper
- .GenerateMapper(source, TestHelperOptions.AllowDiagnostics)
- .Should()
- .HaveDiagnostic(DiagnosticDescriptors.QueryableProjectionMappingsDoNotSupportReferenceHandling)
- .HaveAssertedAllDiagnostics();
- }
}
diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs
index e0b98f8d7e3..34d8ad2002b 100644
--- a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs
+++ b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs
@@ -92,7 +92,8 @@ private static string BuildAttribute(TestSourceBuilderOptions options)
Attribute(options.IgnoreObsoleteMembersStrategy),
Attribute(options.RequiredMappingStrategy),
Attribute(options.IncludedMembers),
- Attribute(options.PreferParameterlessConstructors)
+ Attribute(options.PreferParameterlessConstructors),
+ Attribute(options.MaxRecursionDepth),
}.WhereNotNull();
return $"[Mapper({string.Join(", ", attrs)})]";
@@ -110,6 +111,9 @@ private static string BuildAttribute(TestSourceBuilderOptions options)
private static string? Attribute(bool? value, [CallerArgumentExpression("value")] string? expression = null) =>
value.HasValue ? Attribute(value.Value ? "true" : "false", expression) : null;
+ private static string? Attribute(int? value, [CallerArgumentExpression("value")] string? expression = null) =>
+ value.HasValue ? Attribute(value.Value.ToString(), expression) : null;
+
private static string? Attribute(string? value, [CallerArgumentExpression("value")] string? expression = null)
{
if (value == null)
diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs
index a3edbe320f3..35c2414b34d 100644
--- a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs
+++ b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs
@@ -18,7 +18,8 @@ public record TestSourceBuilderOptions(
RequiredMappingStrategy? RequiredMappingStrategy = null,
MemberVisibility? IncludedMembers = null,
bool Static = false,
- bool PreferParameterlessConstructors = true
+ bool PreferParameterlessConstructors = true,
+ int? MaxRecursionDepth = null
)
{
public const string DefaultMapperClassName = "Mapper";
diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.AssemblyDefaultShouldWork#MyMapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.AssemblyDefaultShouldWork#MyMapper.g.verified.cs
new file mode 100644
index 00000000000..bfd87c863df
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.AssemblyDefaultShouldWork#MyMapper.g.verified.cs
@@ -0,0 +1,21 @@
+//HintName: MyMapper.g.cs
+//
+#nullable enable
+public partial class MyMapper
+{
+ partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable src)
+ {
+#nullable disable
+ return System.Linq.Queryable.Select(src, x => new global::B()
+ {
+ Parent = x.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent.Parent != null ? default : default,
+ } : default,
+ } : default,
+ });
+#nullable enable
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.AttributeShouldOverrideAssemblyDefault#MyMapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.AttributeShouldOverrideAssemblyDefault#MyMapper.g.verified.cs
new file mode 100644
index 00000000000..6ac222f52f6
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.AttributeShouldOverrideAssemblyDefault#MyMapper.g.verified.cs
@@ -0,0 +1,27 @@
+//HintName: MyMapper.g.cs
+//
+#nullable enable
+public partial class MyMapper
+{
+ partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable src)
+ {
+#nullable disable
+ return System.Linq.Queryable.Select(src, x => new global::B()
+ {
+ Parent = x.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent.Parent.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent.Parent.Parent.Parent != null ? default : default,
+ } : default,
+ } : default,
+ } : default,
+ } : default,
+ });
+#nullable enable
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.IndirectReferenceLoop#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.IndirectReferenceLoop#Mapper.g.verified.cs
new file mode 100644
index 00000000000..41f270da051
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.IndirectReferenceLoop#Mapper.g.verified.cs
@@ -0,0 +1,75 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source)
+ {
+#nullable disable
+ return System.Linq.Queryable.Select(source, x => new global::B()
+ {
+ StringValue = x.StringValue,
+ Parent = new global::D()
+ {
+ Parent = new global::B()
+ {
+ StringValue = x.Parent.Parent.StringValue,
+ Parent = new global::D()
+ {
+ Parent = new global::B()
+ {
+ StringValue = x.Parent.Parent.Parent.Parent.StringValue,
+ Parent = new global::D()
+ {
+ Parent = new global::B()
+ {
+ StringValue = x.Parent.Parent.Parent.Parent.Parent.Parent.StringValue,
+ Parent = new global::D()
+ {
+ Parent = new global::B()
+ {
+ StringValue = x.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.StringValue,
+ Parent = new global::D()
+ {
+ Parent = new global::B()
+ {
+ StringValue = x.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.StringValue,
+ Parent = new global::D()
+ {
+ Parent = new global::B()
+ {
+ StringValue = x.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.StringValue,
+ Parent = new global::D()
+ {
+ Parent = new global::B()
+ {
+ StringValue = x.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.StringValue,
+ Parent = new global::D()
+ {
+ Parent = new global::B()
+ {
+ StringValue = x.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.StringValue,
+ Parent = new global::D()
+ {
+ Parent = default,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ });
+#nullable enable
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.MethodAttributeOverridesClassSetRecursionDepthForLoop#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.MethodAttributeOverridesClassSetRecursionDepthForLoop#Mapper.g.verified.cs
new file mode 100644
index 00000000000..00fb31449ab
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.MethodAttributeOverridesClassSetRecursionDepthForLoop#Mapper.g.verified.cs
@@ -0,0 +1,35 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable src)
+ {
+#nullable disable
+ return System.Linq.Queryable.Select(src, x => new global::B()
+ {
+ Parent = x.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent.Parent != null ? default : default,
+ } : default,
+ } : default,
+ });
+#nullable enable
+ }
+
+ partial global::B Map(global::A src)
+ {
+ var target = new global::B();
+ if (src.Parent != null)
+ {
+ target.Parent = Map(src.Parent);
+ }
+ else
+ {
+ target.Parent = null;
+ }
+ return target;
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.MethodAttributeSetRecursionDepthForLoop#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.MethodAttributeSetRecursionDepthForLoop#Mapper.g.verified.cs
new file mode 100644
index 00000000000..00fb31449ab
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.MethodAttributeSetRecursionDepthForLoop#Mapper.g.verified.cs
@@ -0,0 +1,35 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable src)
+ {
+#nullable disable
+ return System.Linq.Queryable.Select(src, x => new global::B()
+ {
+ Parent = x.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent.Parent != null ? default : default,
+ } : default,
+ } : default,
+ });
+#nullable enable
+ }
+
+ partial global::B Map(global::A src)
+ {
+ var target = new global::B();
+ if (src.Parent != null)
+ {
+ target.Parent = Map(src.Parent);
+ }
+ else
+ {
+ target.Parent = null;
+ }
+ return target;
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.ReferenceLoopCtor#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.ReferenceLoopCtor#Mapper.g.verified.cs
new file mode 100644
index 00000000000..7c0332c0d5c
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.ReferenceLoopCtor#Mapper.g.verified.cs
@@ -0,0 +1,12 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source)
+ {
+#nullable disable
+ return System.Linq.Queryable.Select(source, x => new global::B(x.Parent != null ? new global::B(x.Parent.Parent != null ? new global::B(x.Parent.Parent.Parent != null ? new global::B(x.Parent.Parent.Parent.Parent != null ? new global::B(x.Parent.Parent.Parent.Parent.Parent != null ? new global::B(x.Parent.Parent.Parent.Parent.Parent.Parent != null ? new global::B(x.Parent.Parent.Parent.Parent.Parent.Parent.Parent != null ? new global::B(x.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent != null ? new global::B(x.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent != null ? default : default) : default) : default) : default) : default) : default) : default) : default) : default));
+#nullable enable
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.ReferenceLoopInitProperty#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.ReferenceLoopInitProperty#Mapper.g.verified.cs
new file mode 100644
index 00000000000..1e91408940a
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.ReferenceLoopInitProperty#Mapper.g.verified.cs
@@ -0,0 +1,39 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source)
+ {
+#nullable disable
+ return System.Linq.Queryable.Select(source, x => new global::B()
+ {
+ Parent = x.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent.Parent.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent.Parent.Parent.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent.Parent.Parent.Parent.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent.Parent.Parent.Parent.Parent.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent != null ? default : default,
+ } : default,
+ } : default,
+ } : default,
+ } : default,
+ } : default,
+ } : default,
+ } : default,
+ } : default,
+ });
+#nullable enable
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursionDepthToOne#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursionDepthToOne#Mapper.g.verified.cs
new file mode 100644
index 00000000000..49888db769d
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursionDepthToOne#Mapper.g.verified.cs
@@ -0,0 +1,18 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source)
+ {
+#nullable disable
+ return System.Linq.Queryable.Select(source, x => new global::B()
+ {
+ Parent = x.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent != null ? default : default,
+ } : default,
+ });
+#nullable enable
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursionDepthToTwo#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursionDepthToTwo#Mapper.g.verified.cs
new file mode 100644
index 00000000000..f2f51217947
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursionDepthToTwo#Mapper.g.verified.cs
@@ -0,0 +1,21 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source)
+ {
+#nullable disable
+ return System.Linq.Queryable.Select(source, x => new global::B()
+ {
+ Parent = x.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent.Parent != null ? default : default,
+ } : default,
+ } : default,
+ });
+#nullable enable
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopInitProperty#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursionDepthToZero#Mapper.g.verified.cs
similarity index 86%
rename from test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopInitProperty#Mapper.g.verified.cs
rename to test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursionDepthToZero#Mapper.g.verified.cs
index 8588c1304af..a8785be7d64 100644
--- a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopInitProperty#Mapper.g.verified.cs
+++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursionDepthToZero#Mapper.g.verified.cs
@@ -9,7 +9,7 @@ public partial class Mapper
#nullable disable
return System.Linq.Queryable.Select(source, x => new global::B()
{
- Parent = x.Parent != null ? new global::B() : default,
+ Parent = x.Parent != null ? default : default,
});
#nullable enable
}
diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursiveDepthToOne#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursiveDepthToOne#Mapper.g.verified.cs
new file mode 100644
index 00000000000..49888db769d
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursiveDepthToOne#Mapper.g.verified.cs
@@ -0,0 +1,18 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source)
+ {
+#nullable disable
+ return System.Linq.Queryable.Select(source, x => new global::B()
+ {
+ Parent = x.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent != null ? default : default,
+ } : default,
+ });
+#nullable enable
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursiveDepthToTwo#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursiveDepthToTwo#Mapper.g.verified.cs
new file mode 100644
index 00000000000..f2f51217947
--- /dev/null
+++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursiveDepthToTwo#Mapper.g.verified.cs
@@ -0,0 +1,21 @@
+//HintName: Mapper.g.cs
+//
+#nullable enable
+public partial class Mapper
+{
+ private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source)
+ {
+#nullable disable
+ return System.Linq.Queryable.Select(source, x => new global::B()
+ {
+ Parent = x.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent != null ? new global::B()
+ {
+ Parent = x.Parent.Parent.Parent != null ? default : default,
+ } : default,
+ } : default,
+ });
+#nullable enable
+ }
+}
\ No newline at end of file
diff --git a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopCtor#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursiveDepthToZero#Mapper.g.verified.cs
similarity index 81%
rename from test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopCtor#Mapper.g.verified.cs
rename to test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursiveDepthToZero#Mapper.g.verified.cs
index c5a4bb025c9..a8785be7d64 100644
--- a/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionTest.ReferenceLoopCtor#Mapper.g.verified.cs
+++ b/test/Riok.Mapperly.Tests/_snapshots/QueryableProjectionLoopTest.SetRecursiveDepthToZero#Mapper.g.verified.cs
@@ -7,7 +7,10 @@ public partial class Mapper
private partial global::System.Linq.IQueryable Map(global::System.Linq.IQueryable source)
{
#nullable disable
- return System.Linq.Queryable.Select(source, x => new global::B(x.Parent != null ? new global::B() : default));
+ return System.Linq.Queryable.Select(source, x => new global::B()
+ {
+ Parent = x.Parent != null ? default : default,
+ });
#nullable enable
}
}
\ No newline at end of file