Skip to content

Commit 9721856

Browse files
committed
allow compile time behaviors on types
1 parent f5f0ba4 commit 9721856

15 files changed

+856
-145
lines changed

README.md

Lines changed: 39 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -71,71 +71,69 @@ var clone = original.FastDeepClone();
7171

7272
## Advanced Usage
7373

74-
Sometimes, you might want to exclude certain fields/events/properties from cloning:
75-
```csharp
76-
private class TestPropsWithIgnored
77-
{
78-
[FastClonerIgnore] // <-- decorate with [FastClonerIgnore] or [NonSerialized]
79-
public string B { get; set; } = "My string";
80-
public int A { get; set; } = 10;
81-
}
74+
### Customizing Clone Behavior
8275

83-
TestPropsWithIgnored original = new TestPropsWithIgnored { A = 42, B = "Test value" };
84-
TestPropsWithIgnored clone = original.DeepClone(); // clone.B is null (default value of a given type)
85-
```
76+
FastCloner supports behavior attributes that control how types and members are cloned:
8677

87-
### Shallow Cloning Members
78+
| Behavior | Effect |
79+
|----------|--------|
80+
| `Clone` | Deep recursive copy |
81+
| `Reference` | Return original instance unchanged |
82+
| `Shallow` | `MemberwiseClone` without recursion |
83+
| `Ignore` | Return `default` |
8884

89-
When you need to copy a reference directly without deep cloning its contents, use `[FastClonerShallow]`.
85+
#### Compile-time (Attributes)
86+
87+
Apply attributes to **types** or **members**. Member-level attributes override type-level:
9088

9189
```csharp
92-
public class TreeNode
90+
[FastClonerReference] // Type-level: all usages preserve reference
91+
public class SharedService { }
92+
93+
public class MyClass
9394
{
94-
public string Name { get; set; }
95+
public SharedService Svc { get; set; } // Uses type-level → Reference
96+
97+
[FastClonerBehavior(CloneBehavior.Clone)] // Member-level override → Clone
98+
public SharedService ClonedSvc { get; set; }
9599

96-
[FastClonerShallow] // <-- reference copied directly
97-
public TreeNode Parent { get; set; }
100+
[FastClonerIgnore] // → null/default
101+
public CancellationToken Token { get; set; }
98102

99-
public List<TreeNode> Children { get; set; }
103+
[FastClonerShallow] // → Reference copied directly
104+
public ParentNode Parent { get; set; }
100105
}
101-
102-
TreeNode child = new TreeNode {
103-
Name = "Child",
104-
Parent = new TreeNode { Name = "Root" }
105-
};
106-
TreeNode clone = child.DeepClone();
107106
```
108107

109-
This differs from `[FastClonerIgnore]` which leaves the member as `null`/default. With `[FastClonerShallow]`, the original reference is preserved.
108+
Shorthand attributes: `[FastClonerIgnore]`, `[FastClonerShallow]`, `[FastClonerReference]`
109+
Explicit: `[FastClonerBehavior(CloneBehavior.X)]`
110110

111-
### Customizing Reflection Cloning
111+
#### Runtime (Reflection only)
112112

113-
Use `FastCloner.FastCloner.SetTypeBehavior<T>` to configure how specific types are cloned globally.
113+
Configure type behavior dynamically. Runtime settings are checked **before** attributes:
114114

115115
```csharp
116-
FastCloner.FastCloner.SetTypeBehavior<MySingletonService>(CloneBehavior.Skip);
116+
FastCloner.FastCloner.SetTypeBehavior<MySingleton>(CloneBehavior.Reference);
117+
FastCloner.FastCloner.ClearTypeBehavior<MySingleton>(); // Reset one
118+
FastCloner.FastCloner.ClearAllTypeBehaviors(); // Reset all
117119
```
118120

