Skip to content

Feat cloning by member visibility#42

Merged
lofcz merged 8 commits into
nextfrom
feat-cloning-by-member-visibility
May 10, 2026
Merged

Feat cloning by member visibility#42
lofcz merged 8 commits into
nextfrom
feat-cloning-by-member-visibility

Conversation

@lofcz

@lofcz lofcz commented May 10, 2026

Copy link
Copy Markdown
Owner

Supersedes #41

Copilot AI review requested due to automatic review settings May 10, 2026 12:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a type-level member-visibility policy to FastCloner and updates both the runtime cloner and source generator to better handle cloning of non-public members (via UnsafeAccessor on .NET 8+ and a runtime-bridge proxy on older TFMs). This supersedes the prior approach of skipping private/protected members in the generator by enabling compatible access paths instead.

Changes:

  • Introduces [FastClonerVisibility] / FastClonerMemberVisibility to filter eligible members and resets policy-excluded slots to default values during runtime cloning.
  • Extends the source generator to clone non-public fields/properties using UnsafeAccessor (net8+) or a generated proxy that binds to a runtime “bridge” contract (older TFMs).
  • Adds/updates tests and documentation for the new visibility policy and non-public member cloning behavior.

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/FastCloner/FastClonerSourceGeneratorBridgeContractAttributes.cs Adds internal attributes that define the runtime bridge contract for the source generator.
src/FastCloner/FastClonerSourceGeneratorBridge.cs Implements runtime bridge methods for non-public field/property cloning and member resolution.
src/FastCloner/Code/FastClonerVisibilityAttribute.cs Introduces visibility policy attribute + flags enum for member eligibility.
src/FastCloner/Code/FastClonerExprGenerator.cs Applies visibility filtering in runtime type-shape building and resets excluded members to defaults.
src/FastCloner/Code/FastClonerCache.cs Adds caching for attributed type visibility and minor cache API cleanup.
src/FastCloner.Tests/VisibilityAttributeRuntimeTests.cs Adds runtime tests covering visibility policies and override behavior.
src/FastCloner.Tests/SourceGeneratorEdgeCaseTests.cs Expands SG edge-case coverage for private/protected/internal accessors and visibility policy.
src/FastCloner.Tests/FastCloner.Tests.csproj Updates TUnit package version.
src/FastCloner.Tests/BridgeProxyEmitterTests.cs Adds tests validating bridge proxy emission and stability.
src/FastCloner.SourceGenerator/StateRequirementAnalyzer.cs Replaces target-typed new(...) with explicit generic constructions.
src/FastCloner.SourceGenerator/NonPublicAccessorEmitter.cs Adds codegen for non-public member access (UnsafeAccessor or runtime bridge proxy).
src/FastCloner.SourceGenerator/MemberModel.cs Adds visibility/access strategy metadata to the member model and accessibility mapping helpers.
src/FastCloner.SourceGenerator/MemberCollector.cs Adds visibility-policy filtering and collects non-public members for accessor-based cloning.
src/FastCloner.SourceGenerator/MemberCloneGenerator.cs Routes cloning through non-public accessor emitters when needed.
src/FastCloner.SourceGenerator/FastClonerIncrementalGenerator.cs Collects bridge contract, emits proxy when applicable, and reports new diagnostics for skipped members.
src/FastCloner.SourceGenerator/ContextCodeGenerator.cs Minor ctor invocation cleanup using named args.
src/FastCloner.SourceGenerator/CloneGeneratorContext.cs Stores bridge contract + tracks generated non-public accessors and skipped members.
src/FastCloner.SourceGenerator/CloneCodeGenerator.cs Emits accessor blocks (including generic shells) and threads bridge contract through generation.
src/FastCloner.SourceGenerator/BridgeProxyEmitter.cs Generates a proxy class that binds to runtime bridge methods via reflection + delegates (older TFMs).
src/FastCloner.SourceGenerator/BridgeContractCollector.cs Extracts bridge contract metadata from referenced FastCloner runtime assembly.
src/FastCloner.SourceGenerator/BridgeContract.cs Defines bridge contract/method spec models used by emitter and generator.
src/FastCloner.SourceGenerator/AssemblyInfo.cs Adds InternalsVisibleTo for generator tests.
README.md Documents the new [FastClonerVisibility] behavior and override rules.
.bench-harness-current/Program.cs Removes bench harness program.
.bench-harness-current/BenchHarness.csproj Removes bench harness project.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +93 to +97
private static FastClonerMemberVisibility GetVisibilityPolicyFromType(INamedTypeSymbol type)
{
foreach (AttributeData attr in type.GetAttributes())
{
if (attr.AttributeClass?.ToDisplayString() != "FastCloner.Code.FastClonerVisibilityAttribute")
Comment on lines +57 to +61
if (!HasExplicitMemberBehaviorAttribute(property, compilation))
{
members.Add(new MemberAnalysis(MemberModel.Create(property, nullabilityEnabled, compilation, behavior), property.Type));
FastClonerMemberVisibility memberMask = MemberModel.MapAccessibility(property.DeclaredAccessibility);
if ((visibilityPolicy & memberMask) == 0)
continue;
Comment on lines 48 to 52
bool isPopulatableCollection = IsPopulatableCollectionType(property.Type);
bool hasSetter = property.SetMethod != null;

// Include property if it has an accessible setter OR it's a getter-only populatable collection
if (hasAccessibleSetter || isPopulatableCollection)
if (hasSetter || isPopulatableCollection)
{
Comment on lines +110 to +114
private static bool HasExplicitMemberBehaviorAttribute(ISymbol member, Compilation compilation)
{
INamedTypeSymbol? behaviorAttribute = compilation.GetTypeByMetadataName("FastCloner.Code.FastClonerBehaviorAttribute");
INamedTypeSymbol? shallowAttribute = compilation.GetTypeByMetadataName("FastCloner.Code.FastClonerShallowAttribute");
INamedTypeSymbol? referenceAttribute = compilation.GetTypeByMetadataName("FastCloner.Code.FastClonerReferenceAttribute");
Comment on lines +160 to +163
: (accessor.IsBackingFieldStorage
? $"{accessorPrefix}{accessor.AccessorMethodName}({structRefSource}{sourceVar})"
: throw new System.InvalidOperationException(
$"FastCloner SG: cannot read non-public, non-field member '{accessor.MemberName}' via UnsafeAccessor (would need a getter accessor)."));
Comment on lines +955 to +958
private static FastClonerMemberVisibility PropertyVisibilityMask(PropertyInfo property)
{
MethodInfo? accessor = property.GetSetMethod(nonPublic: true) ?? property.GetGetMethod(nonPublic: true);
if (accessor is null)
Comment on lines +171 to +172
"Either upgrade the consumer to .NET 8+, or install the FastCloner runtime package " +
", or apply [FastClonerVisibility] / [FastClonerIgnore] to opt out explicitly.",
@github-actions

github-actions Bot commented May 10, 2026

Copy link
Copy Markdown

Deep Clone Benchmarks

  • OS: ubuntu-latest
  • Generated (UTC): 2026-05-10 15:33:54

Current FastCloner vs DeepCloner

Benchmark DeepCloner FastCloner Delta Time DC Alloc FC Alloc Delta Alloc
SmallObject 86.79 ns 68.95 ns -21% faster 184 B 48 B -74% less
FileSpec 489.06 ns 283.57 ns -42% faster 920 B 416 B -55% less
StringArray_1000 531.85 ns 549.11 ns +3% slower 8,160 B 8,024 B ~same
SmallObjectWithCollections 622.58 ns 353.75 ns -43% faster 1,096 B 576 B -47% less
DynamicWithDictionary 1,292.33 ns 995.11 ns -23% faster 2,712 B 1,600 B -41% less
MediumNestedObject 1,601.11 ns 1,045.29 ns -35% faster 3,416 B 1,616 B -53% less
DynamicWithNestedObject 1,803.05 ns 1,253.99 ns -30% faster 3,560 B 1,776 B -50% less
DynamicWithArray 5,528.88 ns 4,672.48 ns -15% faster 8,800 B 2,744 B -69% less
LargeEventDocument_10MB 66,399.79 ns 37,891.40 ns -43% faster 129,792 B 49,824 B -62% less
ObjectList_100 165,979.55 ns 105,976.74 ns -36% faster 318,888 B 149,816 B -53% less
ObjectDictionary_50 857,624.87 ns 176,709.74 ns -79% faster 549,668 B 218,768 B -60% less
LargeLogBatch_10MB 6,138,577.40 ns 3,619,739.02 ns -41% faster 3,564,656 B 2,649,098 B -26% less

FastCloner vs latest next baseline

  • Baseline generated (UTC): 2026-05-03 14:54:12
  • Regression thresholds: time > 5%, alloc > 5%
Status Benchmark Delta Time Delta Alloc
DynamicWithArray +4% slower ~same
DynamicWithDictionary ~same ~same
DynamicWithNestedObject -3% faster ~same
FileSpec +3% slower ~same
LargeEventDocument_10MB +2% slower ~same
LargeLogBatch_10MB -2% faster ~same
MediumNestedObject ~same ~same
ObjectDictionary_50 -5% faster ~same
ObjectList_100 ~same ~same
SmallObject ~same ~same
SmallObjectWithCollections -3% faster ~same
StringArray_1000 ~same ~same

Regressions

  • none

Improvements

  • none

Mixed changes

  • none

Copilot AI review requested due to automatic review settings May 10, 2026 14:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 6 comments.

}

[AttributeUsage(AttributeTargets.Method, Inherited = false)]
internal sealed class FastClonerSourceGeneratorBridgeMemberAttribute : Attribute;
Comment on lines +447 to +449
if (skipReadonly)
continue;

Comment on lines +71 to +78
bool participatesInInitializer =
(member is { IsProperty: true, IsInitOnly: true } || member.IsRequired);
if (!participatesInInitializer)
continue;

if (member.AccessorStrategy != NonPublicAccessorStrategy.None)
continue;

Comment on lines +850 to +864
if (hasNonDefaultPolicy)
{
PropertyInfo? backedProperty = TryGetBackingPropertyForField(field);
bool hasExplicit = HasExplicitMemberBehavior(field) ||
(backedProperty is not null && HasExplicitMemberBehavior(backedProperty));
if (!hasExplicit)
{
FastClonerMemberVisibility memberMask = backedProperty is not null
? PropertyVisibilityMask(backedProperty)
: FieldVisibilityMask(field);
if ((visibilityPolicy & memberMask) == 0)
{
(excludedByVisibility ??= []).Add(field);
continue;
}
Comment on lines +200 to +228
private static void WriteRuntimeBridgeCall(
CloneGeneratorContext context,
StringBuilder sb,
MemberModel member,
NonPublicAccessor accessor,
string resultVar,
string sourceVar,
string indent)
{
string accessorPrefix = context.GetNonPublicAccessorPrefix();
string proxyFqn = "global::" + context.BridgeContract.ProxyTypeFullName;

if (accessor.IsBackingFieldStorage)
{
string fiRef = $"{accessorPrefix}{accessor.AccessorMethodName}_FI";
if (member.IsShallowClone)
{
sb.AppendLine($"{indent}{proxyFqn}.CopyField({sourceVar}, {resultVar}, {fiRef});");
}
else
{
sb.AppendLine($"{indent}{proxyFqn}.DeepCloneField({sourceVar}, {resultVar}, {fiRef});");
}
}
else
{
string piRef = $"{accessorPrefix}{accessor.AccessorMethodName}_PI";
sb.AppendLine($"{indent}{proxyFqn}.DeepCloneProperty({sourceVar}, {resultVar}, {piRef});");
}
Comment on lines +13 to 16

if (member.AccessorStrategy != NonPublicAccessorStrategy.None)
return string.Empty;

Copilot AI review requested due to automatic review settings May 10, 2026 15:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 35 changed files in this pull request and generated 3 comments.

Comment on lines +449 to +454

expressionList.Add(Expression.Call(
Expression.Constant(fieldInfo),
fieldSetMethod,
Expression.Convert(toLocal, typeof(object)),
Expression.Convert(Expression.Default(fieldInfo.FieldType), typeof(object))));
Comment on lines +16 to +26
/// <summary>
/// Pure-internal members. Combines with <see cref="Protected"/> to also include
/// <c>protected internal</c> and <c>private protected</c>.
/// </summary>
Internal = 1 << 1,

/// <summary>
/// Protected members. Combines with <see cref="Internal"/> to also include
/// <c>protected internal</c> and <c>private protected</c>.
/// </summary>
Protected = 1 << 2,
Comment on lines +55 to +56
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)]
public sealed class FastClonerVisibilityAttribute : Attribute
@lofcz lofcz merged commit 50386f8 into next May 10, 2026
15 checks passed
@MatthewSteeples

Copy link
Copy Markdown
Contributor

The FastCloner.SourceGenerator package also needs publishing after this change

@lofcz

lofcz commented May 12, 2026

Copy link
Copy Markdown
Owner Author

@MatthewSteeples sorry, there was some versioning drift, published SourceGenerator 1.2.1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants