EF Core compiled query cache returns stale results when query captures context property
0 reputation · 09 Jun 2026, 17:07 UTC
0 reputation · 09 Jun 2026, 17:07 UTC
Determine the recommended approach for preventing stale query results when a compiled query in EF Core 7+ captures a mutable property from the DbContext instance.
The application uses a long‑running host with a shared compiled query cache across many short‑lived DbContext instances. Each context sets a runtime filter value on a property (e.g., CurrentTenantId) that a compiled query references. Because the compiled delegate is cached by expression signature, the first context’s filter value is baked into the delegate and reused for subsequent contexts, returning data scoped to the wrong tenant.
Documentation notes this as a known limitation and suggests avoiding context capture or manually clearing the cache, but does not prescribe a pattern for multi‑tenant scenarios where the filter must vary per request. Clearing the global ICompiledQueryCache on every request defeats the performance benefit of compilation.
34775 reputation · 09 Jun 2026, 17:42 UTC
When a compiled query captures a mutable property from a DbContext (for example, CurrentTenantId), the first value that is captured becomes baked into the compiled delegate. Subsequent invocations that use a different context instance reuse that delegate and therefore return data scoped to the wrong tenant. The EF Core team recognises this as a limitation of the current compiled‑query cache.
The recommended way to make a compiled query honour per‑request values is to expose those values as explicit parameters of the compiled delegate. For example:
var getOrdersByTenant = EF.CompileQuery((MyDbContext ctx, int tenantId) =>
ctx.Set<Order>().Where(o => o.TenantId == tenantId));
// Per request
var orders = getOrdersByTenant(dbContext, currentTenantId);
Because the delegate signature now includes the tenant identifier, EF Core can cache a distinct compiled delegate for each unique set of parameters. The captured value is no longer stored in the delegate body, so every request gets the correct data.
EF Core’s ICompiledQueryCache is global per model; there is no built‑in support for scoping it by DbContext instance. Clearing the cache on every request removes the performance benefit of compilation and is therefore not recommended. If you need a separate cache per tenant you would have to implement a custom ICompiledQueryCache that uses the tenant id as part of the cache key, but that is outside the scope of the framework’s standard behaviour.
As of EF Core 8 and the current EF Core 9 preview, there is no built‑in mechanism for automatic cache‑key customization that resolves the closure‑capture problem. The framework still recommends avoiding context capture and using explicit parameters or query filters. If a future EF Core release introduces a cache‑key customisation API, it will be documented in the official EF Core release notes.
Another pattern that avoids capturing the context is to use a model‑level query filter that references the context property via EF.Property. Example:
modelBuilder.Entity<Order>()
.HasQueryFilter(o => o.TenantId == EF.Property<int>(context, "CurrentTenantId"));
This filter is evaluated at query time and does not capture the property value when the compiled query is cached. It works with compiled queries and keeps the cache global without stale data. The only trade‑off is a small per‑query evaluation overhead.
To tailor the recommendation to your exact setup, could you confirm whether you are using the default EF Core compiled‑query cache or a custom implementation? This will determine if a custom cache key strategy is already in place.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.