Analyzer rule
Spec: LC036 - DbContext Captured Across Threads
EF Core LINQ performance analyzer and Roslyn analyzer for catching query issues at compile time.
Spec: LC036 - DbContext Captured Across Threads
Goal
Detect a single DbContext captured into multi-threaded delegates.
The Problem
DbContext is not thread-safe. Capturing the same instance into Task.Run(...), Task.Factory.StartNew(...), Parallel.For(...), Parallel.ForEach(...), Parallel.Invoke(...), thread-pool work, Thread, or timer callbacks can lead to race conditions and undefined behavior.
Example Violation
Task.Run(() => db.Users.ToList());
Task.Factory.StartNew(() => db.SaveChanges());
Parallel.For(0, ids.Count, _ => db.Users.Count());
Parallel.ForEach(ids, _ => db.Users.Count());
Parallel.Invoke(() => db.SaveChanges());
Task.Run(new Action(() => db.SaveChanges()));
new Thread(() => db.SaveChanges()).Start();
new Timer(_ => db.SaveChanges(), null, 0, 1000);
int Work() => db.SaveChanges();
Task.Run(Work);
Safer Shape
Create a separate context inside each background delegate, create a scope inside the callback, or use IDbContextFactory<TContext>.
Task.Run(() =>
{
using var db = factory.CreateDbContext();
return db.Users.Count();
});
Analyzer Logic
ID: LC036
Category: Safety
Severity: Warning
Notes
This rule is advisory only and stays silent when the delegate creates its own context, obtains one from a scope created inside the callback, or captures only scalar/materialized values. Capturing a DbContext field, property, local, or parameter from outside the callback is unsafe because the work can run after or concurrently with the original scope.
LC036 also inspects local functions passed directly as thread-work callbacks. It does not broadly inspect arbitrary method groups because method ownership, dependency lifetime, and call dispatch are harder to prove without creating noisy diagnostics.