Skip to content

Commit 8f1b513

Browse files
committed
introduce CloneBehavior
1 parent 069d368 commit 8f1b513

File tree

10 files changed

+498
-244
lines changed

10 files changed

+498
-244
lines changed

README.md

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,18 +108,26 @@ TreeNode clone = child.DeepClone();
108108

109109
This differs from `[FastClonerIgnore]` which leaves the member as `null`/default. With `[FastClonerShallow]`, the original reference is preserved.
110110

111-
You might also need to exclude certain types from ever being cloned. To do that, put offending types on a blacklist:
112-
```cs
113-
FastCloner.FastCloner.IgnoreType(typeof(PropertyChangedEventHandler)); // or FastCloner.FastCloner.IgnoreTypes([ .. ])
111+
You might also need to customize how certain types are handled globally. Use `FastCloner.FastCloner.SetTypeBehavior<T>` to configure specific behaviors.
112+
113+
```csharp
114+
FastCloner.FastCloner.SetTypeBehavior<MySingletonService>(CloneBehavior.Skip);
114115
```
115116

116-
If needed, the types can be removed from the blacklist later:
117-
```cs
118-
// note: doing this invalidates precompiled expressions and clears the cache,
119-
// performance will be negatively affected until the cache is repopulated
120-
FastCloner.FastCloner.ClearIgnoredTypes();
117+
Available behaviors:
118+
* `Clone`: Deep recursive copy.
119+
* `Reference`: Return the original instance.
120+
* `Shallow`: Top-level `MemberwiseClone`.
121+
* `Skip`: Return `default`, skip cloning.
122+
123+
To reset behavior:
124+
```csharp
125+
FastCloner.FastCloner.ClearTypeBehavior<MySingletonService>();
126+
FastCloner.FastCloner.ClearAllTypeBehaviors();
121127
```
122128

129+
>*Note: Changing type behavior automatically invalidates the internal cache, which may temporarily impact performance. Configure this at application startup.*
130+
123131
Cache can be invalidated to reduce the memory footprint, if needed:
124132

