EFEF Core Handbook

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)

Detached context.Add(entity) Added SaveChanges() → INSERT SQL Unchanged
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)

Detached context.Attach(entity) Unchanged
var product = new Product { Id = 1, Name = "Laptop", Price = 25000 };
context.Attach(product);                // State: Unchanged — SQL yok

11.4 Property Değiştirme → Update

Unchanged entity.Name = "..." (property değişimi) Modified SaveChanges() → UPDATE SQL Unchanged
var product = await context.Products.FindAsync(1);  // State: Unchanged
product.Price = 30000;                              // State: Modified
await context.SaveChangesAsync();                   // UPDATE → State: Unchanged

11.5 Silme (Remove)

Unchanged context.Remove(entity) Deleted SaveChanges() → DELETE SQL Detached
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ış

Detached Add Added Save Unchanged Modify Modified Save Unchanged Detached Attach Unchanged Unchanged Remove Deleted Save Detached

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)
AsNoTracking() En hizli - Include yoksa ideal AsNoTrackingWithIdentityResolution() Include + tekrarli entity = bellek tasarrufu SQL sonucu (3 satir, ayni Customer): Order 1 | CustomerId=5 | Ali Order 2 | CustomerId=5 | Ali Order 3 | CustomerId=5 | Ali Materialize Bellekte 3 ayri nesne: Customer A Id=5 Ali (0x1A2B) Customer B Id=5 Ali (0x3C4D) Customer C Id=5 Ali (0x5E6F) Order 1 Order 2 Order 3 obj1 == obj2 : FALSE 3 nesne x ayni veri SQL sonucu (ayni 3 satir): Order 1 | CustomerId=5 | Ali Order 2 | CustomerId=5 | Ali Order 3 | CustomerId=5 | Ali Materialize + Identity Map Bellekte tek nesne (paylasilir): Customer (tek!) Id=5 Ali (0x1A2B) Order 1 Order 2 Order 3 Identity Map (sorgu suresince gecici) PK=5 var mi? Evet = mevcut referansi dondur obj1 == obj2 : TRUE 1 nesne, 3 referans (bellek 1/3)

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. MaxBatchSize ile 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
    }
}