@@ -199,43 +199,52 @@ private static bool ParameterTypesMatch(
199199 }
200200
201201 /// <summary>
202- /// Sentinel stored in <see cref="_reflectionFactoryCache "/> to represent
202+ /// Sentinel stored in <see cref="_reflectionCache "/> to represent
203203 /// "no generated type found for this member", distinguishing it from a not-yet-populated entry.
204204 /// <see cref="ConcurrentDictionary{TKey,TValue}"/> does not allow null values, so a sentinel is required.
205205 /// </summary>
206- private readonly static Func < LambdaExpression > _reflectionNotFoundSentinel = static ( ) => null ! ;
206+ private readonly static LambdaExpression _reflectionNullSentinel =
207+ Expression . Lambda ( Expression . Empty ( ) ) ;
207208
208209 /// <summary>
209- /// Caches a pre-compiled <c>Func<LambdaExpression></c> delegate per <see cref="MemberInfo"/>
210- /// so that <c>Assembly.GetType</c>, <c>GetMethod</c>, <c>MakeGenericType</c>, and
211- /// <c>MakeGenericMethod</c> are only paid once per member. All subsequent calls execute
212- /// native JIT-compiled code with zero reflection overhead.
210+ /// Caches the fully-resolved <see cref="LambdaExpression"/> per <see cref="MemberInfo"/>
211+ /// for the reflection-based slow path.
212+ /// On the first call per member the reflection work (<c>Assembly.GetType</c>, <c>GetMethod</c>,
213+ /// <c>MakeGenericType</c>, <c>MakeGenericMethod</c>) is performed once and the resulting
214+ /// expression tree is stored here; subsequent calls return the cached reference directly,
215+ /// eliminating expression-tree re-construction on every access.
216+ /// This is especially important for constructors whose object-initializer trees are
217+ /// significantly more expensive to build than simple method-body trees.
213218 /// </summary>
214- private readonly static ConcurrentDictionary < MemberInfo , Func < LambdaExpression > > _reflectionFactoryCache = new ( ) ;
219+ private readonly static ConcurrentDictionary < MemberInfo , LambdaExpression > _reflectionCache = new ( ) ;
215220
216221 /// <summary>
217222 /// Resolves the <see cref="LambdaExpression"/> for a <c>[Projectable]</c> member using the
218223 /// reflection-based slow path only, bypassing the static registry.
219- /// Useful for benchmarking and for members not yet in the registry (e.g. open-generic types).
224+ /// The result is cached after the first call, so subsequent calls return the cached expression
225+ /// without any reflection or expression-tree construction overhead.
226+ /// Useful for members not yet in the registry (e.g. open-generic types).
220227 /// </summary>
221228 public static LambdaExpression ? FindGeneratedExpressionViaReflection ( MemberInfo projectableMemberInfo )
222229 {
223- var factory = _reflectionFactoryCache . GetOrAdd ( projectableMemberInfo , static mi => BuildReflectionFactory ( mi ) ) ;
224- return ReferenceEquals ( factory , _reflectionNotFoundSentinel ) ? null : factory . Invoke ( ) ;
230+ var result = _reflectionCache . GetOrAdd ( projectableMemberInfo ,
231+ static mi => BuildReflectionExpression ( mi ) ?? _reflectionNullSentinel ) ;
232+ return ReferenceEquals ( result , _reflectionNullSentinel ) ? null : result ;
225233 }
226234
227235 /// <summary>
228- /// Performs the one-time reflection work for a member and returns a compiled native delegate
229- /// (or <see cref="_reflectionNotFoundSentinel"/> if no generated type exists).
236+ /// Performs the one-time reflection work for a member: locates the generated expression
237+ /// accessor (inline or external-class path), invokes it, and returns the resulting
238+ /// <see cref="LambdaExpression"/>. Returns <c>null</c> if no generated type is found.
230239 /// <para>
231- /// We use <c>Expression.Lambda<TDelegate>(...).Compile() </c> rather than
232- /// <c>Delegate.CreateDelegate</c> because the generated <c>Expression()</c> factory method
233- /// returns <c>Expression<TDelegate></c> (a subtype of <see cref="LambdaExpression"/>), and
234- /// <c>CreateDelegate</c> requires an exact return-type match in most runtime environments.
235- /// The expression-tree wrapper handles the covariant cast cleanly and compiles to native code .
240+ /// Using <c>MethodInfo.Invoke </c> rather than a compiled delegate is appropriate here because
241+ /// the result is cached in <see cref="_reflectionCache"/> — the invocation cost is paid only
242+ /// on cache misses, and subsequent EF Core queries reuse the cached expression. Under
243+ /// contention the value factory may be invoked more than once, but only a single expression
244+ /// instance is ultimately stored per member .
236245 /// </para>
237246 /// </summary>
238- private static Func < LambdaExpression > BuildReflectionFactory ( MemberInfo projectableMemberInfo )
247+ private static LambdaExpression ? BuildReflectionExpression ( MemberInfo projectableMemberInfo )
239248 {
240249 var declaringType = projectableMemberInfo . DeclaringType
241250 ?? throw new InvalidOperationException ( "Expected a valid type here" ) ;
@@ -295,7 +304,7 @@ private static Func<LambdaExpression> BuildReflectionFactory(MemberInfo projecta
295304
296305 if ( expressionFactoryType is null )
297306 {
298- return _reflectionNotFoundSentinel ;
307+ return null ;
299308 }
300309
301310 if ( expressionFactoryType . IsGenericTypeDefinition )
@@ -307,20 +316,15 @@ private static Func<LambdaExpression> BuildReflectionFactory(MemberInfo projecta
307316
308317 if ( expressionFactoryMethod is null )
309318 {
310- return _reflectionNotFoundSentinel ;
319+ return null ;
311320 }
312321
313322 if ( projectableMemberInfo is MethodInfo mi && mi . GetGenericArguments ( ) is { Length : > 0 } methodGenericArgs )
314323 {
315324 expressionFactoryMethod = expressionFactoryMethod . MakeGenericMethod ( methodGenericArgs ) ;
316325 }
317326
318- // Compile a native delegate: () => (LambdaExpression)GeneratedClass.Expression()
319- // Expression.Call + Convert handles the covariant return type (Expression<TDelegate> → LambdaExpression).
320- // The one-time Compile() cost is amortized; all subsequent calls are direct native-code invocations.
321- var call = Expression . Call ( expressionFactoryMethod ) ;
322- var cast = Expression . Convert ( call , typeof ( LambdaExpression ) ) ;
323- return Expression . Lambda < Func < LambdaExpression > > ( cast ) . Compile ( ) ;
327+ return expressionFactoryMethod . Invoke ( null , null ) as LambdaExpression ;
324328 }
325329
326330 /// <summary>
0 commit comments