119-
Available behaviors:
120-
* `Clone`: Deep recursive copy.
121-
* `Reference`: Return the original instance.
122-
* `Shallow`: Top-level `MemberwiseClone`.
123-
* `Skip`: Return `default`, skip cloning.
121+
> **Note**: Changing runtime behavior invalidates the cache. Try to configure once at startup, or use compile-time attributes when possible.
124122
125-
To reset a behavior:
126-
```csharp
127-
FastCloner.FastCloner.ClearTypeBehavior<MySingletonService>();
128-
FastCloner.FastCloner.ClearAllTypeBehaviors();
129-
```
123+
#### Precedence (highest to lowest)
130124

131-
>*Note: Changing type behavior at runtime automatically invalidates the internal cache, which may temporarily impact performance. Try to configure behaviors once, at application startup.*
125+
1. Runtime `SetTypeBehavior<T>()`
126+
2. Member-level attribute
127+
3. Type-level attribute on member's type
128+
4. Default behavior
132129

133-
Cache can be invalidated to reduce the memory footprint, if needed:
130+
### Cache Management
134131

135132
```csharp
136-
FastCloner.FastCloner.ClearCache();
133+
FastCloner.FastCloner.ClearCache(); // Free memory from reflection cache
137134
```
138135

136+
139137
### Generic Classes and Abstract Types
140138

141139
The source generator automatically discovers which concrete types your generic classes and abstract hierarchies are used with:

src/FastCloner.SourceGenerator/ImplicitTypeAnalyzer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ bool TryHandleComponent(ITypeSymbol componentType, out MemberModel? componentMem
104104
HasGetter: true,
105105
HasSetter: true,
106106
SetterIsAccessible: true,
107-
IsShallowClone: false
107+
MemberBehavior: MemberCloneBehavior.Clone
108108
);
109109
return true;
110110
}

src/FastCloner.SourceGenerator/MemberCollector.cs

Lines changed: 128 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33

44
namespace FastCloner.SourceGenerator;
55

6+
/// <summary>
7+
/// Represents the member-level clone behavior as determined by attributes.
8+
/// Mirrors FastCloner.Code.CloneBehavior enum values.
9+
/// </summary>
10+
internal enum MemberCloneBehavior
11+
{
12+
Clone = 0, // Default: deep clone
13+
Reference = 1, // Copy reference directly
14+
Shallow = 2, // MemberwiseClone (treated same as Reference for members)
15+
Ignore = 3 // Skip, set to default
16+
}
17+
618
internal record struct MemberAnalysis(MemberModel Model, ITypeSymbol Type);
719

820
internal static class MemberCollector
@@ -46,10 +58,10 @@ public static List<MemberAnalysis> GetMembers(
4658
// Include property if it has an accessible setter OR it's a getter-only populatable collection
4759
if (hasAccessibleSetter || isPopulatableCollection)
4860
{
49-
if (!HasIgnoreAttribute(property, compilation))
61+
MemberCloneBehavior behavior = GetMemberBehavior(property, compilation);
62+
if (behavior != MemberCloneBehavior.Ignore)
5063
{
51-
bool isShallow = HasShallowAttribute(property, compilation);
52-
members.Add(new MemberAnalysis(MemberModel.Create(property, nullabilityEnabled, compilation, isShallow), property.Type));
64+
members.Add(new MemberAnalysis(MemberModel.Create(property, nullabilityEnabled, compilation, behavior), property.Type));
5365
}
5466
}
5567
}
@@ -58,10 +70,10 @@ public static List<MemberAnalysis> GetMembers(
5870
{
5971
if (field.IsConst) continue; // Skip const fields
6072

61-
if (!HasIgnoreAttribute(field, compilation))
73+
MemberCloneBehavior behavior = GetMemberBehavior(field, compilation);
74+
if (behavior != MemberCloneBehavior.Ignore)
6275
{
63-
bool isShallow = HasShallowAttribute(field, compilation);
64-
members.Add(new MemberAnalysis(MemberModel.Create(field, nullabilityEnabled, compilation, isShallow), field.Type));
76+
members.Add(new MemberAnalysis(MemberModel.Create(field, nullabilityEnabled, compilation, behavior), field.Type));
6577
}
6678
}
6779
}
@@ -138,46 +150,136 @@ private static bool IsPopulatableCollectionType(ITypeSymbol type)
138150
return false;
139151
}
140152

