Skip to content

Commit b896fbe

Browse files
committed
add [FastClonerPreserveIdentity]
1 parent 51d4eff commit b896fbe

36 files changed

+3007
-805
lines changed

README.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,57 @@ public struct MyHandle
269269

270270
This is the default behavior for system types like `System.Net.Http.Headers.HeaderDescriptor` to prevent breaking internal framework logic. Use this attribute if your custom structs behave similarly.
271271

272+
### Identity Preservation
273+
274+
By default, FastCloner prioritizes performance by not tracking object identity during cloning. This means if the same object instance appears multiple times in your graph, each reference becomes a separate clone.
275+
276+
For scenarios where you need to preserve object identity (e.g., shared references should remain shared in the clone), use `[FastClonerPreserveIdentity]`:
277+
278+
```csharp
279+
[FastClonerClonable]
280+
[FastClonerPreserveIdentity] // Enable identity tracking for this type
281+
public class Document
282+
{
283+
public User Author { get; set; }
284+
public User LastEditor { get; set; } // May reference the same User as Author
285+
}
286+
287+
var doc = new Document { Author = user, LastEditor = user };
288+
var clone = doc.FastDeepClone();
289+
// clone.Author == clone.LastEditor (same cloned instance)
290+
```
291+
292+
The attribute can be applied at type level or member level:
293+
294+
```csharp
295+
[FastClonerClonable]
296+
public class Container
297+
{
298+
// Only this member tracks identity
299+
[FastClonerPreserveIdentity]
300+
public List<Node> Nodes { get; set; }
301+
302+
// This member clones without identity tracking (faster)
303+
public List<Item> Items { get; set; }
304+
}
305+
```
306+
307+
You can also explicitly disable identity preservation for a member when the type has it enabled:
308+
309+
```csharp
310+
[FastClonerClonable]
311+
[FastClonerPreserveIdentity]
312+
public class Graph
313+
{
314+
public Node Root { get; set; }
315+
316+
[FastClonerPreserveIdentity(false)] // Opt out for this member
317+
public List<string> Labels { get; set; }
318+
}
319+
```
320+
321+
> **Note**: Identity preservation adds overhead for tracking seen objects. Circular references are always detected regardless of this setting.
322+
272323
## Limitations
273324

274325
- Cloning unmanaged resources, such as `IntPtr`s may result in side-effects, as there is no metadata for the length of buffers such pointers often point to.
@@ -314,7 +365,7 @@ FastCloner's source generator is carefully engineered for zero impact on IDE res
314365

315366
## Contributing
316367

