Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: add recursive depth support to projection mappings #878

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Riok.Mapperly.Abstractions/MapperAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,9 @@ public class MapperAttribute : Attribute
/// partial methods are discovered.
/// </summary>
public bool AutoUserMappings { get; set; } = true;

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

namespace Riok.Mapperly.Abstractions;

/// <summary>
/// Defines the maximum recursion depth that an IQueryable mapping will use.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
[Conditional("MAPPERLY_ABSTRACTIONS_SCOPE_RUNTIME")]
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(uint maxRecursionDepth)
{
MaxRecursionDepth = maxRecursionDepth;
}

/// <summary>
/// The maximum recursion depth used when mapping IQueryable members.
/// </summary>
public uint 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 @@ -155,3 +155,8 @@ Riok.Mapperly.Abstractions.UserMappingAttribute.Default.get -> bool
Riok.Mapperly.Abstractions.UserMappingAttribute.Default.set -> void
Riok.Mapperly.Abstractions.MapPropertyAttribute.Use.get -> string?
Riok.Mapperly.Abstractions.MapPropertyAttribute.Use.set -> void
Riok.Mapperly.Abstractions.MapperAttribute.MaxRecursionDepth.get -> uint
Riok.Mapperly.Abstractions.MapperAttribute.MaxRecursionDepth.set -> void
Riok.Mapperly.Abstractions.MapperMaxRecursionDepthAttribute
Riok.Mapperly.Abstractions.MapperMaxRecursionDepthAttribute.MapperMaxRecursionDepthAttribute(uint maxRecursionDepth) -> void
Riok.Mapperly.Abstractions.MapperMaxRecursionDepthAttribute.MaxRecursionDepth.get -> uint
5 changes: 5 additions & 0 deletions src/Riok.Mapperly/Configuration/MapperConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,9 @@ public record MapperConfiguration
/// Whether to consider non-partial methods in a mapper as user implemented mapping methods.
/// </summary>
public bool? AutoUserMappings { get; init; }

/// <summary>
/// Defines the maximum recursion depth that an IQueryable mapping will use.
/// </summary>
public uint? 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 @@ -62,6 +62,9 @@ public static MapperAttribute Merge(MapperConfiguration mapperConfiguration, Map
mapper.AutoUserMappings =
mapperConfiguration.AutoUserMappings ?? defaultMapperConfiguration.AutoUserMappings ?? mapper.AutoUserMappings;

mapper.MaxRecursionDepth =
mapperConfiguration.MaxRecursionDepth ?? defaultMapperConfiguration.MaxRecursionDepth ?? mapper.MaxRecursionDepth;

return mapper;
}
}
7 changes: 5 additions & 2 deletions src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ MapperConfiguration defaultMapperConfiguration
Array.Empty<string>(),
Array.Empty<MemberMappingConfiguration>(),
mapper.IgnoreObsoleteMembersStrategy,
mapper.RequiredMappingStrategy
mapper.RequiredMappingStrategy,
mapper.MaxRecursionDepth
),
Array.Empty<DerivedTypeMappingConfiguration>()
);
Expand Down Expand Up @@ -86,6 +87,7 @@ private MembersMappingConfiguration BuildMembersConfig(MappingConfigurationRefer
.AccessFirstOrDefault<MapperIgnoreObsoleteMembersAttribute>(configRef.Method)
?.IgnoreObsoleteStrategy;
var requiredMapping = _dataAccessor.AccessFirstOrDefault<MapperRequiredMappingAttribute>(configRef.Method)?.RequiredMappingStrategy;
var maxRecursionDepth = _dataAccessor.AccessFirstOrDefault<MapperMaxRecursionDepthAttribute>(configRef.Method)?.MaxRecursionDepth;

// ignore the required mapping / ignore obsolete as the same attribute is used for other mapping types
// e.g. enum to enum
Expand Down Expand Up @@ -116,7 +118,8 @@ private MembersMappingConfiguration BuildMembersConfig(MappingConfigurationRefer
ignoredTargetMembers,
memberConfigurations,
ignoreObsolete ?? MapperConfiguration.Members.IgnoreObsoleteMembersStrategy,
requiredMapping ?? MapperConfiguration.Members.RequiredMappingStrategy
requiredMapping ?? MapperConfiguration.Members.RequiredMappingStrategy,
maxRecursionDepth ?? MapperConfiguration.Members.MaxRecursionDepth
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ public record MembersMappingConfiguration(
IReadOnlyCollection<string> IgnoredTargets,
IReadOnlyCollection<MemberMappingConfiguration> ExplicitMappings,
IgnoreObsoleteMembersStrategy IgnoreObsoleteMembersStrategy,
RequiredMappingStrategy RequiredMappingStrategy
RequiredMappingStrategy RequiredMappingStrategy,
uint MaxRecursionDepth
);
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,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.Members.MaxRecursionDepth + 2
? new DefaultMemberMapping(mappingKey.Source, mappingKey.Target)
: null;
}

var mapping = InlinedMappings.Find(mappingKey, out var isInlined);
if (mapping == null)
return null;
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 @@ -18,6 +18,7 @@ public class MappingBuilderContext : SimpleMappingBuilderContext
private readonly FormatProviderCollection _formatProviders;
private CollectionInfos? _collectionInfos;
private DictionaryInfos? _dictionaryInfos;
internal readonly MappingRecursionDepthTracker _parentTypes;

public MappingBuilderContext(
SimpleMappingBuilderContext parentCtx,
Expand All @@ -31,6 +32,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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ private MapperConfiguration NewMapperConfiguration()
if (type == typeof(bool))
return !modifiedValue;

if (type == typeof(uint))
return (uint)1;

if (type.IsEnum)
return type.GetEnumValues().GetValue(modifiedValue ? 1 : 0);

Expand Down
Loading