Change Tracker Deep Dive
Internals do Change Tracker, Identity Resolution, DetectChanges, entidades desconectadas, concorrencia e Bulk Operations
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.
InternalEntityEntry β o coracao do rastreamento
Para cada entidade rastreada, o EF Core cria um InternalEntityEntry que armazena:
| Campo | O que armazena | Usado para |
|---|---|---|
EntityState | Added, Modified, Deleted, Unchanged, Detached | Decidir qual SQL gerar no SaveChanges |
OriginalValues | Snapshot dos valores no momento do load/attach | Comparar com CurrentValues para detectar mudancas |
CurrentValues | Valores atuais da entidade | Gerar os parametros do SQL |
NavigationState | Quais navigation properties foram carregadas | Evitar re-load e fix-up de relacoes |
StoreGeneratedValues | Valores 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:
| Mecanismo | Como funciona | Trade-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. |
| Notification | Entidade 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. | Metodo | Chama DetectChanges? | Motivo |
|---|---|---|
SaveChanges() | Sim | Precisa saber o que mudou para gerar SQL |
ChangeTracker.Entries() | Sim | Deve retornar estados corretos |
DbSet.Find() | Sim | Verifica Change Tracker antes de ir ao banco |
DbSet.Local | Sim | Retorna view das entidades rastreadas |
query.ToList() | Nao | Queries vao direto ao banco |
db.Entry(entity) | Nao | Retorna 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.