141-
private static bool HasIgnoreAttribute(ISymbol member, Compilation compilation)
153+
/// <summary>
154+
/// Gets the clone behavior for a type by checking for FastClonerBehaviorAttribute on the type definition.
155+
/// </summary>
156+
private static MemberCloneBehavior? GetTypeBehavior(ITypeSymbol type, Compilation compilation)
142157
{
158+
INamedTypeSymbol? behaviorAttribute = compilation.GetTypeByMetadataName("FastCloner.Code.FastClonerBehaviorAttribute");
143159
INamedTypeSymbol? ignoreAttribute = compilation.GetTypeByMetadataName("FastCloner.Code.FastClonerIgnoreAttribute");
144-
INamedTypeSymbol? nonSerializedAttribute = compilation.GetTypeByMetadataName("System.NonSerializedAttribute");
160+
INamedTypeSymbol? shallowAttribute = compilation.GetTypeByMetadataName("FastCloner.Code.FastClonerShallowAttribute");
161+
INamedTypeSymbol? referenceAttribute = compilation.GetTypeByMetadataName("FastCloner.Code.FastClonerReferenceAttribute");
145162

146-
foreach (AttributeData? attr in member.GetAttributes())
163+
foreach (AttributeData attr in type.GetAttributes())
147164
{
148-
if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, ignoreAttribute))
165+
INamedTypeSymbol? attrClass = attr.AttributeClass;
166+
if (attrClass == null)
167+
continue;
168+
169+
if (ignoreAttribute != null && SymbolEqualityComparer.Default.Equals(attrClass, ignoreAttribute))
149170
{
150-
// Check if Ignored property is true (default is true)
151-
if (attr.ConstructorArguments.Length == 0)
152-
return true;
153-
if (attr.ConstructorArguments.Length > 0 && attr.ConstructorArguments[0].Value is bool ignored)
154-
return ignored;
155-
return true;
171+
return attr.ConstructorArguments.Length switch
172+
{
173+
0 => MemberCloneBehavior.Ignore,
174+
> 0 when attr.ConstructorArguments[0].Value is bool ignored => ignored ? MemberCloneBehavior.Ignore : null,
175+
_ => MemberCloneBehavior.Ignore
176+
};
177+
}
178+
179+
if (shallowAttribute != null && SymbolEqualityComparer.Default.Equals(attrClass, shallowAttribute))
180+
{
181+
return MemberCloneBehavior.Shallow;
156182
}
157183

158-
if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, nonSerializedAttribute))
184+
if (referenceAttribute != null && SymbolEqualityComparer.Default.Equals(attrClass, referenceAttribute))
159185
{
160-
return true;
186+
return MemberCloneBehavior.Reference;
187+
}
188+
189+
if (behaviorAttribute != null && SymbolEqualityComparer.Default.Equals(attrClass, behaviorAttribute))
190+
{
191+
if (attr.ConstructorArguments.Length > 0 && attr.ConstructorArguments[0].Value is int behaviorInt)
192+
{
193+
return (MemberCloneBehavior)behaviorInt;
194+
}
161195
}
162196
}
163197

164-
return false;
198+
return null;
165199
}
166200

