Skip to content

AOT-compatible static projection registry + SyntaxFactory-based emission#166

Draft
Copilot wants to merge 7 commits intomasterfrom
copilot/implement-aot-compatible-projection-registry
Draft

AOT-compatible static projection registry + SyntaxFactory-based emission#166
Copilot wants to merge 7 commits intomasterfrom
copilot/implement-aot-compatible-projection-registry

Conversation

Copy link
Contributor

Copilot AI commented Mar 3, 2026

  • SyntaxFactory-based EmitRegistry
  • Registry vs reflection benchmark
  • Compact registry generation: Register helper + const BindingFlags allFlags
  • RegistryTests.cs with 8 tests
  • Fix ConcurrentDictionary null storage: sentinel delegate _nullRegistry = _ => null! avoids storing null values; callers use ReferenceEquals to detect "no registry"
  • Fix BuildTypeArrayExpr lambda syntax: name => (ExpressionSyntax)TypeOfExpression(...) (typed-lambda syntax not valid in netstandard2.0 target)
  • Fix ImmutableArray structural equality: new EquatableImmutableArray struct with element-wise SequenceEqual equality and polynomial GetHashCode (no System.HashCode — not available in netstandard2.0); implicit conversion to/from ImmutableArray<string> keeps all callers unchanged
  • Fix EndsWith string comparison: both calls now use StringComparison.Ordinal

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement AOT-compatible static projection registry Add AOT-compatible static projection registry + trim-safe replacer init Mar 3, 2026
Copilot finished work on behalf of PhenX March 3, 2026 21:02
…benchmark comparison

Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
Copilot AI changed the title Add AOT-compatible static projection registry + trim-safe replacer init AOT-compatible static projection registry + SyntaxFactory-based emission Mar 3, 2026
Copilot finished work on behalf of PhenX March 3, 2026 21:35
@PhenX
Copy link
Collaborator

PhenX commented Mar 3, 2026

Benchmark result

image

@PhenX
Copy link
Collaborator

PhenX commented Mar 4, 2026

@copilot the generated code for the registry is too repetitive, find a way to specify the registry data in an efficient data structure and create an efficient method to fill the registry.
Add tests for this registry too

Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 4, 2026

@copilot the generated code for the registry is too repetitive, find a way to specify the registry data in an efficient data structure and create an efficient method to fill the regi...

Done in 7e49dc7.

The generated Build() now uses a single Register helper method instead of repeating a 9-line block per entry. Each projectable becomes one line:

private static Dictionary<nint, LambdaExpression> Build()
{
    const BindingFlags allFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
    var map = new Dictionary<nint, LambdaExpression>();
    Register(map, typeof(global::Foo.C).GetProperty("IdPlus1", allFlags)?.GetMethod, "...Foo_C_IdPlus1");
    Register(map, typeof(global::Foo.C).GetMethod("Add", allFlags, null, new global::System.Type[] { typeof(int) }, null), "...Foo_C_Add_P0_int");
    return map;
}

private static void Register(Dictionary<nint, LambdaExpression> map, MethodBase? m, string exprClass)
{
    if (m is null) return;
    var exprType = m.DeclaringType?.Assembly.GetType(exprClass);
    var exprMethod = exprType?.GetMethod("Expression", BindingFlags.Static | BindingFlags.NonPublic);
    if (exprMethod is not null)
        map[m.MethodHandle.Value] = (LambdaExpression)exprMethod.Invoke(null, null)!;
}

Also added RegistryTests.cs with 8 tests covering: no projectables → no registry, property/method/overload entries, generic class skipped, const allFlags present, Register helper uses m.DeclaringType?.Assembly, and multi-entry count validation.

Copilot finished work on behalf of PhenX March 4, 2026 08:45
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a source-generated static projection registry to enable AOT-friendlier, low-overhead [Projectable] resolution by preferring an O(1) registry lookup over repeated reflection, and adds tests/benchmarks to validate and measure the impact.

