From 7d1aa00ebaf72f7a5be229270314f12a9d6e382c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Mar 2026 20:24:03 +0000
Subject: [PATCH 01/12] Initial plan
From ed52f7704ae5d5748cdb257754a2c510c14905df Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Mar 2026 20:55:04 +0000
Subject: [PATCH 02/12] Implement AOT-compatible static projection registry
Co-authored-by: PhenX <42170+PhenX@users.noreply.github.com>
---
.../ExpressionResolverBenchmark.cs | 40 +++
.../Helpers/TestEntity.cs | 3 +
.../PlainOverhead.cs | 1 +
.../ProjectableExtensionMethods.cs | 1 +
.../ProjectableMethods.cs | 1 +
.../ProjectableProperties.cs | 1 +
.../ResolverOverhead.cs | 72 +++++
.../IsExternalInit.cs | 6 +
.../ProjectableRegistryEntry.cs | 21 ++
.../ProjectionExpressionGenerator.cs | 254 ++++++++++++++++++
.../Services/ProjectableExpressionReplacer.cs | 24 +-
.../Services/ProjectionExpressionResolver.cs | 37 ++-
.../ProjectionExpressionGeneratorTestsBase.cs | 51 +++-
13 files changed, 491 insertions(+), 21 deletions(-)
create mode 100644 benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ExpressionResolverBenchmark.cs
create mode 100644 benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ResolverOverhead.cs
create mode 100644 src/EntityFrameworkCore.Projectables.Generator/IsExternalInit.cs
create mode 100644 src/EntityFrameworkCore.Projectables.Generator/ProjectableRegistryEntry.cs
diff --git a/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ExpressionResolverBenchmark.cs b/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ExpressionResolverBenchmark.cs
new file mode 100644
index 0000000..e2803de
--- /dev/null
+++ b/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ExpressionResolverBenchmark.cs
@@ -0,0 +1,40 @@
+using System.Linq.Expressions;
+using System.Reflection;
+using BenchmarkDotNet.Attributes;
+using EntityFrameworkCore.Projectables.Benchmarks.Helpers;
+using EntityFrameworkCore.Projectables.Services;
+
+namespace EntityFrameworkCore.Projectables.Benchmarks
+{
+ ///
+ /// Micro-benchmarks in
+ /// isolation (no EF Core overhead) to directly compare the registry lookup path against
+ /// the previous per-call reflection chain.
+ ///
+ [MemoryDiagnoser]
+ public class ExpressionResolverBenchmark
+ {
+ private static readonly MemberInfo _propertyMember =
+ typeof(TestEntity).GetProperty(nameof(TestEntity.IdPlus1))!;
+
+ private static readonly MemberInfo _methodMember =
+ typeof(TestEntity).GetMethod(nameof(TestEntity.IdPlus1Method))!;
+
+ private static readonly MemberInfo _methodWithParamMember =
+ typeof(TestEntity).GetMethod(nameof(TestEntity.IdPlusDelta), new[] { typeof(int) })!;
+
+ private readonly ProjectionExpressionResolver _resolver = new();
+
+ [Benchmark(Baseline = true)]
+ public LambdaExpression? ResolveProperty()
+ => _resolver.FindGeneratedExpression(_propertyMember);
+
+ [Benchmark]
+ public LambdaExpression? ResolveMethod()
+ => _resolver.FindGeneratedExpression(_methodMember);
+
+ [Benchmark]
+ public LambdaExpression? ResolveMethodWithParam()
+ => _resolver.FindGeneratedExpression(_methodWithParamMember);
+ }
+}
diff --git a/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/Helpers/TestEntity.cs b/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/Helpers/TestEntity.cs
index 4bc741c..68a04d8 100644
--- a/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/Helpers/TestEntity.cs
+++ b/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/Helpers/TestEntity.cs
@@ -15,5 +15,8 @@ public class TestEntity
[Projectable]
public int IdPlus1Method() => Id + 1;
+
+ [Projectable]
+ public int IdPlusDelta(int delta) => Id + delta;
}
}
diff --git a/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/PlainOverhead.cs b/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/PlainOverhead.cs
index d064b7d..b9cda85 100644
--- a/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/PlainOverhead.cs
+++ b/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/PlainOverhead.cs
@@ -9,6 +9,7 @@
namespace EntityFrameworkCore.Projectables.Benchmarks
{
+ [MemoryDiagnoser]
public class PlainOverhead
{
[Benchmark(Baseline = true)]
diff --git a/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ProjectableExtensionMethods.cs b/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ProjectableExtensionMethods.cs
index fdb0f9f..467d470 100644
--- a/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ProjectableExtensionMethods.cs
+++ b/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ProjectableExtensionMethods.cs
@@ -9,6 +9,7 @@
namespace EntityFrameworkCore.Projectables.Benchmarks
{
+ [MemoryDiagnoser]
public class ProjectableExtensionMethods
{
const int innerLoop = 10000;
diff --git a/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ProjectableMethods.cs b/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ProjectableMethods.cs
index 785b52c..0618627 100644
--- a/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ProjectableMethods.cs
+++ b/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ProjectableMethods.cs
@@ -8,6 +8,7 @@
namespace EntityFrameworkCore.Projectables.Benchmarks
{
+ [MemoryDiagnoser]
public class ProjectableMethods
{
const int innerLoop = 10000;
diff --git a/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ProjectableProperties.cs b/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ProjectableProperties.cs
index 6f2fa57..3a2e2be 100644
--- a/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ProjectableProperties.cs
+++ b/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ProjectableProperties.cs
@@ -8,6 +8,7 @@
namespace EntityFrameworkCore.Projectables.Benchmarks
{
+ [MemoryDiagnoser]
public class ProjectableProperties
{
const int innerLoop = 10000;
diff --git a/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ResolverOverhead.cs b/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ResolverOverhead.cs
new file mode 100644
index 0000000..3876641
--- /dev/null
+++ b/benchmarks/EntityFrameworkCore.Projectables.Benchmarks/ResolverOverhead.cs
@@ -0,0 +1,72 @@
+using System.Linq;
+using BenchmarkDotNet.Attributes;
+using EntityFrameworkCore.Projectables.Benchmarks.Helpers;
+using Microsoft.EntityFrameworkCore;
+
+namespace EntityFrameworkCore.Projectables.Benchmarks
+{
+ ///
+ /// Measures the per-DbContext cold-start cost of resolver lookup by creating a new
+ /// on every iteration. The previous benchmarks reuse a single
+ /// DbContext for 10 000 iterations, so the resolver cache is warm after the first query —
+ /// these benchmarks expose the cost of the very first query per context.
+ ///
+ [MemoryDiagnoser]
+ public class ResolverOverhead
+ {
+ const int Iterations = 1000;
+
+ /// Baseline: no projectables, new DbContext per query.
+ [Benchmark(Baseline = true)]
+ public void WithoutProjectables_FreshDbContext()
+ {
+ for (int i = 0; i < Iterations; i++)
+ {
+ using var dbContext = new TestDbContext(false);
+ dbContext.Entities.Select(x => x.Id + 1).ToQueryString();
+ }
+ }
+
+ ///
+ /// New DbContext per query with a projectable property.
+ /// After the registry is in place this should approach baseline overhead.
+ ///
+ [Benchmark]
+ public void WithProjectables_FreshDbContext_Property()
+ {
+ for (int i = 0; i < Iterations; i++)
+ {
+ using var dbContext = new TestDbContext(true, false);
+ dbContext.Entities.Select(x => x.IdPlus1).ToQueryString();
+ }
+ }
+
+ ///
+ /// New DbContext per query with a projectable method.
+ /// After the registry is in place this should approach baseline overhead.
+ ///
+ [Benchmark]
+ public void WithProjectables_FreshDbContext_Method()
+ {
+ for (int i = 0; i < Iterations; i++)
+ {
+ using var dbContext = new TestDbContext(true, false);
+ dbContext.Entities.Select(x => x.IdPlus1Method()).ToQueryString();
+ }
+ }
+
+ ///
+ /// New DbContext per query with a projectable method that takes a parameter,
+ /// exercising parameter-type disambiguation in the registry key.
+ ///
+ [Benchmark]
+ public void WithProjectables_FreshDbContext_MethodWithParam()
+ {
+ for (int i = 0; i < Iterations; i++)
+ {
+ using var dbContext = new TestDbContext(true, false);
+ dbContext.Entities.Select(x => x.IdPlusDelta(5)).ToQueryString();
+ }
+ }
+ }
+}
diff --git a/src/EntityFrameworkCore.Projectables.Generator/IsExternalInit.cs b/src/EntityFrameworkCore.Projectables.Generator/IsExternalInit.cs
new file mode 100644
index 0000000..bd4930e
--- /dev/null
+++ b/src/EntityFrameworkCore.Projectables.Generator/IsExternalInit.cs
@@ -0,0 +1,6 @@
+// Polyfill for C# 9 record types when targeting netstandard2.0 or netstandard2.1
+// The compiler requires this type to exist in order to use init-only setters (used by records).
+namespace System.Runtime.CompilerServices
+{
+ internal sealed class IsExternalInit { }
+}
diff --git a/src/EntityFrameworkCore.Projectables.Generator/ProjectableRegistryEntry.cs b/src/EntityFrameworkCore.Projectables.Generator/ProjectableRegistryEntry.cs
new file mode 100644
index 0000000..4e27f8a
--- /dev/null
+++ b/src/EntityFrameworkCore.Projectables.Generator/ProjectableRegistryEntry.cs
@@ -0,0 +1,21 @@
+using System.Collections.Immutable;
+
+namespace EntityFrameworkCore.Projectables.Generator
+{
+ ///
+ /// Incremental-pipeline-safe representation of a single projectable member.
+ /// Contains only primitive types and ImmutableArray<string> so that value equality
+ /// works correctly across incremental generation steps.
+ ///
+ internal sealed record ProjectableRegistryEntry(
+ string DeclaringTypeFullName,
+ string MemberKind,
+ string MemberLookupName,
+ string GeneratedClassFullName,
+ bool IsGenericClass,
+ int ClassTypeParamCount,
+ bool IsGenericMethod,
+ int MethodTypeParamCount,
+ ImmutableArray ParameterTypeNames
+ );
+}
diff --git a/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs b/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs
index 440caa5..8f0c9bd 100644
--- a/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs
+++ b/src/EntityFrameworkCore.Projectables.Generator/ProjectionExpressionGenerator.cs
@@ -3,6 +3,9 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
using System.Text;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
@@ -47,6 +50,18 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
// Generate the source using the compilation and enums
context.RegisterImplementationSourceOutput(compilationAndMemberPairs,
static (spc, source) => Execute(source.Item1, source.Item2, spc));
+
+ // Build the projection registry: collect all entries and emit a single registry file
+ IncrementalValuesProvider registryEntries =
+ compilationAndMemberPairs.Select(
+ static (pair, _) => ExtractRegistryEntry(pair.Item1, pair.Item2));
+
+ IncrementalValueProvider> allEntries =
+ registryEntries.Collect();
+
+ context.RegisterImplementationSourceOutput(
+ allEntries,
+ static (spc, entries) => EmitRegistry(entries, spc));
}
static SyntaxTriviaList BuildSourceDocComment(ConstructorDeclarationSyntax ctor, Compilation compilation)
@@ -226,5 +241,244 @@ static TypeArgumentListSyntax GetLambdaTypeArgumentListSyntax(ProjectableDescrip
return lambdaTypeArguments;
}
}
+
+#nullable restore
+
+ ///
+ /// Extracts a from a member declaration.
+ /// Returns null when the member does not have [Projectable], is an extension member,
+ /// or cannot be represented in the registry (e.g. a generic class member or generic method).
+ ///
+ static ProjectableRegistryEntry? ExtractRegistryEntry(MemberDeclarationSyntax member, Compilation compilation)
+ {
+ var semanticModel = compilation.GetSemanticModel(member.SyntaxTree);
+ var memberSymbol = semanticModel.GetDeclaredSymbol(member);
+
+ if (memberSymbol is null)
+ return null;
+
+ // Verify [Projectable] attribute
+ var projectableAttributeTypeSymbol = compilation.GetTypeByMetadataName("EntityFrameworkCore.Projectables.ProjectableAttribute");
+ var projectableAttribute = memberSymbol.GetAttributes()
+ .FirstOrDefault(x => x.AttributeClass?.Name == "ProjectableAttribute");
+
+ if (projectableAttribute is null ||
+ !SymbolEqualityComparer.Default.Equals(projectableAttribute.AttributeClass, projectableAttributeTypeSymbol))
+ return null;
+
+ // Skip C# 14 extension type members — they require special handling (fall back to reflection)
+ if (memberSymbol.ContainingType is { IsExtension: true })
+ return null;
+
+ var containingType = memberSymbol.ContainingType;
+ bool isGenericClass = containingType.TypeParameters.Length > 0;
+
+ // Determine member kind and lookup name
+ string memberKind;
+ string memberLookupName;
+ ImmutableArray parameterTypeNames = ImmutableArray.Empty;
+ int methodTypeParamCount = 0;
+ bool isGenericMethod = false;
+
+ if (memberSymbol is IMethodSymbol methodSymbol)
+ {
+ isGenericMethod = methodSymbol.TypeParameters.Length > 0;
+ methodTypeParamCount = methodSymbol.TypeParameters.Length;
+
+ if (methodSymbol.MethodKind is MethodKind.Constructor or MethodKind.StaticConstructor)
+ {
+ memberKind = "Constructor";
+ memberLookupName = "_ctor";
+ }
+ else
+ {
+ memberKind = "Method";
+ memberLookupName = memberSymbol.Name;
+ }
+
+ parameterTypeNames = methodSymbol.Parameters
+ .Select(p => p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))
+ .ToImmutableArray();
+ }
+ else
+ {
+ memberKind = "Property";
+ memberLookupName = memberSymbol.Name;
+ }
+
+ // Build the generated class name using the same logic as Execute
+ string? classNamespace = containingType.ContainingNamespace.IsGlobalNamespace
+ ? null
+ : containingType.ContainingNamespace.ToDisplayString();
+
+ var nestedTypePath = GetRegistryNestedTypePath(containingType);
+
+ var generatedClassName = ProjectionExpressionClassNameGenerator.GenerateName(
+ classNamespace,
+ nestedTypePath,
+ memberLookupName,
+ parameterTypeNames.IsEmpty ? null : (IEnumerable)parameterTypeNames);
+
+ var generatedClassFullName = "EntityFrameworkCore.Projectables.Generated." + generatedClassName;
+
+ var declaringTypeFullName = containingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+
+ return new ProjectableRegistryEntry(
+ DeclaringTypeFullName: declaringTypeFullName,
+ MemberKind: memberKind,
+ MemberLookupName: memberLookupName,
+ GeneratedClassFullName: generatedClassFullName,
+ IsGenericClass: isGenericClass,
+ ClassTypeParamCount: containingType.TypeParameters.Length,
+ IsGenericMethod: isGenericMethod,
+ MethodTypeParamCount: methodTypeParamCount,
+ ParameterTypeNames: parameterTypeNames);
+ }
+
+ static IEnumerable GetRegistryNestedTypePath(INamedTypeSymbol typeSymbol)
+ {
+ if (typeSymbol.ContainingType is not null)
+ {
+ foreach (var name in GetRegistryNestedTypePath(typeSymbol.ContainingType))
+ yield return name;
+ }
+ yield return typeSymbol.Name;
+ }
+
+ ///
+ /// Emits the ProjectionRegistry.g.cs file that aggregates all projectable members
+ /// into a single static dictionary keyed by .
+ ///
+ static void EmitRegistry(ImmutableArray entries, SourceProductionContext context)
+ {
+ var validEntries = entries
+ .Where(e => e is not null)
+ .Select(e => e!)
+ .ToList();
+
+ if (validEntries.Count == 0)
+ return;
+
+ var sb = new StringBuilder();
+ sb.AppendLine("// ");
+ sb.AppendLine("#nullable disable");
+ sb.AppendLine("using System;");
+ sb.AppendLine("using System.Collections.Generic;");
+ sb.AppendLine("using System.Linq.Expressions;");
+ sb.AppendLine("using System.Reflection;");
+ sb.AppendLine();
+ sb.AppendLine("namespace EntityFrameworkCore.Projectables.Generated");
+ sb.AppendLine("{");
+ sb.AppendLine(" [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]");
+ sb.AppendLine(" internal static class ProjectionRegistry");
+ sb.AppendLine(" {");
+ sb.AppendLine(" // Keyed by RuntimeMethodHandle.Value (a stable nint pointer for the method/getter/ctor).");
+ sb.AppendLine(" // Populated once at type initialization; shared across the entire AppDomain lifetime.");
+ sb.AppendLine(" private static readonly Dictionary _map = Build();");
+ sb.AppendLine();
+ sb.AppendLine(" /// ");
+ sb.AppendLine(" /// Returns the pre-built LambdaExpression for the given [Projectable] member,");
+ sb.AppendLine(" /// or null if the member is not registered (e.g. open-generic members).");
+ sb.AppendLine(" /// ");
+ sb.AppendLine(" public static LambdaExpression TryGet(MemberInfo member)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" var handle = GetHandle(member);");
+ sb.AppendLine(" return handle.HasValue && _map.TryGetValue(handle.Value, out var expr) ? expr : null;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+ sb.AppendLine(" private static nint? GetHandle(MemberInfo member) => member switch");
+ sb.AppendLine(" {");
+ sb.AppendLine(" MethodInfo m => m.MethodHandle.Value,");
+ sb.AppendLine(" PropertyInfo p => p.GetMethod?.MethodHandle.Value,");
+ sb.AppendLine(" ConstructorInfo c => c.MethodHandle.Value,");
+ sb.AppendLine(" _ => null");
+ sb.AppendLine(" };");
+ sb.AppendLine();
+ sb.AppendLine(" private static Dictionary Build()");
+ sb.AppendLine(" {");
+ sb.AppendLine(" var map = new Dictionary();");
+
+ foreach (var entry in validEntries)
+ {
+ EmitRegistryEntry(sb, entry);
+ }
+
+ sb.AppendLine(" return map;");
+ sb.AppendLine(" }");
+ sb.AppendLine(" }");
+ sb.AppendLine("}");
+
+ context.AddSource("ProjectionRegistry.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8));
+ }
+
+ static void EmitRegistryEntry(StringBuilder sb, ProjectableRegistryEntry entry)
+ {
+ if (entry.IsGenericClass)
+ {
+ sb.AppendLine($" // TODO: generic class — {entry.GeneratedClassFullName} (falls back to reflection)");
+ return;
+ }
+
+ if (entry.IsGenericMethod)
+ {
+ sb.AppendLine($" // TODO: generic method — {entry.GeneratedClassFullName} (falls back to reflection)");
+ return;
+ }
+
+ const string flags = "global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Instance | global::System.Reflection.BindingFlags.Static";
+
+ sb.AppendLine(" {");
+ sb.AppendLine($" var t = typeof({entry.DeclaringTypeFullName});");
+
+ switch (entry.MemberKind)
+ {
+ case "Property":
+ sb.AppendLine($" var m = t.GetProperty(\"{entry.MemberLookupName}\", {flags})?.GetMethod;");
+ break;
+
+ case "Method":
+ {
+ var typeArray = BuildTypeArray(entry.ParameterTypeNames);
+ sb.AppendLine($" var m = t.GetMethod(\"{entry.MemberLookupName}\", {flags}, null, {typeArray}, null);");
+ break;
+ }
+
+ case "Constructor":
+ {
+ var typeArray = BuildTypeArray(entry.ParameterTypeNames);
+ sb.AppendLine($" var m = t.GetConstructor({flags}, null, {typeArray}, null);");
+ break;
+ }
+
+ default:
+ sb.AppendLine(" }");
+ return;
+ }
+
+ sb.AppendLine(" if (m is not null)");
+ sb.AppendLine(" {");
+ sb.AppendLine($" var exprType = t.Assembly.GetType(\"{entry.GeneratedClassFullName}\");");
+ sb.AppendLine(" var exprMethod = exprType?.GetMethod(\"Expression\", global::System.Reflection.BindingFlags.Static | global::System.Reflection.BindingFlags.NonPublic);");
+ sb.AppendLine(" if (exprMethod is not null)");
+ sb.AppendLine(" map[m.MethodHandle.Value] = (global::System.Linq.Expressions.LambdaExpression)exprMethod.Invoke(null, null)!;");
+ sb.AppendLine(" }");
+ sb.AppendLine(" }");
+ }
+
+ static string BuildTypeArray(ImmutableArray parameterTypeNames)
+ {
+ if (parameterTypeNames.IsEmpty)
+ return "global::System.Type.EmptyTypes";
+
+ var sb = new StringBuilder("new global::System.Type[] { ");
+ for (int i = 0; i < parameterTypeNames.Length; i++)
+ {
+ if (i > 0) sb.Append(", ");
+ sb.Append($"typeof({parameterTypeNames[i]})");
+ }
+ sb.Append(" }");
+ return sb.ToString();
+ }
+
}
}
diff --git a/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs b/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs
index 16e547a..e342a6e 100644
--- a/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs
+++ b/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs
@@ -21,27 +21,19 @@ public sealed class ProjectableExpressionReplacer : ExpressionVisitor
private readonly bool _trackingByDefault;
private IEntityType? _entityType;
- private readonly MethodInfo _select;
- private readonly MethodInfo _where;
+ // Extract MethodInfo via expression trees (trim-safe; computed once per AppDomain)
+ private static readonly MethodInfo _select =
+ ((MethodCallExpression)((Expression, IQueryable