167-
private static bool HasShallowAttribute(ISymbol member, Compilation compilation)
201+
/// <summary>
202+
/// Gets the clone behavior for a member by checking:
203+
/// 1. Member-level FastClonerBehaviorAttribute (highest priority)
204+
/// 2. [NonSerialized] attribute (treat as Ignore)
205+
/// 3. Type-level FastClonerBehaviorAttribute on the member's type (lowest priority)
206+
/// </summary>
207+
private static MemberCloneBehavior GetMemberBehavior(ISymbol member, ITypeSymbol memberType, Compilation compilation)
168208
{
209+
// Get all relevant attribute types
210+
INamedTypeSymbol? behaviorAttribute = compilation.GetTypeByMetadataName("FastCloner.Code.FastClonerBehaviorAttribute");
211+
INamedTypeSymbol? ignoreAttribute = compilation.GetTypeByMetadataName("FastCloner.Code.FastClonerIgnoreAttribute");
169212
INamedTypeSymbol? shallowAttribute = compilation.GetTypeByMetadataName("FastCloner.Code.FastClonerShallowAttribute");
170-
if (shallowAttribute == null)
171-
return false;
213+
INamedTypeSymbol? referenceAttribute = compilation.GetTypeByMetadataName("FastCloner.Code.FastClonerReferenceAttribute");
214+
INamedTypeSymbol? nonSerializedAttribute = compilation.GetTypeByMetadataName("System.NonSerializedAttribute");
172215

173-
foreach (AttributeData? attr in member.GetAttributes())
216+
// 1. Check for member-level attributes first (highest priority)
217+
foreach (AttributeData attr in member.GetAttributes())
174218
{
175-
if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, shallowAttribute))
219+
INamedTypeSymbol? attrClass = attr.AttributeClass;
220+
if (attrClass == null)
221+
continue;
222+
223+
// Check for specific derived attributes first (shorthand attributes)
224+
if (ignoreAttribute != null && SymbolEqualityComparer.Default.Equals(attrClass, ignoreAttribute))
176225
{
177-
return true;
226+
return attr.ConstructorArguments.Length switch
227+
{
228+
// Check if Ignored property is true (default is true)
229+
0 => MemberCloneBehavior.Ignore,
230+
> 0 when attr.ConstructorArguments[0].Value is bool ignored => ignored ? MemberCloneBehavior.Ignore : MemberCloneBehavior.Clone,
231+
_ => MemberCloneBehavior.Ignore
232+
};
233+
}
234+
235+
if (shallowAttribute != null && SymbolEqualityComparer.Default.Equals(attrClass, shallowAttribute))
236+
{
237+
return MemberCloneBehavior.Shallow;
238+
}
239+
240+
if (referenceAttribute != null && SymbolEqualityComparer.Default.Equals(attrClass, referenceAttribute))
241+
{
242+
return MemberCloneBehavior.Reference;
243+
}
244+
245+
// Check for base FastClonerBehaviorAttribute with explicit behavior parameter
246+
if (behaviorAttribute != null && SymbolEqualityComparer.Default.Equals(attrClass, behaviorAttribute))
247+
{
248+
if (attr.ConstructorArguments.Length > 0 && attr.ConstructorArguments[0].Value is int behaviorInt)
249+
{
250+
return (MemberCloneBehavior)behaviorInt;
251+
}
252+
}
253+
254+
// Check for [NonSerialized] - treat as Ignore
255+
if (nonSerializedAttribute != null && SymbolEqualityComparer.Default.Equals(attrClass, nonSerializedAttribute))
256+
{
257+
return MemberCloneBehavior.Ignore;
178258
}
179259
}
180260

181-
return false;
261+
// 2. Check for type-level attribute on the member's type
262+
MemberCloneBehavior? typeBehavior = GetTypeBehavior(memberType, compilation);
263+
264+
return typeBehavior ?? MemberCloneBehavior.Clone;
265+
}
266+
267+
/// <summary>
268+
/// Gets the clone behavior for a member (overload for backward compatibility).
269+
/// </summary>
270+
private static MemberCloneBehavior GetMemberBehavior(ISymbol member, Compilation compilation)
271+
{
272+
ITypeSymbol? memberType = member switch
273+
{
274+
IFieldSymbol f => f.Type,
275+
IPropertySymbol p => p.Type,
276+
IEventSymbol e => e.Type,
277+
_ => null
278+
};
279+
280+
return memberType != null
281+
? GetMemberBehavior(member, memberType, compilation)
282+
: MemberCloneBehavior.Clone;
182283
}
183284
}
285+

