Entity Framework Core, .NET uygulamalarinda veri erişim katmanini hızlandırmak icin güçlü bir ORM sunar. Ancak ORM'in sagladigi kolaylik, yanlış kullanımda ciddi performans sorunlarina yol acabilir. N+1 sorgu problemi, gereksiz change tracking, geniş Include zincirleri ve client-side evaluation en yaygın darbogazlardir. Bu yazida EF Core performansini sistematik olarak iyilestirmek icin kanitlanmis teknikleri, olcme araclarini ve mimari karar noktalarini ele aliyoruz.
Change tracking stratejisi
EF Core varsayilan olarak entity'leri izler ve SaveChanges sirasinda degisiklikleri algilar. Salt okuma senaryolarinda tracking gereksiz bellek ve CPU tuketir. AsNoTracking() kullanımı okuma sorgularinda standart olmalidir:
var products = await _db.Products
.AsNoTracking()
.Where(p => p.IsActive)
.ToListAsync(ct);
.NET 8 ile gelen AsNoTrackingWithIdentityResolution(), ilişkili entity'lerde referans tutarliligini korurken tracking maliyetini dusurur. Hangi modun uygun olduğu veri sekline baglidir.
Yazma senaryolarinda tracking gereklidir ancak sorgu kapsami dar tutulmalidir. Tüm tabloyu cekip bir alani guncellemek yerine execute update kullanılabilir:
await _db.Products
.Where(p => p.CategoryId == categoryId)
.ExecuteUpdateAsync(s => s.SetProperty(p => p.IsActive, false), ct);
N+1 problemini onlemek
N+1 problemi, ana sorgu sonrasi her ilişkili kayıt icin ayri sorgu calismasidir. Çözüm yollari:
- Include / ThenInclude: Eager loading ile ilişkiler tek round-trip'te yuklenir.
- Projection: Select ile sadece ihtiyaç duyulan alanlar cekilir.
- Split query: Kartezyen patlamasini onlemek icin sorgu bolunur.
Projection ornegi:
var orders = await _db.Orders
.AsNoTracking()
.Select(o => new OrderSummaryDto(
o.Id,
o.CustomerName,
o.Lines.Sum(l => l.Quantity * l.UnitPrice)))
.ToListAsync(ct);
Bu yaklaşım hem N+1'i onler hem de gereksiz kolon transferini engeller.
Sorgu analizi ve loglama
EF Core'un urettigi SQL'i görmek icin LogLevel ayari veya interceptor kullanılır:
optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging(isDevelopment);
Production'da sensitive data logging kapali olmalidir. DbCommandInterceptor ile yavas sorgular otomatik loglanabilir. Application Insights veya OpenTelemetry ile sorgu suresi metrik olarak izlenmelidir.
Indeks ve sorgu plani
ORM hızlı kod yazmayi sağlar ama veritabanı indeks tasarımını degistirmez. Sik filtrelenen kolonlarda indeks olmali, composite indeksler sorgu pattern'ine gore tasarlanmalidir. EF Core migration'lari indeks tanimlarini kod olarak tutar:
modelBuilder.Entity<Product>()
.HasIndex(p => new { p.CategoryId, p.IsActive });
Execution plan analizi (SQL Server'da actual plan, PostgreSQL'de EXPLAIN ANALYZE) ORM disi ama zorunlu bir adimdir.
Pagination ve streaming
Büyük veri setlerinde ToListAsync yerine sayfalama kullanılmalıdır. Offset pagination basit ama büyük offset'lerde yavaslar; cursor/keyset pagination tercih edilebilir:
var page = await _db.Products
.AsNoTracking()
.Where(p => p.Id > lastId)
.OrderBy(p => p.Id)
.Take(pageSize)
.ToListAsync(ct);
Cok büyük export senaryolarinda AsAsyncEnumerable ile streaming dusunulebilir.
Compiled query ve cache
Sik çalışan sorgular icin compiled query kullanımı plan derleme maliyetini azaltir:
private static readonly Func<AppDbContext, int, Task<Product?>> GetById =
EF.CompileAsyncQuery((AppDbContext db, int id) =>
db.Products.FirstOrDefault(p => p.Id == id));
Second level cache (EasyCaching, FusionCache entegrasyonu) read-heavy senaryolarda veritabanı yukunu dusurur. Cache invalidation stratejisi onceden tasarlanmalidir.
Bulk işlemler
Binlerce kayıt güncelleme veya silme isleminde SaveChanges dongusu yerine bulk extension veya raw SQL tercih edilir. EF Core 7+ ExecuteDelete ve ExecuteUpdate toplu islemleri destekler. Cok büyük import'larda SqlBulkCopy veya COPY (PostgreSQL) dusunulebilir.
Connection ve context yaşam dongusu
DbContext scoped olarak kaydedilmeli; singleton yapmak thread safety ihlali yaratir. Uzun suren islemlerde context'i erken tutmak connection pool'u tuketir. Background service'lerde scope factory ile her is icin yeni context acilmalidir:
await using var scope = _scopeFactory.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
Global query filter ve soft delete
Soft delete icin global query filter kullanıldığında performans etkisi goz onunde bulundurulmalidir. Her sorguya eklenen filter indeks kullanımını etkileyebilir. Partial index veya filtered index tasarımı gerekebilir.
Concurrency
Optimistic concurrency icin row version veya concurrency token kullanılmalıdır. SaveChangesConflictException yakalanip retry veya kullanıcıya bilgi verilmelidir. Pessimistic locking nadiren ve bilincli kullanılmalıdır.
Test ve benchmark
Performans regresyonlarini yakalamak icin:
- Integration testlerde sorgu sayisi assert edilebilir (EF Core 5+ event'leri).
- BenchmarkDotNet ile repository metotlari olculur.
- Load test (k6, NBomber) ile gerçek throughput olculur.
Mimari alternatifler
Okuma agir sistemlerde CQRS ile write model EF Core, read model Dapper veya raw SQL ile beslenebilir. Her katman icin doğru arac seçimi performansi belirler. EF Core her senaryo icin en hızlı seçenek degildir; ancak çoğu is uygulamasi icin yeterli performans doğru kullanimla elde edilir.
Checklist
EF Core performans checklist'i: okuma sorgularinda AsNoTracking, projection tercih et, Include derinligini sinirla, N+1 icin test yaz, indeksleri gozden gecir, yavas sorgulari logla, pagination kullan, bulk islemlerde ExecuteUpdate/Delete düşün, DbContext scope'unu doğru yonet, production'da sensitive logging kapat. Bu maddeler sistematik uygulandiginda veri erişim katmanı ongorulebilir ve ölçeklenebilir hale gelir.
Split query ve cartesian explosion
Birden fazla collection Include kullanıldığında SQL kartezyen carpim uretebilir ve satir sayisi patlar. AsSplitQuery() her collection icin ayri sorgu calistirarak veri transferini azaltir. Trade-off: round-trip sayisi artar. Ag agir ve veritabanı yakın senaryolarda split query genellikle kazan sağlar.
Raw SQL ve FromSqlInterpolated
Karmaşık rapor sorgularinda FromSqlInterpolated veya Dapper kullanılabilir. Parametreli sorgu SQL injection'i onler. EF Core entity mapping ile raw SQL sonuclari strongly typed kalir.
Migration ve production performansi
Büyük tablolarda indeks ekleme online index operasyonlari ile yapilmali. Migration script'leri production'da dry-run edilmeli. Long-running migration icin bakım penceresi veya blue-green deployment planlanmalidir.
DbContext pooling
AddDbContextPool context instance'larini yeniden kullanarak allocation maliyetini dusurur. Pool kullanırken context state'inin her kiralama sonrasi temizlendiginden emin olunmalidir. Read-only servislerde pool ile birlikte AsNoTracking standart olmalidir.
Interceptors ile gozlemlenebilirlik
IDbCommandInterceptor ile yavas sorgular otomatik loglanir. ISaveChangesInterceptor ile audit alanlari otomatik doldurulabilir. Interceptor'lar test ortaminda devre disi birakilarak birim testler sadelestirilebilir.
Owned types ve value object mapping
Domain value object'leri owned entity olarak map edildiginde tek tabloda saklanabilir. Bu model karmasikligi azaltir ancak sorgu ve indeks tasarımını etkiler. Value converter ile primitive kolonlara map etmek bazi senaryolarda daha performanslidir.
Temporal tables ve history
SQL Server temporal table destegi EF Core 7+ ile gelmistir. Gecmis veri sorgulari performans etkisi yaratir; history tablosu icin ayri indeks planlanmalidir. Audit gereksinimi olan sistemlerde alternatif olarak explicit audit tablosu tercih edilebilir.