Skip to content

Commit

Permalink
feat: add MaxDepth for recursive queries
Browse files Browse the repository at this point in the history
  • Loading branch information
TimothyMakkison committed Jan 23, 2024
1 parent 09f409b commit ad6ab87
Show file tree
Hide file tree
Showing 29 changed files with 662 additions and 51 deletions.
5 changes: 5 additions & 0 deletions src/Riok.Mapperly.Abstractions/MapperAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,9 @@ public class MapperAttribute : Attribute
/// When <c>false</c>, accessible constructors are ordered in descending order by their parameter count.
/// </summary>
public bool PreferParameterlessConstructors { get; set; } = true;

/// <summary>
/// Defines the maximum recursion depth that an IQueryable mapping will use.
/// </summary>
public int MaxRecursionDepth { get; set; } = 8;
}
22 changes: 22 additions & 0 deletions src/Riok.Mapperly.Abstractions/MapperMaxRecursionDepthAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Riok.Mapperly.Abstractions;

/// <summary>
/// Defines the maximum recursion depth that an IQueryable mapping will use.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public sealed class MapperMaxRecursionDepthAttribute : Attribute
{
/// <summary>
/// Defines the maximum recursion depth that an IQueryable mapping will use.
/// </summary>
/// <param name="maxRecursionDepth">The maximum recursion depth used when mapping IQueryable members.</param>
public MapperMaxRecursionDepthAttribute(int maxRecursionDepth)
{
MaxRecursionDepth = maxRecursionDepth;
}

/// <summary>
/// The maximum recursion depth used when mapping IQueryable members.
/// </summary>
public int MaxRecursionDepth { get; }
}
5 changes: 5 additions & 0 deletions src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,8 @@ Riok.Mapperly.Abstractions.ReferenceHandling.PreserveReferenceHandler.SetReferen
Riok.Mapperly.Abstractions.ReferenceHandling.PreserveReferenceHandler.TryGetReference<TSource, TTarget>(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
5 changes: 5 additions & 0 deletions src/Riok.Mapperly/Configuration/MapperConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,9 @@ public record MapperConfiguration
/// When <c>false</c>, accessible constructors are ordered in descending order by their parameter count.
/// </summary>
public bool? PreferParameterlessConstructors { get; init; }

/// <summary>
/// Defines the maximum recursion depth that an IQueryable mapping will use.
/// </summary>
public int? MaxRecursionDepth { get; init; }
}
3 changes: 3 additions & 0 deletions src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
10 changes: 8 additions & 2 deletions src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ MapperConfiguration defaultMapperConfiguration
Array.Empty<string>(),
Array.Empty<PropertyMappingConfiguration>(),
Mapper.IgnoreObsoleteMembersStrategy,
Mapper.RequiredMappingStrategy
Mapper.RequiredMappingStrategy,
Mapper.MaxRecursionDepth
),
Array.Empty<DerivedTypeMappingConfiguration>()
);
Expand Down Expand Up @@ -79,13 +80,18 @@ private PropertiesMappingConfiguration BuildPropertiesConfig(IMethodSymbol metho
var requiredMapping = _dataAccessor.Access<MapperRequiredMappingAttribute>(method).FirstOrDefault() is not { } methodWarnUnmapped
? _defaultConfiguration.Properties.RequiredMappingStrategy
: methodWarnUnmapped.RequiredMappingStrategy;
var maxRecursionDepth = _dataAccessor.Access<MapperMaxRecursionDepthAttribute>(method).FirstOrDefault()
is not { } methodMaxRecursionDepth
? _defaultConfiguration.Properties.MaxRecursionDepth
: methodMaxRecursionDepth.MaxRecursionDepth;

return new PropertiesMappingConfiguration(
ignoredSourceProperties,
ignoredTargetProperties,
propertyConfigurations,
ignoreObsolete,
requiredMapping
requiredMapping,
maxRecursionDepth
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ public record PropertiesMappingConfiguration(
IReadOnlyCollection<string> IgnoredTargets,
IReadOnlyCollection<PropertyMappingConfiguration> ExplicitMappings,
IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy,
RequiredMappingStrategy RequiredMappingStrategy
RequiredMappingStrategy RequiredMappingStrategy,
int MaxRecursionDepth
);
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ conversionType is not MappingConversionType.EnumToString and not MappingConversi
/// <returns>The <see cref="INewInstanceMapping"/> if a mapping was found or <c>null</c> if none was found.</returns>
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;

Expand Down
5 changes: 5 additions & 0 deletions src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class MappingBuilderContext : SimpleMappingBuilderContext
{
private readonly FormatProviderCollection _formatProviders;
private CollectionInfos? _collectionInfos;
internal readonly MappingRecursionDepthTracker _parentTypes;

public MappingBuilderContext(
SimpleMappingBuilderContext parentCtx,
Expand All @@ -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));
Expand Down
42 changes: 42 additions & 0 deletions src/Riok.Mapperly/Descriptors/MappingRecursionDepthTracker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.Collections.Immutable;

namespace Riok.Mapperly.Descriptors;

/// <summary>
/// Immutable wrapper for <see cref="ImmutableDictionary&lt;TypeMappingKey, int&gt;"/> which tracks the parent types for a mapping.
/// Used to detect self referential loops.
/// </summary>
/// <param name="parentTypes">Dictionary tracking how many times a type has been seen.</param>
public readonly struct MappingRecursionDepthTracker(ImmutableDictionary<TypeMappingKey, int> parentTypes)
{
/// <summary>
/// Increments how many times a <see cref="TypeMappingKey"/> has been mapped.
/// Used to track how many times a parent context has mapped a type.
/// </summary>
/// <param name="typeMappingKey">The mapped type.</param>
/// <returns>A new <see cref="MappingRecursionDepthTracker"/> with the updated key.</returns>
public MappingRecursionDepthTracker AddOrIncrement(TypeMappingKey typeMappingKey)
{
var mappingRecursionCount = parentTypes.GetValueOrDefault(typeMappingKey);
var newParentTypes = parentTypes.SetItem(typeMappingKey, mappingRecursionCount + 1);
return new(newParentTypes);
}

/// <summary>
/// Gets the number of times a <see cref="TypeMappingKey"/> has been mapped by the parent contexts.
/// </summary>
/// <param name="typeMappingKey">The candidate mapping.</param>
/// <returns>The number of times the <see cref="TypeMappingKey"/> has been mapped.</returns>
public int GetDepth(TypeMappingKey typeMappingKey) => parentTypes.GetValueOrDefault(typeMappingKey);

/// <summary>
/// Creates a new <see cref="MappingRecursionDepthTracker"/> containing the initial type mapping.
/// </summary>
/// <param name="mappingKey">Initial <see cref="TypeMappingKey"/> value.</param>
/// <returns>A <see cref="MappingRecursionDepthTracker"/> containing the initial type mapping.</returns>
public static MappingRecursionDepthTracker Create(TypeMappingKey mappingKey)
{
var dict = ImmutableDictionary<TypeMappingKey, int>.Empty;
return new(dict.Add(mappingKey, 1));
}
}
16 changes: 16 additions & 0 deletions src/Riok.Mapperly/Descriptors/Mappings/DefaultMemberMapping.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper;

namespace Riok.Mapperly.Descriptors.Mappings;

/// <summary>
/// Represents a mapping that returns default.
/// <code>
/// target = default;
/// </code>
/// </summary>
public class DefaultMemberMapping(ITypeSymbol sourceType, ITypeSymbol targetType) : NewInstanceMapping(sourceType, targetType)
{
public override ExpressionSyntax Build(TypeMappingBuildContext ctx) => DefaultLiteral();
}
9 changes: 9 additions & 0 deletions src/Riok.Mapperly/Diagnostics/DiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
Loading

0 comments on commit ad6ab87

Please sign in to comment.