Skip to content

Commit 426f6e9

Browse files
committed
FastClonerContext
1 parent d1528b0 commit 426f6e9

21 files changed

+1382
-94
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>netstandard2.0</TargetFramework>
54
<LangVersion>preview</LangVersion>
65
<Nullable>enable</Nullable>
6+
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
77
</PropertyGroup>
88

99
</Project>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System;
2+
using System.Diagnostics.CodeAnalysis;
3+
4+
namespace FastCloner.SourceGenerator.Shared;
5+
6+
/// <summary>
7+
/// Base class for FastCloner contexts.
8+
/// Inherit from this class and annotate with [FastClonerRegister] to generate a cloning context.
9+
/// </summary>
10+
public abstract class FastClonerContext
11+
{
12+
/// <summary>
13+
/// Clones the object using the generated dispatch logic or falls back to reflection.
14+
/// </summary>
15+
/// <param name="input">The object to clone.</param>
16+
/// <returns>The cloned object.</returns>
17+
public abstract object Clone(object input);
18+
19+
/// <summary>
20+
/// Checks if the type is handled by this context (i.e. has a source generated clone method).
21+
/// </summary>
22+
/// <param name="type">The type to check.</param>
23+
/// <returns>True if the type is handled, false otherwise.</returns>
24+
public abstract bool IsHandled(Type type);
25+
26+
/// <summary>
27+
/// Tries to clone the object using the source generated clone method.
28+
/// Returns false if the type is not handled by this context.
29+
/// </summary>
30+
/// <param name="input">The object to clone.</param>
31+
/// <param name="clone">The cloned object if successful, null otherwise.</param>
32+
/// <returns>True if the object was cloned, false otherwise.</returns>
33+
public abstract bool TryClone(object input, [NotNullWhen(true)] out object? clone);
34+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System;
2+
3+
namespace FastCloner.SourceGenerator.Shared;
4+
5+
/// <summary>
6+
/// Registers a type to be included in the source generated FastClonerContext.
7+
/// </summary>
8+
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
9+
public class FastClonerRegisterAttribute : Attribute
10+
{
11+
public Type[] TypesToRegister { get; }
12+
13+
public FastClonerRegisterAttribute(params Type[] typesToRegister)
14+
{
15+
TypesToRegister = typesToRegister;
16+
}
17+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System.Collections.Concurrent;
2+
using System.Collections.Generic;
3+
using System.Runtime.CompilerServices;
4+
5+
namespace FastCloner.SourceGenerator.Shared;
6+
7+
/// <summary>
8+
/// State for tracking circular references during cloning.
9+
/// Used by source-generated clone methods to detect and handle cycles.
10+
/// Thread-safe for concurrent clone operations.
11+
/// </summary>
12+
public sealed class FcGeneratedCloneState
13+
{
14+
private readonly ConcurrentDictionary<object, object> _knownRefs = new ConcurrentDictionary<object, object>(ReferenceEqualityComparer.Instance);
15+
16+
/// <summary>
17+
/// Registers a known reference mapping from original to clone.
18+
/// </summary>
19+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
20+
public void AddKnownRef(object original, object clone)
21+
{
22+
if (original != null)
23+
{
24+
_knownRefs.TryAdd(original, clone);
25+
}
26+
}
27+
28+
/// <summary>
29+
/// Gets the previously cloned object for the given original, if any.
30+
/// </summary>
31+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
32+
public object? GetKnownRef(object original)
33+
{
34+
if (original == null) return null;
35+
return _knownRefs.TryGetValue(original, out var clone) ? clone : null;
36+
}
37+
38+
/// <summary>
39+
/// Reference equality comparer for proper circular reference detection.
40+
/// Uses object identity (ReferenceEquals) rather than value equality.
41+
/// </summary>
42+
private sealed class ReferenceEqualityComparer : IEqualityComparer<object>
43+
{
44+
public static readonly ReferenceEqualityComparer Instance = new ReferenceEqualityComparer();
45+
46+
bool IEqualityComparer<object>.Equals(object? x, object? y) => ReferenceEquals(x, y);
47+
int IEqualityComparer<object>.GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
48+
}
49+
}
50+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System;
2+
3+
#if !NET5_0_OR_GREATER
4+
5+
// ReSharper disable once CheckNamespace
6+
namespace System.Diagnostics.CodeAnalysis
7+
{
8+
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
9+
sealed class NotNullWhenAttribute : Attribute
10+
{
11+
public NotNullWhenAttribute(bool returnValue)
12+
{
13+
ReturnValue = returnValue;
14+
}
15+
16+
public bool ReturnValue { get; }
17+
}
18+
}
19+
20+
#endif
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
using System.Collections.Generic;
2+
using System.Text;
3+
4+
namespace FastCloner.SourceGenerator;
5+
6+
/// <summary>
7+
/// Helper class for generating class clone body code.
8+
/// Shared between ContextCodeGenerator and CloneCodeGenerator to avoid duplication.
9+
/// </summary>
10+
internal static class ClassCloneBodyGenerator
11+
{
12+
/// <summary>
13+
/// Checks if FormatterServices using statement is needed for the given type.
14+
/// </summary>
15+
public static bool NeedsFormatterServices(TypeModel model)
16+
{
17+
return !model.HasParameterlessConstructor && !model.IsStruct;
18+
}
19+
20+
/// <summary>
21+
/// Checks if any of the given types need FormatterServices.
22+
/// </summary>
23+
public static bool NeedsFormatterServices(IEnumerable<TypeModel> types)
24+
{
25+
foreach (var type in types)
26+
{
27+
if (NeedsFormatterServices(type))
28+
{
29+
return true;
30+
}
31+
}
32+
return false;
33+
}
34+
35+
/// <summary>
36+
/// Writes the code to instantiate a class, handling both parameterless and non-parameterless constructors.
37+
/// </summary>
38+
/// <param name="sb">StringBuilder to write to</param>
39+
/// <param name="typeName">Name of the type to instantiate</param>
40+
/// <param name="hasParameterlessConstructor">Whether the type has a parameterless constructor</param>
41+
public static void WriteInstanceCreation(StringBuilder sb, string typeName, bool hasParameterlessConstructor)
42+
{
43+
if (hasParameterlessConstructor)
44+
{
45+
sb.AppendLine($" var result = new {typeName}();");
46+
}
47+
else
48+
{
49+
// Use FormatterServices.GetUninitializedObject to create instance without calling constructor
50+
sb.AppendLine($" var result = ({typeName})FormatterServices.GetUninitializedObject(typeof({typeName}));");
51+
}
52+
}
53+
54+
/// <summary>
55+
/// Writes the complete class clone body code.
56+
/// </summary>
57+
/// <param name="ctx">The clone generator context</param>
58+
/// <param name="typeName">Name of the type to clone</param>
59+
/// <param name="useState">Whether to use state tracking for circular references</param>
60+
/// <param name="stateVarName">Name of the state variable (null if not using state)</param>
61+
/// <param name="useNullConditional">Whether to use null-conditional operator (?.) when calling AddKnownRef</param>
62+
public static void WriteClassCloneBody(
63+
CloneGeneratorContext ctx,
64+
string typeName,
65+
bool useState,
66+
string? stateVarName = null,
67+
bool useNullConditional = false)
68+
{
69+
var sb = ctx.Source;
70+
var hasParameterlessConstructor = ctx.Model.HasParameterlessConstructor;
71+
72+
if (useState)
73+
{
74+
// When tracking circular references, we must register the instance BEFORE cloning members
75+
// to avoid infinite recursion (StackOverflowException) in case of cycles.
76+
// This requires us to instantiate first, then register, then assign members.
77+
WriteInstanceCreation(sb, typeName, hasParameterlessConstructor);
78+
79+
var stateVarForAdd = stateVarName ?? "state";
80+
var nullConditional = useNullConditional ? "?" : "";
81+
sb.AppendLine($" {stateVarForAdd}{nullConditional}.AddKnownRef(source, result);");
82+
sb.AppendLine();
83+
84+
foreach (var member in ctx.Model.Members)
85+
{
86+
MemberCloneGenerator.WriteMemberCloning(ctx, member, "result", "source", stateVarForAdd);
87+
}
88+
89+
sb.AppendLine();
90+
sb.AppendLine(" return result;");
91+
}
92+
else
93+
{
94+
// For non-circular types, we can use object initializer syntax if constructor exists
95+
if (hasParameterlessConstructor)
96+
{
97+
sb.AppendLine($" var result = new {typeName}");
98+
sb.AppendLine(" {");
99+
100+
var memberAssignments = new List<string>();
101+
foreach (var member in ctx.Model.Members)
102+
{
103+
var assignment = MemberCloneGenerator.GetMemberAssignment(ctx, member, "source", "null");
104+
if (!string.IsNullOrEmpty(assignment))
105+
{
106+
memberAssignments.Add($" {assignment}");
107+
}
108+
}
109+
110+
if (memberAssignments.Count > 0)
111+
{
112+
sb.AppendLine(string.Join(",\n", memberAssignments));
113+
}
114+
115+
sb.AppendLine(" };");
116+
}
117+
else
118+
{
119+
// Use FormatterServices.GetUninitializedObject to create instance without calling constructor
120+
WriteInstanceCreation(sb, typeName, hasParameterlessConstructor);
121+
122+
// Then assign members individually (no state needed for non-circular types)
123+
foreach (var member in ctx.Model.Members)
124+
{
125+
MemberCloneGenerator.WriteMemberCloning(ctx, member, "result", "source", "null");
126+
}
127+
}
128+
129+
sb.AppendLine();
130+
sb.AppendLine(" return result;");
131+
}
132+
}
133+
}
134+

src/FastCloner.SourceGenerator/CloneCodeGenerator.cs

Lines changed: 13 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ private void WriteUsings()
4646
sb.AppendLine("using System;");
4747
sb.AppendLine("using System.Collections.Generic;");
4848
sb.AppendLine("using System.Reflection;");
49+
sb.AppendLine("using FastCloner.SourceGenerator.Shared;");
50+
51+
// Add FormatterServices if type doesn't have parameterless constructor
52+
if (ClassCloneBodyGenerator.NeedsFormatterServices(_context.Model))
53+
{
54+
sb.AppendLine("using System.Runtime.Serialization;");
55+
}
4956

5057
// Only add FastCloner using if the library is available
5158
if (_context.IsFastClonerAvailable)
@@ -73,6 +80,8 @@ private void WriteExtensionClass()
7380
var fullTypeName = _context.Model.FullyQualifiedName;
7481
var sb = _context.Source;
7582

83+
// FcGeneratedCloneState is now in FastCloner.SourceGenerator.Shared - no need to generate it
84+
7685
sb.AppendLine(" /// <summary>");
7786
sb.AppendLine($" /// Extension methods for cloning {_context.Model.Name}.");
7887
sb.AppendLine(" /// </summary>");
@@ -82,12 +91,8 @@ private void WriteExtensionClass()
8291
// Generate public FastDeepClone() without state parameter
8392
WritePublicFastDeepCloneMethod(typeName, fullTypeName);
8493

85-
// Only generate InternalFastDeepClone if circular references are possible
86-
// (for types without circular refs, the public method inlines the logic directly)
87-
if (_context.CanHaveCircularReferences)
88-
{
89-
WritePrivateFastDeepCloneMethod(typeName, fullTypeName);
90-
}
94+
// Always generate InternalFastDeepClone for Clonable types, even if they don't detect circular references
95+
WritePrivateFastDeepCloneMethod(typeName, fullTypeName);
9196

9297
// Generate Cloner<T> helper class (private nested class)
9398
// Must be generated before helpers to register its dependencies
@@ -96,9 +101,6 @@ private void WriteExtensionClass()
96101
// Generate helper methods for nested types
97102
CollectionHelperGenerator.GenerateHelpers(_context);
98103

99-
// Generate FastCloneState helper class (private nested class)
100-
WriteFastCloneStateClass();
101-
102104
sb.AppendLine(" }");
103105
}
104106

@@ -218,71 +220,10 @@ private void WriteStructCloneBody(string typeName, string stateVarName = "state"
218220

219221
private void WriteClassCloneBody(string typeName, string fullTypeName, bool useState, string? stateVarName = null)
220222
{
221-
var sb = _context.Source;
222-
// Use object initializer syntax instead of MemberwiseClone for better performance
223-
// Create new instance using object initializer
224-
sb.AppendLine($" var result = new {typeName}");
225-
sb.AppendLine(" {");
226-
227-
// Generate property/field assignments in object initializer
228-
var memberAssignments = new List<string>();
229-
foreach (var member in _context.Model.Members)
230-
{
231-
// Pass null if state isn't needed, or use the provided stateVarName (or "state" as default)
232-
var stateVar = useState ? (stateVarName ?? "state") : "null";
233-
var assignment = MemberCloneGenerator.GetMemberAssignment(_context, member, "source", stateVar);
234-
if (!string.IsNullOrEmpty(assignment))
235-
{
236-
memberAssignments.Add($" {assignment}");
237-
}
238-
}
239-
240-
if (memberAssignments.Count > 0)
241-
{
242-
sb.AppendLine(string.Join(",\n", memberAssignments));
243-
}
244-
245-
sb.AppendLine(" };");
246-
if (useState)
247-
{
248-
var stateVarForAdd = stateVarName ?? "state";
249-
sb.AppendLine($" {stateVarForAdd}?.AddKnownRef(source, result);");
250-
}
251-
252-
sb.AppendLine();
253-
sb.AppendLine(" return result;");
223+
// Use null-conditional operator for CloneCodeGenerator since state might be null
224+
ClassCloneBodyGenerator.WriteClassCloneBody(_context, typeName, useState, stateVarName, useNullConditional: true);
254225
}
255226

256-
private void WriteFastCloneStateClass()
257-
{
258-
// Only generate state class if it's actually needed
259-
if (!_context.NeedsStateClass)
260-
return;
261-
262-
var sb = _context.Source;
263-
sb.AppendLine();
264-
sb.AppendLine(" /// <summary>");
265-
sb.AppendLine(" /// State for tracking circular references during cloning.");
266-
sb.AppendLine(" /// </summary>");
267-
sb.AppendLine(" private class FcGeneratedCloneState");
268-
sb.AppendLine(" {");
269-
sb.AppendLine(" private readonly Dictionary<object, object> _knownRefs = new Dictionary<object, object>();");
270-
sb.AppendLine();
271-
sb.AppendLine(" public void AddKnownRef(object original, object clone)");
272-
sb.AppendLine(" {");
273-
sb.AppendLine(" if (original != null)");
274-
sb.AppendLine(" {");
275-
sb.AppendLine(" _knownRefs[original] = clone;");
276-
sb.AppendLine(" }");
277-
sb.AppendLine(" }");
278-
sb.AppendLine();
279-
sb.AppendLine(" public object? GetKnownRef(object original)");
280-
sb.AppendLine(" {");
281-
sb.AppendLine(" if (original == null) return null;");
282-
sb.AppendLine(" return _knownRefs.TryGetValue(original, out var clone) ? clone : null;");
283-
sb.AppendLine(" }");
284-
sb.AppendLine(" }");
285-
}
286227

287228
private void WriteClonerClass()
288229
{

0 commit comments

Comments
 (0)