src/FastCloner.SourceGenerator/MemberModel.cs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,19 @@ internal readonly record struct MemberModel(
8383
bool HasGetter, // Whether the property has a getter
8484
bool HasSetter, // Whether the property has a setter (regular, not init-only)
8585
bool SetterIsAccessible, // Whether the setter is publicly accessible (not private/protected)
86-
bool IsShallowClone // Whether the member should be shallow cloned (has [FastClonerShallow] attribute)
86+
MemberCloneBehavior MemberBehavior // The clone behavior for this member (Clone, Reference, Shallow, Ignore)
8787
) : IEquatable<MemberModel>
8888
{
89-
public static MemberModel Create(IPropertySymbol property, bool nullabilityEnabled, Compilation compilation, bool isShallowClone = false)
89+
/// <summary>
90+
/// Returns true if the member should have its reference copied directly without deep cloning.
91+
/// This applies to both Shallow and Reference behaviors.
92+
/// </summary>
93+
public bool ShouldCopyReference => MemberBehavior == MemberCloneBehavior.Shallow || MemberBehavior == MemberCloneBehavior.Reference;
94+
95+
// Legacy property for backward compatibility
96+
public bool IsShallowClone => ShouldCopyReference;
97+
98+
public static MemberModel Create(IPropertySymbol property, bool nullabilityEnabled, Compilation compilation, MemberCloneBehavior memberBehavior = MemberCloneBehavior.Clone)
9099
{
91100
(MemberTypeKind typeKind, string? elementName, string? keyName, string? valueName, bool elementSafe, bool elementClonable, bool keySafe, bool keyClonable, bool valSafe, bool valClonable, bool requiresFastCloner, CollectionKind collectionKind, string? concreteType, int arrayRank)
92101
= AnalyzeType(property.Type, compilation);
@@ -132,10 +141,10 @@ public static MemberModel Create(IPropertySymbol property, bool nullabilityEnabl
132141
hasGetter,
133142
hasSetter,
134143
setterIsAccessible,
135-
isShallowClone);
144+
memberBehavior);
136145
}
137146

138-
public static MemberModel Create(IFieldSymbol field, bool nullabilityEnabled, Compilation compilation, bool isShallowClone = false)
147+
public static MemberModel Create(IFieldSymbol field, bool nullabilityEnabled, Compilation compilation, MemberCloneBehavior memberBehavior = MemberCloneBehavior.Clone)
139148
{
140149
(MemberTypeKind typeKind, string? elementName, string? keyName, string? valueName, bool elementSafe, bool elementClonable, bool keySafe, bool keyClonable, bool valSafe, bool valClonable, bool requiresFastCloner, CollectionKind collectionKind, string? concreteType, int arrayRank)
141150
= AnalyzeType(field.Type, compilation);
@@ -175,7 +184,7 @@ public static MemberModel Create(IFieldSymbol field, bool nullabilityEnabled, Co
175184
hasGetter,
176185
hasSetter,
177186
setterIsAccessible,
178-
isShallowClone);
187+
memberBehavior);
179188
}
180189

181190
private static (MemberTypeKind kind, string? elem, string? key, string? val, bool elemSafe, bool elemClon, bool keySafe, bool keyClon, bool valSafe, bool valClon, bool requiresFastCloner, CollectionKind collKind, string? concreteType, int arrayRank)

src/FastCloner.SourceGenerator/NestedTypeCollector.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public static void Collect(
8484
true, // HasGetter - helper methods always have access
8585
true, // HasSetter - helper methods always have access
8686
true, // SetterIsAccessible - helper methods always have access
87-
false // IsShallowClone - helper methods are never shallow
87+
MemberCloneBehavior.Clone // MemberBehavior - helper methods use default cloning
8888
);
8989

9090
if (!nestedTypes.ContainsKey(model.TypeFullName))
@@ -142,7 +142,7 @@ public static void Collect(
142142
true, // HasGetter - helper methods always have access
143143
true, // HasSetter - helper methods always have access
144144
true, // SetterIsAccessible - helper methods always have access
145-
false // IsShallowClone - helper methods are never shallow
145+
MemberCloneBehavior.Clone // MemberBehavior - helper methods use default cloning
146146
);
147147

148148
if (!nestedTypes.ContainsKey(model.TypeFullName))

0 commit comments

Comments
 (0)