Entity Framework Core, .NET ekosistemindeki en güçlü ORM'dir. Ancak her güçlü araç gibi, yanlış kullanımda ciddi performans sorunlarına zemin hazırlıyor. Bu yazıda üretim ortamında karşılaştığım ve en sık gördüğüm tuzakları belgeledim.
1. N+1 Sorgu Problemi
En yaygın ve en sinsi performans sorunudur. 100 order varsa 101 SQL sorgusu çalışır:
// Kötü: Lazy loading ile N+1
var orders = await db.Orders
.Where(o => o.CreatedAt > DateTime.Today.AddDays(-7))
.ToListAsync(); // 1 sorgu: SELECT * FROM Orders
foreach (var order in orders)
{
// Her order için ayrı sorgu!
foreach (var item in order.Items) // SELECT * FROM OrderItems WHERE OrderId = @id
Console.WriteLine(item.ProductName);
}
// 100 order = 101 sorgu
// İyi: Eager loading ile tek sorgu
var orders = await db.Orders
.Where(o => o.CreatedAt > DateTime.Today.AddDays(-7))
.Include(o => o.Items) // JOIN ile tek sorguda çeker
.ThenInclude(i => i.Product) // İlişkili Product da gelir
.ToListAsync();
Çok Fazla Include de Sorun Çıkarır
// Kötü: Aşırı Include — kartezyen çarpım problemi
var orders = await db.Orders
.Include(o => o.Items)
.Include(o => o.Payments)
.Include(o => o.Shipments)
.Include(o => o.Notes)
.ToListAsync();
// Items × Payments × Shipments satır kombinasyonu gelir
// İyi: AsSplitQuery — her Include için ayrı sorgu
var orders = await db.Orders
.Include(o => o.Items)
.Include(o => o.Payments)
.AsSplitQuery() // Ayrı sorgular, kartezyen çarpım yok
.ToListAsync();
2. Change Tracking Overhead'i
EF Core, çektiği her entity'yi ChangeTracker'a kaydeder. Sadece okuma yapacaksanız bu tamamen gereksizdir:
// Kötü: 10.000 kayıt çekiyorsunuz ama hepsi tracking'e ekleniyor
var products = await db.Products
.Where(p => p.CategoryId == 5)
.ToListAsync(); // 10.000 entity → ChangeTracker belleği şişer
// İyi: Sadece okuyacaksanız AsNoTracking
var products = await db.Products
.AsNoTracking()
.Where(p => p.CategoryId == 5)
.ToListAsync();
// En iyi: Projeksiyon + NoTracking birlikte
var dtos = await db.Products
.AsNoTracking()
.Where(p => p.CategoryId == 5)
.Select(p => new ProductDto(p.Id, p.Name, p.Price))
.ToListAsync();
Benchmark sonuçlarına göre AsNoTracking() büyük listelerde bellek kullanımını %35–50 azaltıyor.
3. Projeksiyon Kullanmamak
Tüm kolonu çekip sonra birkaç field kullanmak, hem veri transferi hem bellek açısından verimsizdir:
// Kötü: 50 kolonlu User tablosunun tamamı geliyor
var users = await db.Users.ToListAsync();
var names = users.Select(u => $"{u.FirstName} {u.LastName}").ToList();
// İyi: SQL'de sadece gerekli alanlar SELECT edilir
var names = await db.Users
.Select(u => $"{u.FirstName} {u.LastName}")
.ToListAsync();
// Daha iyi: Tip güvenli DTO projeksiyonu
var summaries = await db.Orders
.Where(o => o.Status == OrderStatus.Pending)
.Select(o => new OrderSummary
{
Id = o.Id,
CustomerName = o.Customer.FullName, // JOIN otomatik yapılır
TotalAmount = o.Items.Sum(i => i.Price * i.Quantity),
ItemCount = o.Items.Count
})
.ToListAsync();
4. ExecuteUpdateAsync ve ExecuteDeleteAsync
EF Core 7+ ile toplu güncelleme ve silme işlemleri artık entity yüklemeden yapılabiliyor:
// Kötü: 500 kayıt için 500 UPDATE
var expiredTokens = await db.Tokens
.Where(t => t.ExpiresAt < DateTime.UtcNow)
.ToListAsync(); // 500 kayıt belleğe çekiliyor
foreach (var token in expiredTokens)
token.IsRevoked = true;
await db.SaveChangesAsync(); // 500 ayrı UPDATE komutu
// İyi: Tek SQL UPDATE
await db.Tokens
.Where(t => t.ExpiresAt < DateTime.UtcNow)
.ExecuteUpdateAsync(s => s
.SetProperty(t => t.IsRevoked, true)
.SetProperty(t => t.RevokedAt, DateTime.UtcNow));
// ExecuteDeleteAsync ile toplu silme
await db.AuditLogs
.Where(l => l.CreatedAt < DateTime.UtcNow.AddYears(-2))
.ExecuteDeleteAsync(); // Tek DELETE ifadesi
5. Compiled Query ile Tekrarlayan Sorgu Maliyetini Azaltma
EF Core her sorguyu LINQ ağacından SQL'e çevirir. Aynı sorgu defalarca çalışıyorsa bu derleme maliyeti birikir. EF.CompileAsyncQuery ile bunu bir kez yapabilirsiniz:
// Compiled query tanımı — uygulama başlarken bir kez derlenir
private static readonly Func<AppDbContext, int, Task<Order?>> GetOrderById =
EF.CompileAsyncQuery((AppDbContext db, int id) =>
db.Orders
.Include(o => o.Items)
.FirstOrDefault(o => o.Id == id));
// Kullanım — her çağrıda LINQ→SQL çevirisi yapılmaz
public async Task<Order?> FindAsync(int id)
=> await GetOrderById(_db, id);
// Liste için
private static readonly Func<AppDbContext, int, IAsyncEnumerable<Product>> GetByCategory =
EF.CompileAsyncQuery((AppDbContext db, int categoryId) =>
db.Products.Where(p => p.CategoryId == categoryId));
public IAsyncEnumerable<Product> GetByCategoryAsync(int categoryId)
=> GetByCategory(_db, categoryId);
6. SQL Loglarını İzleyin
EF Core'un ne ürettiğini görmeden optimize etmek körlük etmektir:
// Development ortamında SQL loglaması
options.UseSqlServer(connectionString)
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging() // Parametre değerlerini de göster
.EnableDetailedErrors();
// Sadece yavaş sorguları logla (production uyumlu)
options.UseSqlServer(connectionString)
.LogTo(
filter: (eventId, _) => eventId == RelationalEventId.CommandExecuted,
logger: (eventData) =>
{
var cmd = (CommandExecutedEventData)eventData;
if (cmd.Duration.TotalMilliseconds > 500) // 500ms+ sorgular
logger.LogWarning("Yavaş sorgu ({Ms}ms): {Sql}",
cmd.Duration.TotalMilliseconds, cmd.Command.CommandText);
});
7. İndeks Eksikliği
Bu yazılım değil veritabanı tarafında bir sorun ama EF Core ile kolayca çözülüyor:
// Fluent API ile indeks tanımı
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>(entity =>
{
// Sık filtrelenen alanlar
entity.HasIndex(o => o.Status);
entity.HasIndex(o => o.CustomerId);
entity.HasIndex(o => o.CreatedAt);
// Birleşik indeks (sık birlikte kullanılan filtreler)
entity.HasIndex(o => new { o.Status, o.CreatedAt });
// Unique indeks
entity.HasIndex(o => o.OrderNumber).IsUnique();
});
}
Özet Kontrol Listesi
- Liste sorgularında
AsNoTracking()kullanın - Sadece gereken alanları
Select()ile çekin - Birden fazla koleksiyon include ediyorsanız
AsSplitQuery()deneyin - Toplu güncelleme/silmede
ExecuteUpdateAsync/ExecuteDeleteAsynckullanın - Sık çalışan sorgularda
EF.CompileAsyncQuerydeğerlendirin - SQL loglarını izleyin; 500ms+ sorguları belgeleyin
- Sık filtrelenen sütunlara indeks ekleyin