Skip to content

Commit

Permalink
feat: add configuration whether to deep clone objects (#10)
Browse files Browse the repository at this point in the history
Add a configuration whether objects are directly assigned if possible or need to be deeply cloned
By default direct assignments are allowed
  • Loading branch information
latonz committed Feb 15, 2022
1 parent 89f96c4 commit baf98fd
Show file tree
Hide file tree
Showing 12 changed files with 108 additions and 12 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ The attributes defined in `Riok.Mapperly.Abstractions` can be used to customize
The `MapperAttribute` provides options to customize the generated mapper class.
The generated class name, the instance field name and the default enum mapping strategy is adjustable.

### Copy behaviour

By default, Mapperly does not create deep copies of objects to improve performance.
If an object can be directly assigned to the target, it will do so
(eg. if the source and target type are both `Car[]`, the array and its entries will not be cloned).
To create deep copies, set the `UseDeepCloning` property on the `MapperAttribute` to `true`.

#### Properties

On each mapping method declaration property mappings can be customized.
Expand Down
8 changes: 8 additions & 0 deletions src/Riok.Mapperly.Abstractions/MapperAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,12 @@ public sealed class MapperAttribute : Attribute
/// Can be overwritten on specific enums via mapping method configurations.
/// </summary>
public EnumMappingStrategy EnumMappingStrategy { get; set; } = EnumMappingStrategy.ByValue;

/// <summary>
/// Whether to always deep copy objects.
/// Eg. when the type <c>Person[]</c> should be mapped to the same type <c>Person[]</c>,
/// with <c><see cref="UseDeepCloning"/>=true</c>, the same array is reused.
/// With <c><see cref="UseDeepCloning"/>=false</c>, the array and each person is cloned.
/// </summary>
public bool UseDeepCloning { get; set; }
}
9 changes: 6 additions & 3 deletions src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class DescriptorBuilder
private static readonly IReadOnlyCollection<MappingBuilder> _mappingBuilders = new MappingBuilder[]
{
SpecialTypeMappingBuilder.TryBuildMapping,
ImmutableTypeMappingBuilder.TryBuildMapping,
DirectAssignmentMappingBuilder.TryBuildMapping,
DictionaryMappingBuilder.TryBuildMapping,
EnumerableMappingBuilder.TryBuildMapping,
ImplicitCastMappingBuilder.TryBuildMapping,
Expand Down Expand Up @@ -56,14 +56,16 @@ public class DescriptorBuilder
_context = sourceContext;
Compilation = compilation;
_mapperDescriptor = new MapperDescriptor(mapperSymbol.Name);
Configure();
MapperConfiguration = Configure();
}

internal IReadOnlyDictionary<Type, Attribute> DefaultConfigurations => _defaultConfigurations;

internal Compilation Compilation { get; }

private void Configure()
public MapperAttribute MapperConfiguration { get; }

private MapperAttribute Configure()
{
var mapperAttribute = AttributeDataAccessor.AccessFirstOrDefault<MapperAttribute>(Compilation, _mapperSymbol) ?? new();
if (!_mapperSymbol.ContainingNamespace.IsGlobalNamespace)
Expand All @@ -77,6 +79,7 @@ private void Configure()
_mapperDescriptor.InstanceName = mapperAttribute.InstanceName;

_defaultConfigurations.Add(typeof(MapEnumAttribute), new MapEnumAttribute(mapperAttribute.EnumMappingStrategy));
return mapperAttribute;
}

private string BuildName()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

namespace Riok.Mapperly.Descriptors.MappingBuilder;

