Why do two seemingly identical database queries return different values? That’s the question that kicked off an hours-long debugging session, one that revealed a subtle but dangerous interaction between .NET Entity Framework’s Identity Resolution and improper DbContext management.
Entity Framework Core tracks every entity it loads by primary key. If a helper class creates its own DbContext instead of sharing one through Dependency Injection, the two contexts maintain separate identity caches. Changes made in one are invisible to the other, leading to silent data resets that are incredibly hard to trace.
The Problem
The scenario was straightforward on the surface. An API endpoint increments and saves a “Counter” property on a Parts entity mid-execution. The database reflects the correct value after the save. But by the time the endpoint finishes, the Counter resets to its original value, as if the increment never happened.
The culprit line? A completely unrelated save operation on the same entity, updating different fields. Somehow, saving unrelated properties was overwriting the Counter with a stale value. The Counter wasn’t being touched anywhere else in the code, so where was the old value coming from?
// These two lines return DIFFERENT Counter values
// even though they query the same row at the same time:
var counter1 = dbContext.Parts.Find(partId).Counter;
// Returns: 5 (stale, from identity cache)
var counter2 = dbContext.Parts
.AsNoTracking()
.First(p => p.Id == partId).Counter;
// Returns: 6 (correct, fresh from database)
The Debugging Journey
Tracking this down required a methodical approach. Each step peeled back another layer of the problem, and each result added to the confusion before the full picture emerged.
Pinpoint the Reset
By commenting out lines one at a time, the exact statement that resets the Counter was identified: an SaveChanges() call that updates completely unrelated fields on the same entity. The Counter value in the database is correct before this line runs, and wrong after.
Inspect the In-Memory Object
Logging the part instance’s Counter value showed it was already stale, holding the original value, not the incremented one. But the increment and save had already succeeded. Where was this stale copy coming from?
Query a Fresh Copy
Selecting another copy of the same row from the database using a standard LINQ query also returned the wrong Counter value, even though the database showed the correct value at that exact moment. Two queries, same row, both stale.
Bypass the Cache
As a final test, querying the Counter value with AsNoTracking(), which skips EF’s caching layer entirely, returned the correct value. This confirmed the problem wasn’t in the database. It was in EF’s own tracking system.
The Root Cause
The answer comes down to two concepts colliding: Entity Framework’s Identity Resolution and the way the DbContext was being managed in this codebase.
Identity Resolution (EF Core)
When Entity Framework returns an entity from the database, it tracks that instance by its primary key. Any subsequent query for the same entity returns the already-tracked instance from memory rather than hitting the database again. This improves performance and reduces memory usage, but it means in-memory values can drift from what’s actually in the database.
Identity Resolution on its own isn’t enough to cause this bug. The real issue was how the codebase managed its DbContext instances.
The API endpoint’s logic was split between a parent controller and a helper class. Instead of receiving the DbContext through Dependency Injection (or even as a constructor argument), the helper class instantiated its own new AppDbContext(). This created two completely separate tracking contexts, each with its own Identity Resolution cache, oblivious to changes made by the other.
Here’s what happened step by step: The parent controller incremented the Counter and saved it through its DbContext, the database now holds the correct value. Then the helper class, using its own separate DbContext, queried the same entity. Its Identity Resolution cache still held the original, pre-increment value. When the helper saved its unrelated changes, it overwrote the Counter with the stale cached value.
2
separate DbContext instances were active in the same API request, each maintaining its own identity cache, silently fighting over the same data.
The Fix
Always use Dependency Injection for your DbContext. Never instantiate new DbContext() inside helper classes, services, or utility methods. One HTTP request should use one DbContext instance, giving every component the same source of truth and the same Identity Resolution cache.
The fix was simple once the root cause was clear. Instead of the helper class creating its own DbContext, the existing context is passed through the constructor:
// BEFORE: helper creates its own context (broken)
public class PartHelper
{
private readonly AppDbContext _db = new AppDbContext();
// This context has NO knowledge of changes
// made by the parent controller's context
}
// AFTER: context is injected (correct)
public class PartHelper
{
private readonly AppDbContext _db;
public PartHelper(AppDbContext db) => _db = db;
// Now shares the same tracking cache as the parent
}
With a single shared DbContext, both the parent controller and the helper class read from and write to the same Identity Resolution cache. The Counter increment is visible everywhere, and no save operation can silently overwrite it with a stale value.
The Takeaway
Going back to the original mystery, those two “identical” queries that returned different values, the explanation is now clear. The first query (Find()) consults the Identity Resolution cache and returns the tracked instance with its stale Counter value. The second query (AsNoTracking()) bypasses the cache entirely and fetches the real, current value from the database.
This kind of bug is particularly dangerous because it’s silent. No exceptions are thrown. No logs indicate a problem. The data just quietly reverts, and everything looks normal until someone notices the numbers don’t add up.
Quick reference: dbContext.Entity.Find(id) returns the tracked, potentially stale instance from the Identity Resolution cache. dbContext.Entity.AsNoTracking().First(...) always fetches fresh data from the database. When debugging unexpected values, AsNoTracking() is your fastest way to check if Identity Resolution is the culprit.
The process to find the solution was far more complex than the solution itself, but that’s often how it goes with framework-level bugs. Understanding Identity Resolution isn’t just useful for debugging; it’s essential knowledge for building reliable .NET applications that don’t silently corrupt their own data.