Skip to content

Commit

Permalink
feat: support reference loops (#218)
Browse files Browse the repository at this point in the history
  • Loading branch information
latonz authored Jan 11, 2023
1 parent e1cb032 commit fd18a62
Show file tree
Hide file tree
Showing 146 changed files with 2,392 additions and 785 deletions.
2 changes: 1 addition & 1 deletion Riok.Mapperly.sln
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riok.Mapperly.Abstractions"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riok.Mapperly.Tests", "test\Riok.Mapperly.Tests\Riok.Mapperly.Tests.csproj", "{284E2122-CE48-4A5A-A045-3A3F941DA5C3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riok.Mapperly.Abstractions.Test", "test\Riok.Mapperly.Abstractions.Test\Riok.Mapperly.Abstractions.Test.csproj", "{C3C40A0A-168F-4A66-B9F9-FC80D2F26306}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riok.Mapperly.Abstractions.Tests", "test\Riok.Mapperly.Abstractions.Tests\Riok.Mapperly.Abstractions.Tests.csproj", "{C3C40A0A-168F-4A66-B9F9-FC80D2F26306}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
2 changes: 2 additions & 0 deletions docs/docs/02-configuration/09-void-mapping-methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ If an existing object instance should be used as target, you can define the mapp
[Mapper]
public partial class CarMapper
{
// highlight-start
public partial void CarToCarDto(Car car, CarDto dto);
// highlight-end
}
```

Expand Down
40 changes: 40 additions & 0 deletions docs/docs/02-configuration/10-reference-handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Reference handling

Mapperly can support mapping object structures with circular references.
To opt in for reference handling set `UseReferenceHandling` to `true`:
```csharp
// highlight-start
[Mapper(UseReferenceHandling = true)]
// highlight-end
public partial class CarMapper
{
public partial void CarToCarDto(Car car, CarDto dto);
}
```

This enables the usage of a default reference handler
which reuses the same target object instance if encountered the same source object instance.

## Custom reference handler

To use a custom `IReferenceHandler` implementation,
a parameter of the type `Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler`
annotated with the `Riok.Mapperly.Abstractions.ReferenceHandling.ReferenceHandlerAttribute`
can be added to the mapping method.

```csharp
// highlight-start
[Mapper(UseReferenceHandling = true)]
// highlight-end
public partial class CarMapper
{
// highlight-start
public partial void CarToCarDto(Car car, CarDto dto, [ReferenceHandler] IReferenceHandler myRefHandler);
// highlight-end
}
```

## User implemented mappings

To make use of the `IReferenceHandler` in a user implemented mapping method,
add a parameter as described in the section "[Custom reference handler](#custom-reference-handler)".
10 changes: 10 additions & 0 deletions src/Riok.Mapperly.Abstractions/MapperAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Riok.Mapperly.Abstractions.ReferenceHandling;

namespace Riok.Mapperly.Abstractions;

/// <summary>
Expand Down Expand Up @@ -60,4 +62,12 @@ public sealed class MapperAttribute : Attribute
/// </example>
/// </summary>
public MappingConversionType EnabledConversions { get; set; } = MappingConversionType.All;

/// <summary>
/// Enables the reference handling feature.
/// Disabled by default for performance reasons.
/// When enabled, an <see cref="IReferenceHandler"/> instance is passed through the mapping methods
/// to keep track of and reuse existing target object instances.
/// </summary>
public bool UseReferenceHandling { get; set; }
}
11 changes: 11 additions & 0 deletions src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,14 @@ Riok.Mapperly.Abstractions.MappingConversionType.None = 0 -> Riok.Mapperly.Abstr
Riok.Mapperly.Abstractions.MappingConversionType.ParseMethod = 8 -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MappingConversionType.StringToEnum = 32 -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MappingConversionType.ToStringMethod = 16 -> Riok.Mapperly.Abstractions.MappingConversionType
Riok.Mapperly.Abstractions.MapperAttribute.UseReferenceHandling.get -> bool
Riok.Mapperly.Abstractions.MapperAttribute.UseReferenceHandling.set -> void
Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler
Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler.PreserveReferenceHandler() -> void
Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler.SetReference<TSource, TTarget>(TSource source, TTarget target) -> void
Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler.TryGetReference<TSource, TTarget>(TSource source, out TTarget? target) -> bool
Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler
Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler.SetReference<TSource, TTarget>(TSource source, TTarget target) -> void
Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler.TryGetReference<TSource, TTarget>(TSource source, out TTarget? target) -> bool
Riok.Mapperly.Abstractions.ReferenceHandling.ReferenceHandlerAttribute
Riok.Mapperly.Abstractions.ReferenceHandling.ReferenceHandlerAttribute.ReferenceHandlerAttribute() -> void
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Diagnostics.CodeAnalysis;

namespace Riok.Mapperly.Abstractions.ReferenceHandling;

/// <summary>
/// A reference handler can store and resolve references
/// of mapping target objects.
/// </summary>
public interface IReferenceHandler
{
/// <summary>
/// Before an object is created by Mapperly this method is called.
/// It can attempt to resolve existing target object instances based on the source object instance.
/// If <c>false</c> is returned, Mapperly creates a new instance of the target class.
/// If <c>true</c> is returned, target has to be non-null.
/// Mapperly then uses the target instance.
/// </summary>
/// <param name="source">The source object instance.</param>
/// <param name="target">The resolved target object instance or <c>null</c> if none could be resolved.</param>
/// <typeparam name="TSource">The type of the source object.</typeparam>
/// <typeparam name="TTarget">The target object type.</typeparam>
/// <returns></returns>
bool TryGetReference<TSource, TTarget>(TSource source, [NotNullWhen(true)] out TTarget? target)
where TSource : notnull
where TTarget : notnull;

/// <summary>
/// Stores the created target instance.
/// Called by Mapperly just after a new target object instance is created.
/// </summary>
/// <param name="source">The source object instance.</param>
/// <param name="target">The target object instance.</param>
/// <typeparam name="TSource">The type of the source object.</typeparam>
/// <typeparam name="TTarget">The type of the target object.</typeparam>
void SetReference<TSource, TTarget>(TSource source, TTarget target)
where TSource : notnull
where TTarget : notnull;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Diagnostics.CodeAnalysis;

namespace Riok.Mapperly.Abstractions.ReferenceHandling.Internal;

/// <summary>
/// A <see cref="IReferenceHandler"/> implementation
/// which returns the same target object instance if encountered the same source object instance.
/// Do not use directly. Should only be used by Mapperly generated code.
/// API surface is not subject to semantic releases and may break in any release.
/// </summary>
public sealed class PreserveReferenceHandler : IReferenceHandler
{
private readonly Dictionary<(Type, Type), ReferenceHolder> _referenceHolders = new();

/// <inheritdoc cref="IReferenceHandler.TryGetReference{TSource,TTarget}"/>
public bool TryGetReference<TSource, TTarget>(TSource source, [NotNullWhen(true)] out TTarget? target)
where TSource : notnull
where TTarget : notnull
{
var refHolder = GetReferenceHolder<TSource, TTarget>();
return refHolder.TryGetRef(source, out target);
}

/// <inheritdoc cref="IReferenceHandler.SetReference{TSource,TTarget}"/>
public void SetReference<TSource, TTarget>(TSource source, TTarget target)
where TSource : notnull
where TTarget : notnull
=> GetReferenceHolder<TSource, TTarget>().SetRef(source, target);

private ReferenceHolder GetReferenceHolder<TSource, TTarget>()
{
var mapping = (typeof(TSource), typeof(TTarget));
if (_referenceHolders.TryGetValue(mapping, out var refHolder))
return refHolder;

return _referenceHolders[mapping] = new();
}

private class ReferenceHolder
{
private readonly Dictionary<object, object> _references = new(ReferenceEqualityComparer<object>.Instance);

public bool TryGetRef<TSource, TTarget>(TSource source, [NotNullWhen(true)] out TTarget? target)
where TSource : notnull
where TTarget : notnull
{
if (_references.TryGetValue(source, out var targetObj))
{
target = (TTarget)targetObj;
return true;
}

target = default;
return false;
}

public void SetRef<TSource, TTarget>(TSource source, TTarget target)
where TSource : notnull
where TTarget : notnull
{
_references[source] = target;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Runtime.CompilerServices;

namespace Riok.Mapperly.Abstractions.ReferenceHandling.Internal;

/// <summary>
/// Defines methods to support the comparison of objects for reference equality.
/// </summary>
/// <typeparam name="T">The type of objects to compare.</typeparam>
internal sealed class ReferenceEqualityComparer<T> : IEqualityComparer<T>
{
// cannot use System.Collections.Generic.ReferenceEqualityComparer since it is not available in netstandard2.0

/// <summary>
/// A <see cref="ReferenceEqualityComparer{T}"/> instance.
/// </summary>
public static readonly IEqualityComparer<T> Instance = new ReferenceEqualityComparer<T>();

private ReferenceEqualityComparer()
{
}

bool IEqualityComparer<T>.Equals(T? x, T? y)
=> ReferenceEquals(x, y);

int IEqualityComparer<T>.GetHashCode(T obj)
=> RuntimeHelpers.GetHashCode(obj);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Riok.Mapperly.Abstractions.ReferenceHandling;

/// <summary>
/// Marks a mapping method parameter as a <see cref="IReferenceHandler"/>.
/// The type of the parameter needs to be <see cref="IReferenceHandler"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class ReferenceHandlerAttribute : Attribute
{
}
2 changes: 2 additions & 0 deletions src/Riok.Mapperly/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,5 @@ RMG022 | Mapper | Error | Invalid object factory signature
Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
RMG023 | Mapper | Error | Mapping source property for a required target property not found
RMG024 | Mapper | Error | The reference handler parameter is not of the correct type
RMG025 | Mapper | Error | To use reference handling it needs to be enabled on the mapper attribute
Loading

0 comments on commit fd18a62

Please sign in to comment.