public static class ImmutableTypeMappingBuilder
public static class DirectAssignmentMappingBuilder
{
public static TypeMapping? TryBuildMapping(MappingBuilderContext ctx)
{
return SymbolEqualityComparer.IncludeNullability.Equals(ctx.Source, ctx.Target) && ctx.Source.IsImmutable()
return SymbolEqualityComparer.IncludeNullability.Equals(ctx.Source, ctx.Target)
&& (!ctx.MapperConfiguration.UseDeepCloning || ctx.Source.IsImmutable())
? new DirectAssignmentMapping(ctx.Source)
: null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public static class ExplicitCastMappingBuilder
{
public static CastMapping? TryBuildMapping(MappingBuilderContext ctx)
{
if (!ctx.Source.IsImmutable() && !ctx.Target.IsImmutable())
if (ctx.MapperConfiguration.UseDeepCloning && !ctx.Source.IsImmutable() && !ctx.Target.IsImmutable())
return null;

var conversion = ctx.Compilation.ClassifyConversion(ctx.Source, ctx.Target);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public static class ImplicitCastMappingBuilder
{
public static CastMapping? TryBuildMapping(MappingBuilderContext ctx)
{
if (!ctx.Source.IsImmutable() && !ctx.Target.IsImmutable())
if (ctx.MapperConfiguration.UseDeepCloning && !ctx.Source.IsImmutable() && !ctx.Target.IsImmutable())
return null;

var conversion = ctx.Compilation.ClassifyConversion(ctx.Source, ctx.Target);
Expand Down
3 changes: 3 additions & 0 deletions src/Riok.Mapperly/Descriptors/SimpleMappingBuilderContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;

namespace Riok.Mapperly.Descriptors;

Expand All @@ -13,6 +14,8 @@ public SimpleMappingBuilderContext(DescriptorBuilder builder)

public Compilation Compilation => _builder.Compilation;

public MapperAttribute MapperConfiguration => _builder.MapperConfiguration;

public void ReportDiagnostic(DiagnosticDescriptor descriptor, ISymbol? location, params object[] messageArgs)
=> ReportDiagnostic(descriptor, location?.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax(), messageArgs);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Riok.Mapperly.Tests.Mapping;

public class ValueTypeTest
public class DirectAssignmentTest
{
[Fact]
public void CustomReadOnlyStructToSameCustomReadOnlyStruct()
Expand Down
39 changes: 38 additions & 1 deletion test/Riok.Mapperly.Tests/Mapping/EnumerableTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ public void ArrayToArrayOfPrimitiveTypes()
var source = TestSourceBuilder.Mapping(
"int[]",
"int[]");
TestHelper.GenerateSingleMapperMethodBody(source)
.Should()
.Be("return source;");
}

[Fact]
public void ArrayToArrayOfPrimitiveTypesDeepCloning()
{
var source = TestSourceBuilder.Mapping(
"int[]",
"int[]",
TestSourceBuilderOptions.Default with { UseDeepCloning = true });
TestHelper.GenerateSingleMapperMethodBody(source)
.Should()
.Be("return (int[])source.Clone();");
Expand All @@ -20,6 +32,19 @@ public void ArrayCustomClassToArrayCustomClass()
"B[]",
"B[]",
"class B { public int Value {get; set; }}");
TestHelper.GenerateSingleMapperMethodBody(source)
.Should()
.Be("return source;");
}

[Fact]
public void ArrayCustomClassToArrayCustomClassDeepCloning()
{
var source = TestSourceBuilder.Mapping(
"B[]",
"B[]",
TestSourceBuilderOptions.Default with { UseDeepCloning = true },
"class B { public int Value { get; set; }}");
TestHelper.GenerateMapperMethodBody(source)
.Should()
.Be("return System.Linq.Enumerable.ToArray(System.Linq.Enumerable.Select(source, x => MapToB(x)));");
Expand All @@ -31,6 +56,18 @@ public void ArrayToArrayOfString()
var source = TestSourceBuilder.Mapping(
"string[]",
"string[]");
TestHelper.GenerateSingleMapperMethodBody(source)
.Should()
.Be("return source;");
}

[Fact]
public void ArrayToArrayOfStringDeepCloning()
{
var source = TestSourceBuilder.Mapping(
"string[]",
"string[]",
TestSourceBuilderOptions.Default with { UseDeepCloning = true });
TestHelper.GenerateSingleMapperMethodBody(source)
.Should()
.Be("return (string[])source.Clone();");
Expand Down Expand Up @@ -66,7 +103,7 @@ public void EnumerableToEnumerableOfPrimitiveTypes()
"IEnumerable<int>");
TestHelper.GenerateSingleMapperMethodBody(source)
.Should()
.Be("return (System.Collections.Generic.IEnumerable<int>)source;");
.Be("return source;");
}

[Fact]
Expand Down
29 changes: 28 additions & 1 deletion test/Riok.Mapperly.Tests/Mapping/ObjectPropertyTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ public void SameType()
"A",
"class A { public string StringValue { get; set; } }");

TestHelper.GenerateSingleMapperMethodBody(source)
.Should()
.Be("return source;");
}

[Fact]
public void SameTypeDeepCopies()
{
var source = TestSourceBuilder.Mapping(
"A",
"A",
TestSourceBuilderOptions.Default with { UseDeepCloning = true },
"class A { public string StringValue { get; set; } }");

TestHelper.GenerateSingleMapperMethodBody(source)
.Should()
.Be(@"var target = new A();
Expand All @@ -41,10 +55,23 @@ public void CustomRefStructToSameCustomStruct()
"A",
"A",
"ref struct A {}");
TestHelper.GenerateSingleMapperMethodBody(source)
.Should()
.Be("return source;");
}

[Fact]
public void CustomRefStructToSameCustomStructDeepCloning()
{
var source = TestSourceBuilder.Mapping(
"A",
"A",
TestSourceBuilderOptions.Default with { UseDeepCloning = true },
"ref struct A {}");
TestHelper.GenerateSingleMapperMethodBody(source)
.Should()
.Be(@"var target = new A();
return target;");
return target;".ReplaceLineEndings());
}

[Fact]
Expand Down
9 changes: 8 additions & 1 deletion test/Riok.Mapperly.Tests/TestSourceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,21 @@ public static string MapperWithBody(string body, TestSourceBuilderOptions? optio
{(options.Namespace != null ? $"namespace {options.Namespace};" : string.Empty)}
[Mapper]
{BuildAttribute(options)}
public {(options.AsInterface ? "interface I" : "abstract class ")}Mapper
{{
{body}
}}
";
}

private static string BuildAttribute(TestSourceBuilderOptions options)
{
return options.UseDeepCloning
? "[Mapper(UseDeepCloning = true)]"
: "[Mapper]";
}

public static string MapperWithBodyAndTypes(string body, params string[] types)
=> MapperWithBodyAndTypes(body, null, types);

Expand Down
5 changes: 4 additions & 1 deletion test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
namespace Riok.Mapperly.Tests;

public record TestSourceBuilderOptions(bool AsInterface = true, string? Namespace = null)
public record TestSourceBuilderOptions(
bool AsInterface = true,
string? Namespace = null,
bool UseDeepCloning = false)
{
public static readonly TestSourceBuilderOptions Default = new();
}

0 comments on commit baf98fd

Please sign in to comment.