Explain tracking vs no-tracking queries in EF Core.

4 minintermediateEF-Corechange-trackingperformance

Quick Answer

Tracking queries (the default) load entities into the change tracker so EF Core can detect modifications and persist them on `SaveChanges`. No-tracking queries (`AsNoTracking()`) skip this, returning lighter, faster results that can't be updated directly. Use tracking when you'll modify and save entities; use no-tracking for read-only reads to reduce memory and CPU overhead.

Detailed Answer

Tracking Queries (Default) EF Core keeps track of entity changes in the change tracker for SaveChanges().

// Tracking enabled by default
var product = context.Products.First();
product.Price = 99.99m;
context.SaveChanges(); // Change tracked and saved

How Change Tracking Works:

var product = context.Products.Find(1);

// Check tracking state
var entry = context.Entry(product);
Console.WriteLine(entry.State); // EntityState.Unchanged

product.Name = "Updated";
Console.WriteLine(entry.State); // EntityState.Modified

// See what changed
foreach (var property in entry.Properties)
{
    if (property.IsModified)
    {
        Console.WriteLine($"{property.Metadata.Name}: " +
            $"{property.OriginalValue} -> {property.CurrentValue}");
    }
}

No-Tracking Queries Entities are not tracked, improving performance for read-only scenarios.

// No-tracking for single query
var products = context.Products
    .AsNoTracking()
    .Where(p => p.IsActive)
    .ToList();

// Changes are NOT tracked
products[0].Price = 99.99m;
context.SaveChanges(); // Nothing saved!

Global No-Tracking Configuration:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(connectionString)
        .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
}

// Override for specific query
var product = context.Products
    .AsTracking()
    .First();

AsNoTrackingWithIdentityResolution

// Maintains identity resolution within query without full tracking
var orders = context.Orders
    .Include(o => o.Customer)
    .Include(o => o.OrderItems)
        .ThenInclude(oi => oi.Product)
    .AsNoTrackingWithIdentityResolution() // Same customer instance reused
    .ToList();

Comparison:

AspectTrackingNo-Tracking
PerformanceSlower (memory overhead)30-40% faster
MemoryHigherLower
Use CaseUpdate/Delete operationsRead-only queries
Identity ResolutionAutomaticNot by default
SaveChanges()Detects changesNo changes detected

When to Use Each:

Use Tracking:

  • Updating or deleting entities
  • Need automatic change detection
  • Working with related entities
  • Short-lived contexts

Use No-Tracking:

  • Read-only queries
  • Display/reporting
  • API GET endpoints
  • Large result sets
  • Performance critical scenarios

Manual Tracking Control:

// Detach entity
context.Entry(product).State = EntityState.Detached;

// Attach and mark as modified
context.Attach(product);
context.Entry(product).State = EntityState.Modified;

// Track specific properties
context.Entry(product).Property(p => p.Price).IsModified = true;