ORTA
Change Tracking & Performance
EF Core, DB'den çektiğin her entity'nin o anki değerlerini bir kopya olarak saklar. SaveChanges() dediğinde eski ve yeni değerleri karşılaştırır, sadece değişen alanlar için UPDATE üretir.
Veritabanı sağlayıcısı
Bu sayfadaki eşleşen örnekleri seçilen sağlayıcıya göre gösterir.
Nasıl Çalışır?
var product = context.Products.First(p => p.Id == 1);
// EF: "Price=100 olarak kaydettim"
product.Price = 150;
await context.SaveChangesAsync();
// EF: "Price 100'den 150'ye değişmiş, Name aynı kalmış"
// → UPDATE Products SET Price = 150 WHERE Id = 1
// Sadece Price güncellenir! Name SQL'e dahil olmaz.
Bu sayede EF Core gereksiz sütun güncellemesi yapmaz — hem ağ trafiği azalır hem de DB'deki index'ler gereksiz yere yeniden yazılmaz.
Entity States (Durumlar)
| State | Anlamı | SaveChanges'ta |
|---|---|---|
Added |
Yeni eklendi, DB'de yok | INSERT |
Modified |
Var, değişti | UPDATE |
Deleted |
Silinecek | DELETE |
Unchanged |
Var, değişmedi | Hiçbir şey |
Detached |
EF izlemiyor | Hiçbir şey |
11.2 Yeni Entity Ekleme (Add)
var product = new Product { Name = "Laptop", Price = 25000 };
context.Products.Add(product); // State: Added
await context.SaveChangesAsync(); // INSERT → State: Unchanged
11.3 Mevcut Entity İzlemeye Alma (Attach)
var product = new Product { Id = 1, Name = "Laptop", Price = 25000 };
context.Attach(product); // State: Unchanged — SQL yok
11.4 Property Değiştirme → Update
var product = await context.Products.FindAsync(1); // State: Unchanged
product.Price = 30000; // State: Modified
await context.SaveChangesAsync(); // UPDATE → State: Unchanged
11.5 Silme (Remove)
var product = await context.Products.FindAsync(1); // State: Unchanged
context.Products.Remove(product); // State: Deleted
await context.SaveChangesAsync(); // DELETE → State: Detached
11.6 State Geçişleri — Özet Akış
DetectChanges — Ne Zaman Çalışır?
// EF Core otomatik olarak DetectChanges çağırır:
// - SaveChanges / SaveChangesAsync
// - context.Entry(entity)
// - ChangeTracker.Entries()
// Manuel çağrı (nadiren gerekir)
context.ChangeTracker.DetectChanges();
// Performans: Büyük batch'lerde DetectChanges'ı devre dışı bırakma
context.ChangeTracker.AutoDetectChangesEnabled = false;
try
{
for (int i = 0; i < 10000; i++)
context.Products.Add(products[i]);
context.ChangeTracker.DetectChanges(); // Tek seferde çağır
await context.SaveChangesAsync();
}
finally
{
context.ChangeTracker.AutoDetectChangesEnabled = true;
}
// Durumu görmek
var state = context.Entry(product).State; // EntityState.Modified
// Manuel state atama (disconnected senaryo — API'den gelen entity)
context.Entry(product).State = EntityState.Modified; // Tüm property'leri UPDATE eder
// veya sadece değişen alanı işaretle:
context.Attach(product);
context.Entry(product).Property(p => p.Price).IsModified = true;
Salt Okunur Sorgular (AsNoTracking)
// ✅ Salt okunur sorgular (tracking kapalı — daha hızlı)
var products = context.Products
.AsNoTracking()
.Where(p => p.IsActive)
.ToList();
// SaveChanges() çağırsan bile bu entity'ler güncellenmez — read-only!
AsNoTracking vs AsNoTrackingWithIdentityResolution
Problem: AsNoTracking() ile aynı entity birden fazla satırda gelirse, her biri ayrı C# nesnesi olur:
// ❌ AsNoTracking — aynı Category, farklı instance'lar
var orders = context.Orders
.AsNoTracking()
.Include(o => o.Customer)
.ToList();
// Order #1 → Customer { Id=5, Name="Ali" } ← instance A (0x1A2B)
// Order #2 → Customer { Id=5, Name="Ali" } ← instance B (0x3C4D) FARKLI obje!
// Order #3 → Customer { Id=5, Name="Ali" } ← instance C (0x5E6F) YİNE FARKLI!
Console.WriteLine(orders[0].Customer == orders[1].Customer); // FALSE
// Bellek: 3 ayrı Customer nesnesi (aynı veri, 3x bellek)
// ✅ AsNoTrackingWithIdentityResolution — aynı PK = aynı instance
var orders = context.Orders
.AsNoTrackingWithIdentityResolution()
.Include(o => o.Customer)
.ToList();
// Order #1 → Customer { Id=5, Name="Ali" } ← instance A (0x1A2B)
// Order #2 → ↗ aynı instance A'yı referans eder
// Order #3 → ↗ aynı instance A'yı referans eder
Console.WriteLine(orders[0].Customer == orders[1].Customer); // TRUE ✅
// Bellek: Sadece 1 Customer nesnesi (3 order aynı referansı paylaşır)
Ne zaman hangisini kullan?
| Senaryo | Method | Neden |
|---|---|---|
| Düz liste, Include yok | AsNoTracking() |
En hızlı, identity resolution gereksiz |
| Include var, aynı entity çok tekrar ediyor | AsNoTrackingWithIdentityResolution() |
Bellek tasarrufu + tutarlı referanslar |
| Veriyi güncelleyeceksin | Hiçbiri (normal tracking) | SaveChanges çalışsın |
| Raporlama / dashboard | AsNoTrackingWithIdentityResolution() |
Büyük veri, tekrarlı join'ler |
// Gerçek dünya örneği: 1000 siparişi 5 müşteriye ait
var orders = context.Orders
.AsNoTracking() // → 1000 Order + 1000 Customer nesnesi (!)
.Include(o => o.Customer)
.ToList();
var orders2 = context.Orders
.AsNoTrackingWithIdentityResolution() // → 1000 Order + sadece 5 Customer nesnesi ✓
.Include(o => o.Customer)
.ToList();
Performance İpuçları
// ✅ 1. Sadece ihtiyacın olan alanları çek (Projection)
var dtos = context.Products
.Select(p => new { p.Id, p.Name, p.Price })
.ToList();
// ✅ 2. Toplu okuma için tracking kapat
var readOnly = context.Products.AsNoTracking().ToList();
// ✅ 3. Toplu güncelleme (EF Core 7+) — tek SQL, Change Tracker bypass
await context.Products
.Where(p => p.Stock == 0)
.ExecuteUpdateAsync(s => s.SetProperty(p => p.IsActive, false));
// ✅ 4. Toplu silme (EF Core 7+)
await context.Products
.Where(p => p.IsDeleted && p.DeletedAt < DateTime.UtcNow.AddDays(-30))
.ExecuteDeleteAsync();
// ✅ 5. Uzun yaşayan context'te bellek temizliği
context.ChangeTracker.Clear(); // Tüm tracked entity'leri detach eder
// Background service, hosted service gibi yerlerde her batch sonrası çağır
ExecuteUpdate SQL çıktısı:
-- Tek SQL, entity yüklenmeden direkt güncelleme
UPDATE [Products]
SET [IsActive] = CAST(0 AS bit)
WHERE [Stock] = 0;
-- Tek SQL, entity yüklenmeden direkt güncelleme
UPDATE products
SET is_active = FALSE
WHERE stock = 0;
Change Tracking Limitleri:
Limit / Pratik Sınır Değer Ne Olur? Tracked entity sayısı (ideal) < 1.000 Performans iyi Tracked entity sayısı (max pratik) < 10.000 DetectChanges()yavaşlar (~100ms+)10.000+ tracked entity SaveChanges süresi katlanır, bellek şişer 100.000+ tracked entity OutOfMemory riski, dakikalarca SaveChanges Kurallar:
- Read-only sorgularda her zaman
AsNoTracking()kullan → %40-50 daha hızlı- Background job / batch işlemlerde her 500-1000 kayıtta
context.ChangeTracker.Clear()çağır- Raporlama sorgularında
AsNoTrackingWithIdentityResolution()→ tracking yok ama aynı entity tek instance olarak paylaşılır (bellek tasarrufu)- Tek bir
SaveChanges()çağrısında max 10.000 değişiklik gönder (parametre limiti!)SQL Parametre Limiti (SaveChanges'ı etkiler):
Provider Max parametre/sorgu Sonuç PostgreSQL 65.535 Aşılırsa EF otomatik batch'ler (sorun yok) SQL Server 2.100 Aşılırsa EF otomatik batch'ler SQLite 999 Çok düşük — küçük batch'ler oluşur EF Core parametre limitini aşınca otomatik batch'lere böler — crash olmaz ama çok sayıda roundtrip olabilir.
MaxBatchSizeile kontrol et.
/ Change Tracking Sık Yapılan Hatalar:
// ❌ YANLIŞ: Her kayıt için ayrı SaveChanges — N roundtrip
foreach (var product in products)
{
product.Price *= 1.1m;
await context.SaveChangesAsync(); // 💀 1000 ürün = 1000 roundtrip!
}
// ✅ DOĞRU: Tüm değişiklikleri yap, sonra tek SaveChanges
foreach (var product in products)
product.Price *= 1.1m;
await context.SaveChangesAsync(); // ✅ 1 batch (veya birkaç batch)
// ✅✅ EN İYİ: ExecuteUpdate — entity yükleme bile yok
await context.Products.ExecuteUpdateAsync(
s => s.SetProperty(p => p.Price, p => p.Price * 1.1m)); // ✅ Tek UPDATE SQL
// ❌ YANLIŞ: Background service'de context'i tekrar tekrar kullanma
public class ImportService : BackgroundService
{
private readonly AppDbContext _context; // ❌ Singleton context = bellek leak!
protected override async Task ExecuteAsync(...)
{
foreach (var batch in GetBatches())
{
_context.AddRange(batch); // Tracker sonsuz büyür!
await _context.SaveChangesAsync();
}
}
}
// ✅ DOĞRU: Her batch için yeni scope/context
public class ImportService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
protected override async Task ExecuteAsync(...)
{
foreach (var batch in GetBatches())
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
context.AddRange(batch);
await context.SaveChangesAsync();
} // Her iterasyonda context dispose → bellek temiz
}
}