125133
```csharp

src/FastCloner.Tests/FailureHypothesisTests.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,21 +85,22 @@ public void ConcurrentBag_Should_Be_Deep_Cloned_Correctly()
8585
}
8686

8787
[Test]
88-
public void CancellationTokenSource_Should_Be_Deep_Cloned_Independently()
88+
public void CancellationTokenSource_Should_Be_Reference_Copied()
8989
{
9090
using var original = new CancellationTokenSource();
9191
var clone = original.DeepClone();
9292

93-
Assert.That(clone, Is.Not.SameAs(original));
93+
Assert.That(clone, Is.SameAs(original));
9494
Assert.That(clone.IsCancellationRequested, Is.False);
9595

9696
// Cancel original
9797
original.Cancel();
9898

9999
Assert.That(original.IsCancellationRequested, Is.True);
100-
Assert.That(clone.IsCancellationRequested, Is.False, "Clone should not be cancelled when original is cancelled");
100+
// Since it is a reference copy, the clone (same object) MUST be cancelled too.
101+
Assert.That(clone.IsCancellationRequested, Is.True, "Clone is the same object, so it should be cancelled");
101102

102-
// Cancel clone
103+
// Cancel clone (safe to call again)
103104
clone.Cancel();
104105
Assert.That(clone.IsCancellationRequested, Is.True);
105106
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using FastCloner.SourceGenerator.Shared;
2+
using NUnit.Framework;
3+
using System;
4+
using System.Threading;
5+
6+
namespace FastCloner.Tests.SourceGenerator
7+
{
8+
// Define a class that uses the Source Generator
9+
[FastClonerClonable]
10+
public partial class SafetyTestClass
11+
{
12+
public CancellationTokenSource? Cts { get; set; }
13+
public WeakReference<string>? WeakRef { get; set; }
14+
public WeakReference? NonGenericWeakRef { get; set; }
15+
public System.Threading.Tasks.Task? Task { get; set; }
16+
}
17+
18+
[TestFixture]
19+
public class SourceGeneratorSafetyTests
20+
{
21+
[Test]
22+
public void Verify_SourceGenerator_Uses_ReferenceCopy_For_SpecialTypes()
23+
{
24+
// Arrange
25+
var source = new SafetyTestClass
26+
{
27+
Cts = new CancellationTokenSource(),
28+
WeakRef = new WeakReference<string>("test"),
29+
NonGenericWeakRef = new WeakReference("test"),
30+
Task = System.Threading.Tasks.Task.CompletedTask
31+
};
32+
33+
// Act
34+
// This calls the Source Generated FastDeepClone() method
35+
var clone = source.FastDeepClone();
36+
37+
// Assert
38+
Assert.That(clone, Is.Not.SameAs(source));
39+
40+
// CancellationTokenSource should be Reference Copy
41+
Assert.That(clone.Cts, Is.SameAs(source.Cts), "CTS should be reference copied by Source Generator");
42+
43+
// WeakReference<T> should be Reference Copy
44+
Assert.That(clone.WeakRef, Is.SameAs(source.WeakRef), "WeakReference<T> should be reference copied by Source Generator");
45+
46+
// WeakReference (non-generic) should be Reference Copy
47+
Assert.That(clone.NonGenericWeakRef, Is.SameAs(source.NonGenericWeakRef), "WeakReference should be reference copied by Source Generator");
48+
49+
// Task should be Reference Copy
50+
Assert.That(clone.Task, Is.SameAs(source.Task), "Task should be reference copied by Source Generator");
51+
}
52+
}
53+
}

src/FastCloner.Tests/SpecialCaseTests.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using System.Net.Http.Headers;
1515
using System.Numerics;
1616
using System.Reflection;
17+
using System.Runtime.CompilerServices;
1718
using System.Runtime.InteropServices;
1819
using System.Text;
1920
using System.Text.Json.Nodes;
@@ -795,6 +796,64 @@ public void Test_Action_Delegate_Clone()
795796
Assert.That(clonedResult, Is.EquivalentTo(originalResult), "Both delegates should produce the same result");
796797
}
797798

799+
[Test]
800+
public void ConditionalWeakTable_DeepClone_VerifyBehavior()
801+
{
802+
ConditionalWeakTable<string, string> cwt = new ConditionalWeakTable<string, string>();
803+
string key = "key";
804+
string val = "value";
805+
cwt.Add(key, val);
806+
807+
var clone = cwt.DeepClone();
808+
809+
Assert.That(clone, Is.Not.SameAs(cwt));
810+
811+
if (clone.TryGetValue(key, out string? clonedVal))
812+
{
813+
Console.WriteLine("Clone found key: " + clonedVal);
814+
}
815+
else
816+
{
817+
Console.WriteLine("Clone did NOT find key");
818+
}
819+
}
820+
821+
[Test]
822+
public void WeakReferenceGeneric_DeepClone_VerifyBehavior()
823+
{
824+
string target = "target";
825+
WeakReference<string> weak = new WeakReference<string>(target);
826+
827+
var clone = weak.DeepClone();
828+
829+
Assert.That(clone, Is.SameAs(weak));
830+
831+
bool hasTarget = clone.TryGetTarget(out string? clonedTarget);
832+
833+
Console.WriteLine($"Original target alive: {weak.TryGetTarget(out _)}");
834+
Console.WriteLine($"Clone target alive: {hasTarget}");
835+
}
836+
837+
[Test]
838+
public void CancellationTokenSource_DeepClone_VerifyBehavior()
839+
{
840+
CancellationTokenSource cts = new CancellationTokenSource();
841+
var clone = cts.DeepClone();
842+
Assert.That(clone, Is.SameAs(cts));
843+
}
844+
845+
[Test]
846+
public void CancellationTokenSource_DeepClone_VerifySafety()
847+
{
848+
// CancellationTokenSource manages native handles and cannot be safely deep cloned by value.
849+
// It is now treated as a "Safe" type, meaning DeepClone returns the SAME instance.
850+
851+
using var cts = new CancellationTokenSource();
852+
var clone = cts.DeepClone();
853+
854+
// Assert it is the SAME object (Reference Copy)
855+
Assert.That(clone, Is.SameAs(cts));
856+
}
798857
[Test]
799858
public void Test_Static_Action_Delegate_Clone()
800859
{
@@ -3286,4 +3345,22 @@ public void ValueTask_From_Task_Should_Be_Safe()
32863345

32873346
Assert.That(clone.Result, Is.EqualTo(42));
32883347
}
3348+
3349+
3350+
[Test]
3351+
public void WeakReferenceGeneric_DeepClone_VerifySafety()
3352+
{
3353+
string target = "target";
3354+
WeakReference<string> weak = new WeakReference<string>(target);
3355+
3356+
var clone = weak.DeepClone();
3357+
3358+
// Assert it is the SAME object (Reference Copy)
3359+
Assert.That(clone, Is.SameAs(weak));
3360+
3361+
bool hasTarget = clone.TryGetTarget(out string? clonedTarget);
3362+
3363+
Console.WriteLine($"Original target alive: {weak.TryGetTarget(out _)}");
3364+
Console.WriteLine($"Clone target alive: {hasTarget}");
3365+
}
32893366
}

0 commit comments

Comments
 (0)