Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions .bench-harness-current/BenchHarness.csproj

This file was deleted.

34 changes: 0 additions & 34 deletions .bench-harness-current/Program.cs

This file was deleted.

18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,24 @@ if (ctx.TryClone(obj, out var cloned))
}
```

### Member Visibility

By default, all members are eligible for cloning regardless of access modifier. Apply `[FastClonerVisibility]` to a type to restrict cloning to a specific subset:

```csharp
[FastClonerVisibility(FastClonerMemberVisibility.Public | FastClonerMemberVisibility.Internal)]
public class Dto
{
public int Id { get; set; } // cloned
internal string Tag; // cloned
private string _secret; // skipped
}
```

The policy applies to both reflection and source-generated paths; excluded members are left at their default value on the clone.

The visibility filter runs before the behavior pipeline and is bypassed for any member carrying a member-level behavior attribute (`[FastClonerBehavior]`, `[FastClonerIgnore]`, `[FastClonerShallow]`, `[FastClonerReference]`), so those members are always included with their declared behavior.

### Nullability Trust

The generator can be instructed to fully trust nullability annotations. When `[FastClonerTrustNullability]` attribute is applied, FastCloner will skip null checks for non-nullable reference types (e.g., `string` vs `string?`), assuming the contract is valid.
Expand Down
3 changes: 3 additions & 0 deletions src/FastCloner.SourceGenerator/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("FastCloner.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100bd882e7dcc8a5cfdd6e2193c66bb144ab67e83964b825035b4b05ccdc40a5a7218a497799f98f98e628c38492fa227ad1579c7d701934ea2e30459a4dabfcfa498fc9f4dc6d3e3f118e4df6615aced3da480ea45d30832ddbfd56acebc957ee6345944d2e9b82705a725276146baadf08cf7a8612fd4f3ceb8b8ec9529b975c2")]
22 changes: 22 additions & 0 deletions src/FastCloner.SourceGenerator/BridgeContract.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace FastCloner.SourceGenerator;

internal sealed record BridgeMethodSpec(
string Name,
string ReturnTypeFqn,
EquatableArray<string> ParameterTypeFqns)
{
public bool IsVoid => ReturnTypeFqn == "void";
}

internal sealed record BridgeContract(
string BridgeTypeMetadataName,
string ProxyTypeFullName,
EquatableArray<BridgeMethodSpec> Methods,
bool IsAvailable)
{
public static BridgeContract Empty { get; } = new BridgeContract(
BridgeTypeMetadataName: string.Empty,
ProxyTypeFullName: string.Empty,
Methods: EquatableArray<BridgeMethodSpec>.Empty,
IsAvailable: false);
}
64 changes: 64 additions & 0 deletions src/FastCloner.SourceGenerator/BridgeContractCollector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;

namespace FastCloner.SourceGenerator;

internal static class BridgeContractCollector
{
private const string BridgeTypeMetadataName = "FastCloner.FastClonerSourceGeneratorBridge";
private const string BridgeAttrFqn = "FastCloner.FastClonerSourceGeneratorBridgeAttribute";
private const string BridgeMemberAttrFqn = "FastCloner.FastClonerSourceGeneratorBridgeMemberAttribute";

public static BridgeContract Collect(Compilation compilation)
{
INamedTypeSymbol? bridge = compilation.GetTypeByMetadataName(BridgeTypeMetadataName);

AttributeData? bridgeAttr = bridge?.GetAttributes()
.FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == BridgeAttrFqn);
if (bridgeAttr == null || bridgeAttr.ConstructorArguments.Length == 0 || bridgeAttr.ConstructorArguments[0].Value is not string proxyFqn || string.IsNullOrEmpty(proxyFqn))
return BridgeContract.Empty;

List<BridgeMethodSpec> methods = [];

foreach (ISymbol member in bridge?.GetMembers() ?? [])
{
if (member is not IMethodSymbol method)
continue;
if (method.MethodKind != MethodKind.Ordinary || !method.IsStatic)
continue;

bool hasMemberAttr = method.GetAttributes()
.Any(a => a.AttributeClass?.ToDisplayString() == BridgeMemberAttrFqn);
if (!hasMemberAttr)
continue;

string returnTypeFqn = method.ReturnsVoid
? "void"
: method.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);

string[] paramFqns = new string[method.Parameters.Length];
for (int i = 0; i < method.Parameters.Length; i++)
{
paramFqns[i] = method.Parameters[i].Type
.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
}

methods.Add(new BridgeMethodSpec(
Name: method.Name,
ReturnTypeFqn: returnTypeFqn,
ParameterTypeFqns: new EquatableArray<string>(paramFqns)));
}

if (methods.Count == 0)
return BridgeContract.Empty;

BridgeMethodSpec[] sortedMethods = methods.OrderBy(m => m.Name, System.StringComparer.Ordinal).ToArray();

return new BridgeContract(
BridgeTypeMetadataName: BridgeTypeMetadataName,
ProxyTypeFullName: proxyFqn!,
Methods: new EquatableArray<BridgeMethodSpec>(sortedMethods),
IsAvailable: true);
}
}
158 changes: 158 additions & 0 deletions src/FastCloner.SourceGenerator/BridgeProxyEmitter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
using System.Text;

namespace FastCloner.SourceGenerator;

internal static class BridgeProxyEmitter
{
public const string HintName = "FastCloner_SGBridgeProxy.g.cs";

public static bool ShouldEmit(TargetFramework targetFramework, BridgeContract contract)
{
if (targetFramework >= TargetFramework.Net8)
return false;

return contract is { IsAvailable: true, Methods.Count: > 0 };
}

public static string Emit(BridgeContract contract)
{
string proxyFqn = contract.ProxyTypeFullName;
int lastDot = proxyFqn.LastIndexOf('.');
string proxyNamespace = lastDot >= 0 ? proxyFqn.Substring(0, lastDot) : string.Empty;
string proxyTypeName = lastDot >= 0 ? proxyFqn.Substring(lastDot + 1) : proxyFqn;

StringBuilder sb = new StringBuilder(2048);

sb.AppendLine("#nullable disable");
sb.AppendLine("using System;");
sb.AppendLine("using System.Reflection;");
sb.AppendLine();

bool hasNamespace = proxyNamespace.Length > 0;
if (hasNamespace)
{
sb.Append("namespace ").Append(proxyNamespace).AppendLine();
sb.AppendLine("{");
}

string indent = hasNamespace ? " " : string.Empty;

sb.Append(indent).AppendLine("[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]");
sb.Append(indent).AppendLine("[global::System.Runtime.CompilerServices.CompilerGenerated]");
sb.Append(indent).Append("internal static class ").AppendLine(proxyTypeName);
sb.Append(indent).AppendLine("{");

string body = hasNamespace ? indent + " " : " ";

sb.Append(body).Append("private const string BridgeTypeName = \"").Append(contract.BridgeTypeMetadataName).AppendLine("\";");
sb.AppendLine();
sb.Append(body).AppendLine("private static readonly Type BridgeType = ResolveBridgeType();");

foreach (BridgeMethodSpec method in contract.Methods)
{
sb.AppendLine();
string delegateType = BuildDelegateType(method);
string parameterTypeofList = BuildTypeofList(method.ParameterTypeFqns);

sb.Append(body).Append("internal static readonly ").Append(delegateType).Append(' ').Append(method.Name).AppendLine(" =");
sb.Append(body).Append(" (").Append(delegateType).AppendLine(")Delegate.CreateDelegate(");
sb.Append(body).Append(" typeof(").Append(delegateType).AppendLine("),");
sb.Append(body).Append(" ResolveStaticMethod(\"").Append(method.Name).Append('\"');
if (parameterTypeofList.Length > 0)
{
sb.Append(", ").Append(parameterTypeofList);
}
sb.AppendLine("));");
}

sb.AppendLine();
sb.Append(body).AppendLine("private static Type ResolveBridgeType()");
sb.Append(body).AppendLine("{");
sb.Append(body).AppendLine(" Assembly runtimeAssembly = typeof(global::FastCloner.Code.FastClonerGenerator).Assembly;");
sb.Append(body).AppendLine(" Type type = runtimeAssembly.GetType(BridgeTypeName, throwOnError: false);");
sb.Append(body).AppendLine(" if (type == null)");
sb.Append(body).AppendLine(" {");
sb.Append(body).AppendLine(" throw new InvalidOperationException(");
sb.Append(body).AppendLine(" \"FastCloner source generator: the runtime bridge type '\" + BridgeTypeName +");
sb.Append(body).AppendLine(" \"' was not found in assembly '\" + runtimeAssembly.FullName +");
sb.Append(body).AppendLine(" \"'. The FastCloner runtime version may be incompatible with the source generator. \" +");
sb.Append(body).AppendLine(" \"Update the FastCloner package to a matching version.\");");
sb.Append(body).AppendLine(" }");
sb.Append(body).AppendLine(" return type;");
sb.Append(body).AppendLine("}");

sb.AppendLine();
sb.Append(body).AppendLine("private static MethodInfo ResolveStaticMethod(string name, params Type[] parameterTypes)");
sb.Append(body).AppendLine("{");
sb.Append(body).AppendLine(" const BindingFlags flags = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public;");
sb.Append(body).AppendLine(" MethodInfo method = BridgeType.GetMethod(name, flags, binder: null, types: parameterTypes, modifiers: null);");
sb.Append(body).AppendLine(" if (method == null)");
sb.Append(body).AppendLine(" {");
sb.Append(body).AppendLine(" throw new InvalidOperationException(");
sb.Append(body).AppendLine(" \"FastCloner source generator: bridge method '\" + name +");
sb.Append(body).AppendLine(" \"' was not found on '\" + BridgeType.FullName +");
sb.Append(body).AppendLine(" \"'. The FastCloner runtime version may be incompatible with the source generator.\");");
sb.Append(body).AppendLine(" }");
sb.Append(body).AppendLine(" return method;");
sb.Append(body).AppendLine("}");

sb.Append(indent).AppendLine("}");

if (hasNamespace)
{
sb.AppendLine("}");
}

return sb.ToString();
}

private static string BuildDelegateType(BridgeMethodSpec method)
{
if (method.IsVoid)
{
if (method.ParameterTypeFqns.Count == 0)
{
return "global::System.Action";
}

return "global::System.Action<" + Join(method.ParameterTypeFqns) + ">";
}

if (method.ParameterTypeFqns.Count == 0)
{
return "global::System.Func<" + method.ReturnTypeFqn + ">";
}

return "global::System.Func<" + Join(method.ParameterTypeFqns) + ", " + method.ReturnTypeFqn + ">";
}

private static string BuildTypeofList(EquatableArray<string> parameterTypeFqns)
{
string[]? items = parameterTypeFqns.GetArray();
if (items is null || items.Length == 0)
return string.Empty;

StringBuilder sb = new StringBuilder();
for (int i = 0; i < items.Length; i++)
{
if (i > 0) sb.Append(", ");
sb.Append("typeof(").Append(items[i]).Append(')');
}
return sb.ToString();
}

private static string Join(EquatableArray<string> items)
{
string[]? array = items.GetArray();
if (array is null || array.Length == 0)
return string.Empty;

StringBuilder sb = new StringBuilder();
for (int i = 0; i < array.Length; i++)
{
if (i > 0) sb.Append(", ");
sb.Append(array[i]);
}
return sb.ToString();
}
}
Loading
Loading