Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion docs/docs/breaking-changes/5-0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` deep cloning order

When deep cloning a `Stack<T>`, Mapperly now preserves the order of elements by default.
Expand All @@ -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
{
// ...
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/configuration/conversions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<>` |
Expand Down
14 changes: 9 additions & 5 deletions docs/docs/configuration/mapper.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,27 @@ 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
{
...
}
```

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
Expand All @@ -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
{
// ...
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/queryable-projections.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration/user-implemented-methods.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions src/Riok.Mapperly.Abstractions/CloningStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Riok.Mapperly.Abstractions;

/// <summary>
/// Specifies whether and how to copy objects of the same type and complex types like collections and spans.
/// </summary>
public enum CloningStrategy
{
/// <summary>
/// Default behaviour, the original instance will be returned
/// </summary>
None,

/// <summary>
/// Always deep copy objects.
/// Eg. when the type <c>Person[]</c> should be mapped to the same type <c>Person[]</c>,
/// the array and each person is cloned.
/// </summary>
DeepCloning,

/// <summary>
/// Always shallow copy objects.
/// Eg. when the type <c>Person</c> should be mapped to the same type <c>Person</c>,
/// a new instance will be returned with the same values for all properties.
/// References will be kept.
/// </summary>
ShallowCloning,
}
12 changes: 12 additions & 0 deletions src/Riok.Mapperly.Abstractions/MapperAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.ComponentModel;
using System.Diagnostics;
using Riok.Mapperly.Abstractions.ReferenceHandling;

Expand Down Expand Up @@ -65,8 +66,19 @@ public class MapperAttribute : Attribute
/// when <c>false</c>, the same array is reused.
/// when <c>true</c>, the array and each person is cloned.
/// </summary>
/// <remarks>
/// To maintain compatibility with the previous versions, <see cref="UseDeepCloning"/> will still take precedence over
/// <see cref="CloningStrategy"/> until it will be removed.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("Use 'CloningStrategy' instead. If this is set to true, `CloningStrategy.DeepClone` is always used.")]
public bool UseDeepCloning { get; set; }

/// <summary>
/// Specifies whether and how to copy objects of the same type and complex types like collections and spans.
/// </summary>
public CloningStrategy CloningStrategy { get; set; } = CloningStrategy.None;

/// <summary>
/// The strategy to use when cloning a <see cref="System.Collections.Generic.Stack{T}"/>.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions src/Riok.Mapperly/Configuration/MapperConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ public record MapperConfiguration
/// </summary>
public bool? UseDeepCloning { get; init; }

public CloningStrategy? CloningStrategy { get; init; }

/// <summary>
/// The strategy to use when cloning a <see cref="System.Collections.Generic.Stack{T}"/>.
/// </summary>
Expand Down
4 changes: 4 additions & 0 deletions src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;

Expand Down
11 changes: 8 additions & 3 deletions src/Riok.Mapperly/Configuration/MapperConfigurationReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ SupportedFeatures supportedFeatures
),
new MembersMappingConfiguration([], [], [], [], [], mapper.IgnoreObsoleteMembersStrategy, mapper.RequiredMappingStrategy),
[],
mapper.UseDeepCloning,
mapper.UseDeepCloning ? CloningStrategy.DeepCloning : mapper.CloningStrategy,
mapper.StackCloningStrategy,
supportedFeatures
);
Expand All @@ -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);
Expand All @@ -88,7 +92,7 @@ bool supportsDeepCloning
enumConfig,
membersConfig,
derivedTypesConfig,
supportsDeepCloning && MapperConfiguration.Mapper.UseDeepCloning,
supportsDeepCloning ? cloningStrategy : CloningStrategy.None,
MapperConfiguration.StackCloningStrategy,
MapperConfiguration.SupportedFeatures
);
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/Riok.Mapperly/Configuration/MappingConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public record MappingConfiguration(
EnumMappingConfiguration Enum,
MembersMappingConfiguration Members,
IReadOnlyCollection<DerivedTypeMappingConfiguration> DerivedTypes,
bool UseDeepCloning,
CloningStrategy CloningStrategy,
StackCloningStrategy StackCloningStrategy,
SupportedFeatures SupportedFeatures
)
Expand Down
8 changes: 8 additions & 0 deletions src/Riok.Mapperly/Descriptors/MappingBuilderContext.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -88,6 +89,13 @@ protected MappingBuilderContext(
/// <inheritdoc cref="MappingBuilders.MappingBuilder.NewInstanceMappings"/>
public IReadOnlyDictionary<TypeMappingKey, INewInstanceMapping> NewInstanceMappings => MappingBuilder.NewInstanceMappings;

/// <summary>
/// Determines if mapping code should be emitted in cases where direct assignments or casts could be used instead.
/// </summary>
public bool UseCloning =>
Configuration.CloningStrategy == CloningStrategy.DeepCloning
|| (HasUserSymbol && Configuration.CloningStrategy == CloningStrategy.ShallowCloning);

/// <summary>
/// Tries to find an existing mapping with the provided name.
/// If none is found, <c>null</c> is returned.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -320,9 +314,9 @@ private static (bool CanMapWithLinq, string? CollectMethod) ResolveCollectMethod
return (true, ToArrayMethodName);

// if the target is an IEnumerable<T> 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<T> or IEnumerable<T>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public static class ExplicitCastMappingBuilder
if (!ctx.IsConversionEnabled(MappingConversionType.ExplicitCast))
return null;

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

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

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

// ClassifyConversion does not check if tuple field member names are the same
Expand Down
Loading