From 6fe742820d6e008dde3e4c19115af5d625d6bf5a Mon Sep 17 00:00:00 2001 From: Alessandro Losi Date: Thu, 15 Jan 2026 11:47:31 +0100 Subject: [PATCH 1/9] Proposed solution for #2113 to allow shallow clones when mapping between same types --- .../MapperUseShallowCloningAttribute.cs | 12 ++++++++++++ .../DirectAssignmentMappingBuilder.cs | 18 +++++++++++++----- .../ImplicitCastMappingBuilder.cs | 3 +++ .../DeepCloningMapperTest.cs | 9 +++++++++ .../AvoidReturningSourceReferenceMapper.cs | 17 +++++++++++++++++ 5 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 src/Riok.Mapperly.Abstractions/MapperUseShallowCloningAttribute.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/Mapper/AvoidReturningSourceReferenceMapper.cs diff --git a/src/Riok.Mapperly.Abstractions/MapperUseShallowCloningAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperUseShallowCloningAttribute.cs new file mode 100644 index 0000000000..1374c408ab --- /dev/null +++ b/src/Riok.Mapperly.Abstractions/MapperUseShallowCloningAttribute.cs @@ -0,0 +1,12 @@ +using System.Diagnostics; + +namespace Riok.Mapperly.Abstractions; + +/// +/// 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. +/// +[AttributeUsage(AttributeTargets.Method)] +[Conditional("MAPPERLY_ABSTRACTIONS_SCOPE_RUNTIME")] +public sealed class MapperUseShallowCloningAttribute : Attribute { } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/DirectAssignmentMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/DirectAssignmentMappingBuilder.cs index ee53b1c802..5d3550ef84 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/DirectAssignmentMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/DirectAssignmentMappingBuilder.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using Riok.Mapperly.Abstractions; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Helpers; @@ -8,10 +9,17 @@ 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.HasUserSymbol && ctx.AttributeAccessor.HasAttribute(ctx.UserSymbol!)) + return null; + + if ( + !SymbolEqualityComparer.IncludeNullability.Equals(ctx.Source, ctx.Target) + || (ctx.Configuration.UseDeepCloning && !ctx.Source.IsImmutable()) + ) + { + return null; + } + + return new DirectAssignmentMapping(ctx.Source); } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/ImplicitCastMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/ImplicitCastMappingBuilder.cs index 2e44a35597..1d631c21f5 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/ImplicitCastMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/ImplicitCastMappingBuilder.cs @@ -14,6 +14,9 @@ public static class ImplicitCastMappingBuilder if (ctx.Configuration.UseDeepCloning && !ctx.Source.IsImmutable() && !ctx.Target.IsImmutable()) return null; + if (ctx.HasUserSymbol && ctx.AttributeAccessor.HasAttribute(ctx.UserSymbol!)) + return null; + // ClassifyConversion does not check if tuple field member names are the same // if tuple check isn't done then (A: int, B: int) -> (B: int, A: int) would be mapped // return source; instead of return (B: source.A, A: source.B); diff --git a/test/Riok.Mapperly.IntegrationTests/DeepCloningMapperTest.cs b/test/Riok.Mapperly.IntegrationTests/DeepCloningMapperTest.cs index 5e92541675..d5e9fdb693 100644 --- a/test/Riok.Mapperly.IntegrationTests/DeepCloningMapperTest.cs +++ b/test/Riok.Mapperly.IntegrationTests/DeepCloningMapperTest.cs @@ -35,5 +35,14 @@ public void RunIdMappingShouldWork() source.ShouldNotBeSameAs(copy); copy.IdValue.ShouldBe(20); } + + [Fact] + public void RunMappingWithMapperAvoidReturningSourceReference() + { + var source = new TestObject(255, -1, 7) { RequiredValue = 999 }; + var copy = AvoidReturningSourceReferenceMapper.Copy(source); + source.ShouldNotBeSameAs(copy); + copy.RequiredValue.ShouldBe(999); + } } } diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/AvoidReturningSourceReferenceMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/AvoidReturningSourceReferenceMapper.cs new file mode 100644 index 0000000000..6602a74537 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/AvoidReturningSourceReferenceMapper.cs @@ -0,0 +1,17 @@ +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.IntegrationTests.Models; + +namespace Riok.Mapperly.IntegrationTests.Mapper +{ + [Mapper(UseDeepCloning = false)] + public static partial class AvoidReturningSourceReferenceMapper + { + [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); + } +} From 3930302d05dd9df53e75701d0e5475592370f0e8 Mon Sep 17 00:00:00 2001 From: Alessandro Losi Date: Fri, 16 Jan 2026 10:34:28 +0100 Subject: [PATCH 2/9] Implemented some of the changes suggested by @latonz --- .../MapperConfigurationReader.cs | 18 +- .../MembersMappingConfiguration.cs | 6 +- .../Descriptors/MappingBuilderContext.cs | 6 + .../DirectAssignmentMappingBuilder.cs | 9 +- .../EnumerableMappingBuilder.cs | 6 +- .../ExplicitCastMappingBuilder.cs | 2 +- .../ImplicitCastMappingBuilder.cs | 5 +- .../MappingBuilders/MemoryMappingBuilder.cs | 2 +- .../MappingBuilders/ToObjectMappingBuilder.cs | 2 +- ...ApiTest.PublicApiHasNotChanged.verified.cs | 6 + .../DeepCloningMapperTest.cs | 9 - ...renceMapper.cs => ShallowCloningMapper.cs} | 5 +- .../ShallowCloningMapperTest.cs | 48 ++++ ...t.RunMappingShouldWork_NET9_0.verified.txt | 223 ++++++++++++++++++ ...SnapshotGeneratedSource_NET8_0.verified.cs | 114 +++++++++ 15 files changed, 429 insertions(+), 32 deletions(-) rename test/Riok.Mapperly.IntegrationTests/Mapper/{AvoidReturningSourceReferenceMapper.cs => ShallowCloningMapper.cs} (79%) create mode 100644 test/Riok.Mapperly.IntegrationTests/ShallowCloningMapperTest.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/ShallowCloningMapperTest.RunMappingShouldWork_NET9_0.verified.txt create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/ShallowCloningMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs index df543acf54..1266e1164a 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs @@ -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, @@ -239,10 +248,12 @@ private MembersMappingConfiguration BuildMembersConfig(MappingConfigurationRefer .AccessFirstOrDefault(configRef.Method) ?.IgnoreObsoleteStrategy; var requiredMapping = _dataAccessor.AccessFirstOrDefault(configRef.Method)?.RequiredMappingStrategy; + var useShallowCloning = _dataAccessor.AccessFirstOrDefault(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); @@ -276,7 +287,8 @@ private MembersMappingConfiguration BuildMembersConfig(MappingConfigurationRefer memberConfigurations, nestedMembersConfigurations, ignoreObsolete, - requiredMapping + requiredMapping, + useShallowCloning != null ); } diff --git a/src/Riok.Mapperly/Configuration/MembersMappingConfiguration.cs b/src/Riok.Mapperly/Configuration/MembersMappingConfiguration.cs index 0185798b48..5d3daa2fdc 100644 --- a/src/Riok.Mapperly/Configuration/MembersMappingConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/MembersMappingConfiguration.cs @@ -10,7 +10,8 @@ public record MembersMappingConfiguration( IReadOnlyCollection ExplicitMappings, IReadOnlyCollection NestedMappings, IgnoreObsoleteMembersStrategy? IgnoreObsoleteMembersStrategy, - RequiredMappingStrategy? RequiredMappingStrategy + RequiredMappingStrategy? RequiredMappingStrategy, + bool UseShallowCloning ) { public IEnumerable GetMembersWithExplicitConfigurations(MappingSourceTarget sourceTarget) @@ -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 ); } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs index 562960649f..042ad1ae8f 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs @@ -88,6 +88,12 @@ protected MappingBuilderContext( /// public IReadOnlyDictionary NewInstanceMappings => MappingBuilder.NewInstanceMappings; + /// + /// Determines if mapping code should be emitted in cases where direct assignments or casts could be used instead. + /// + // TODO not finished + public bool UseCloning => Configuration.UseDeepCloning || (HasUserSymbol && Configuration.Members.UseShallowCloning); + /// /// Tries to find an existing mapping with the provided name. /// If none is found, null is returned. diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/DirectAssignmentMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/DirectAssignmentMappingBuilder.cs index 5d3550ef84..7fd356f506 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/DirectAssignmentMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/DirectAssignmentMappingBuilder.cs @@ -9,13 +9,12 @@ public static class DirectAssignmentMappingBuilder { public static NewInstanceMapping? TryBuildMapping(MappingBuilderContext ctx) { - if (ctx.HasUserSymbol && ctx.AttributeAccessor.HasAttribute(ctx.UserSymbol!)) + if (ctx.UseCloning && !ctx.Source.IsImmutable()) + { return null; + } - if ( - !SymbolEqualityComparer.IncludeNullability.Equals(ctx.Source, ctx.Target) - || (ctx.Configuration.UseDeepCloning && !ctx.Source.IsImmutable()) - ) + if (!SymbolEqualityComparer.IncludeNullability.Equals(ctx.Source, ctx.Target)) { return null; } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs index 4ced38b721..56051a91da 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs @@ -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; } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/ExplicitCastMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/ExplicitCastMappingBuilder.cs index a3612a68ba..4aaa342fcb 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/ExplicitCastMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/ExplicitCastMappingBuilder.cs @@ -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 diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/ImplicitCastMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/ImplicitCastMappingBuilder.cs index 1d631c21f5..5a6a5d799c 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/ImplicitCastMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/ImplicitCastMappingBuilder.cs @@ -11,10 +11,7 @@ public static class ImplicitCastMappingBuilder if (!ctx.IsConversionEnabled(MappingConversionType.ImplicitCast)) return null; - if (ctx.Configuration.UseDeepCloning && !ctx.Source.IsImmutable() && !ctx.Target.IsImmutable()) - return null; - - if (ctx.HasUserSymbol && ctx.AttributeAccessor.HasAttribute(ctx.UserSymbol!)) + if (ctx.UseCloning && !ctx.Source.IsImmutable() && !ctx.Target.IsImmutable()) return null; // ClassifyConversion does not check if tuple field member names are the same diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/MemoryMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/MemoryMappingBuilder.cs index 5230d7359c..c97631eedf 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/MemoryMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/MemoryMappingBuilder.cs @@ -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); diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/ToObjectMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/ToObjectMappingBuilder.cs index 7fd6952150..97a060c861 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/ToObjectMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/ToObjectMappingBuilder.cs @@ -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) diff --git a/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs b/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs index cf1fd5fecd..56f7110ccf 100644 --- a/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs +++ b/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs @@ -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 { diff --git a/test/Riok.Mapperly.IntegrationTests/DeepCloningMapperTest.cs b/test/Riok.Mapperly.IntegrationTests/DeepCloningMapperTest.cs index d5e9fdb693..5e92541675 100644 --- a/test/Riok.Mapperly.IntegrationTests/DeepCloningMapperTest.cs +++ b/test/Riok.Mapperly.IntegrationTests/DeepCloningMapperTest.cs @@ -35,14 +35,5 @@ public void RunIdMappingShouldWork() source.ShouldNotBeSameAs(copy); copy.IdValue.ShouldBe(20); } - - [Fact] - public void RunMappingWithMapperAvoidReturningSourceReference() - { - var source = new TestObject(255, -1, 7) { RequiredValue = 999 }; - var copy = AvoidReturningSourceReferenceMapper.Copy(source); - source.ShouldNotBeSameAs(copy); - copy.RequiredValue.ShouldBe(999); - } } } diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/AvoidReturningSourceReferenceMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/ShallowCloningMapper.cs similarity index 79% rename from test/Riok.Mapperly.IntegrationTests/Mapper/AvoidReturningSourceReferenceMapper.cs rename to test/Riok.Mapperly.IntegrationTests/Mapper/ShallowCloningMapper.cs index 6602a74537..723457442f 100644 --- a/test/Riok.Mapperly.IntegrationTests/Mapper/AvoidReturningSourceReferenceMapper.cs +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/ShallowCloningMapper.cs @@ -4,8 +4,11 @@ namespace Riok.Mapperly.IntegrationTests.Mapper { [Mapper(UseDeepCloning = false)] - public static partial class AvoidReturningSourceReferenceMapper + 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))] diff --git a/test/Riok.Mapperly.IntegrationTests/ShallowCloningMapperTest.cs b/test/Riok.Mapperly.IntegrationTests/ShallowCloningMapperTest.cs new file mode 100644 index 0000000000..63ce65e588 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/ShallowCloningMapperTest.cs @@ -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); + } + } +} diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ShallowCloningMapperTest.RunMappingShouldWork_NET9_0.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/ShallowCloningMapperTest.RunMappingShouldWork_NET9_0.verified.txt new file mode 100644 index 0000000000..d30a7e609e --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ShallowCloningMapperTest.RunMappingShouldWork_NET9_0.verified.txt @@ -0,0 +1,223 @@ +{ + CtorValue: 7, + CtorValue2: 100, + IntValue: 10, + IntInitOnlyValue: 3, + RequiredValue: 4, + UnmappedValue: 10, + StringValue: fooBar, + RenamedStringValue: fooBar2, + Flattening: { + IdValue: 10 + }, + NullableFlattening: { + IdValue: 100 + }, + UnflatteningIdValue: 20, + NullableUnflatteningIdValue: 200, + NestedNullable: { + IntValue: 100 + }, + NestedNullableTargetNotNullable: {}, + NestedMember: { + NestedMemberId: 12, + NestedMemberObject: { + IntValue: 22 + } + }, + StringNullableTargetNotNullable: fooBar3, + TupleValue: { + Item1: 10, + Item2: 20 + }, + RecursiveObject: { + CtorValue: 5, + CtorValue2: 100, + RequiredValue: 4, + UnmappedValue: 10, + StringValue: , + RenamedStringValue: , + Flattening: {}, + ImmutableArrayValue: null, + ImmutableQueueValue: [], + ImmutableStackValue: [], + EnumValue: Value10, + EnumName: Value30, + EnumReverseStringValue: DtoValue3, + ExposePrivateValue: 16, + ExposeGenericPrivateValue: { + ExposedId: 10, + ExposedValue: { + Value: 3.3 + } + } + }, + SourceTargetSameObjectType: { + CtorValue: 8, + CtorValue2: 100, + IntValue: 99, + RequiredValue: 98, + UnmappedValue: 10, + StringValue: , + RenamedStringValue: , + Flattening: {}, + NestedMember: { + NestedMemberId: 123, + NestedMemberObject: { + IntValue: 223 + } + }, + ImmutableArrayValue: null, + ImmutableQueueValue: [], + ImmutableStackValue: [], + EnumReverseStringValue: , + ExposePrivateValue: 19, + ExposeGenericPrivateValue: { + ExposedId: 10, + ExposedValue: { + Value: 3.3 + } + } + }, + NullableReadOnlyObjectCollection: [ + { + IntValue: 10 + }, + { + IntValue: 20 + } + ], + MemoryValue: { + Length: 3, + IsEmpty: false + }, + StackValue: [ + 3, + 2, + 1 + ], + QueueValue: [ + 1, + 2, + 3 + ], + ImmutableArrayValue: [ + 1, + 2, + 3 + ], + ImmutableListValue: [ + 1, + 2, + 3 + ], + ImmutableQueueValue: [ + 1, + 2, + 3 + ], + ImmutableStackValue: [ + 3, + 2, + 1 + ], + ImmutableSortedSetValue: [ + 1, + 2, + 3 + ], + ImmutableDictionaryValue: { + 1: 1, + 2: 2, + 3: 3 + }, + ImmutableSortedDictionaryValue: { + 1: 1, + 2: 2, + 3: 3 + }, + ExistingISet: [ + 1, + 2, + 3 + ], + ExistingHashSet: [ + 1, + 2, + 3 + ], + ExistingSortedSet: [ + 1, + 2, + 3 + ], + ExistingList: [ + 1, + 2, + 3 + ], + ISet: [ + 1, + 2, + 3 + ], + IReadOnlySet: [ + 1, + 2, + 3 + ], + HashSet: [ + 1, + 2, + 3 + ], + SortedSet: [ + 1, + 2, + 3 + ], + EnumValue: Value10, + FlagsEnumValue: V1, V4, + EnumName: Value10, + EnumRawValue: Value20, + EnumStringValue: Value30, + EnumReverseStringValue: DtoValue3, + SubObject: { + SubIntValue: 2, + BaseIntValue: 1 + }, + DateTimeValue: 2020-01-03 15:10:05 Utc, + DateTimeValueTargetDateOnly: 2020-01-03 15:10:05 Utc, + DateTimeValueTargetTimeOnly: 2020-01-03 15:10:05 Utc, + ToByteArrayWithInstanceMethod: Guid_1, + WithCreateMethod: { + Value: 10 + }, + WithCreateFromMethod: { + Value: 20 + }, + WithFromSingleMethod: { + Value: 30 + }, + WithCreateParamsMethod: { + Value: 40 + }, + WithCreateFromParamsMethod: { + Value: 50 + }, + WithFromShortParamsMethod: { + Value: 60 + }, + WithToDecimalMethod: { + Value: 70 + }, + ExposePrivateValue: 18, + ExposeGenericPrivateValue: { + ExposedId: 10, + ExposedValue: { + Value: 3.3 + } + }, + SumComponent1: 32, + SumComponent2: 64 +} \ No newline at end of file diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ShallowCloningMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/ShallowCloningMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs new file mode 100644 index 0000000000..9834e9582d --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ShallowCloningMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs @@ -0,0 +1,114 @@ +// +#nullable enable +namespace Riok.Mapperly.IntegrationTests.Mapper +{ + public static partial class ShallowCloningMapper + { + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public static partial global::Riok.Mapperly.IntegrationTests.Models.IdObject Copy(global::Riok.Mapperly.IntegrationTests.Models.IdObject src) + { + var target = new global::Riok.Mapperly.IntegrationTests.Models.IdObject(); + target.IdValue = src.IdValue; + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public static partial global::Riok.Mapperly.IntegrationTests.Models.TestObject Copy(global::Riok.Mapperly.IntegrationTests.Models.TestObject src) + { + var target = new global::Riok.Mapperly.IntegrationTests.Models.TestObject(src.CtorValue, ctorValue2: src.CtorValue2) + { + IntInitOnlyValue = src.IntInitOnlyValue, + RequiredValue = src.RequiredValue, + }; + target.IntValue = src.IntValue; + target.StringValue = src.StringValue; + target.RenamedStringValue = src.RenamedStringValue; + target.Flattening = Copy(src.Flattening); + if (src.NullableFlattening != null) + { + target.NullableFlattening = Copy(src.NullableFlattening); + } + else + { + target.NullableFlattening = null; + } + target.UnflatteningIdValue = src.UnflatteningIdValue; + target.NullableUnflatteningIdValue = src.NullableUnflatteningIdValue; + target.NestedNullable = src.NestedNullable; + target.NestedNullableTargetNotNullable = src.NestedNullableTargetNotNullable; + target.NestedMember = src.NestedMember; + target.StringNullableTargetNotNullable = src.StringNullableTargetNotNullable; + target.TupleValue = src.TupleValue; + if (src.RecursiveObject != null) + { + target.RecursiveObject = Copy(src.RecursiveObject); + } + else + { + target.RecursiveObject = null; + } + if (src.SourceTargetSameObjectType != null) + { + target.SourceTargetSameObjectType = Copy(src.SourceTargetSameObjectType); + } + else + { + target.SourceTargetSameObjectType = null; + } + target.NullableReadOnlyObjectCollection = src.NullableReadOnlyObjectCollection; + target.MemoryValue = src.MemoryValue; + target.StackValue = src.StackValue; + target.QueueValue = src.QueueValue; + target.ImmutableArrayValue = src.ImmutableArrayValue; + target.ImmutableListValue = src.ImmutableListValue; + target.ImmutableQueueValue = src.ImmutableQueueValue; + target.ImmutableStackValue = src.ImmutableStackValue; + target.ImmutableSortedSetValue = src.ImmutableSortedSetValue; + target.ImmutableDictionaryValue = src.ImmutableDictionaryValue; + target.ImmutableSortedDictionaryValue = src.ImmutableSortedDictionaryValue; + foreach (var item in src.ExistingISet) + { + target.ExistingISet.Add(item); + } + target.ExistingHashSet.EnsureCapacity(src.ExistingHashSet.Count + target.ExistingHashSet.Count); + foreach (var item1 in src.ExistingHashSet) + { + target.ExistingHashSet.Add(item1); + } + foreach (var item2 in src.ExistingSortedSet) + { + target.ExistingSortedSet.Add(item2); + } + target.ExistingList.EnsureCapacity(src.ExistingList.Count + target.ExistingList.Count); + foreach (var item3 in src.ExistingList) + { + target.ExistingList.Add(item3); + } + target.ISet = src.ISet; + target.IReadOnlySet = src.IReadOnlySet; + target.HashSet = src.HashSet; + target.SortedSet = src.SortedSet; + target.EnumValue = src.EnumValue; + target.FlagsEnumValue = src.FlagsEnumValue; + target.EnumName = src.EnumName; + target.EnumRawValue = src.EnumRawValue; + target.EnumStringValue = src.EnumStringValue; + target.EnumReverseStringValue = src.EnumReverseStringValue; + target.SubObject = src.SubObject; + target.DateTimeValue = src.DateTimeValue; + target.DateTimeValueTargetDateOnly = src.DateTimeValueTargetDateOnly; + target.DateTimeValueTargetTimeOnly = src.DateTimeValueTargetTimeOnly; + target.ToByteArrayWithInstanceMethod = src.ToByteArrayWithInstanceMethod; + target.WithCreateMethod = src.WithCreateMethod; + target.WithCreateFromMethod = src.WithCreateFromMethod; + target.WithFromSingleMethod = src.WithFromSingleMethod; + target.WithCreateParamsMethod = src.WithCreateParamsMethod; + target.WithCreateFromParamsMethod = src.WithCreateFromParamsMethod; + target.WithFromShortParamsMethod = src.WithFromShortParamsMethod; + target.WithToDecimalMethod = src.WithToDecimalMethod; + target.SumComponent1 = src.SumComponent1; + target.SumComponent2 = src.SumComponent2; + return target; + } + } +} \ No newline at end of file From ec83c020112e2a8c302f6e3fdc042e6b0e8691e9 Mon Sep 17 00:00:00 2001 From: Alessandro Losi Date: Tue, 3 Feb 2026 12:11:23 +0100 Subject: [PATCH 3/9] WIP: CloningBehaviour enum The implementation is currently not working because applying ShallowClone to the Mapper level results in a "recursive shallow clone" which defeats the purpose of the shallow clone itself. --- .../CloningBehaviour.cs | 27 ++ .../MapperAttribute.cs | 6 + .../Riok.Mapperly.Abstractions.csproj | 3 + .../Configuration/MapperConfiguration.cs | 2 + .../MapperConfigurationMerger.cs | 3 + .../MapperConfigurationReader.cs | 33 ++- .../Configuration/MappingConfiguration.cs | 2 +- .../MembersMappingConfiguration.cs | 8 +- .../Descriptors/MappingBuilderContext.cs | 4 +- .../EnumerableMappingBuilder.cs | 4 +- .../MappingBuilders/MemoryMappingBuilder.cs | 8 +- .../MappingBuilders/SpanMappingBuilder.cs | 5 +- ...ApiTest.PublicApiHasNotChanged.verified.cs | 14 +- ...epCloningWithCloningBehaviourMapperTest.cs | 39 +++ .../Mapper/DeepCloningMapper.cs | 13 + .../Mapper/ShallowCloningMapper.cs | 4 +- ...t.RunMappingShouldWork_NET9_0.verified.txt | 229 ++++++++++++++++ ...SnapshotGeneratedSource_NET8_0.verified.cs | 258 ++++++++++++++++++ .../Helpers/MapperConfigurationBuilderTest.cs | 2 + .../Mapping/ShallowCloneTest.cs | 26 ++ test/Riok.Mapperly.Tests/TestSourceBuilder.cs | 1 + .../TestSourceBuilderOptions.cs | 5 + 22 files changed, 656 insertions(+), 40 deletions(-) create mode 100644 src/Riok.Mapperly.Abstractions/CloningBehaviour.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/DeepCloningWithCloningBehaviourMapperTest.cs create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningBehaviourMapperTest.RunMappingShouldWork_NET9_0.verified.txt create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningBehaviourMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs create mode 100644 test/Riok.Mapperly.Tests/Mapping/ShallowCloneTest.cs diff --git a/src/Riok.Mapperly.Abstractions/CloningBehaviour.cs b/src/Riok.Mapperly.Abstractions/CloningBehaviour.cs new file mode 100644 index 0000000000..6ff193ae65 --- /dev/null +++ b/src/Riok.Mapperly.Abstractions/CloningBehaviour.cs @@ -0,0 +1,27 @@ +namespace Riok.Mapperly.Abstractions; + +/// +/// Specifies whether and how to copy objects of the same type and complex types like collections and spans. +/// +public enum CloningBehaviour +{ + /// + /// Default behaviour, the original instance will be returned + /// + None, + + /// + /// Always deep copy objects. + /// Eg. when the type Person[] should be mapped to the same type Person[], + /// the array and each person is cloned. + /// + DeepCloning, + + /// + /// Always shallow copy objects. + /// Eg. when the type Person should be mapped to the same type Person, + /// a new instance will be returned with the same values for all properties. + /// References will be kept. + /// + ShallowCloning, +} diff --git a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs index f0e1190379..aa6bc61b26 100644 --- a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs +++ b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs @@ -65,8 +65,14 @@ public class MapperAttribute : Attribute /// when false, the same array is reused. /// when true, the array and each person is cloned. /// + [Obsolete("Please use the property CloningBehaviour")] public bool UseDeepCloning { get; set; } + /// + /// Specifies whether and how to copy objects of the same type and complex types like collections and spans. + /// + public CloningBehaviour CloningBehaviour { get; set; } = CloningBehaviour.None; + /// /// The strategy to use when cloning a . /// diff --git a/src/Riok.Mapperly.Abstractions/Riok.Mapperly.Abstractions.csproj b/src/Riok.Mapperly.Abstractions/Riok.Mapperly.Abstractions.csproj index f8945b151d..4925060985 100644 --- a/src/Riok.Mapperly.Abstractions/Riok.Mapperly.Abstractions.csproj +++ b/src/Riok.Mapperly.Abstractions/Riok.Mapperly.Abstractions.csproj @@ -7,4 +7,7 @@ + + + diff --git a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs index 68893d366a..6573d43cfb 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs @@ -65,6 +65,8 @@ public record MapperConfiguration /// public bool? UseDeepCloning { get; init; } + public CloningBehaviour? CloningBehaviour { get; init; } + /// /// The strategy to use when cloning a . /// diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs index 4d625f49f3..f2e6661a3c 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs @@ -61,6 +61,9 @@ public static MapperAttribute MergeToAttribute(MapperConfiguration mapperConfigu mapper.UseDeepCloning = mapperConfiguration.UseDeepCloning ?? defaultMapperConfiguration.UseDeepCloning ?? mapper.UseDeepCloning; + mapper.CloningBehaviour = + mapperConfiguration.CloningBehaviour ?? defaultMapperConfiguration.CloningBehaviour ?? mapper.CloningBehaviour; + mapper.StackCloningStrategy = mapperConfiguration.StackCloningStrategy ?? defaultMapperConfiguration.StackCloningStrategy ?? mapper.StackCloningStrategy; diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs index 1266e1164a..e5518f978d 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs @@ -51,18 +51,9 @@ SupportedFeatures supportedFeatures mapper.RequiredEnumMappingStrategy, mapper.EnumNamingStrategy ), - new MembersMappingConfiguration( - [], - [], - [], - [], - [], - mapper.IgnoreObsoleteMembersStrategy, - mapper.RequiredMappingStrategy, - UseShallowCloning: false - ), + new MembersMappingConfiguration([], [], [], [], [], mapper.IgnoreObsoleteMembersStrategy, mapper.RequiredMappingStrategy), [], - mapper.UseDeepCloning, + mapper.UseDeepCloning ? CloningBehaviour.DeepCloning : mapper.CloningBehaviour, mapper.StackCloningStrategy, supportedFeatures ); @@ -87,7 +78,11 @@ bool supportsDeepCloning ) { if (reference.Method == null) - return supportsDeepCloning ? MapperConfiguration : MapperConfiguration with { UseDeepCloning = false }; + return supportsDeepCloning ? MapperConfiguration : MapperConfiguration with { CloningBehaviour = CloningBehaviour.None }; + + var cloningBehaviour = MapperConfiguration.Mapper.UseDeepCloning + ? CloningBehaviour.DeepCloning + : MapperConfiguration.Mapper.CloningBehaviour; var enumConfig = BuildEnumConfig(reference); var membersConfig = BuildMembersConfig(reference); @@ -97,7 +92,7 @@ bool supportsDeepCloning enumConfig, membersConfig, derivedTypesConfig, - supportsDeepCloning && MapperConfiguration.Mapper.UseDeepCloning, + supportsDeepCloning ? cloningBehaviour : CloningBehaviour.None, MapperConfiguration.StackCloningStrategy, MapperConfiguration.SupportedFeatures ); @@ -248,12 +243,16 @@ private MembersMappingConfiguration BuildMembersConfig(MappingConfigurationRefer .AccessFirstOrDefault(configRef.Method) ?.IgnoreObsoleteStrategy; var requiredMapping = _dataAccessor.AccessFirstOrDefault(configRef.Method)?.RequiredMappingStrategy; - var useShallowCloning = _dataAccessor.AccessFirstOrDefault(configRef.Method); + //var useShallowCloning = _dataAccessor.AccessFirstOrDefault(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 || useShallowCloning != null; + 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); @@ -287,8 +286,8 @@ private MembersMappingConfiguration BuildMembersConfig(MappingConfigurationRefer memberConfigurations, nestedMembersConfigurations, ignoreObsolete, - requiredMapping, - useShallowCloning != null + requiredMapping + //useShallowCloning != null ); } diff --git a/src/Riok.Mapperly/Configuration/MappingConfiguration.cs b/src/Riok.Mapperly/Configuration/MappingConfiguration.cs index 0c9b06a848..b79e02d0bf 100644 --- a/src/Riok.Mapperly/Configuration/MappingConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/MappingConfiguration.cs @@ -8,7 +8,7 @@ public record MappingConfiguration( EnumMappingConfiguration Enum, MembersMappingConfiguration Members, IReadOnlyCollection DerivedTypes, - bool UseDeepCloning, + CloningBehaviour CloningBehaviour, StackCloningStrategy StackCloningStrategy, SupportedFeatures SupportedFeatures ) diff --git a/src/Riok.Mapperly/Configuration/MembersMappingConfiguration.cs b/src/Riok.Mapperly/Configuration/MembersMappingConfiguration.cs index 5d3daa2fdc..b9084f4af9 100644 --- a/src/Riok.Mapperly/Configuration/MembersMappingConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/MembersMappingConfiguration.cs @@ -10,8 +10,8 @@ public record MembersMappingConfiguration( IReadOnlyCollection ExplicitMappings, IReadOnlyCollection NestedMappings, IgnoreObsoleteMembersStrategy? IgnoreObsoleteMembersStrategy, - RequiredMappingStrategy? RequiredMappingStrategy, - bool UseShallowCloning + RequiredMappingStrategy? RequiredMappingStrategy +//CloningBehaviour CloningBehaviour ) { public IEnumerable GetMembersWithExplicitConfigurations(MappingSourceTarget sourceTarget) @@ -36,8 +36,8 @@ public MembersMappingConfiguration Include(MembersMappingConfiguration? otherCon ExplicitMappings.Concat(otherConfiguration?.ExplicitMappings ?? []).ToList(), NestedMappings.Concat(otherConfiguration?.NestedMappings ?? []).ToList(), IgnoreObsoleteMembersStrategy ?? otherConfiguration?.IgnoreObsoleteMembersStrategy, - RequiredMappingStrategy ?? otherConfiguration?.RequiredMappingStrategy, - UseShallowCloning || otherConfiguration?.UseShallowCloning == true + RequiredMappingStrategy ?? otherConfiguration?.RequiredMappingStrategy + //CloningBehaviour // TODO what to do in this case? ); } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs index 042ad1ae8f..1b5427cf43 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using Microsoft.CodeAnalysis; +using Riok.Mapperly.Abstractions; using Riok.Mapperly.Configuration; using Riok.Mapperly.Descriptors.Constructors; using Riok.Mapperly.Descriptors.Enumerables; @@ -91,8 +92,7 @@ protected MappingBuilderContext( /// /// Determines if mapping code should be emitted in cases where direct assignments or casts could be used instead. /// - // TODO not finished - public bool UseCloning => Configuration.UseDeepCloning || (HasUserSymbol && Configuration.Members.UseShallowCloning); + public bool UseCloning => Configuration.CloningBehaviour != CloningBehaviour.None; /// /// Tries to find an existing mapping with the provided name. diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs index 56051a91da..c49b7ba93d 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs @@ -198,7 +198,7 @@ private static INewInstanceMapping BuildArrayToArrayMapping(MappingBuilderContex // use a for loop mapping otherwise. if (elementMapping.IsSynthetic) { - return ctx.Configuration.UseDeepCloning + return ctx.Configuration.CloningBehaviour == CloningBehaviour.DeepCloning ? new ArrayCloneMapping(ctx.Source, ctx.Target) : new CastMapping(ctx.Source, ctx.Target); } @@ -318,7 +318,7 @@ private static (bool CanMapWithLinq, string? CollectMethod) ResolveCollectMethod // if the target is an IEnumerable don't collect at all // except deep cloning is enabled. var targetIsIEnumerable = ctx.CollectionInfos!.Target.CollectionType == CollectionType.IEnumerable; - if (targetIsIEnumerable && !ctx.Configuration.UseDeepCloning) + if (targetIsIEnumerable && ctx.Configuration.CloningBehaviour != CloningBehaviour.DeepCloning) return (true, null); // if the target is IReadOnlyCollection or IEnumerable diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/MemoryMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/MemoryMappingBuilder.cs index c97631eedf..8e713d2f64 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/MemoryMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/MemoryMappingBuilder.cs @@ -117,7 +117,7 @@ public static class MemoryMappingBuilder private static NewInstanceMapping? BuildMemoryToArrayMapping(MappingBuilderContext ctx, INewInstanceMapping elementMapping) { - if (!elementMapping.IsSynthetic || ctx.Configuration.UseDeepCloning) + if (!elementMapping.IsSynthetic || ctx.Configuration.CloningBehaviour == CloningBehaviour.DeepCloning) return BuildSpanToArrayMethodMapping(ctx, elementMapping); return new SourceObjectMethodMapping(ctx.Source, ctx.Target, ToArrayMethodName); @@ -125,7 +125,7 @@ public static class MemoryMappingBuilder private static NewInstanceMapping? BuildMemoryToSpanMapping(MappingBuilderContext ctx, INewInstanceMapping elementMapping) { - if (!elementMapping.IsSynthetic || ctx.Configuration.UseDeepCloning) + if (!elementMapping.IsSynthetic || ctx.Configuration.CloningBehaviour == CloningBehaviour.DeepCloning) return BuildMemoryToSpanMethod(ctx, elementMapping); return new SourceObjectMemberMapping(ctx.Source, ctx.Target, SpanMemberName); @@ -133,7 +133,7 @@ public static class MemoryMappingBuilder private static INewInstanceMapping BuildArrayToMemoryMapping(MappingBuilderContext ctx, INewInstanceMapping elementMapping) { - if (!elementMapping.IsSynthetic || ctx.Configuration.UseDeepCloning) + if (!elementMapping.IsSynthetic || ctx.Configuration.CloningBehaviour == CloningBehaviour.DeepCloning) return new ArrayForMapping( ctx.Source, ctx.Types.GetArrayType(elementMapping.TargetType), @@ -155,7 +155,7 @@ private static INewInstanceMapping BuildArrayToMemoryMapping(MappingBuilderConte private static NewInstanceMapping? BuildMemoryToMemoryMapping(MappingBuilderContext ctx, INewInstanceMapping elementMapping) { - if (!elementMapping.IsSynthetic || ctx.Configuration.UseDeepCloning) + if (!elementMapping.IsSynthetic || ctx.Configuration.CloningBehaviour == CloningBehaviour.DeepCloning) return BuildSpanToArrayMethodMapping(ctx, elementMapping); return new CastMapping(ctx.Source, ctx.Target); diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/SpanMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/SpanMappingBuilder.cs index 94b5619fb9..30d7d4bb98 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/SpanMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/SpanMappingBuilder.cs @@ -44,7 +44,10 @@ public static class SpanMappingBuilder // if the source is Span/ReadOnlySpan or Array and target is Span/ReadOnlySpan // and element type is the same, then direct cast (CollectionType.Span or CollectionType.ReadOnlySpan or CollectionType.Array, CollectionType.Span or CollectionType.ReadOnlySpan) - when elementMapping.IsSynthetic && !ctx.Configuration.UseDeepCloning => new CastMapping(ctx.Source, ctx.Target), + when elementMapping.IsSynthetic && ctx.Configuration.CloningBehaviour != CloningBehaviour.DeepCloning => new CastMapping( + ctx.Source, + ctx.Target + ), // otherwise map each value into an Array _ => BuildToArrayOrMap(ctx, elementMapping), diff --git a/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs b/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs index 56f7110ccf..86086024c5 100644 --- a/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs +++ b/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs @@ -1,6 +1,12 @@ [assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")] namespace Riok.Mapperly.Abstractions { + public enum CloningBehaviour + { + None = 0, + DeepCloning = 1, + ShallowCloning = 2, + } public enum EnumMappingStrategy { ByValue = 0, @@ -131,6 +137,7 @@ public class MapperAttribute : System.Attribute public MapperAttribute() { } public bool AllowNullPropertyAssignment { get; set; } public bool AutoUserMappings { get; set; } + public Riok.Mapperly.Abstractions.CloningBehaviour CloningBehaviour { get; set; } public Riok.Mapperly.Abstractions.MappingConversionType EnabledConversions { get; set; } public bool EnumMappingIgnoreCase { get; set; } public Riok.Mapperly.Abstractions.EnumMappingStrategy EnumMappingStrategy { get; set; } @@ -145,6 +152,7 @@ public MapperAttribute() { } public Riok.Mapperly.Abstractions.StackCloningStrategy StackCloningStrategy { get; set; } public bool ThrowOnMappingNullMismatch { get; set; } public bool ThrowOnPropertyMappingNullMismatch { get; set; } + [System.Obsolete("Please use the property CloningBehaviour")] public bool UseDeepCloning { get; set; } public bool UseReferenceHandling { get; set; } } @@ -208,12 +216,6 @@ 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 { diff --git a/test/Riok.Mapperly.IntegrationTests/DeepCloningWithCloningBehaviourMapperTest.cs b/test/Riok.Mapperly.IntegrationTests/DeepCloningWithCloningBehaviourMapperTest.cs new file mode 100644 index 0000000000..f9057076c4 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/DeepCloningWithCloningBehaviourMapperTest.cs @@ -0,0 +1,39 @@ +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 DeepCloningWithCloningBehaviourMapperTest : BaseMapperTest + { + [Fact] + [VersionedSnapshot(Versions.NET8_0)] + public Task SnapshotGeneratedSource() + { + var path = GetGeneratedMapperFilePath(nameof(DeepCloningMapperWithCloningBehaviour)); + return Verifier.VerifyFile(path); + } + + [Fact] + [VersionedSnapshot(Versions.NET8_0 | Versions.NET9_0)] + public Task RunMappingShouldWork() + { + var model = NewTestObj(); + var dto = DeepCloningMapperWithCloningBehaviour.Copy(model); + return Verifier.Verify(dto); + } + + [Fact] + public void RunIdMappingShouldWork() + { + var source = new IdObject { IdValue = 20 }; + var copy = DeepCloningMapperWithCloningBehaviour.Copy(source); + source.ShouldNotBeSameAs(copy); + copy.IdValue.ShouldBe(20); + } + } +} diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/DeepCloningMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/DeepCloningMapper.cs index 010962861f..4f17e860df 100644 --- a/test/Riok.Mapperly.IntegrationTests/Mapper/DeepCloningMapper.cs +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/DeepCloningMapper.cs @@ -15,4 +15,17 @@ public static partial class DeepCloningMapper [MapperIgnoreObsoleteMembers] public static partial TestObject Copy(TestObject src); } + + [Mapper(CloningBehaviour = CloningBehaviour.DeepCloning)] + public static partial class DeepCloningMapperWithCloningBehaviour + { + public static partial IdObject Copy(IdObject src); + + [MapperIgnoreSource(nameof(TestObject.IgnoredIntValue))] + [MapperIgnoreSource(nameof(TestObject.IgnoredStringValue))] + [MapperIgnoreSource(nameof(TestObject.ImmutableHashSetValue))] + [MapperIgnoreSource(nameof(TestObject.SpanValue))] + [MapperIgnoreObsoleteMembers] + public static partial TestObject Copy(TestObject src); + } } diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/ShallowCloningMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/ShallowCloningMapper.cs index 723457442f..cbd0985055 100644 --- a/test/Riok.Mapperly.IntegrationTests/Mapper/ShallowCloningMapper.cs +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/ShallowCloningMapper.cs @@ -3,10 +3,9 @@ namespace Riok.Mapperly.IntegrationTests.Mapper { - [Mapper(UseDeepCloning = false)] + [Mapper(CloningBehaviour = CloningBehaviour.ShallowCloning)] public static partial class ShallowCloningMapper { - [MapperUseShallowCloning] public static partial IdObject Copy(IdObject src); [MapperIgnoreSource(nameof(TestObject.IgnoredIntValue))] @@ -14,7 +13,6 @@ public static partial class ShallowCloningMapper [MapperIgnoreSource(nameof(TestObject.ImmutableHashSetValue))] [MapperIgnoreSource(nameof(TestObject.SpanValue))] [MapperIgnoreObsoleteMembers] - [MapperUseShallowCloning] public static partial TestObject Copy(TestObject src); } } diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningBehaviourMapperTest.RunMappingShouldWork_NET9_0.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningBehaviourMapperTest.RunMappingShouldWork_NET9_0.verified.txt new file mode 100644 index 0000000000..51f40f465b --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningBehaviourMapperTest.RunMappingShouldWork_NET9_0.verified.txt @@ -0,0 +1,229 @@ +{ + CtorValue: 7, + CtorValue2: 100, + IntValue: 10, + IntInitOnlyValue: 3, + RequiredValue: 4, + UnmappedValue: 10, + StringValue: fooBar, + RenamedStringValue: fooBar2, + Flattening: { + IdValue: 10 + }, + NullableFlattening: { + IdValue: 100 + }, + UnflatteningIdValue: 20, + NullableUnflatteningIdValue: 200, + NestedNullable: { + IntValue: 100 + }, + NestedNullableTargetNotNullable: {}, + NestedMember: { + NestedMemberId: 12, + NestedMemberObject: { + IntValue: 22 + } + }, + StringNullableTargetNotNullable: fooBar3, + TupleValue: { + Item1: 10, + Item2: 20 + }, + RecursiveObject: { + CtorValue: 5, + CtorValue2: 100, + RequiredValue: 4, + UnmappedValue: 10, + StringValue: , + RenamedStringValue: , + Flattening: {}, + MemoryValue: { + IsEmpty: true + }, + ImmutableArrayValue: null, + ImmutableQueueValue: [], + ImmutableStackValue: [], + EnumValue: Value10, + EnumName: Value30, + EnumReverseStringValue: DtoValue3, + ExposePrivateValue: 16, + ExposeGenericPrivateValue: { + ExposedId: 10, + ExposedValue: { + Value: 3.3 + } + } + }, + SourceTargetSameObjectType: { + CtorValue: 8, + CtorValue2: 100, + IntValue: 99, + RequiredValue: 98, + UnmappedValue: 10, + StringValue: , + RenamedStringValue: , + Flattening: {}, + NestedMember: { + NestedMemberId: 123, + NestedMemberObject: { + IntValue: 223 + } + }, + MemoryValue: { + IsEmpty: true + }, + ImmutableArrayValue: null, + ImmutableQueueValue: [], + ImmutableStackValue: [], + EnumReverseStringValue: , + ExposePrivateValue: 19, + ExposeGenericPrivateValue: { + ExposedId: 10, + ExposedValue: { + Value: 3.3 + } + } + }, + NullableReadOnlyObjectCollection: [ + { + IntValue: 10 + }, + { + IntValue: 20 + } + ], + MemoryValue: { + Length: 3, + IsEmpty: false + }, + StackValue: [ + 3, + 2, + 1 + ], + QueueValue: [ + 1, + 2, + 3 + ], + ImmutableArrayValue: [ + 1, + 2, + 3 + ], + ImmutableListValue: [ + 1, + 2, + 3 + ], + ImmutableQueueValue: [ + 1, + 2, + 3 + ], + ImmutableStackValue: [ + 3, + 2, + 1 + ], + ImmutableSortedSetValue: [ + 1, + 2, + 3 + ], + ImmutableDictionaryValue: { + 1: 1, + 2: 2, + 3: 3 + }, + ImmutableSortedDictionaryValue: { + 1: 1, + 2: 2, + 3: 3 + }, + ExistingISet: [ + 1, + 2, + 3 + ], + ExistingHashSet: [ + 1, + 2, + 3 + ], + ExistingSortedSet: [ + 1, + 2, + 3 + ], + ExistingList: [ + 1, + 2, + 3 + ], + ISet: [ + 1, + 2, + 3 + ], + IReadOnlySet: [ + 1, + 2, + 3 + ], + HashSet: [ + 1, + 2, + 3 + ], + SortedSet: [ + 1, + 2, + 3 + ], + EnumValue: Value10, + FlagsEnumValue: V1, V4, + EnumName: Value10, + EnumRawValue: Value20, + EnumStringValue: Value30, + EnumReverseStringValue: DtoValue3, + SubObject: { + SubIntValue: 2, + BaseIntValue: 1 + }, + DateTimeValue: 2020-01-03 15:10:05 Utc, + DateTimeValueTargetDateOnly: 2020-01-03 15:10:05 Utc, + DateTimeValueTargetTimeOnly: 2020-01-03 15:10:05 Utc, + ToByteArrayWithInstanceMethod: Guid_1, + WithCreateMethod: { + Value: 10 + }, + WithCreateFromMethod: { + Value: 20 + }, + WithFromSingleMethod: { + Value: 30 + }, + WithCreateParamsMethod: { + Value: 40 + }, + WithCreateFromParamsMethod: { + Value: 50 + }, + WithFromShortParamsMethod: { + Value: 60 + }, + WithToDecimalMethod: { + Value: 70 + }, + ExposePrivateValue: 18, + ExposeGenericPrivateValue: { + ExposedId: 10, + ExposedValue: { + Value: 3.3 + } + }, + SumComponent1: 32, + SumComponent2: 64 +} \ No newline at end of file diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningBehaviourMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningBehaviourMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs new file mode 100644 index 0000000000..48be9fbb05 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningBehaviourMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs @@ -0,0 +1,258 @@ +// +#nullable enable +namespace Riok.Mapperly.IntegrationTests.Mapper +{ + public static partial class DeepCloningMapperWithCloningBehaviour + { + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public static partial global::Riok.Mapperly.IntegrationTests.Models.IdObject Copy(global::Riok.Mapperly.IntegrationTests.Models.IdObject src) + { + var target = new global::Riok.Mapperly.IntegrationTests.Models.IdObject(); + target.IdValue = src.IdValue; + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public static partial global::Riok.Mapperly.IntegrationTests.Models.TestObject Copy(global::Riok.Mapperly.IntegrationTests.Models.TestObject src) + { + var target = new global::Riok.Mapperly.IntegrationTests.Models.TestObject(src.CtorValue, ctorValue2: src.CtorValue2) + { + IntInitOnlyValue = src.IntInitOnlyValue, + RequiredValue = src.RequiredValue, + }; + target.IntValue = src.IntValue; + target.StringValue = src.StringValue; + target.RenamedStringValue = src.RenamedStringValue; + target.Flattening = Copy(src.Flattening); + if (src.NullableFlattening != null) + { + target.NullableFlattening = Copy(src.NullableFlattening); + } + else + { + target.NullableFlattening = null; + } + target.UnflatteningIdValue = src.UnflatteningIdValue; + target.NullableUnflatteningIdValue = src.NullableUnflatteningIdValue; + if (src.NestedNullable != null) + { + target.NestedNullable = MapToTestObjectNested(src.NestedNullable); + } + else + { + target.NestedNullable = null; + } + if (src.NestedNullableTargetNotNullable != null) + { + target.NestedNullableTargetNotNullable = MapToTestObjectNested(src.NestedNullableTargetNotNullable); + } + else + { + target.NestedNullableTargetNotNullable = null; + } + if (src.NestedMember != null) + { + target.NestedMember = MapToTestObjectNestedMember(src.NestedMember); + } + else + { + target.NestedMember = null; + } + target.StringNullableTargetNotNullable = src.StringNullableTargetNotNullable; + if (src.TupleValue != null) + { + target.TupleValue = MapToValueTupleOfStringAndString(src.TupleValue.Value); + } + else + { + target.TupleValue = null; + } + if (src.RecursiveObject != null) + { + target.RecursiveObject = Copy(src.RecursiveObject); + } + else + { + target.RecursiveObject = null; + } + if (src.SourceTargetSameObjectType != null) + { + target.SourceTargetSameObjectType = Copy(src.SourceTargetSameObjectType); + } + else + { + target.SourceTargetSameObjectType = null; + } + if (src.NullableReadOnlyObjectCollection != null) + { + target.NullableReadOnlyObjectCollection = MapToTestObjectNestedArray(src.NullableReadOnlyObjectCollection); + } + else + { + target.NullableReadOnlyObjectCollection = null; + } + target.MemoryValue = src.MemoryValue.Span.ToArray(); + target.StackValue = new global::System.Collections.Generic.Stack(global::System.Linq.Enumerable.Reverse(src.StackValue)); + target.QueueValue = new global::System.Collections.Generic.Queue(src.QueueValue); + target.ImmutableArrayValue = src.ImmutableArrayValue; + target.ImmutableListValue = src.ImmutableListValue; + target.ImmutableQueueValue = src.ImmutableQueueValue; + target.ImmutableStackValue = src.ImmutableStackValue; + target.ImmutableSortedSetValue = src.ImmutableSortedSetValue; + target.ImmutableDictionaryValue = src.ImmutableDictionaryValue; + target.ImmutableSortedDictionaryValue = src.ImmutableSortedDictionaryValue; + foreach (var item in src.ExistingISet) + { + target.ExistingISet.Add(item); + } + target.ExistingHashSet.EnsureCapacity(src.ExistingHashSet.Count + target.ExistingHashSet.Count); + foreach (var item1 in src.ExistingHashSet) + { + target.ExistingHashSet.Add(item1); + } + foreach (var item2 in src.ExistingSortedSet) + { + target.ExistingSortedSet.Add(item2); + } + target.ExistingList.EnsureCapacity(src.ExistingList.Count + target.ExistingList.Count); + foreach (var item3 in src.ExistingList) + { + target.ExistingList.Add(item3); + } + target.ISet = global::System.Linq.Enumerable.ToHashSet(src.ISet); + target.IReadOnlySet = global::System.Linq.Enumerable.ToHashSet(src.IReadOnlySet); + target.HashSet = global::System.Linq.Enumerable.ToHashSet(src.HashSet); + target.SortedSet = new global::System.Collections.Generic.SortedSet(src.SortedSet); + target.EnumValue = src.EnumValue; + target.FlagsEnumValue = src.FlagsEnumValue; + target.EnumName = src.EnumName; + target.EnumRawValue = src.EnumRawValue; + target.EnumStringValue = src.EnumStringValue; + target.EnumReverseStringValue = src.EnumReverseStringValue; + if (src.SubObject != null) + { + target.SubObject = MapToInheritanceSubObject(src.SubObject); + } + else + { + target.SubObject = null; + } + target.DateTimeValue = src.DateTimeValue; + target.DateTimeValueTargetDateOnly = src.DateTimeValueTargetDateOnly; + target.DateTimeValueTargetTimeOnly = src.DateTimeValueTargetTimeOnly; + target.ToByteArrayWithInstanceMethod = src.ToByteArrayWithInstanceMethod; + if (src.WithCreateMethod != null) + { + target.WithCreateMethod = global::Riok.Mapperly.IntegrationTests.Models.ConvertWithStaticMethodObject.ToConvertWithStaticMethodObject(src.WithCreateMethod); + } + else + { + target.WithCreateMethod = null; + } + if (src.WithCreateFromMethod != null) + { + target.WithCreateFromMethod = global::Riok.Mapperly.IntegrationTests.Models.ConvertWithStaticMethodObject.ToConvertWithStaticMethodObject(src.WithCreateFromMethod); + } + else + { + target.WithCreateFromMethod = null; + } + if (src.WithFromSingleMethod != null) + { + target.WithFromSingleMethod = global::Riok.Mapperly.IntegrationTests.Models.ConvertWithStaticMethodObject.ToConvertWithStaticMethodObject(src.WithFromSingleMethod); + } + else + { + target.WithFromSingleMethod = null; + } + if (src.WithCreateParamsMethod != null) + { + target.WithCreateParamsMethod = global::Riok.Mapperly.IntegrationTests.Models.ConvertWithStaticMethodObject.ToConvertWithStaticMethodObject(src.WithCreateParamsMethod); + } + else + { + target.WithCreateParamsMethod = null; + } + if (src.WithCreateFromParamsMethod != null) + { + target.WithCreateFromParamsMethod = global::Riok.Mapperly.IntegrationTests.Models.ConvertWithStaticMethodObject.ToConvertWithStaticMethodObject(src.WithCreateFromParamsMethod); + } + else + { + target.WithCreateFromParamsMethod = null; + } + if (src.WithFromShortParamsMethod != null) + { + target.WithFromShortParamsMethod = global::Riok.Mapperly.IntegrationTests.Models.ConvertWithStaticMethodObject.ToConvertWithStaticMethodObject(src.WithFromShortParamsMethod); + } + else + { + target.WithFromShortParamsMethod = null; + } + if (src.WithToDecimalMethod != null) + { + target.WithToDecimalMethod = global::Riok.Mapperly.IntegrationTests.Models.ConvertWithStaticMethodObject.ToConvertWithStaticMethodObject(src.WithToDecimalMethod); + } + else + { + target.WithToDecimalMethod = null; + } + target.SumComponent1 = src.SumComponent1; + target.SumComponent2 = src.SumComponent2; + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private static global::Riok.Mapperly.IntegrationTests.Models.TestObjectNested MapToTestObjectNested(global::Riok.Mapperly.IntegrationTests.Models.TestObjectNested source) + { + var target = new global::Riok.Mapperly.IntegrationTests.Models.TestObjectNested(); + target.IntValue = source.IntValue; + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private static global::Riok.Mapperly.IntegrationTests.Models.TestObjectNestedMember MapToTestObjectNestedMember(global::Riok.Mapperly.IntegrationTests.Models.TestObjectNestedMember source) + { + var target = new global::Riok.Mapperly.IntegrationTests.Models.TestObjectNestedMember(); + target.NestedMemberId = source.NestedMemberId; + if (source.NestedMemberObject != null) + { + target.NestedMemberObject = MapToTestObjectNested(source.NestedMemberObject); + } + else + { + target.NestedMemberObject = null; + } + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private static (string A, string) MapToValueTupleOfStringAndString((string A, string) source) + { + var target = (A: source.A, source.Item2); + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private static global::Riok.Mapperly.IntegrationTests.Models.TestObjectNested[] MapToTestObjectNestedArray(global::System.Collections.Generic.IReadOnlyCollection source) + { + var target = new global::Riok.Mapperly.IntegrationTests.Models.TestObjectNested[source.Count]; + var i = 0; + foreach (var item in source) + { + target[i] = MapToTestObjectNested(item); + i++; + } + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private static global::Riok.Mapperly.IntegrationTests.Models.InheritanceSubObject MapToInheritanceSubObject(global::Riok.Mapperly.IntegrationTests.Models.InheritanceSubObject source) + { + var target = new global::Riok.Mapperly.IntegrationTests.Models.InheritanceSubObject(); + target.SubIntValue = source.SubIntValue; + target.BaseIntValue = source.BaseIntValue; + return target; + } + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/Helpers/MapperConfigurationBuilderTest.cs b/test/Riok.Mapperly.Tests/Helpers/MapperConfigurationBuilderTest.cs index 67d4d422e8..3d7f8dbd0b 100644 --- a/test/Riok.Mapperly.Tests/Helpers/MapperConfigurationBuilderTest.cs +++ b/test/Riok.Mapperly.Tests/Helpers/MapperConfigurationBuilderTest.cs @@ -71,6 +71,7 @@ public void ShouldMergeMapperConfigurationsWithEmptyDefaultMapperConfiguration() mapper.ThrowOnPropertyMappingNullMismatch.ShouldBeTrue(); mapper.AllowNullPropertyAssignment.ShouldBeTrue(); mapper.UseDeepCloning.ShouldBeTrue(); + mapper.CloningBehaviour.ShouldBe(CloningBehaviour.ShallowCloning); mapper.EnabledConversions.ShouldBe(MappingConversionType.Constructor); mapper.UseReferenceHandling.ShouldBeTrue(); mapper.IgnoreObsoleteMembersStrategy.ShouldBe(IgnoreObsoleteMembersStrategy.Source); @@ -107,6 +108,7 @@ private MapperConfiguration NewMapperConfiguration() ThrowOnPropertyMappingNullMismatch = true, AllowNullPropertyAssignment = true, UseDeepCloning = true, + CloningBehaviour = CloningBehaviour.ShallowCloning, EnabledConversions = MappingConversionType.Constructor, UseReferenceHandling = true, IgnoreObsoleteMembersStrategy = IgnoreObsoleteMembersStrategy.Source, diff --git a/test/Riok.Mapperly.Tests/Mapping/ShallowCloneTest.cs b/test/Riok.Mapperly.Tests/Mapping/ShallowCloneTest.cs new file mode 100644 index 0000000000..f697de9a35 --- /dev/null +++ b/test/Riok.Mapperly.Tests/Mapping/ShallowCloneTest.cs @@ -0,0 +1,26 @@ +namespace Riok.Mapperly.Tests.Mapping; + +public class ShallowCloneTest +{ + [Fact] + public void ShallowCloneShouldNotReturnOriginalInstance() + { + var source = TestSourceBuilder.Mapping( + "A", + "A", + TestSourceBuilderOptions.WithShallowCloning, + "class A { public int Value { get; set; } public List List { get; set; } }" + ); + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::A(); + target.Value = source.Value; + target.List = source.List; + return target; + """ + ); + } +} diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs index 5861f4b988..2a4b059617 100644 --- a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs +++ b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs @@ -107,6 +107,7 @@ private static string BuildAttribute(TestSourceBuilderOptions options) Attribute(options.IncludedConstructors), Attribute(options.PreferParameterlessConstructors), Attribute(options.AutoUserMappings), + Attribute(options.CloningBehaviour), }.WhereNotNull(); return $"[Mapper({string.Join(", ", attrs)})]"; diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs index 6e166ff415..f318d70cdb 100644 --- a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs +++ b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs @@ -8,6 +8,7 @@ public record TestSourceBuilderOptions( string MapperClassName = TestSourceBuilderOptions.DefaultMapperClassName, string? MapperBaseClassName = null, bool? UseDeepCloning = null, + CloningBehaviour? CloningBehaviour = null, StackCloningStrategy? StackCloningStrategy = null, bool? UseReferenceHandling = null, bool? ThrowOnMappingNullMismatch = null, @@ -36,6 +37,10 @@ public record TestSourceBuilderOptions( ); public static readonly TestSourceBuilderOptions AsStatic = new(Static: true); public static readonly TestSourceBuilderOptions WithDeepCloning = new(UseDeepCloning: true); + + public static readonly TestSourceBuilderOptions WithShallowCloning = new( + CloningBehaviour: Abstractions.CloningBehaviour.ShallowCloning + ); public static readonly TestSourceBuilderOptions AllConversionsWithDeepCloning = new TestSourceBuilderOptions( UseDeepCloning: true, EnabledConversions: MappingConversionType.All From 2aabcd7c40091fc4fa16f350147a5c7d322fe15b Mon Sep 17 00:00:00 2001 From: Alessandro Losi Date: Thu, 5 Feb 2026 16:41:49 +0100 Subject: [PATCH 4/9] Fixed some issues with flawed `UseCloning` logic --- .../MapperUseShallowCloningAttribute.cs | 12 ------------ .../Riok.Mapperly.Abstractions.csproj | 3 --- .../Configuration/MapperConfigurationReader.cs | 9 ++------- .../Descriptors/MappingBuilderContext.cs | 4 +++- .../DirectAssignmentMappingBuilder.cs | 1 - 5 files changed, 5 insertions(+), 24 deletions(-) delete mode 100644 src/Riok.Mapperly.Abstractions/MapperUseShallowCloningAttribute.cs diff --git a/src/Riok.Mapperly.Abstractions/MapperUseShallowCloningAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperUseShallowCloningAttribute.cs deleted file mode 100644 index 1374c408ab..0000000000 --- a/src/Riok.Mapperly.Abstractions/MapperUseShallowCloningAttribute.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Diagnostics; - -namespace Riok.Mapperly.Abstractions; - -/// -/// 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. -/// -[AttributeUsage(AttributeTargets.Method)] -[Conditional("MAPPERLY_ABSTRACTIONS_SCOPE_RUNTIME")] -public sealed class MapperUseShallowCloningAttribute : Attribute { } diff --git a/src/Riok.Mapperly.Abstractions/Riok.Mapperly.Abstractions.csproj b/src/Riok.Mapperly.Abstractions/Riok.Mapperly.Abstractions.csproj index 4925060985..f8945b151d 100644 --- a/src/Riok.Mapperly.Abstractions/Riok.Mapperly.Abstractions.csproj +++ b/src/Riok.Mapperly.Abstractions/Riok.Mapperly.Abstractions.csproj @@ -7,7 +7,4 @@ - - - diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs index e5518f978d..cf66909b82 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs @@ -243,16 +243,11 @@ private MembersMappingConfiguration BuildMembersConfig(MappingConfigurationRefer .AccessFirstOrDefault(configRef.Method) ?.IgnoreObsoleteStrategy; var requiredMapping = _dataAccessor.AccessFirstOrDefault(configRef.Method)?.RequiredMappingStrategy; - //var useShallowCloning = _dataAccessor.AccessFirstOrDefault(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 /*|| useShallowCloning != null*/ - ; + var hasMemberConfigs = ignoredSourceMembers.Count > 0 || ignoredTargetMembers.Count > 0 || memberConfigurations.Count > 0; + if (hasMemberConfigs && (configRef.Source.IsEnum() || configRef.Target.IsEnum())) { _diagnostics.ReportDiagnostic(DiagnosticDescriptors.MemberConfigurationOnNonMemberMapping, configRef.Method); diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs index 1b5427cf43..c12ffaf047 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs @@ -92,7 +92,9 @@ protected MappingBuilderContext( /// /// Determines if mapping code should be emitted in cases where direct assignments or casts could be used instead. /// - public bool UseCloning => Configuration.CloningBehaviour != CloningBehaviour.None; + public bool UseCloning => + Configuration.CloningBehaviour == CloningBehaviour.DeepCloning + || (HasUserSymbol && Configuration.CloningBehaviour == CloningBehaviour.ShallowCloning); /// /// Tries to find an existing mapping with the provided name. diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/DirectAssignmentMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/DirectAssignmentMappingBuilder.cs index 7fd356f506..3466558346 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/DirectAssignmentMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/DirectAssignmentMappingBuilder.cs @@ -1,5 +1,4 @@ using Microsoft.CodeAnalysis; -using Riok.Mapperly.Abstractions; using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Helpers; From 4c146b0cec095e17841e147bcdef0ba68fa80bf5 Mon Sep 17 00:00:00 2001 From: Alessandro Losi Date: Fri, 6 Feb 2026 15:35:03 +0100 Subject: [PATCH 5/9] Renamed CloningBehaviour into CloningStrategy --- ...CloningBehaviour.cs => CloningStrategy.cs} | 2 +- .../MapperAttribute.cs | 10 +- .../Configuration/MapperConfiguration.cs | 2 +- .../MapperConfigurationMerger.cs | 5 +- .../MapperConfigurationReader.cs | 13 +- .../Configuration/MappingConfiguration.cs | 2 +- .../MembersMappingConfiguration.cs | 2 - .../Descriptors/MappingBuilderContext.cs | 4 +- .../EnumerableMappingBuilder.cs | 4 +- .../MappingBuilders/MemoryMappingBuilder.cs | 8 +- .../MappingBuilders/SpanMappingBuilder.cs | 2 +- src/Riok.Mapperly/Riok.Mapperly.targets | 1 + ...ApiTest.PublicApiHasNotChanged.verified.cs | 7 +- ...epCloningWithCloningStrategyMapperTest.cs} | 8 +- .../Mapper/DeepCloningMapper.cs | 4 +- .../Mapper/ShallowCloningMapper.cs | 2 +- ...t.RunMappingShouldWork_NET9_0.verified.txt | 229 ++++++++++++++++ ...SnapshotGeneratedSource_NET8_0.verified.cs | 258 ++++++++++++++++++ .../Helpers/MapperConfigurationBuilderTest.cs | 4 +- test/Riok.Mapperly.Tests/TestSourceBuilder.cs | 2 +- .../TestSourceBuilderOptions.cs | 6 +- 21 files changed, 533 insertions(+), 42 deletions(-) rename src/Riok.Mapperly.Abstractions/{CloningBehaviour.cs => CloningStrategy.cs} (96%) rename test/Riok.Mapperly.IntegrationTests/{DeepCloningWithCloningBehaviourMapperTest.cs => DeepCloningWithCloningStrategyMapperTest.cs} (78%) create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningStrategyMapperTest.RunMappingShouldWork_NET9_0.verified.txt create mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningStrategyMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs diff --git a/src/Riok.Mapperly.Abstractions/CloningBehaviour.cs b/src/Riok.Mapperly.Abstractions/CloningStrategy.cs similarity index 96% rename from src/Riok.Mapperly.Abstractions/CloningBehaviour.cs rename to src/Riok.Mapperly.Abstractions/CloningStrategy.cs index 6ff193ae65..ebce7920f3 100644 --- a/src/Riok.Mapperly.Abstractions/CloningBehaviour.cs +++ b/src/Riok.Mapperly.Abstractions/CloningStrategy.cs @@ -3,7 +3,7 @@ /// /// Specifies whether and how to copy objects of the same type and complex types like collections and spans. /// -public enum CloningBehaviour +public enum CloningStrategy { /// /// Default behaviour, the original instance will be returned diff --git a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs index aa6bc61b26..c2d1690939 100644 --- a/src/Riok.Mapperly.Abstractions/MapperAttribute.cs +++ b/src/Riok.Mapperly.Abstractions/MapperAttribute.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using System.Diagnostics; using Riok.Mapperly.Abstractions.ReferenceHandling; @@ -65,13 +66,18 @@ public class MapperAttribute : Attribute /// when false, the same array is reused. /// when true, the array and each person is cloned. /// - [Obsolete("Please use the property CloningBehaviour")] + /// + /// To maintain compatibility with the previous versions, will still take precedence over + /// until it will be removed. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Use 'CloningStrategy' instead. If this is set to true, `CloningStrategy.DeepClone` is always used.")] public bool UseDeepCloning { get; set; } /// /// Specifies whether and how to copy objects of the same type and complex types like collections and spans. /// - public CloningBehaviour CloningBehaviour { get; set; } = CloningBehaviour.None; + public CloningStrategy CloningStrategy { get; set; } = CloningStrategy.None; /// /// The strategy to use when cloning a . diff --git a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs index 6573d43cfb..57a7d7620e 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfiguration.cs @@ -65,7 +65,7 @@ public record MapperConfiguration /// public bool? UseDeepCloning { get; init; } - public CloningBehaviour? CloningBehaviour { get; init; } + public CloningStrategy? CloningStrategy { get; init; } /// /// The strategy to use when cloning a . diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs index f2e6661a3c..8b7285944d 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs @@ -16,6 +16,7 @@ public static MapperConfiguration Merge(MapperConfiguration highPriority, Mapper highPriority.ThrowOnPropertyMappingNullMismatch ?? lowPriority.ThrowOnPropertyMappingNullMismatch, AllowNullPropertyAssignment = highPriority.AllowNullPropertyAssignment ?? lowPriority.AllowNullPropertyAssignment, UseDeepCloning = highPriority.UseDeepCloning ?? lowPriority.UseDeepCloning, + CloningStrategy = highPriority.CloningStrategy ?? lowPriority.CloningStrategy, StackCloningStrategy = highPriority.StackCloningStrategy ?? lowPriority.StackCloningStrategy, EnabledConversions = highPriority.EnabledConversions ?? lowPriority.EnabledConversions, UseReferenceHandling = highPriority.UseReferenceHandling ?? lowPriority.UseReferenceHandling, @@ -61,8 +62,8 @@ public static MapperAttribute MergeToAttribute(MapperConfiguration mapperConfigu mapper.UseDeepCloning = mapperConfiguration.UseDeepCloning ?? defaultMapperConfiguration.UseDeepCloning ?? mapper.UseDeepCloning; - mapper.CloningBehaviour = - mapperConfiguration.CloningBehaviour ?? defaultMapperConfiguration.CloningBehaviour ?? mapper.CloningBehaviour; + mapper.CloningStrategy = + mapperConfiguration.CloningStrategy ?? defaultMapperConfiguration.CloningStrategy ?? mapper.CloningStrategy; mapper.StackCloningStrategy = mapperConfiguration.StackCloningStrategy ?? defaultMapperConfiguration.StackCloningStrategy ?? mapper.StackCloningStrategy; diff --git a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs index cf66909b82..776a876f33 100644 --- a/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs +++ b/src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs @@ -53,7 +53,7 @@ SupportedFeatures supportedFeatures ), new MembersMappingConfiguration([], [], [], [], [], mapper.IgnoreObsoleteMembersStrategy, mapper.RequiredMappingStrategy), [], - mapper.UseDeepCloning ? CloningBehaviour.DeepCloning : mapper.CloningBehaviour, + mapper.UseDeepCloning ? CloningStrategy.DeepCloning : mapper.CloningStrategy, mapper.StackCloningStrategy, supportedFeatures ); @@ -78,11 +78,11 @@ bool supportsDeepCloning ) { if (reference.Method == null) - return supportsDeepCloning ? MapperConfiguration : MapperConfiguration with { CloningBehaviour = CloningBehaviour.None }; + return supportsDeepCloning ? MapperConfiguration : MapperConfiguration with { CloningStrategy = CloningStrategy.None }; - var cloningBehaviour = MapperConfiguration.Mapper.UseDeepCloning - ? CloningBehaviour.DeepCloning - : MapperConfiguration.Mapper.CloningBehaviour; + var cloningStrategy = MapperConfiguration.Mapper.UseDeepCloning + ? CloningStrategy.DeepCloning + : MapperConfiguration.Mapper.CloningStrategy; var enumConfig = BuildEnumConfig(reference); var membersConfig = BuildMembersConfig(reference); @@ -92,7 +92,7 @@ bool supportsDeepCloning enumConfig, membersConfig, derivedTypesConfig, - supportsDeepCloning ? cloningBehaviour : CloningBehaviour.None, + supportsDeepCloning ? cloningStrategy : CloningStrategy.None, MapperConfiguration.StackCloningStrategy, MapperConfiguration.SupportedFeatures ); @@ -282,7 +282,6 @@ private MembersMappingConfiguration BuildMembersConfig(MappingConfigurationRefer nestedMembersConfigurations, ignoreObsolete, requiredMapping - //useShallowCloning != null ); } diff --git a/src/Riok.Mapperly/Configuration/MappingConfiguration.cs b/src/Riok.Mapperly/Configuration/MappingConfiguration.cs index b79e02d0bf..d58d0fedc8 100644 --- a/src/Riok.Mapperly/Configuration/MappingConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/MappingConfiguration.cs @@ -8,7 +8,7 @@ public record MappingConfiguration( EnumMappingConfiguration Enum, MembersMappingConfiguration Members, IReadOnlyCollection DerivedTypes, - CloningBehaviour CloningBehaviour, + CloningStrategy CloningStrategy, StackCloningStrategy StackCloningStrategy, SupportedFeatures SupportedFeatures ) diff --git a/src/Riok.Mapperly/Configuration/MembersMappingConfiguration.cs b/src/Riok.Mapperly/Configuration/MembersMappingConfiguration.cs index b9084f4af9..0185798b48 100644 --- a/src/Riok.Mapperly/Configuration/MembersMappingConfiguration.cs +++ b/src/Riok.Mapperly/Configuration/MembersMappingConfiguration.cs @@ -11,7 +11,6 @@ public record MembersMappingConfiguration( IReadOnlyCollection NestedMappings, IgnoreObsoleteMembersStrategy? IgnoreObsoleteMembersStrategy, RequiredMappingStrategy? RequiredMappingStrategy -//CloningBehaviour CloningBehaviour ) { public IEnumerable GetMembersWithExplicitConfigurations(MappingSourceTarget sourceTarget) @@ -37,7 +36,6 @@ public MembersMappingConfiguration Include(MembersMappingConfiguration? otherCon NestedMappings.Concat(otherConfiguration?.NestedMappings ?? []).ToList(), IgnoreObsoleteMembersStrategy ?? otherConfiguration?.IgnoreObsoleteMembersStrategy, RequiredMappingStrategy ?? otherConfiguration?.RequiredMappingStrategy - //CloningBehaviour // TODO what to do in this case? ); } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs index c12ffaf047..814b09b923 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs @@ -93,8 +93,8 @@ protected MappingBuilderContext( /// Determines if mapping code should be emitted in cases where direct assignments or casts could be used instead. /// public bool UseCloning => - Configuration.CloningBehaviour == CloningBehaviour.DeepCloning - || (HasUserSymbol && Configuration.CloningBehaviour == CloningBehaviour.ShallowCloning); + Configuration.CloningStrategy == CloningStrategy.DeepCloning + || (HasUserSymbol && Configuration.CloningStrategy == CloningStrategy.ShallowCloning); /// /// Tries to find an existing mapping with the provided name. diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs index c49b7ba93d..669cb14d58 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs @@ -198,7 +198,7 @@ private static INewInstanceMapping BuildArrayToArrayMapping(MappingBuilderContex // use a for loop mapping otherwise. if (elementMapping.IsSynthetic) { - return ctx.Configuration.CloningBehaviour == CloningBehaviour.DeepCloning + return ctx.Configuration.CloningStrategy == CloningStrategy.DeepCloning ? new ArrayCloneMapping(ctx.Source, ctx.Target) : new CastMapping(ctx.Source, ctx.Target); } @@ -318,7 +318,7 @@ private static (bool CanMapWithLinq, string? CollectMethod) ResolveCollectMethod // if the target is an IEnumerable don't collect at all // except deep cloning is enabled. var targetIsIEnumerable = ctx.CollectionInfos!.Target.CollectionType == CollectionType.IEnumerable; - if (targetIsIEnumerable && ctx.Configuration.CloningBehaviour != CloningBehaviour.DeepCloning) + if (targetIsIEnumerable && ctx.Configuration.CloningStrategy != CloningStrategy.DeepCloning) return (true, null); // if the target is IReadOnlyCollection or IEnumerable diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/MemoryMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/MemoryMappingBuilder.cs index 8e713d2f64..9907f49a2b 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/MemoryMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/MemoryMappingBuilder.cs @@ -117,7 +117,7 @@ public static class MemoryMappingBuilder private static NewInstanceMapping? BuildMemoryToArrayMapping(MappingBuilderContext ctx, INewInstanceMapping elementMapping) { - if (!elementMapping.IsSynthetic || ctx.Configuration.CloningBehaviour == CloningBehaviour.DeepCloning) + if (!elementMapping.IsSynthetic || ctx.Configuration.CloningStrategy == CloningStrategy.DeepCloning) return BuildSpanToArrayMethodMapping(ctx, elementMapping); return new SourceObjectMethodMapping(ctx.Source, ctx.Target, ToArrayMethodName); @@ -125,7 +125,7 @@ public static class MemoryMappingBuilder private static NewInstanceMapping? BuildMemoryToSpanMapping(MappingBuilderContext ctx, INewInstanceMapping elementMapping) { - if (!elementMapping.IsSynthetic || ctx.Configuration.CloningBehaviour == CloningBehaviour.DeepCloning) + if (!elementMapping.IsSynthetic || ctx.Configuration.CloningStrategy == CloningStrategy.DeepCloning) return BuildMemoryToSpanMethod(ctx, elementMapping); return new SourceObjectMemberMapping(ctx.Source, ctx.Target, SpanMemberName); @@ -133,7 +133,7 @@ public static class MemoryMappingBuilder private static INewInstanceMapping BuildArrayToMemoryMapping(MappingBuilderContext ctx, INewInstanceMapping elementMapping) { - if (!elementMapping.IsSynthetic || ctx.Configuration.CloningBehaviour == CloningBehaviour.DeepCloning) + if (!elementMapping.IsSynthetic || ctx.Configuration.CloningStrategy == CloningStrategy.DeepCloning) return new ArrayForMapping( ctx.Source, ctx.Types.GetArrayType(elementMapping.TargetType), @@ -155,7 +155,7 @@ private static INewInstanceMapping BuildArrayToMemoryMapping(MappingBuilderConte private static NewInstanceMapping? BuildMemoryToMemoryMapping(MappingBuilderContext ctx, INewInstanceMapping elementMapping) { - if (!elementMapping.IsSynthetic || ctx.Configuration.CloningBehaviour == CloningBehaviour.DeepCloning) + if (!elementMapping.IsSynthetic || ctx.Configuration.CloningStrategy == CloningStrategy.DeepCloning) return BuildSpanToArrayMethodMapping(ctx, elementMapping); return new CastMapping(ctx.Source, ctx.Target); diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/SpanMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/SpanMappingBuilder.cs index 30d7d4bb98..fea885d625 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/SpanMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/SpanMappingBuilder.cs @@ -44,7 +44,7 @@ public static class SpanMappingBuilder // if the source is Span/ReadOnlySpan or Array and target is Span/ReadOnlySpan // and element type is the same, then direct cast (CollectionType.Span or CollectionType.ReadOnlySpan or CollectionType.Array, CollectionType.Span or CollectionType.ReadOnlySpan) - when elementMapping.IsSynthetic && ctx.Configuration.CloningBehaviour != CloningBehaviour.DeepCloning => new CastMapping( + when elementMapping.IsSynthetic && ctx.Configuration.CloningStrategy != CloningStrategy.DeepCloning => new CastMapping( ctx.Source, ctx.Target ), diff --git a/src/Riok.Mapperly/Riok.Mapperly.targets b/src/Riok.Mapperly/Riok.Mapperly.targets index d33552ef66..2ea450d858 100644 --- a/src/Riok.Mapperly/Riok.Mapperly.targets +++ b/src/Riok.Mapperly/Riok.Mapperly.targets @@ -14,6 +14,7 @@ + diff --git a/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs b/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs index 86086024c5..bdd8f16188 100644 --- a/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs +++ b/test/Riok.Mapperly.Abstractions.Tests/_snapshots/PublicApiTest.PublicApiHasNotChanged.verified.cs @@ -1,7 +1,7 @@ [assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")] namespace Riok.Mapperly.Abstractions { - public enum CloningBehaviour + public enum CloningStrategy { None = 0, DeepCloning = 1, @@ -137,7 +137,7 @@ public class MapperAttribute : System.Attribute public MapperAttribute() { } public bool AllowNullPropertyAssignment { get; set; } public bool AutoUserMappings { get; set; } - public Riok.Mapperly.Abstractions.CloningBehaviour CloningBehaviour { get; set; } + public Riok.Mapperly.Abstractions.CloningStrategy CloningStrategy { get; set; } public Riok.Mapperly.Abstractions.MappingConversionType EnabledConversions { get; set; } public bool EnumMappingIgnoreCase { get; set; } public Riok.Mapperly.Abstractions.EnumMappingStrategy EnumMappingStrategy { get; set; } @@ -152,7 +152,8 @@ public MapperAttribute() { } public Riok.Mapperly.Abstractions.StackCloningStrategy StackCloningStrategy { get; set; } public bool ThrowOnMappingNullMismatch { get; set; } public bool ThrowOnPropertyMappingNullMismatch { get; set; } - [System.Obsolete("Please use the property CloningBehaviour")] + [System.Obsolete("Use \'CloningStrategy\' instead. If this is set to true, `CloningStrategy.DeepClone" + + "` is always used.")] public bool UseDeepCloning { get; set; } public bool UseReferenceHandling { get; set; } } diff --git a/test/Riok.Mapperly.IntegrationTests/DeepCloningWithCloningBehaviourMapperTest.cs b/test/Riok.Mapperly.IntegrationTests/DeepCloningWithCloningStrategyMapperTest.cs similarity index 78% rename from test/Riok.Mapperly.IntegrationTests/DeepCloningWithCloningBehaviourMapperTest.cs rename to test/Riok.Mapperly.IntegrationTests/DeepCloningWithCloningStrategyMapperTest.cs index f9057076c4..02edd9e547 100644 --- a/test/Riok.Mapperly.IntegrationTests/DeepCloningWithCloningBehaviourMapperTest.cs +++ b/test/Riok.Mapperly.IntegrationTests/DeepCloningWithCloningStrategyMapperTest.cs @@ -8,13 +8,13 @@ namespace Riok.Mapperly.IntegrationTests { - public class DeepCloningWithCloningBehaviourMapperTest : BaseMapperTest + public class DeepCloningWithCloningStrategyMapperTest : BaseMapperTest { [Fact] [VersionedSnapshot(Versions.NET8_0)] public Task SnapshotGeneratedSource() { - var path = GetGeneratedMapperFilePath(nameof(DeepCloningMapperWithCloningBehaviour)); + var path = GetGeneratedMapperFilePath(nameof(DeepCloningMapperWithCloningStrategy)); return Verifier.VerifyFile(path); } @@ -23,7 +23,7 @@ public Task SnapshotGeneratedSource() public Task RunMappingShouldWork() { var model = NewTestObj(); - var dto = DeepCloningMapperWithCloningBehaviour.Copy(model); + var dto = DeepCloningMapperWithCloningStrategy.Copy(model); return Verifier.Verify(dto); } @@ -31,7 +31,7 @@ public Task RunMappingShouldWork() public void RunIdMappingShouldWork() { var source = new IdObject { IdValue = 20 }; - var copy = DeepCloningMapperWithCloningBehaviour.Copy(source); + var copy = DeepCloningMapperWithCloningStrategy.Copy(source); source.ShouldNotBeSameAs(copy); copy.IdValue.ShouldBe(20); } diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/DeepCloningMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/DeepCloningMapper.cs index 4f17e860df..00cad0b9d1 100644 --- a/test/Riok.Mapperly.IntegrationTests/Mapper/DeepCloningMapper.cs +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/DeepCloningMapper.cs @@ -16,8 +16,8 @@ public static partial class DeepCloningMapper public static partial TestObject Copy(TestObject src); } - [Mapper(CloningBehaviour = CloningBehaviour.DeepCloning)] - public static partial class DeepCloningMapperWithCloningBehaviour + [Mapper(CloningStrategy = CloningStrategy.DeepCloning)] + public static partial class DeepCloningMapperWithCloningStrategy { public static partial IdObject Copy(IdObject src); diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/ShallowCloningMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/ShallowCloningMapper.cs index cbd0985055..c9b2f5991d 100644 --- a/test/Riok.Mapperly.IntegrationTests/Mapper/ShallowCloningMapper.cs +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/ShallowCloningMapper.cs @@ -3,7 +3,7 @@ namespace Riok.Mapperly.IntegrationTests.Mapper { - [Mapper(CloningBehaviour = CloningBehaviour.ShallowCloning)] + [Mapper(CloningStrategy = CloningStrategy.ShallowCloning)] public static partial class ShallowCloningMapper { public static partial IdObject Copy(IdObject src); diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningStrategyMapperTest.RunMappingShouldWork_NET9_0.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningStrategyMapperTest.RunMappingShouldWork_NET9_0.verified.txt new file mode 100644 index 0000000000..51f40f465b --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningStrategyMapperTest.RunMappingShouldWork_NET9_0.verified.txt @@ -0,0 +1,229 @@ +{ + CtorValue: 7, + CtorValue2: 100, + IntValue: 10, + IntInitOnlyValue: 3, + RequiredValue: 4, + UnmappedValue: 10, + StringValue: fooBar, + RenamedStringValue: fooBar2, + Flattening: { + IdValue: 10 + }, + NullableFlattening: { + IdValue: 100 + }, + UnflatteningIdValue: 20, + NullableUnflatteningIdValue: 200, + NestedNullable: { + IntValue: 100 + }, + NestedNullableTargetNotNullable: {}, + NestedMember: { + NestedMemberId: 12, + NestedMemberObject: { + IntValue: 22 + } + }, + StringNullableTargetNotNullable: fooBar3, + TupleValue: { + Item1: 10, + Item2: 20 + }, + RecursiveObject: { + CtorValue: 5, + CtorValue2: 100, + RequiredValue: 4, + UnmappedValue: 10, + StringValue: , + RenamedStringValue: , + Flattening: {}, + MemoryValue: { + IsEmpty: true + }, + ImmutableArrayValue: null, + ImmutableQueueValue: [], + ImmutableStackValue: [], + EnumValue: Value10, + EnumName: Value30, + EnumReverseStringValue: DtoValue3, + ExposePrivateValue: 16, + ExposeGenericPrivateValue: { + ExposedId: 10, + ExposedValue: { + Value: 3.3 + } + } + }, + SourceTargetSameObjectType: { + CtorValue: 8, + CtorValue2: 100, + IntValue: 99, + RequiredValue: 98, + UnmappedValue: 10, + StringValue: , + RenamedStringValue: , + Flattening: {}, + NestedMember: { + NestedMemberId: 123, + NestedMemberObject: { + IntValue: 223 + } + }, + MemoryValue: { + IsEmpty: true + }, + ImmutableArrayValue: null, + ImmutableQueueValue: [], + ImmutableStackValue: [], + EnumReverseStringValue: , + ExposePrivateValue: 19, + ExposeGenericPrivateValue: { + ExposedId: 10, + ExposedValue: { + Value: 3.3 + } + } + }, + NullableReadOnlyObjectCollection: [ + { + IntValue: 10 + }, + { + IntValue: 20 + } + ], + MemoryValue: { + Length: 3, + IsEmpty: false + }, + StackValue: [ + 3, + 2, + 1 + ], + QueueValue: [ + 1, + 2, + 3 + ], + ImmutableArrayValue: [ + 1, + 2, + 3 + ], + ImmutableListValue: [ + 1, + 2, + 3 + ], + ImmutableQueueValue: [ + 1, + 2, + 3 + ], + ImmutableStackValue: [ + 3, + 2, + 1 + ], + ImmutableSortedSetValue: [ + 1, + 2, + 3 + ], + ImmutableDictionaryValue: { + 1: 1, + 2: 2, + 3: 3 + }, + ImmutableSortedDictionaryValue: { + 1: 1, + 2: 2, + 3: 3 + }, + ExistingISet: [ + 1, + 2, + 3 + ], + ExistingHashSet: [ + 1, + 2, + 3 + ], + ExistingSortedSet: [ + 1, + 2, + 3 + ], + ExistingList: [ + 1, + 2, + 3 + ], + ISet: [ + 1, + 2, + 3 + ], + IReadOnlySet: [ + 1, + 2, + 3 + ], + HashSet: [ + 1, + 2, + 3 + ], + SortedSet: [ + 1, + 2, + 3 + ], + EnumValue: Value10, + FlagsEnumValue: V1, V4, + EnumName: Value10, + EnumRawValue: Value20, + EnumStringValue: Value30, + EnumReverseStringValue: DtoValue3, + SubObject: { + SubIntValue: 2, + BaseIntValue: 1 + }, + DateTimeValue: 2020-01-03 15:10:05 Utc, + DateTimeValueTargetDateOnly: 2020-01-03 15:10:05 Utc, + DateTimeValueTargetTimeOnly: 2020-01-03 15:10:05 Utc, + ToByteArrayWithInstanceMethod: Guid_1, + WithCreateMethod: { + Value: 10 + }, + WithCreateFromMethod: { + Value: 20 + }, + WithFromSingleMethod: { + Value: 30 + }, + WithCreateParamsMethod: { + Value: 40 + }, + WithCreateFromParamsMethod: { + Value: 50 + }, + WithFromShortParamsMethod: { + Value: 60 + }, + WithToDecimalMethod: { + Value: 70 + }, + ExposePrivateValue: 18, + ExposeGenericPrivateValue: { + ExposedId: 10, + ExposedValue: { + Value: 3.3 + } + }, + SumComponent1: 32, + SumComponent2: 64 +} \ No newline at end of file diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningStrategyMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningStrategyMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs new file mode 100644 index 0000000000..fdd19ba606 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningStrategyMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs @@ -0,0 +1,258 @@ +// +#nullable enable +namespace Riok.Mapperly.IntegrationTests.Mapper +{ + public static partial class DeepCloningMapperWithCloningStrategy + { + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public static partial global::Riok.Mapperly.IntegrationTests.Models.IdObject Copy(global::Riok.Mapperly.IntegrationTests.Models.IdObject src) + { + var target = new global::Riok.Mapperly.IntegrationTests.Models.IdObject(); + target.IdValue = src.IdValue; + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + public static partial global::Riok.Mapperly.IntegrationTests.Models.TestObject Copy(global::Riok.Mapperly.IntegrationTests.Models.TestObject src) + { + var target = new global::Riok.Mapperly.IntegrationTests.Models.TestObject(src.CtorValue, ctorValue2: src.CtorValue2) + { + IntInitOnlyValue = src.IntInitOnlyValue, + RequiredValue = src.RequiredValue, + }; + target.IntValue = src.IntValue; + target.StringValue = src.StringValue; + target.RenamedStringValue = src.RenamedStringValue; + target.Flattening = Copy(src.Flattening); + if (src.NullableFlattening != null) + { + target.NullableFlattening = Copy(src.NullableFlattening); + } + else + { + target.NullableFlattening = null; + } + target.UnflatteningIdValue = src.UnflatteningIdValue; + target.NullableUnflatteningIdValue = src.NullableUnflatteningIdValue; + if (src.NestedNullable != null) + { + target.NestedNullable = MapToTestObjectNested(src.NestedNullable); + } + else + { + target.NestedNullable = null; + } + if (src.NestedNullableTargetNotNullable != null) + { + target.NestedNullableTargetNotNullable = MapToTestObjectNested(src.NestedNullableTargetNotNullable); + } + else + { + target.NestedNullableTargetNotNullable = null; + } + if (src.NestedMember != null) + { + target.NestedMember = MapToTestObjectNestedMember(src.NestedMember); + } + else + { + target.NestedMember = null; + } + target.StringNullableTargetNotNullable = src.StringNullableTargetNotNullable; + if (src.TupleValue != null) + { + target.TupleValue = MapToValueTupleOfStringAndString(src.TupleValue.Value); + } + else + { + target.TupleValue = null; + } + if (src.RecursiveObject != null) + { + target.RecursiveObject = Copy(src.RecursiveObject); + } + else + { + target.RecursiveObject = null; + } + if (src.SourceTargetSameObjectType != null) + { + target.SourceTargetSameObjectType = Copy(src.SourceTargetSameObjectType); + } + else + { + target.SourceTargetSameObjectType = null; + } + if (src.NullableReadOnlyObjectCollection != null) + { + target.NullableReadOnlyObjectCollection = MapToTestObjectNestedArray(src.NullableReadOnlyObjectCollection); + } + else + { + target.NullableReadOnlyObjectCollection = null; + } + target.MemoryValue = src.MemoryValue.Span.ToArray(); + target.StackValue = new global::System.Collections.Generic.Stack(global::System.Linq.Enumerable.Reverse(src.StackValue)); + target.QueueValue = new global::System.Collections.Generic.Queue(src.QueueValue); + target.ImmutableArrayValue = src.ImmutableArrayValue; + target.ImmutableListValue = src.ImmutableListValue; + target.ImmutableQueueValue = src.ImmutableQueueValue; + target.ImmutableStackValue = src.ImmutableStackValue; + target.ImmutableSortedSetValue = src.ImmutableSortedSetValue; + target.ImmutableDictionaryValue = src.ImmutableDictionaryValue; + target.ImmutableSortedDictionaryValue = src.ImmutableSortedDictionaryValue; + foreach (var item in src.ExistingISet) + { + target.ExistingISet.Add(item); + } + target.ExistingHashSet.EnsureCapacity(src.ExistingHashSet.Count + target.ExistingHashSet.Count); + foreach (var item1 in src.ExistingHashSet) + { + target.ExistingHashSet.Add(item1); + } + foreach (var item2 in src.ExistingSortedSet) + { + target.ExistingSortedSet.Add(item2); + } + target.ExistingList.EnsureCapacity(src.ExistingList.Count + target.ExistingList.Count); + foreach (var item3 in src.ExistingList) + { + target.ExistingList.Add(item3); + } + target.ISet = global::System.Linq.Enumerable.ToHashSet(src.ISet); + target.IReadOnlySet = global::System.Linq.Enumerable.ToHashSet(src.IReadOnlySet); + target.HashSet = global::System.Linq.Enumerable.ToHashSet(src.HashSet); + target.SortedSet = new global::System.Collections.Generic.SortedSet(src.SortedSet); + target.EnumValue = src.EnumValue; + target.FlagsEnumValue = src.FlagsEnumValue; + target.EnumName = src.EnumName; + target.EnumRawValue = src.EnumRawValue; + target.EnumStringValue = src.EnumStringValue; + target.EnumReverseStringValue = src.EnumReverseStringValue; + if (src.SubObject != null) + { + target.SubObject = MapToInheritanceSubObject(src.SubObject); + } + else + { + target.SubObject = null; + } + target.DateTimeValue = src.DateTimeValue; + target.DateTimeValueTargetDateOnly = src.DateTimeValueTargetDateOnly; + target.DateTimeValueTargetTimeOnly = src.DateTimeValueTargetTimeOnly; + target.ToByteArrayWithInstanceMethod = src.ToByteArrayWithInstanceMethod; + if (src.WithCreateMethod != null) + { + target.WithCreateMethod = global::Riok.Mapperly.IntegrationTests.Models.ConvertWithStaticMethodObject.ToConvertWithStaticMethodObject(src.WithCreateMethod); + } + else + { + target.WithCreateMethod = null; + } + if (src.WithCreateFromMethod != null) + { + target.WithCreateFromMethod = global::Riok.Mapperly.IntegrationTests.Models.ConvertWithStaticMethodObject.ToConvertWithStaticMethodObject(src.WithCreateFromMethod); + } + else + { + target.WithCreateFromMethod = null; + } + if (src.WithFromSingleMethod != null) + { + target.WithFromSingleMethod = global::Riok.Mapperly.IntegrationTests.Models.ConvertWithStaticMethodObject.ToConvertWithStaticMethodObject(src.WithFromSingleMethod); + } + else + { + target.WithFromSingleMethod = null; + } + if (src.WithCreateParamsMethod != null) + { + target.WithCreateParamsMethod = global::Riok.Mapperly.IntegrationTests.Models.ConvertWithStaticMethodObject.ToConvertWithStaticMethodObject(src.WithCreateParamsMethod); + } + else + { + target.WithCreateParamsMethod = null; + } + if (src.WithCreateFromParamsMethod != null) + { + target.WithCreateFromParamsMethod = global::Riok.Mapperly.IntegrationTests.Models.ConvertWithStaticMethodObject.ToConvertWithStaticMethodObject(src.WithCreateFromParamsMethod); + } + else + { + target.WithCreateFromParamsMethod = null; + } + if (src.WithFromShortParamsMethod != null) + { + target.WithFromShortParamsMethod = global::Riok.Mapperly.IntegrationTests.Models.ConvertWithStaticMethodObject.ToConvertWithStaticMethodObject(src.WithFromShortParamsMethod); + } + else + { + target.WithFromShortParamsMethod = null; + } + if (src.WithToDecimalMethod != null) + { + target.WithToDecimalMethod = global::Riok.Mapperly.IntegrationTests.Models.ConvertWithStaticMethodObject.ToConvertWithStaticMethodObject(src.WithToDecimalMethod); + } + else + { + target.WithToDecimalMethod = null; + } + target.SumComponent1 = src.SumComponent1; + target.SumComponent2 = src.SumComponent2; + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private static global::Riok.Mapperly.IntegrationTests.Models.TestObjectNested MapToTestObjectNested(global::Riok.Mapperly.IntegrationTests.Models.TestObjectNested source) + { + var target = new global::Riok.Mapperly.IntegrationTests.Models.TestObjectNested(); + target.IntValue = source.IntValue; + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private static global::Riok.Mapperly.IntegrationTests.Models.TestObjectNestedMember MapToTestObjectNestedMember(global::Riok.Mapperly.IntegrationTests.Models.TestObjectNestedMember source) + { + var target = new global::Riok.Mapperly.IntegrationTests.Models.TestObjectNestedMember(); + target.NestedMemberId = source.NestedMemberId; + if (source.NestedMemberObject != null) + { + target.NestedMemberObject = MapToTestObjectNested(source.NestedMemberObject); + } + else + { + target.NestedMemberObject = null; + } + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private static (string A, string) MapToValueTupleOfStringAndString((string A, string) source) + { + var target = (A: source.A, source.Item2); + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private static global::Riok.Mapperly.IntegrationTests.Models.TestObjectNested[] MapToTestObjectNestedArray(global::System.Collections.Generic.IReadOnlyCollection source) + { + var target = new global::Riok.Mapperly.IntegrationTests.Models.TestObjectNested[source.Count]; + var i = 0; + foreach (var item in source) + { + target[i] = MapToTestObjectNested(item); + i++; + } + return target; + } + + [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] + private static global::Riok.Mapperly.IntegrationTests.Models.InheritanceSubObject MapToInheritanceSubObject(global::Riok.Mapperly.IntegrationTests.Models.InheritanceSubObject source) + { + var target = new global::Riok.Mapperly.IntegrationTests.Models.InheritanceSubObject(); + target.SubIntValue = source.SubIntValue; + target.BaseIntValue = source.BaseIntValue; + return target; + } + } +} \ No newline at end of file diff --git a/test/Riok.Mapperly.Tests/Helpers/MapperConfigurationBuilderTest.cs b/test/Riok.Mapperly.Tests/Helpers/MapperConfigurationBuilderTest.cs index 3d7f8dbd0b..d309fbb99d 100644 --- a/test/Riok.Mapperly.Tests/Helpers/MapperConfigurationBuilderTest.cs +++ b/test/Riok.Mapperly.Tests/Helpers/MapperConfigurationBuilderTest.cs @@ -71,7 +71,7 @@ public void ShouldMergeMapperConfigurationsWithEmptyDefaultMapperConfiguration() mapper.ThrowOnPropertyMappingNullMismatch.ShouldBeTrue(); mapper.AllowNullPropertyAssignment.ShouldBeTrue(); mapper.UseDeepCloning.ShouldBeTrue(); - mapper.CloningBehaviour.ShouldBe(CloningBehaviour.ShallowCloning); + mapper.CloningStrategy.ShouldBe(CloningStrategy.ShallowCloning); mapper.EnabledConversions.ShouldBe(MappingConversionType.Constructor); mapper.UseReferenceHandling.ShouldBeTrue(); mapper.IgnoreObsoleteMembersStrategy.ShouldBe(IgnoreObsoleteMembersStrategy.Source); @@ -108,7 +108,7 @@ private MapperConfiguration NewMapperConfiguration() ThrowOnPropertyMappingNullMismatch = true, AllowNullPropertyAssignment = true, UseDeepCloning = true, - CloningBehaviour = CloningBehaviour.ShallowCloning, + CloningStrategy = CloningStrategy.ShallowCloning, EnabledConversions = MappingConversionType.Constructor, UseReferenceHandling = true, IgnoreObsoleteMembersStrategy = IgnoreObsoleteMembersStrategy.Source, diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs index 2a4b059617..9f513432a6 100644 --- a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs +++ b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs @@ -107,7 +107,7 @@ private static string BuildAttribute(TestSourceBuilderOptions options) Attribute(options.IncludedConstructors), Attribute(options.PreferParameterlessConstructors), Attribute(options.AutoUserMappings), - Attribute(options.CloningBehaviour), + Attribute(options.CloningStrategy), }.WhereNotNull(); return $"[Mapper({string.Join(", ", attrs)})]"; diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs index f318d70cdb..203fd2ca65 100644 --- a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs +++ b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs @@ -8,7 +8,7 @@ public record TestSourceBuilderOptions( string MapperClassName = TestSourceBuilderOptions.DefaultMapperClassName, string? MapperBaseClassName = null, bool? UseDeepCloning = null, - CloningBehaviour? CloningBehaviour = null, + CloningStrategy? CloningStrategy = null, StackCloningStrategy? StackCloningStrategy = null, bool? UseReferenceHandling = null, bool? ThrowOnMappingNullMismatch = null, @@ -38,9 +38,7 @@ public record TestSourceBuilderOptions( public static readonly TestSourceBuilderOptions AsStatic = new(Static: true); public static readonly TestSourceBuilderOptions WithDeepCloning = new(UseDeepCloning: true); - public static readonly TestSourceBuilderOptions WithShallowCloning = new( - CloningBehaviour: Abstractions.CloningBehaviour.ShallowCloning - ); + public static readonly TestSourceBuilderOptions WithShallowCloning = new(CloningStrategy: Abstractions.CloningStrategy.ShallowCloning); public static readonly TestSourceBuilderOptions AllConversionsWithDeepCloning = new TestSourceBuilderOptions( UseDeepCloning: true, EnabledConversions: MappingConversionType.All From 5e9cca4bdb12fe408a0a95af2f601c06ac217a8b Mon Sep 17 00:00:00 2001 From: Alessandro Losi Date: Fri, 6 Feb 2026 15:38:52 +0100 Subject: [PATCH 6/9] Removed obsolete files --- ...t.RunMappingShouldWork_NET9_0.verified.txt | 229 ---------------- ...SnapshotGeneratedSource_NET8_0.verified.cs | 258 ------------------ 2 files changed, 487 deletions(-) delete mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningBehaviourMapperTest.RunMappingShouldWork_NET9_0.verified.txt delete mode 100644 test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningBehaviourMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningBehaviourMapperTest.RunMappingShouldWork_NET9_0.verified.txt b/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningBehaviourMapperTest.RunMappingShouldWork_NET9_0.verified.txt deleted file mode 100644 index 51f40f465b..0000000000 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningBehaviourMapperTest.RunMappingShouldWork_NET9_0.verified.txt +++ /dev/null @@ -1,229 +0,0 @@ -{ - CtorValue: 7, - CtorValue2: 100, - IntValue: 10, - IntInitOnlyValue: 3, - RequiredValue: 4, - UnmappedValue: 10, - StringValue: fooBar, - RenamedStringValue: fooBar2, - Flattening: { - IdValue: 10 - }, - NullableFlattening: { - IdValue: 100 - }, - UnflatteningIdValue: 20, - NullableUnflatteningIdValue: 200, - NestedNullable: { - IntValue: 100 - }, - NestedNullableTargetNotNullable: {}, - NestedMember: { - NestedMemberId: 12, - NestedMemberObject: { - IntValue: 22 - } - }, - StringNullableTargetNotNullable: fooBar3, - TupleValue: { - Item1: 10, - Item2: 20 - }, - RecursiveObject: { - CtorValue: 5, - CtorValue2: 100, - RequiredValue: 4, - UnmappedValue: 10, - StringValue: , - RenamedStringValue: , - Flattening: {}, - MemoryValue: { - IsEmpty: true - }, - ImmutableArrayValue: null, - ImmutableQueueValue: [], - ImmutableStackValue: [], - EnumValue: Value10, - EnumName: Value30, - EnumReverseStringValue: DtoValue3, - ExposePrivateValue: 16, - ExposeGenericPrivateValue: { - ExposedId: 10, - ExposedValue: { - Value: 3.3 - } - } - }, - SourceTargetSameObjectType: { - CtorValue: 8, - CtorValue2: 100, - IntValue: 99, - RequiredValue: 98, - UnmappedValue: 10, - StringValue: , - RenamedStringValue: , - Flattening: {}, - NestedMember: { - NestedMemberId: 123, - NestedMemberObject: { - IntValue: 223 - } - }, - MemoryValue: { - IsEmpty: true - }, - ImmutableArrayValue: null, - ImmutableQueueValue: [], - ImmutableStackValue: [], - EnumReverseStringValue: , - ExposePrivateValue: 19, - ExposeGenericPrivateValue: { - ExposedId: 10, - ExposedValue: { - Value: 3.3 - } - } - }, - NullableReadOnlyObjectCollection: [ - { - IntValue: 10 - }, - { - IntValue: 20 - } - ], - MemoryValue: { - Length: 3, - IsEmpty: false - }, - StackValue: [ - 3, - 2, - 1 - ], - QueueValue: [ - 1, - 2, - 3 - ], - ImmutableArrayValue: [ - 1, - 2, - 3 - ], - ImmutableListValue: [ - 1, - 2, - 3 - ], - ImmutableQueueValue: [ - 1, - 2, - 3 - ], - ImmutableStackValue: [ - 3, - 2, - 1 - ], - ImmutableSortedSetValue: [ - 1, - 2, - 3 - ], - ImmutableDictionaryValue: { - 1: 1, - 2: 2, - 3: 3 - }, - ImmutableSortedDictionaryValue: { - 1: 1, - 2: 2, - 3: 3 - }, - ExistingISet: [ - 1, - 2, - 3 - ], - ExistingHashSet: [ - 1, - 2, - 3 - ], - ExistingSortedSet: [ - 1, - 2, - 3 - ], - ExistingList: [ - 1, - 2, - 3 - ], - ISet: [ - 1, - 2, - 3 - ], - IReadOnlySet: [ - 1, - 2, - 3 - ], - HashSet: [ - 1, - 2, - 3 - ], - SortedSet: [ - 1, - 2, - 3 - ], - EnumValue: Value10, - FlagsEnumValue: V1, V4, - EnumName: Value10, - EnumRawValue: Value20, - EnumStringValue: Value30, - EnumReverseStringValue: DtoValue3, - SubObject: { - SubIntValue: 2, - BaseIntValue: 1 - }, - DateTimeValue: 2020-01-03 15:10:05 Utc, - DateTimeValueTargetDateOnly: 2020-01-03 15:10:05 Utc, - DateTimeValueTargetTimeOnly: 2020-01-03 15:10:05 Utc, - ToByteArrayWithInstanceMethod: Guid_1, - WithCreateMethod: { - Value: 10 - }, - WithCreateFromMethod: { - Value: 20 - }, - WithFromSingleMethod: { - Value: 30 - }, - WithCreateParamsMethod: { - Value: 40 - }, - WithCreateFromParamsMethod: { - Value: 50 - }, - WithFromShortParamsMethod: { - Value: 60 - }, - WithToDecimalMethod: { - Value: 70 - }, - ExposePrivateValue: 18, - ExposeGenericPrivateValue: { - ExposedId: 10, - ExposedValue: { - Value: 3.3 - } - }, - SumComponent1: 32, - SumComponent2: 64 -} \ No newline at end of file diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningBehaviourMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningBehaviourMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs deleted file mode 100644 index 48be9fbb05..0000000000 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/DeepCloningWithCloningBehaviourMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs +++ /dev/null @@ -1,258 +0,0 @@ -// -#nullable enable -namespace Riok.Mapperly.IntegrationTests.Mapper -{ - public static partial class DeepCloningMapperWithCloningBehaviour - { - [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] - public static partial global::Riok.Mapperly.IntegrationTests.Models.IdObject Copy(global::Riok.Mapperly.IntegrationTests.Models.IdObject src) - { - var target = new global::Riok.Mapperly.IntegrationTests.Models.IdObject(); - target.IdValue = src.IdValue; - return target; - } - - [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] - public static partial global::Riok.Mapperly.IntegrationTests.Models.TestObject Copy(global::Riok.Mapperly.IntegrationTests.Models.TestObject src) - { - var target = new global::Riok.Mapperly.IntegrationTests.Models.TestObject(src.CtorValue, ctorValue2: src.CtorValue2) - { - IntInitOnlyValue = src.IntInitOnlyValue, - RequiredValue = src.RequiredValue, - }; - target.IntValue = src.IntValue; - target.StringValue = src.StringValue; - target.RenamedStringValue = src.RenamedStringValue; - target.Flattening = Copy(src.Flattening); - if (src.NullableFlattening != null) - { - target.NullableFlattening = Copy(src.NullableFlattening); - } - else - { - target.NullableFlattening = null; - } - target.UnflatteningIdValue = src.UnflatteningIdValue; - target.NullableUnflatteningIdValue = src.NullableUnflatteningIdValue; - if (src.NestedNullable != null) - { - target.NestedNullable = MapToTestObjectNested(src.NestedNullable); - } - else - { - target.NestedNullable = null; - } - if (src.NestedNullableTargetNotNullable != null) - { - target.NestedNullableTargetNotNullable = MapToTestObjectNested(src.NestedNullableTargetNotNullable); - } - else - { - target.NestedNullableTargetNotNullable = null; - } - if (src.NestedMember != null) - { - target.NestedMember = MapToTestObjectNestedMember(src.NestedMember); - } - else - { - target.NestedMember = null; - } - target.StringNullableTargetNotNullable = src.StringNullableTargetNotNullable; - if (src.TupleValue != null) - { - target.TupleValue = MapToValueTupleOfStringAndString(src.TupleValue.Value); - } - else - { - target.TupleValue = null; - } - if (src.RecursiveObject != null) - { - target.RecursiveObject = Copy(src.RecursiveObject); - } - else - { - target.RecursiveObject = null; - } - if (src.SourceTargetSameObjectType != null) - { - target.SourceTargetSameObjectType = Copy(src.SourceTargetSameObjectType); - } - else - { - target.SourceTargetSameObjectType = null; - } - if (src.NullableReadOnlyObjectCollection != null) - { - target.NullableReadOnlyObjectCollection = MapToTestObjectNestedArray(src.NullableReadOnlyObjectCollection); - } - else - { - target.NullableReadOnlyObjectCollection = null; - } - target.MemoryValue = src.MemoryValue.Span.ToArray(); - target.StackValue = new global::System.Collections.Generic.Stack(global::System.Linq.Enumerable.Reverse(src.StackValue)); - target.QueueValue = new global::System.Collections.Generic.Queue(src.QueueValue); - target.ImmutableArrayValue = src.ImmutableArrayValue; - target.ImmutableListValue = src.ImmutableListValue; - target.ImmutableQueueValue = src.ImmutableQueueValue; - target.ImmutableStackValue = src.ImmutableStackValue; - target.ImmutableSortedSetValue = src.ImmutableSortedSetValue; - target.ImmutableDictionaryValue = src.ImmutableDictionaryValue; - target.ImmutableSortedDictionaryValue = src.ImmutableSortedDictionaryValue; - foreach (var item in src.ExistingISet) - { - target.ExistingISet.Add(item); - } - target.ExistingHashSet.EnsureCapacity(src.ExistingHashSet.Count + target.ExistingHashSet.Count); - foreach (var item1 in src.ExistingHashSet) - { - target.ExistingHashSet.Add(item1); - } - foreach (var item2 in src.ExistingSortedSet) - { - target.ExistingSortedSet.Add(item2); - } - target.ExistingList.EnsureCapacity(src.ExistingList.Count + target.ExistingList.Count); - foreach (var item3 in src.ExistingList) - { - target.ExistingList.Add(item3); - } - target.ISet = global::System.Linq.Enumerable.ToHashSet(src.ISet); - target.IReadOnlySet = global::System.Linq.Enumerable.ToHashSet(src.IReadOnlySet); - target.HashSet = global::System.Linq.Enumerable.ToHashSet(src.HashSet); - target.SortedSet = new global::System.Collections.Generic.SortedSet(src.SortedSet); - target.EnumValue = src.EnumValue; - target.FlagsEnumValue = src.FlagsEnumValue; - target.EnumName = src.EnumName; - target.EnumRawValue = src.EnumRawValue; - target.EnumStringValue = src.EnumStringValue; - target.EnumReverseStringValue = src.EnumReverseStringValue; - if (src.SubObject != null) - { - target.SubObject = MapToInheritanceSubObject(src.SubObject); - } - else - { - target.SubObject = null; - } - target.DateTimeValue = src.DateTimeValue; - target.DateTimeValueTargetDateOnly = src.DateTimeValueTargetDateOnly; - target.DateTimeValueTargetTimeOnly = src.DateTimeValueTargetTimeOnly; - target.ToByteArrayWithInstanceMethod = src.ToByteArrayWithInstanceMethod; - if (src.WithCreateMethod != null) - { - target.WithCreateMethod = global::Riok.Mapperly.IntegrationTests.Models.ConvertWithStaticMethodObject.ToConvertWithStaticMethodObject(src.WithCreateMethod); - } - else - { - target.WithCreateMethod = null; - } - if (src.WithCreateFromMethod != null) - { - target.WithCreateFromMethod = global::Riok.Mapperly.IntegrationTests.Models.ConvertWithStaticMethodObject.ToConvertWithStaticMethodObject(src.WithCreateFromMethod); - } - else - { - target.WithCreateFromMethod = null; - } - if (src.WithFromSingleMethod != null) - { - target.WithFromSingleMethod = global::Riok.Mapperly.IntegrationTests.Models.ConvertWithStaticMethodObject.ToConvertWithStaticMethodObject(src.WithFromSingleMethod); - } - else - { - target.WithFromSingleMethod = null; - } - if (src.WithCreateParamsMethod != null) - { - target.WithCreateParamsMethod = global::Riok.Mapperly.IntegrationTests.Models.ConvertWithStaticMethodObject.ToConvertWithStaticMethodObject(src.WithCreateParamsMethod); - } - else - { - target.WithCreateParamsMethod = null; - } - if (src.WithCreateFromParamsMethod != null) - { - target.WithCreateFromParamsMethod = global::Riok.Mapperly.IntegrationTests.Models.ConvertWithStaticMethodObject.ToConvertWithStaticMethodObject(src.WithCreateFromParamsMethod); - } - else - { - target.WithCreateFromParamsMethod = null; - } - if (src.WithFromShortParamsMethod != null) - { - target.WithFromShortParamsMethod = global::Riok.Mapperly.IntegrationTests.Models.ConvertWithStaticMethodObject.ToConvertWithStaticMethodObject(src.WithFromShortParamsMethod); - } - else - { - target.WithFromShortParamsMethod = null; - } - if (src.WithToDecimalMethod != null) - { - target.WithToDecimalMethod = global::Riok.Mapperly.IntegrationTests.Models.ConvertWithStaticMethodObject.ToConvertWithStaticMethodObject(src.WithToDecimalMethod); - } - else - { - target.WithToDecimalMethod = null; - } - target.SumComponent1 = src.SumComponent1; - target.SumComponent2 = src.SumComponent2; - return target; - } - - [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] - private static global::Riok.Mapperly.IntegrationTests.Models.TestObjectNested MapToTestObjectNested(global::Riok.Mapperly.IntegrationTests.Models.TestObjectNested source) - { - var target = new global::Riok.Mapperly.IntegrationTests.Models.TestObjectNested(); - target.IntValue = source.IntValue; - return target; - } - - [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] - private static global::Riok.Mapperly.IntegrationTests.Models.TestObjectNestedMember MapToTestObjectNestedMember(global::Riok.Mapperly.IntegrationTests.Models.TestObjectNestedMember source) - { - var target = new global::Riok.Mapperly.IntegrationTests.Models.TestObjectNestedMember(); - target.NestedMemberId = source.NestedMemberId; - if (source.NestedMemberObject != null) - { - target.NestedMemberObject = MapToTestObjectNested(source.NestedMemberObject); - } - else - { - target.NestedMemberObject = null; - } - return target; - } - - [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] - private static (string A, string) MapToValueTupleOfStringAndString((string A, string) source) - { - var target = (A: source.A, source.Item2); - return target; - } - - [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] - private static global::Riok.Mapperly.IntegrationTests.Models.TestObjectNested[] MapToTestObjectNestedArray(global::System.Collections.Generic.IReadOnlyCollection source) - { - var target = new global::Riok.Mapperly.IntegrationTests.Models.TestObjectNested[source.Count]; - var i = 0; - foreach (var item in source) - { - target[i] = MapToTestObjectNested(item); - i++; - } - return target; - } - - [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "0.0.1.0")] - private static global::Riok.Mapperly.IntegrationTests.Models.InheritanceSubObject MapToInheritanceSubObject(global::Riok.Mapperly.IntegrationTests.Models.InheritanceSubObject source) - { - var target = new global::Riok.Mapperly.IntegrationTests.Models.InheritanceSubObject(); - target.SubIntValue = source.SubIntValue; - target.BaseIntValue = source.BaseIntValue; - return target; - } - } -} \ No newline at end of file From 44d7cc7fa9949dcff291c0d06fb3e22ba0393283 Mon Sep 17 00:00:00 2001 From: Alessandro Losi Date: Fri, 6 Feb 2026 16:04:14 +0100 Subject: [PATCH 7/9] Implemented feedback suggestions --- .../EnumerableMappingBuilder.cs | 10 +++---- .../MappingBuilders/MemoryMappingBuilder.cs | 8 ++--- .../MappingBuilders/SpanMappingBuilder.cs | 5 +--- .../Mapper/ShallowCloningMapper.cs | 2 ++ .../ShallowCloningMapperTest.cs | 25 ++++++++++++++++ ...SnapshotGeneratedSource_NET8_0.verified.cs | 29 +++---------------- 6 files changed, 40 insertions(+), 39 deletions(-) diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs index 669cb14d58..566ff0d937 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs @@ -95,7 +95,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 + // cannot cast if the method mapping is synthetic, cloning is enabled or target is an unknown collection if (!elementMapping.IsSynthetic || ctx.UseCloning || ctx.CollectionInfos!.Target.CollectionType == CollectionType.None) { return null; @@ -198,9 +198,7 @@ private static INewInstanceMapping BuildArrayToArrayMapping(MappingBuilderContex // use a for loop mapping otherwise. if (elementMapping.IsSynthetic) { - return ctx.Configuration.CloningStrategy == CloningStrategy.DeepCloning - ? new ArrayCloneMapping(ctx.Source, ctx.Target) - : new CastMapping(ctx.Source, ctx.Target); + return ctx.UseCloning ? new ArrayCloneMapping(ctx.Source, ctx.Target) : new CastMapping(ctx.Source, ctx.Target); } // ensure the target is an array and not an interface @@ -316,9 +314,9 @@ private static (bool CanMapWithLinq, string? CollectMethod) ResolveCollectMethod return (true, ToArrayMethodName); // if the target is an IEnumerable don't collect at all - // except deep cloning is enabled. + // except when cloning is enabled. var targetIsIEnumerable = ctx.CollectionInfos!.Target.CollectionType == CollectionType.IEnumerable; - if (targetIsIEnumerable && ctx.Configuration.CloningStrategy != CloningStrategy.DeepCloning) + if (targetIsIEnumerable && !ctx.UseCloning) return (true, null); // if the target is IReadOnlyCollection or IEnumerable diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/MemoryMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/MemoryMappingBuilder.cs index 9907f49a2b..a2ee790392 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/MemoryMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/MemoryMappingBuilder.cs @@ -117,7 +117,7 @@ public static class MemoryMappingBuilder private static NewInstanceMapping? BuildMemoryToArrayMapping(MappingBuilderContext ctx, INewInstanceMapping elementMapping) { - if (!elementMapping.IsSynthetic || ctx.Configuration.CloningStrategy == CloningStrategy.DeepCloning) + if (!elementMapping.IsSynthetic || ctx.UseCloning) return BuildSpanToArrayMethodMapping(ctx, elementMapping); return new SourceObjectMethodMapping(ctx.Source, ctx.Target, ToArrayMethodName); @@ -125,7 +125,7 @@ public static class MemoryMappingBuilder private static NewInstanceMapping? BuildMemoryToSpanMapping(MappingBuilderContext ctx, INewInstanceMapping elementMapping) { - if (!elementMapping.IsSynthetic || ctx.Configuration.CloningStrategy == CloningStrategy.DeepCloning) + if (!elementMapping.IsSynthetic || ctx.UseCloning) return BuildMemoryToSpanMethod(ctx, elementMapping); return new SourceObjectMemberMapping(ctx.Source, ctx.Target, SpanMemberName); @@ -133,7 +133,7 @@ public static class MemoryMappingBuilder private static INewInstanceMapping BuildArrayToMemoryMapping(MappingBuilderContext ctx, INewInstanceMapping elementMapping) { - if (!elementMapping.IsSynthetic || ctx.Configuration.CloningStrategy == CloningStrategy.DeepCloning) + if (!elementMapping.IsSynthetic || ctx.UseCloning) return new ArrayForMapping( ctx.Source, ctx.Types.GetArrayType(elementMapping.TargetType), @@ -155,7 +155,7 @@ private static INewInstanceMapping BuildArrayToMemoryMapping(MappingBuilderConte private static NewInstanceMapping? BuildMemoryToMemoryMapping(MappingBuilderContext ctx, INewInstanceMapping elementMapping) { - if (!elementMapping.IsSynthetic || ctx.Configuration.CloningStrategy == CloningStrategy.DeepCloning) + if (!elementMapping.IsSynthetic || ctx.UseCloning) return BuildSpanToArrayMethodMapping(ctx, elementMapping); return new CastMapping(ctx.Source, ctx.Target); diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/SpanMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/SpanMappingBuilder.cs index fea885d625..c5892d26f7 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/SpanMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/SpanMappingBuilder.cs @@ -44,10 +44,7 @@ public static class SpanMappingBuilder // if the source is Span/ReadOnlySpan or Array and target is Span/ReadOnlySpan // and element type is the same, then direct cast (CollectionType.Span or CollectionType.ReadOnlySpan or CollectionType.Array, CollectionType.Span or CollectionType.ReadOnlySpan) - when elementMapping.IsSynthetic && ctx.Configuration.CloningStrategy != CloningStrategy.DeepCloning => new CastMapping( - ctx.Source, - ctx.Target - ), + when elementMapping.IsSynthetic && !ctx.UseCloning => new CastMapping(ctx.Source, ctx.Target), // otherwise map each value into an Array _ => BuildToArrayOrMap(ctx, elementMapping), diff --git a/test/Riok.Mapperly.IntegrationTests/Mapper/ShallowCloningMapper.cs b/test/Riok.Mapperly.IntegrationTests/Mapper/ShallowCloningMapper.cs index c9b2f5991d..34a206abf9 100644 --- a/test/Riok.Mapperly.IntegrationTests/Mapper/ShallowCloningMapper.cs +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/ShallowCloningMapper.cs @@ -6,6 +6,7 @@ namespace Riok.Mapperly.IntegrationTests.Mapper [Mapper(CloningStrategy = CloningStrategy.ShallowCloning)] public static partial class ShallowCloningMapper { + [UserMapping(Default = false)] public static partial IdObject Copy(IdObject src); [MapperIgnoreSource(nameof(TestObject.IgnoredIntValue))] @@ -13,6 +14,7 @@ public static partial class ShallowCloningMapper [MapperIgnoreSource(nameof(TestObject.ImmutableHashSetValue))] [MapperIgnoreSource(nameof(TestObject.SpanValue))] [MapperIgnoreObsoleteMembers] + [UserMapping(Default = false)] public static partial TestObject Copy(TestObject src); } } diff --git a/test/Riok.Mapperly.IntegrationTests/ShallowCloningMapperTest.cs b/test/Riok.Mapperly.IntegrationTests/ShallowCloningMapperTest.cs index 63ce65e588..397c88e5b3 100644 --- a/test/Riok.Mapperly.IntegrationTests/ShallowCloningMapperTest.cs +++ b/test/Riok.Mapperly.IntegrationTests/ShallowCloningMapperTest.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using Riok.Mapperly.IntegrationTests.Helpers; using Riok.Mapperly.IntegrationTests.Mapper; using Riok.Mapperly.IntegrationTests.Models; @@ -44,5 +45,29 @@ public void RunMappingWithMapperAvoidReturningSourceReference() source.ShouldNotBeSameAs(copy); copy.RequiredValue.ShouldBe(999); } + + [Fact] + public void RunMappingWithMapperAvoidCloningChildObjects() + { + var nested = new TestObjectNested() { IntValue = int.MaxValue }; + + var idObject = new IdObject() { IdValue = 7 }; + + var source = new TestObject(255, -1, 7) + { + RequiredValue = 999, + NestedNullable = nested, + NestedNullableTargetNotNullable = nested, + Flattening = idObject, + }; + var copy = ShallowCloningMapper.Copy(source); + source.ShouldNotBeSameAs(copy); + copy.RequiredValue.ShouldBe(999); + + // check the references are exactly the same + copy.Flattening.ShouldBeSameAs(idObject); + copy.NestedNullable.ShouldBeSameAs(nested); + copy.NestedNullableTargetNotNullable.ShouldBeSameAs(nested); + } } } diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/ShallowCloningMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/ShallowCloningMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs index 9834e9582d..03fc36887a 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/ShallowCloningMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ShallowCloningMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs @@ -23,15 +23,8 @@ public static partial class ShallowCloningMapper target.IntValue = src.IntValue; target.StringValue = src.StringValue; target.RenamedStringValue = src.RenamedStringValue; - target.Flattening = Copy(src.Flattening); - if (src.NullableFlattening != null) - { - target.NullableFlattening = Copy(src.NullableFlattening); - } - else - { - target.NullableFlattening = null; - } + target.Flattening = src.Flattening; + target.NullableFlattening = src.NullableFlattening; target.UnflatteningIdValue = src.UnflatteningIdValue; target.NullableUnflatteningIdValue = src.NullableUnflatteningIdValue; target.NestedNullable = src.NestedNullable; @@ -39,22 +32,8 @@ public static partial class ShallowCloningMapper target.NestedMember = src.NestedMember; target.StringNullableTargetNotNullable = src.StringNullableTargetNotNullable; target.TupleValue = src.TupleValue; - if (src.RecursiveObject != null) - { - target.RecursiveObject = Copy(src.RecursiveObject); - } - else - { - target.RecursiveObject = null; - } - if (src.SourceTargetSameObjectType != null) - { - target.SourceTargetSameObjectType = Copy(src.SourceTargetSameObjectType); - } - else - { - target.SourceTargetSameObjectType = null; - } + target.RecursiveObject = src.RecursiveObject; + target.SourceTargetSameObjectType = src.SourceTargetSameObjectType; target.NullableReadOnlyObjectCollection = src.NullableReadOnlyObjectCollection; target.MemoryValue = src.MemoryValue; target.StackValue = src.StackValue; From 367fffad477bea335491baac036e9af88e303670 Mon Sep 17 00:00:00 2001 From: Alessandro Losi Date: Fri, 6 Feb 2026 16:55:35 +0100 Subject: [PATCH 8/9] Updated docs --- docs/docs/breaking-changes/5-0.md | 14 +++++++++++++- docs/docs/configuration/conversions.md | 4 ++-- docs/docs/configuration/mapper.mdx | 14 +++++++++----- docs/docs/configuration/queryable-projections.mdx | 2 +- .../configuration/user-implemented-methods.mdx | 2 +- 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/docs/docs/breaking-changes/5-0.md b/docs/docs/breaking-changes/5-0.md index 09281f9a6f..46891c0246 100644 --- a/docs/docs/breaking-changes/5-0.md +++ b/docs/docs/breaking-changes/5-0.md @@ -27,6 +27,18 @@ but have not handled the newly discovered members. Ignore these members to restore the previous behavior. See the [private members mapping documentation](../configuration/private-members.mdx) for more details. +## `UseDeepCloning` has been deprecated in favour of `CloningStrategy` + +To support allow the implementation of shallow cloning, the `UseDeepCloning` property has been deprecated. + +Two strategies are available: +- `DeepCloning` - which corresponds to the previous `UseDeepCloning` behaviour +- `ShallowCloning` - which allows to clone an object into a new destination of the same type, + while retaining the same values as the source. All the child properties will be copied as-is, reference types included. + +If you previously used `UseDeepCloning = true`, you can replace it with `CloningStrategy = CloningStrategy.DeepCloning` +to retain the same behaviour. + ## `Stack` deep cloning order When deep cloning a `Stack`, Mapperly now preserves the order of elements by default. @@ -37,7 +49,7 @@ To restore the previous behavior, set `StackCloningStrategy` to `StackCloningStr ```csharp [assembly: MapperDefaults(StackCloningStrategy = StackCloningStrategy.ReverseOrder)] // or -[Mapper(UseDeepCloning = true, StackCloningStrategy = StackCloningStrategy.ReverseOrder)] +[Mapper(CloningStrategy = CloningStrategy.DeepCloning, StackCloningStrategy = StackCloningStrategy.ReverseOrder)] public partial class MyMapper { // ... diff --git a/docs/docs/configuration/conversions.md b/docs/docs/configuration/conversions.md index ce93a0bc96..40485a9fdc 100644 --- a/docs/docs/configuration/conversions.md +++ b/docs/docs/configuration/conversions.md @@ -8,8 +8,8 @@ description: A list of conversions supported by Mapperly Mapperly implements several types of automatic conversions (in order of priority): | Name | Description | Conditions | -| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Direct assignment | Directly assigns the source object to the target | Source type is assignable to the target type and `UseDeepCloning` is `false` | +| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Direct assignment | Directly assigns the source object to the target | Source type is assignable to the target type and no `CloningStrategy` active | | Queryable | Projects the source queryable to the target queryable | Source and target types are `IQueryable<>` | | Dictionary | Maps a source dictionary to an enumerable target | Source type is an `IDictionary<,>` or an `IReadOnlyDictionary<,>` | | Enumerable | Maps an enumerable source to an enumerable target | Source type is an `IEnumerable<>` | diff --git a/docs/docs/configuration/mapper.mdx b/docs/docs/configuration/mapper.mdx index 7b100e52bf..a4682d9166 100644 --- a/docs/docs/configuration/mapper.mdx +++ b/docs/docs/configuration/mapper.mdx @@ -35,14 +35,14 @@ Configurations set via `MapperDefaultsAttribute` take precedence over MSBuild pr ## Copy behavior -By default, Mapperly does not create deep copies of objects to improve performance. +By default, Mapperly does not create 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`. +To create copies, set the `CloningStrategy` property on the `MapperAttribute` to the desired cloning strategy. ```csharp // highlight-start -[Mapper(UseDeepCloning = true)] +[Mapper(CloningStrategy = CloningStrategy.DeepCloning or CloningStrategy.ShallowCloning)] // highlight-end public partial class CarMapper { @@ -50,8 +50,12 @@ public partial class CarMapper } ``` +Using `CloningStrategy.DeepCloning` will perform deep copies, so all reference types will be cloned recursively, +meanwhile by using `CloningStrategy.ShallowCloning` a new instance will be returned with all the valued copied 1:1 from +the source object, without performing any cloning on child properties. + :::note -Deep cloning is not applied to `IQueryable` projection mappings. +Cloning is not applied to `IQueryable` projection mappings. ::: ### Stack cloning strategy @@ -63,7 +67,7 @@ This behavior can be configured via the `StackCloningStrategy` property on the ` - `StackCloningStrategy.ReverseOrder`: Reverses the order of elements (legacy behavior). ```csharp -[Mapper(UseDeepCloning = true, StackCloningStrategy = StackCloningStrategy.ReverseOrder)] +[Mapper(CloningStrategy = CloningStrategy.DeepCloning, StackCloningStrategy = StackCloningStrategy.ReverseOrder)] public partial class MyMapper { // ... diff --git a/docs/docs/configuration/queryable-projections.mdx b/docs/docs/configuration/queryable-projections.mdx index 35f583d1a1..61328f8680 100644 --- a/docs/docs/configuration/queryable-projections.mdx +++ b/docs/docs/configuration/queryable-projections.mdx @@ -53,7 +53,7 @@ such mappings have several limitations: - Enum mappings do not support the `ByName` strategy - Reference handling is not supported - Nullable reference types are disabled -- Deep cloning is not applied +- Cloning is not applied ::: ## Property configurations diff --git a/docs/docs/configuration/user-implemented-methods.mdx b/docs/docs/configuration/user-implemented-methods.mdx index d61974f179..8afe112a4c 100644 --- a/docs/docs/configuration/user-implemented-methods.mdx +++ b/docs/docs/configuration/user-implemented-methods.mdx @@ -102,7 +102,7 @@ See [user-implemented property conversions](./mapper.mdx#user-implemented-proper The mapper will respect the `ref` keyword on the target parameter when using an existing target function, allowing the reference to be updated in the caller. ```csharp - [Mapper(AllowNullPropertyAssignment = false, UseDeepCloning = true, RequiredMappingStrategy = RequiredMappingStrategy.Source)] + [Mapper(AllowNullPropertyAssignment = false, CloningStrategy = CloningStrategy.DeepCloning, RequiredMappingStrategy = RequiredMappingStrategy.Source)] public static partial class UseUserMethodWithRef { [MapProperty(nameof(ArrayObject.IntArray), nameof(ArrayObject.IntArray), Use = nameof(MapArray))] // `Use` is required otherwise it will generate it's own From bb9bd40bf7dc13be79fa4a685b25305fe4c9fb7b Mon Sep 17 00:00:00 2001 From: Alessandro Losi Date: Fri, 6 Feb 2026 17:27:55 +0100 Subject: [PATCH 9/9] Added tests for both cases of shallow cloning with and without [UserMapping(Default = false)] --- .../Mapping/ShallowCloneTest.cs | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/test/Riok.Mapperly.Tests/Mapping/ShallowCloneTest.cs b/test/Riok.Mapperly.Tests/Mapping/ShallowCloneTest.cs index f697de9a35..0d964165c2 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ShallowCloneTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ShallowCloneTest.cs @@ -1,4 +1,6 @@ -namespace Riok.Mapperly.Tests.Mapping; +using Riok.Mapperly.Abstractions; + +namespace Riok.Mapperly.Tests.Mapping; public class ShallowCloneTest { @@ -9,8 +11,52 @@ public void ShallowCloneShouldNotReturnOriginalInstance() "A", "A", TestSourceBuilderOptions.WithShallowCloning, - "class A { public int Value { get; set; } public List List { get; set; } }" + """ + class A + { + public int Value { get; set; } + public List List { get; set; } + public A NestedReference { get; set; } + }" + """ + ); + TestHelper + .GenerateMapper(source) + .Should() + .HaveSingleMethodBody( + """ + var target = new global::A(); + target.Value = source.Value; + target.List = source.List; + target.NestedReference = Map(source.NestedReference); + return target; + """ + ); + } + + [Fact] + public void ShallowCloneWithNoUserMappingsShouldDirectAssignAllProperties() + { + var source = TestSourceBuilder.CSharp( + """ + using Riok.Mapperly.Abstractions; + + [Mapper(CloningStrategy = CloningStrategy.ShallowCloning)] + public partial class Mapper + { + [UserMapping(Default = false)] + public partial A Map(A source); + } + + class A + { + public int Value { get; set; } + public List List { get; set; } + public A NestedReference { get; set; } + } + """ ); + TestHelper .GenerateMapper(source) .Should() @@ -19,6 +65,7 @@ public void ShallowCloneShouldNotReturnOriginalInstance() var target = new global::A(); target.Value = source.Value; target.List = source.List; + target.NestedReference = source.NestedReference; return target; """ );