Skip to content
Draft
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
12 changes: 12 additions & 0 deletions src/Riok.Mapperly.Abstractions/MapperUseShallowCloningAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Diagnostics;

namespace Riok.Mapperly.Abstractions;

/// <summary>
/// A mapping method marked with this attribute will avoid reusing the same source instance,
/// either by directly returning it or by implicit casting, and will always result in a new instance being returned.
/// This attribute will only apply to mapping methods which have the same source and target types.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
[Conditional("MAPPERLY_ABSTRACTIONS_SCOPE_RUNTIME")]
public sealed class MapperUseShallowCloningAttribute : Attribute { }
18 changes: 15 additions & 3 deletions src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,16 @@ SupportedFeatures supportedFeatures
mapper.RequiredEnumMappingStrategy,
mapper.EnumNamingStrategy
),
new MembersMappingConfiguration([], [], [], [], [], mapper.IgnoreObsoleteMembersStrategy, mapper.RequiredMappingStrategy),
new MembersMappingConfiguration(
[],
[],
[],
[],
[],
mapper.IgnoreObsoleteMembersStrategy,
mapper.RequiredMappingStrategy,
UseShallowCloning: false
),
[],
mapper.UseDeepCloning,
mapper.StackCloningStrategy,
Expand Down Expand Up @@ -239,10 +248,12 @@ private MembersMappingConfiguration BuildMembersConfig(MappingConfigurationRefer
.AccessFirstOrDefault<MapperIgnoreObsoleteMembersAttribute>(configRef.Method)
?.IgnoreObsoleteStrategy;
var requiredMapping = _dataAccessor.AccessFirstOrDefault<MapperRequiredMappingAttribute>(configRef.Method)?.RequiredMappingStrategy;
var useShallowCloning = _dataAccessor.AccessFirstOrDefault<MapperUseShallowCloningAttribute>(configRef.Method);

// ignore the required mapping / ignore obsolete as the same attribute is used for other mapping types
// e.g. enum to enum
var hasMemberConfigs = ignoredSourceMembers.Count > 0 || ignoredTargetMembers.Count > 0 || memberConfigurations.Count > 0;
var hasMemberConfigs =
ignoredSourceMembers.Count > 0 || ignoredTargetMembers.Count > 0 || memberConfigurations.Count > 0 || useShallowCloning != null;
if (hasMemberConfigs && (configRef.Source.IsEnum() || configRef.Target.IsEnum()))
{
_diagnostics.ReportDiagnostic(DiagnosticDescriptors.MemberConfigurationOnNonMemberMapping, configRef.Method);
Expand Down Expand Up @@ -276,7 +287,8 @@ private MembersMappingConfiguration BuildMembersConfig(MappingConfigurationRefer
memberConfigurations,
nestedMembersConfigurations,
ignoreObsolete,
requiredMapping
requiredMapping,
useShallowCloning != null
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ public record MembersMappingConfiguration(
IReadOnlyCollection<MemberMappingConfiguration> ExplicitMappings,
IReadOnlyCollection<NestedMembersMappingConfiguration> NestedMappings,
IgnoreObsoleteMembersStrategy? IgnoreObsoleteMembersStrategy,
RequiredMappingStrategy? RequiredMappingStrategy
RequiredMappingStrategy? RequiredMappingStrategy,
bool UseShallowCloning
)
{
public IEnumerable<string> GetMembersWithExplicitConfigurations(MappingSourceTarget sourceTarget)
Expand All @@ -35,7 +36,8 @@ public MembersMappingConfiguration Include(MembersMappingConfiguration? otherCon
ExplicitMappings.Concat(otherConfiguration?.ExplicitMappings ?? []).ToList(),
NestedMappings.Concat(otherConfiguration?.NestedMappings ?? []).ToList(),
IgnoreObsoleteMembersStrategy ?? otherConfiguration?.IgnoreObsoleteMembersStrategy,
RequiredMappingStrategy ?? otherConfiguration?.RequiredMappingStrategy
RequiredMappingStrategy ?? otherConfiguration?.RequiredMappingStrategy,
UseShallowCloning || otherConfiguration?.UseShallowCloning == true
);
}
}
6 changes: 6 additions & 0 deletions src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ protected MappingBuilderContext(
/// <inheritdoc cref="MappingBuilders.MappingBuilder.NewInstanceMappings"/>
public IReadOnlyDictionary<TypeMappingKey, INewInstanceMapping> NewInstanceMappings => MappingBuilder.NewInstanceMappings;

/// <summary>
/// Determines if mapping code should be emitted in cases where direct assignments or casts could be used instead.
/// </summary>
// TODO not finished
public bool UseCloning => Configuration.UseDeepCloning || (HasUserSymbol && Configuration.Members.UseShallowCloning);

/// <summary>
/// Tries to find an existing mapping with the provided name.
/// If none is found, <c>null</c> is returned.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.CodeAnalysis;
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.Descriptors.Mappings;
using Riok.Mapperly.Helpers;

Expand All @@ -8,10 +9,16 @@ public static class DirectAssignmentMappingBuilder
{
public static NewInstanceMapping? TryBuildMapping(MappingBuilderContext ctx)
{
return
SymbolEqualityComparer.IncludeNullability.Equals(ctx.Source, ctx.Target)
&& (!ctx.Configuration.UseDeepCloning || ctx.Source.IsImmutable())
? new DirectAssignmentMapping(ctx.Source)
: null;
if (ctx.UseCloning && !ctx.Source.IsImmutable())
{
return null;
}

if (!SymbolEqualityComparer.IncludeNullability.Equals(ctx.Source, ctx.Target))
{
return null;
}

return new DirectAssignmentMapping(ctx.Source);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,7 @@ public static class EnumerableMappingBuilder
private static NewInstanceMapping? TryBuildCastMapping(MappingBuilderContext ctx, ITypeMapping elementMapping)
{
// cannot cast if the method mapping is synthetic, deep clone is enabled or target is an unknown collection
if (
!elementMapping.IsSynthetic
|| ctx.Configuration.UseDeepCloning
|| ctx.CollectionInfos!.Target.CollectionType == CollectionType.None
)
if (!elementMapping.IsSynthetic || ctx.UseCloning || ctx.CollectionInfos!.Target.CollectionType == CollectionType.None)
{
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public static class ExplicitCastMappingBuilder
if (!ctx.IsConversionEnabled(MappingConversionType.ExplicitCast))
return null;

if (ctx.Configuration.UseDeepCloning && !ctx.Source.IsImmutable() && !ctx.Target.IsImmutable())
if (ctx.UseCloning && !ctx.Source.IsImmutable() && !ctx.Target.IsImmutable())
return null;

// ClassifyConversion does not check if tuple field member names are the same
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public static class ImplicitCastMappingBuilder
if (!ctx.IsConversionEnabled(MappingConversionType.ImplicitCast))
return null;

if (ctx.Configuration.UseDeepCloning && !ctx.Source.IsImmutable() && !ctx.Target.IsImmutable())
if (ctx.UseCloning && !ctx.Source.IsImmutable() && !ctx.Target.IsImmutable())
return null;

// ClassifyConversion does not check if tuple field member names are the same
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public static class MemoryMappingBuilder

private static NewInstanceMapping? BuildSpanToMemoryMapping(MappingBuilderContext ctx, INewInstanceMapping elementMapping)
{
if (elementMapping.IsSynthetic && !ctx.Configuration.UseDeepCloning)
if (elementMapping.IsSynthetic && !ctx.UseCloning)
return new SourceObjectMethodMapping(ctx.Source, ctx.Target, ToArrayMethodName);

var targetArray = ctx.Types.GetArrayType(elementMapping.TargetType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public static class ToObjectMappingBuilder
if (ctx.Target.SpecialType != SpecialType.System_Object)
return null;

if (!ctx.Configuration.UseDeepCloning)
if (!ctx.UseCloning)
return new CastMapping(ctx.Source, ctx.Target);

if (ctx.Source.SpecialType == SpecialType.System_Object)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,12 @@ public sealed class MapperRequiredMappingAttribute : System.Attribute
public MapperRequiredMappingAttribute(Riok.Mapperly.Abstractions.RequiredMappingStrategy requiredMappingStrategy) { }
public Riok.Mapperly.Abstractions.RequiredMappingStrategy RequiredMappingStrategy { get; }
}
[System.AttributeUsage(System.AttributeTargets.Method)]
[System.Diagnostics.Conditional("MAPPERLY_ABSTRACTIONS_SCOPE_RUNTIME")]
public sealed class MapperUseShallowCloningAttribute : System.Attribute
{
public MapperUseShallowCloningAttribute() { }
}
[System.Flags]
public enum MappingConversionType
{
Expand Down
20 changes: 20 additions & 0 deletions test/Riok.Mapperly.IntegrationTests/Mapper/ShallowCloningMapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Riok.Mapperly.Abstractions;
using Riok.Mapperly.IntegrationTests.Models;

namespace Riok.Mapperly.IntegrationTests.Mapper
{
[Mapper(UseDeepCloning = false)]
public static partial class ShallowCloningMapper
{
[MapperUseShallowCloning]
public static partial IdObject Copy(IdObject src);

[MapperIgnoreSource(nameof(TestObject.IgnoredIntValue))]
[MapperIgnoreSource(nameof(TestObject.IgnoredStringValue))]
[MapperIgnoreSource(nameof(TestObject.ImmutableHashSetValue))]
[MapperIgnoreSource(nameof(TestObject.SpanValue))]
[MapperIgnoreObsoleteMembers]
[MapperUseShallowCloning]
public static partial TestObject Copy(TestObject src);
}
}
48 changes: 48 additions & 0 deletions test/Riok.Mapperly.IntegrationTests/ShallowCloningMapperTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.Threading.Tasks;
using Riok.Mapperly.IntegrationTests.Helpers;
using Riok.Mapperly.IntegrationTests.Mapper;
using Riok.Mapperly.IntegrationTests.Models;
using Shouldly;
using VerifyXunit;
using Xunit;

namespace Riok.Mapperly.IntegrationTests
{
public class ShallowCloningMapperTest : BaseMapperTest
{
[Fact]
[VersionedSnapshot(Versions.NET8_0)]
public Task SnapshotGeneratedSource()
{
var path = GetGeneratedMapperFilePath(nameof(ShallowCloningMapper));
return Verifier.VerifyFile(path);
}

[Fact]
[VersionedSnapshot(Versions.NET8_0 | Versions.NET9_0)]
public Task RunMappingShouldWork()
{
var model = NewTestObj();
var dto = ShallowCloningMapper.Copy(model);
return Verifier.Verify(dto);
}

[Fact]
public void RunIdMappingShouldWork()
{
var source = new IdObject { IdValue = 20 };
var copy = ShallowCloningMapper.Copy(source);
source.ShouldNotBeSameAs(copy);
copy.IdValue.ShouldBe(20);
}

[Fact]
public void RunMappingWithMapperAvoidReturningSourceReference()
{
var source = new TestObject(255, -1, 7) { RequiredValue = 999 };
var copy = ShallowCloningMapper.Copy(source);
source.ShouldNotBeSameAs(copy);
copy.RequiredValue.ShouldBe(999);
}
}
}
Loading