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 diff --git a/src/Riok.Mapperly.Abstractions/CloningStrategy.cs b/src/Riok.Mapperly.Abstractions/CloningStrategy.cs new file mode 100644 index 0000000000..ebce7920f3 --- /dev/null +++ b/src/Riok.Mapperly.Abstractions/CloningStrategy.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 CloningStrategy +{ + /// + /// 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..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,8 +66,19 @@ public class MapperAttribute : Attribute /// when false, the same array is reused. /// when true, the array and each person is cloned. /// + /// + /// 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 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 68893d366a..57a7d7620e 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 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 4d625f49f3..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,6 +62,9 @@ public static MapperAttribute MergeToAttribute(MapperConfiguration mapperConfigu mapper.UseDeepCloning = mapperConfiguration.UseDeepCloning ?? defaultMapperConfiguration.UseDeepCloning ?? mapper.UseDeepCloning; + 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 df543acf54..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, + mapper.UseDeepCloning ? CloningStrategy.DeepCloning : mapper.CloningStrategy, mapper.StackCloningStrategy, supportedFeatures ); @@ -78,7 +78,11 @@ bool supportsDeepCloning ) { if (reference.Method == null) - return supportsDeepCloning ? MapperConfiguration : MapperConfiguration with { UseDeepCloning = false }; + return supportsDeepCloning ? MapperConfiguration : MapperConfiguration with { CloningStrategy = CloningStrategy.None }; + + var cloningStrategy = MapperConfiguration.Mapper.UseDeepCloning + ? CloningStrategy.DeepCloning + : MapperConfiguration.Mapper.CloningStrategy; var enumConfig = BuildEnumConfig(reference); var membersConfig = BuildMembersConfig(reference); @@ -88,7 +92,7 @@ bool supportsDeepCloning enumConfig, membersConfig, derivedTypesConfig, - supportsDeepCloning && MapperConfiguration.Mapper.UseDeepCloning, + supportsDeepCloning ? cloningStrategy : CloningStrategy.None, MapperConfiguration.StackCloningStrategy, MapperConfiguration.SupportedFeatures ); @@ -243,6 +247,7 @@ private MembersMappingConfiguration BuildMembersConfig(MappingConfigurationRefer // 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; + if (hasMemberConfigs && (configRef.Source.IsEnum() || configRef.Target.IsEnum())) { _diagnostics.ReportDiagnostic(DiagnosticDescriptors.MemberConfigurationOnNonMemberMapping, configRef.Method); diff --git a/src/Riok.Mapperly/Configuration/MappingConfiguration.cs b/src/Riok.Mapperly/Configuration/MappingConfiguration.cs index 0c9b06a848..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, - bool UseDeepCloning, + CloningStrategy CloningStrategy, StackCloningStrategy StackCloningStrategy, SupportedFeatures SupportedFeatures ) diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs b/src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs index 562960649f..814b09b923 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; @@ -88,6 +89,13 @@ 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. + /// + public bool UseCloning => + Configuration.CloningStrategy == CloningStrategy.DeepCloning + || (HasUserSymbol && Configuration.CloningStrategy == CloningStrategy.ShallowCloning); + /// /// 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 ee53b1c802..3466558346 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/DirectAssignmentMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/DirectAssignmentMappingBuilder.cs @@ -8,10 +8,16 @@ public static class DirectAssignmentMappingBuilder { public static NewInstanceMapping? TryBuildMapping(MappingBuilderContext ctx) { - return - SymbolEqualityComparer.IncludeNullability.Equals(ctx.Source, ctx.Target) - && (!ctx.Configuration.UseDeepCloning || ctx.Source.IsImmutable()) - ? new DirectAssignmentMapping(ctx.Source) - : null; + if (ctx.UseCloning && !ctx.Source.IsImmutable()) + { + return null; + } + + if (!SymbolEqualityComparer.IncludeNullability.Equals(ctx.Source, ctx.Target)) + { + return null; + } + + return new DirectAssignmentMapping(ctx.Source); } } diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs index 4ced38b721..566ff0d937 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/EnumerableMappingBuilder.cs @@ -95,12 +95,8 @@ 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 - ) + // 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; } @@ -202,9 +198,7 @@ private static INewInstanceMapping BuildArrayToArrayMapping(MappingBuilderContex // use a for loop mapping otherwise. if (elementMapping.IsSynthetic) { - return ctx.Configuration.UseDeepCloning - ? 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 @@ -320,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.UseDeepCloning) + if (targetIsIEnumerable && !ctx.UseCloning) return (true, null); // if the target is IReadOnlyCollection or IEnumerable 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 2e44a35597..5a6a5d799c 100644 --- a/src/Riok.Mapperly/Descriptors/MappingBuilders/ImplicitCastMappingBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/MappingBuilders/ImplicitCastMappingBuilder.cs @@ -11,7 +11,7 @@ public static class ImplicitCastMappingBuilder if (!ctx.IsConversionEnabled(MappingConversionType.ImplicitCast)) return null; - if (ctx.Configuration.UseDeepCloning && !ctx.Source.IsImmutable() && !ctx.Target.IsImmutable()) + if (ctx.UseCloning && !ctx.Source.IsImmutable() && !ctx.Target.IsImmutable()) return null; // ClassifyConversion does not check if tuple field member names are the same diff --git a/src/Riok.Mapperly/Descriptors/MappingBuilders/MemoryMappingBuilder.cs b/src/Riok.Mapperly/Descriptors/MappingBuilders/MemoryMappingBuilder.cs index 5230d7359c..a2ee790392 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); @@ -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.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.UseDeepCloning) + 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.UseDeepCloning) + 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.UseDeepCloning) + 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 94b5619fb9..c5892d26f7 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.UseDeepCloning => 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/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/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 cf1fd5fecd..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,6 +1,12 @@ [assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")] namespace Riok.Mapperly.Abstractions { + public enum CloningStrategy + { + 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.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; } @@ -145,6 +152,8 @@ public MapperAttribute() { } public Riok.Mapperly.Abstractions.StackCloningStrategy StackCloningStrategy { get; set; } public bool ThrowOnMappingNullMismatch { get; set; } public bool ThrowOnPropertyMappingNullMismatch { get; set; } + [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/DeepCloningWithCloningStrategyMapperTest.cs b/test/Riok.Mapperly.IntegrationTests/DeepCloningWithCloningStrategyMapperTest.cs new file mode 100644 index 0000000000..02edd9e547 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/DeepCloningWithCloningStrategyMapperTest.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 DeepCloningWithCloningStrategyMapperTest : BaseMapperTest + { + [Fact] + [VersionedSnapshot(Versions.NET8_0)] + public Task SnapshotGeneratedSource() + { + var path = GetGeneratedMapperFilePath(nameof(DeepCloningMapperWithCloningStrategy)); + return Verifier.VerifyFile(path); + } + + [Fact] + [VersionedSnapshot(Versions.NET8_0 | Versions.NET9_0)] + public Task RunMappingShouldWork() + { + var model = NewTestObj(); + var dto = DeepCloningMapperWithCloningStrategy.Copy(model); + return Verifier.Verify(dto); + } + + [Fact] + public void RunIdMappingShouldWork() + { + var source = new IdObject { IdValue = 20 }; + 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 010962861f..00cad0b9d1 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(CloningStrategy = CloningStrategy.DeepCloning)] + public static partial class DeepCloningMapperWithCloningStrategy + { + 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 new file mode 100644 index 0000000000..34a206abf9 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/Mapper/ShallowCloningMapper.cs @@ -0,0 +1,20 @@ +using Riok.Mapperly.Abstractions; +using Riok.Mapperly.IntegrationTests.Models; + +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))] + [MapperIgnoreSource(nameof(TestObject.IgnoredStringValue))] + [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 new file mode 100644 index 0000000000..397c88e5b3 --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/ShallowCloningMapperTest.cs @@ -0,0 +1,73 @@ +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +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); + } + + [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/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.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..03fc36887a --- /dev/null +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/ShallowCloningMapperTest.SnapshotGeneratedSource_NET8_0.verified.cs @@ -0,0 +1,93 @@ +// +#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 = src.Flattening; + target.NullableFlattening = src.NullableFlattening; + 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; + target.RecursiveObject = src.RecursiveObject; + target.SourceTargetSameObjectType = src.SourceTargetSameObjectType; + 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 diff --git a/test/Riok.Mapperly.Tests/Helpers/MapperConfigurationBuilderTest.cs b/test/Riok.Mapperly.Tests/Helpers/MapperConfigurationBuilderTest.cs index 67d4d422e8..d309fbb99d 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.CloningStrategy.ShouldBe(CloningStrategy.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, + CloningStrategy = CloningStrategy.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..0d964165c2 --- /dev/null +++ b/test/Riok.Mapperly.Tests/Mapping/ShallowCloneTest.cs @@ -0,0 +1,73 @@ +using Riok.Mapperly.Abstractions; + +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; } + 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() + .HaveSingleMethodBody( + """ + var target = new global::A(); + target.Value = source.Value; + target.List = source.List; + target.NestedReference = source.NestedReference; + return target; + """ + ); + } +} diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilder.cs b/test/Riok.Mapperly.Tests/TestSourceBuilder.cs index 5861f4b988..9f513432a6 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.CloningStrategy), }.WhereNotNull(); return $"[Mapper({string.Join(", ", attrs)})]"; diff --git a/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs b/test/Riok.Mapperly.Tests/TestSourceBuilderOptions.cs index 6e166ff415..203fd2ca65 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, + CloningStrategy? CloningStrategy = null, StackCloningStrategy? StackCloningStrategy = null, bool? UseReferenceHandling = null, bool? ThrowOnMappingNullMismatch = null, @@ -36,6 +37,8 @@ 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(CloningStrategy: Abstractions.CloningStrategy.ShallowCloning); public static readonly TestSourceBuilderOptions AllConversionsWithDeepCloning = new TestSourceBuilderOptions( UseDeepCloning: true, EnabledConversions: MappingConversionType.All