AOT-compatible static projection registry + SyntaxFactory-based emission#166
AOT-compatible static projection registry + SyntaxFactory-based emission#166
Conversation
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
…benchmark comparison Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
|
@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. |
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
Done in 7e49dc7. The generated 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 |
There was a problem hiding this comment.
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.csaggregating projectables into a static dictionary keyed byMethodHandle.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.
src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs
Outdated
Show resolved
Hide resolved
| } | ||
|
|
||
| var typeofExprs = parameterTypeNames | ||
| .Select(ExpressionSyntax (name) => TypeOfExpression(ParseTypeName(name))) |
There was a problem hiding this comment.
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.
| .Select(ExpressionSyntax (name) => TypeOfExpression(ParseTypeName(name))) | |
| .Select(name => TypeOfExpression(ParseTypeName(name))) |
src/EntityFrameworkCore.Projectables.Generator/ProjectableRegistryEntry.cs
Show resolved
Hide resolved
...s/EntityFrameworkCore.Projectables.Generator.Tests/ProjectionExpressionGeneratorTestsBase.cs
Show resolved
Hide resolved
|
@copilot implement review suggestions |
…bleArray, lambda fix, StringComparison.Ordinal Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
Done in e68b8d6. All four review suggestions implemented:
|

EmitRegistryRegisterhelper +const BindingFlags allFlagsRegistryTests.cswith 8 testsConcurrentDictionarynull storage: sentinel delegate_nullRegistry = _ => null!avoids storing null values; callers useReferenceEqualsto detect "no registry"BuildTypeArrayExprlambda syntax:name => (ExpressionSyntax)TypeOfExpression(...)(typed-lambda syntax not valid in netstandard2.0 target)ImmutableArraystructural equality: newEquatableImmutableArraystruct with element-wiseSequenceEqualequality and polynomialGetHashCode(noSystem.HashCode— not available in netstandard2.0); implicit conversion to/fromImmutableArray<string>keeps all callers unchangedEndsWithstring comparison: both calls now useStringComparison.Ordinal💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.