Changes:

  • Generator emits ProjectionRegistry.g.cs aggregating projectables into a static dictionary keyed by MethodHandle.Value, using SyntaxFactory-based emission.
  • Runtime resolver prefers the registry (cached per-assembly) and exposes an explicit reflection-only slow path for benchmarks/fallback.
  • Tests/benchmarks updated and expanded to cover registry generation and measure cold-start + resolver overhead.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/EntityFrameworkCore.Projectables.Generator.Tests/RegistryTests.cs Adds dedicated tests asserting registry emission and contents.
tests/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTestsBase.cs Wraps generator run results to filter out ProjectionRegistry.g.cs for existing tests.
src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs Adds registry-first lookup with per-assembly caching + reflection-only fallback API.
src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs Makes _select / _where static readonly using expression-tree extraction (trim-safe).
src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs Adds incremental pipeline to collect entries and emit ProjectionRegistry.g.cs.
src/EntityFrameworkCore.Projectables.Generator/ProjectableRegistryEntry.cs Adds record used to represent registry entries in incremental pipeline.
src/EntityFrameworkCore.Projectables.Generator/IsExternalInit.cs Adds IsExternalInit polyfill for record support on netstandard2.0 generator target.
benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ResolverOverhead.cs Adds cold-start per-DbContext resolver overhead benchmark.
benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ExpressionResolverBenchmark.cs Adds micro-benchmarks comparing registry vs reflection resolution.
benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ProjectableProperties.cs Adds MemoryDiagnoser attribute.
benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ProjectableMethods.cs Adds MemoryDiagnoser attribute.
benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ProjectableExtensionMethods.cs Adds MemoryDiagnoser attribute.
benchmarks/EntityFrameworkCore.Projectables.Benchmarks/PlainOverhead.cs Adds MemoryDiagnoser attribute.
benchmarks/EntityFrameworkCore.Projectables.Benchmarks/Helpers/TestEntity.cs Adds IdPlusDelta(int) projectable for parameter-type disambiguation benchmarking.

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

}

var typeofExprs = parameterTypeNames
.Select(ExpressionSyntax (name) => TypeOfExpression(ParseTypeName(name)))
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

BuildTypeArrayExpr currently has an invalid lambda parameter declaration: .Select(ExpressionSyntax (name) => ...) will not compile. It should be a standard lambda (e.g., name => ...) or a typed lambda ((string name) => ...), producing an ExpressionSyntax result.

Suggested change
.Select(ExpressionSyntax (name) => TypeOfExpression(ParseTypeName(name)))
.Select(name => TypeOfExpression(ParseTypeName(name)))

Copilot uses AI. Check for mistakes.
@PhenX
Copy link
Collaborator

PhenX commented Mar 4, 2026

@copilot implement review suggestions

…bleArray, lambda fix, StringComparison.Ordinal

Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 4, 2026

@copilot implement review suggestions

Done in e68b8d6. All four review suggestions implemented:

  1. ConcurrentDictionary null storage — replaced nullable value type with a _nullRegistry sentinel delegate; GetOrAdd always stores a non-null value, callers use ReferenceEquals to distinguish "no registry" from a real delegate.

  2. BuildTypeArrayExpr lambda syntax — changed .Select(ExpressionSyntax (name) => ...) (C# 14 typed-lambda, invalid on netstandard2.0) to .Select(name => (ExpressionSyntax)TypeOfExpression(...)).

  3. ImmutableArray<string> structural equality — added EquatableImmutableArray struct that wraps ImmutableArray<string> with element-wise SequenceEqual equality and a polynomial GetHashCode (no System.HashCode — unavailable on netstandard2.0). Implicit conversions keep all call sites unchanged.

  4. StringComparison.Ordinal — both EndsWith("ProjectionRegistry.g.cs") calls now pass StringComparison.Ordinal.

Copilot finished work on behalf of PhenX March 4, 2026 21:06
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