317-
If you are looking to add new functionality, please open an issue first to verify your intent is aligned with the scope of the project. The library is covered by over [600 tests](https://github.com/lofcz/FastCloner/tree/next/src/FastCloner.Tests), please run them against your work before proposing changes. When reporting issues, providing a minimal reproduction we can plug in as a new test greatly reduces turnaround time.
368+
If you are looking to add new functionality, please open an issue first to verify your intent is aligned with the scope of the project. The library is covered by over [700 tests](https://github.com/lofcz/FastCloner/tree/next/src/FastCloner.Tests), please run them against your work before proposing changes. When reporting issues, providing a minimal reproduction we can plug in as a new test greatly reduces turnaround time.
318369

319370
## License
320371

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using BenchmarkDotNet.Attributes;
2+
using BenchmarkDotNet.Order;
3+
4+
namespace FastCloner.Benchmark;
5+
6+
[RankColumn]
7+
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
8+
[MemoryDiagnoser]
9+
public class BenchClone
10+
{
11+
private ComplexModel model = null!;
12+
13+
[GlobalSetup]
14+
public void Setup()
15+
{
16+
model = TestDataGenerator.CreateSampleModel();
17+
}
18+
19+
[Benchmark(Baseline = true)]
20+
public ComplexModel FastCloner_SourceGen()
21+
{
22+
return model.FastDeepClone();
23+
}
24+
25+
[Benchmark]
26+
public ComplexModel IDeepCloneable_SourceGen()
27+
{
28+
return model.DeepClone();
29+
}
30+
31+
[Benchmark]
32+
public ComplexModel Mapperly_SourceGen()
33+
{
34+
return MapperlyCloner.Clone(model);
35+
}
36+
}

src/FastCloner.Benchmark/FastCloner.Benchmark.csproj

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@
1818
<PackageReference Include="DeepCopy.Expression" Version="1.5.0" />
1919
<PackageReference Include="Dolly" Version="0.0.9" />
2020
<PackageReference Include="FastDeepCloner" Version="1.3.6" />
21+
<PackageReference Include="IDeepCloneable" Version="0.0.27" />
2122
<PackageReference Include="MessagePack" Version="3.1.4" />
2223
<PackageReference Include="NClone" Version="1.2.0" />
2324
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
2425
<PackageReference Include="ObjectCloner" Version="2.2.2" />
2526
<PackageReference Include="protobuf-net" Version="3.2.56" />
26-
<PackageReference Include="Riok.Mapperly" Version="3.7.0" />
27-
<PackageReference Include="System.Runtime.Serialization.Formatters" Version="10.0.0" />
27+
<PackageReference Include="Riok.Mapperly" Version="4.3.1" />
28+
<PackageReference Include="System.Runtime.Serialization.Formatters" Version="10.0.2" />
2829
</ItemGroup>
2930

3031
<ItemGroup>

src/FastCloner.Benchmark/Models.cs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using FastCloner.SourceGenerator.Shared;
2+
using IDeepCloneable;
3+
using Riok.Mapperly.Abstractions;
4+
5+
namespace FastCloner.Benchmark;
6+
7+
/// <summary>
8+
/// Complex model for benchmarking deep cloning operations.
9+
/// </summary>
10+
[DeepCloneable]
11+
[FastClonerClonable]
12+
[FastClonerTrustNullability]
13+
public partial class ComplexModel
14+
{
15+
public string Id { get; set; } = string.Empty;
16+
public string Name { get; set; } = string.Empty;
17+
public int Version { get; set; }
18+
public DateTime CreatedAt { get; set; }
19+
public DateTime? UpdatedAt { get; set; }
20+
public UserInfo? Owner { get; set; }
21+
public List<UserInfo>? Contributors { get; set; }
22+
public Dictionary<string, string>? Metadata { get; set; }
23+
public List<DataItem>? Items { get; set; }
24+
public Settings? Settings { get; set; }
25+
}
26+
27+
public class UserInfo
28+
{
29+
public string UserId { get; set; } = string.Empty;
30+
public string UserName { get; set; } = string.Empty;
31+
public string Email { get; set; } = string.Empty;
32+
public UserRole Role { get; set; }
33+
public ContactInfo? Contact { get; set; }
34+
}
35+
36+
public class ContactInfo
37+
{
38+
public string Phone { get; set; } = string.Empty;
39+
public string Address { get; set; } = string.Empty;
40+
public string City { get; set; } = string.Empty;
41+
public string Country { get; set; } = string.Empty;
42+
}
43+
44+
public class DataItem
45+
{
46+
public string ItemId { get; set; } = string.Empty;
47+
public string Title { get; set; } = string.Empty;
48+
public string Description { get; set; } = string.Empty;
49+
public double Value { get; set; }
50+
public List<string>? Tags { get; set; }
51+
public List<SubItem>? SubItems { get; set; }
52+
public Dictionary<string, string>? Properties { get; set; }
53+
}
54+
55+
public class SubItem
56+
{
57+
public string SubId { get; set; } = string.Empty;
58+
public string Label { get; set; } = string.Empty;
59+
public int Quantity { get; set; }
60+
public decimal Price { get; set; }
61+
}
62+
63+
public class Settings
64+
{
65+
public bool IsEnabled { get; set; }
66+
public int MaxItems { get; set; }
67+
public TimeSpan Timeout { get; set; }
68+
public List<string>? AllowedDomains { get; set; }
69+
public Dictionary<string, int>? Limits { get; set; }
70+
public AdvancedSettings? Advanced { get; set; }
71+
}
72+
73+
public class AdvancedSettings
74+
{
75+
public int CacheSize { get; set; }
76+
public bool UseCompression { get; set; }
77+
public string CompressionLevel { get; set; } = string.Empty;
78+
public List<string>? Features { get; set; }
79+
}
80+
81+
public enum UserRole
82+
{
83+
Guest = 0,
84+
User = 1,
85+
Admin = 2,
86+
Owner = 3,
87+
}
88+
89+
/// <summary>
90+
/// Mapperly mapper for ComplexModel (compile-time code generation).
91+
/// </summary>
92+
[Mapper(UseDeepCloning = true)]
93+
public static partial class MapperlyCloner
94+
{
95+
public static partial ComplexModel Clone(ComplexModel source);
96+
}

src/FastCloner.Benchmark/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ static void Main(string[] args)
1212
ManualConfig config = DefaultConfig.Instance
1313
.WithOptions(ConfigOptions.DisableOptimizationsValidator);
1414

15-
Summary summary = BenchmarkRunner.Run<BenchDynamic>(config);
15+
Summary summary = BenchmarkRunner.Run<BenchClone>(config);
1616
Console.WriteLine(summary);
1717
}
1818
}

0 commit comments

Comments
 (0)