Por que importa: O Change Tracker e o coracao do EF Core. Entender seus internals evita bugs sutis como entidades fantasma, conflitos de concorrencia silenciosos e memory leaks em background services.

Anatomia do Change Tracker

O Change Tracker nao e uma unica estrutura β€” e um conjunto de componentes que trabalham juntos para rastrear cada entidade carregada.

PK lookupPK lookupcomparacomparaDbContextChangeTrackerStateManagerIdentity MapEntityEntry<Product>State + SnapshotEntityEntry<Order>State + SnapshotOriginalValuesproperty[]CurrentValuesproperty[]

InternalEntityEntry β€” o coracao do rastreamento

Para cada entidade rastreada, o EF Core cria um InternalEntityEntry que armazena:

CampoO que armazenaUsado para
EntityStateAdded, Modified, Deleted, Unchanged, DetachedDecidir qual SQL gerar no SaveChanges
OriginalValuesSnapshot dos valores no momento do load/attachComparar com CurrentValues para detectar mudancas
CurrentValuesValores atuais da entidadeGerar os parametros do SQL
NavigationStateQuais navigation properties foram carregadasEvitar re-load e fix-up de relacoes
StoreGeneratedValuesValores gerados pelo banco (IDs, computed columns)Propagar valores apos INSERT
// Inspecionando InternalEntityEntry em profundidade var product = await db.Products.FindAsync(1); product!.Price = 149.99m; var entry = db.Entry(product); // Estado atual Console.WriteLine(entry.State); // Modified // Valores originais (snapshot do momento do load) var original = entry.OriginalValues; Console.WriteLine(original["Price"]); // 99.99 (valor do banco) // Valores atuais var current = entry.CurrentValues; Console.WriteLine(current["Price"]); // 149.99 (valor modificado) // Inspecionar propriedade individual var priceProp = entry.Property(p => p.Price); Console.WriteLine(priceProp.IsModified); // true Console.WriteLine(priceProp.OriginalValue); // 99.99 Console.WriteLine(priceProp.CurrentValue); // 149.99 // Forcar uma propriedade como nao-modificada priceProp.IsModified = false; // Agora o UPDATE nao incluira Price! // Clonar valores para comparacao var dbValues = await entry.GetDatabaseValuesAsync(); if (dbValues != null) { foreach (var prop in entry.Properties) { var dbVal = dbValues[prop.Metadata.Name]; var curVal = prop.CurrentValue; var origVal = prop.OriginalValue; Console.WriteLine( $"{prop.Metadata.Name}: DB={dbVal}, Original={origVal}, Current={curVal}"); } }

Snapshot vs Notification β€” dois modos de deteccao

O EF Core suporta dois mecanismos para detectar mudancas:

MecanismoComo funcionaTrade-off
Snapshot (padrao)Copia todos os valores no load. No DetectChanges(), compara atual vs snapshot.Simples mas consome memoria (2x por entidade). DetectChanges e O(n) sobre todas as entidades.
NotificationEntidade implementa INotifyPropertyChanged. Mudancas sao detectadas imediatamente.Sem custo de snapshot. Mas requer boilerplate nas entidades β€” raramente usado.
// ============================================= // Modo 1: Snapshot (padrao) β€” copia valores no load // ============================================= // EF Core cria um array interno com copia de todas as propriedades // No DetectChanges(), compara atual vs array // Pro: simples, funciona com qualquer POCO // Con: 2x memoria por entidade, DetectChanges() e O(n) // Nada especial necessario β€” funciona out of the box var product = await db.Products.FindAsync(1); product.Price = 99; // mudanca detectada no proximo DetectChanges // ============================================= // Modo 2: Notification β€” entidade avisa sobre mudancas // ============================================= // Sem snapshot, sem DetectChanges() β€” mudanca detectada imediatamente // Pro: zero overhead de deteccao, sem O(n) scan // Con: boilerplate pesado nas entidades public class Product : INotifyPropertyChanged { private decimal _price; public event PropertyChangedEventHandler? PropertyChanged; public decimal Price { get => _price; set { if (_price != value) { _price = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Price))); } } } } // Registrar no modelo modelBuilder.Entity<Product>() .HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangedNotifications); // Na pratica, quase ninguem usa Notification β€” o overhead de snapshot // e aceitavel para 99% dos cenarios

DetectChanges β€” quando e chamado automaticamente

Performance:DetectChanges() e chamado automaticamente antes de SaveChanges, ao acessar ChangeTracker.Entries() e em queries locais. Com milhares de entidades rastreadas, isso pode ser custoso.
MetodoChama DetectChanges?Motivo
SaveChanges()SimPrecisa saber o que mudou para gerar SQL
ChangeTracker.Entries()SimDeve retornar estados corretos
DbSet.Find()SimVerifica Change Tracker antes de ir ao banco
DbSet.LocalSimRetorna view das entidades rastreadas
query.ToList()NaoQueries vao direto ao banco
db.Entry(entity)NaoRetorna estado da entidade especifica
// Performance: desabilitar auto-detect para batches using var db = new AppDbContext(options); // Desabilita DetectChanges automatico db.ChangeTracker.AutoDetectChangesEnabled = false; try { foreach (var dto in thousandsOfDtos) { var entity = new Product { Name = dto.Name, Price = dto.Price }; db.Products.Add(entity); // nao chama DetectChanges } // Chama manualmente antes de salvar db.ChangeTracker.DetectChanges(); await db.SaveChangesAsync(); } finally { // Sempre restaure! db.ChangeTracker.AutoDetectChangesEnabled = true; } // Comparacao de performance (10,000 entidades): // AutoDetect ON: Add() chama DetectChanges a cada insert β†’ ~10s // AutoDetect OFF: Add() e O(1), DetectChanges uma vez no final β†’ ~1s
Como o Change Tracker sabe quais colunas foram modificadas?
No modo Snapshot (padrao): ao carregar a entidade, o EF Core copia todos os valores para um array interno (OriginalValues). Quando DetectChanges() roda, compara cada propriedade do objeto atual com o snapshot. So as propriedades com valores diferentes sao marcadas como IsModified = true, e o UPDATE gerado inclui apenas essas colunas. No modo Notification, a entidade dispara eventos ao mudar, eliminando a necessidade de comparacao.
DetectChanges() e O(n) β€” como otimizar com muitas entidades?
1) AsNoTracking() para reads β€” entidades nao entram no tracker. 2) ChangeTracker.AutoDetectChangesEnabled = false β€” desabilita chamadas automaticas (voce chama manualmente quando precisar). 3) ChangeTracker.Clear() apos batches β€” limpa o tracker. 4) Use ExecuteUpdate/Delete (.NET 7+) para operacoes em massa sem carregar entidades. 5) Em cenarios extremos, use notification entities com INotifyPropertyChanged.