EF Core Change Tracking: Transitioning from Default to Global NoTracking
0 reputation · 31 Dec 2022, 21:29 UTC
0 reputation · 31 Dec 2022, 21:29 UTC
In high-read applications using Entity Framework Core, the default tracking behavior can introduce significant memory overhead due to the identity map and snapshotting processes. While AsNoTracking() provides granular control per query, configuring QueryTrackingBehavior.NoTracking globally in the DbContext options shifts the responsibility of state management entirely to the developer.
This transition affects the standard Unit of Work pattern. When global no-tracking is enabled, entities retrieved via LINQ queries are not monitored by the Change Tracker, meaning subsequent modifications will not be persisted during SaveChanges() unless the entities are manually attached or their states are explicitly updated.
DbSet.Find() differ from LINQ queries when global no-tracking is configured?33275 reputation · 01 Jan 2023, 01:50 UTC
When QueryTrackingBehavior.NoTracking is configured globally, standard LINQ queries (e.g., Where(), First()) return entities that are not tracked by the DbContext. However, DbSet.Find() (and DbSet.FindAsync()) behaves differently because it first checks the local tracker before querying the database.
Even with global no-tracking enabled, Find() will return a tracked entity if it is already present in the context's memory. If the entity is not in memory and is fetched from the database, it is still tracked by default. This is a critical distinction: Find() is designed for primary key lookups and maintains tracking to ensure identity resolution within the current unit of work, regardless of the global query setting.
In a global no-tracking environment, the ChangeTracker does not know which properties have changed. To update a subset of entities without triggering unnecessary database round-trips or updating every single column, use the following strategies:
If you know exactly which entity has changed, use the Update method or manually set the entity state. context.Update(entity) marks the entity as Modified, which tells EF Core to send an UPDATE statement for all properties.
// Marks all properties as modified
context.Update(userProfile);
await context.SaveChangesAsync();
To avoid updating every column (reducing payload size and trigger execution), attach the entity and mark only specific properties as modified. This prevents a full row overwrite.
// Attach the entity to the tracker without marking it Modified
context.Attach(userProfile);
// Mark only the changed property as dirty
context.Entry(userProfile).Property(x => x.Email).IsModified = true;
await context.SaveChangesAsync();
| Method | Tracking State | DB Impact |
|---|---|---|
Update() |
Modified | Updates all columns |
Attach() + IsModified |
Unchanged ➜ Modified | Updates only specified columns |
AsTracking() query |
Tracked | Updates only changed columns (via snapshot) |
These behaviors assume EF Core 3.0+ where QueryTrackingBehavior was introduced. To verify your current state management:
context.ChangeTracker.Entries().Count() after a LINQ query; it should be 0.DbSet.Find(); it should be 1.Diagnostic Detail Needed: Are you using a shared DbContext instance across multiple requests (e.g., a Singleton), or is it scoped per request? Global no-tracking is typically used with scoped contexts to prevent memory leaks, but Singleton contexts require different disposal strategies.
Use comments to ask for clarification. Post a solution as an answer.
33,275 reputation · 31 Dec 2022, 23:44 UTC
When QueryTrackingBehavior.NoTracking is set on the DbContext, most LINQ queries return untracked entities. DbSet.Find() behaves differently: it first checks the local cache and, if not found, retrieves the entity from the database. In EF Core 7 the tracking decision for Find() follows the global setting, but earlier versions (EF Core 3‑5) will still track the result even if the context is in NoTracking mode. Because this varies, it’s safest to test in your target version.
For a batch of non‑tracked entities that need updating, context.AttachRange(entities) followed by context.ChangeTracker.DetectChanges() is the most efficient path. If you only need to modify a few columns, use the Entry() API to mark those properties as Modified:
foreach (var e in entities) {
context.Attach(e);
var entry = context.Entry(e);
entry.Property(x => x.SomeColumn).IsModified = true;
}
await context.SaveChangesAsync();
Using AttachRange avoids a round‑trip per entity and keeps the payload small.