Entity Framework Core: Veritabanı İşlemlerinin Modern Yolu
# Entity Framework Core: Veritabanı Yönetiminin Modern Yolu
Entity Framework Core (EF Core), .NET ekosistemindeki en yaygın ORM çözümüdür. Hızlı geliştirme sağlarken şema evrimi ve sorgu davranışı üzerinde güçlü kontrol sunar. Ancak verimli kullanmak, sadece `SaveChanges()` çağırmaktan çok daha fazlasını gerektirir. Bu yazıda, gerçek üretim sistemlerinde EF Core'u nasıl kurduğumu, optimize ettiğimi ve sürdürdüğümü detaylı şekilde anlatacağım.
DbContext Kurulumu
`DbContext`, EF Core'un kalbidir. Baştan doğru yapılandırmak, ileride acı veren refactor süreçlerinden kurtarır.
public class AppDbContext : DbContext
{
public DbSet<Product> Products => Set<Product>();
public DbSet<Order> Orders => Set<Order>();
public DbSet<Customer> Customers => Set<Customer>();
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
class=class="code-string">"code-comment">// Mevcut assembly'deki tüm konfigürasyonları otomatik uygula
modelBuilder.ApplyConfigurationsFromAssembly(
Assembly.GetExecutingAssembly());
}
}`Program.cs` dosyasında connection pooling ve uygun ayarlarla kayıt edin:
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString(class="code-string">"Default"),
sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(
maxRetryCount: class="code-number">3,
maxRetryDelay: TimeSpan.FromSeconds(class="code-number">10),
errorNumbersToAdd: null);
sqlOptions.CommandTimeout(class="code-number">30);
})
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));Yoğun trafikli API'lerde çalışırken, varsayılan olarak `NoTracking` modunu global düzeyde aktif edip sadece veri değişikliği yapılan yerlerde tracking'e geçmek, gereksiz bellek kullanımını ciddi oranda azalttı.
Fluent API ile Entity Konfigürasyonu
Data Annotation yerine Fluent API kullanmayı kesinlikle tercih ediyorum. Domain entity'lerini temiz tutar ve tüm persistence mantığını ayrı konfigürasyon sınıflarına taşır.
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.ToTable(class="code-string">"Orders");
builder.HasKey(o => o.Id);
builder.Property(o => o.OrderNumber)
.IsRequired()
.HasMaxLength(class="code-number">50);
builder.Property(o => o.TotalAmount)
.HasPrecision(class="code-number">18, class="code-number">2);
builder.Property(o => o.Status)
.HasConversion<string>()
.HasMaxLength(class="code-number">20);
builder.HasOne(o => o.Customer)
.WithMany(c => c.Orders)
.HasForeignKey(o => o.CustomerId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasMany(o => o.OrderItems)
.WithOne(oi => oi.Order)
.HasForeignKey(oi => oi.OrderId)
.OnDelete(DeleteBehavior.Cascade);
class=class="code-string">"code-comment">// Gerçek sorgu kalıplarına dayalı indeksler
builder.HasIndex(o => o.OrderNumber).IsUnique();
builder.HasIndex(o => o.CustomerId);
builder.HasIndex(o => new { o.Status, o.CreatedAt });
}
}Value Object'ler için Owned Types
builder.OwnsOne(c => c.Address, address =>
{
address.Property(a => a.Street).HasMaxLength(class="code-number">200);
address.Property(a => a.City).HasMaxLength(class="code-number">100);
address.Property(a => a.PostalCode).HasMaxLength(class="code-number">10);
address.Property(a => a.Country).HasMaxLength(class="code-number">60);
});Bu yapı, `Address` value object'ini `Customer` tablosuna yerleştirir. Gereksiz JOIN'ler oluşturmadan domain bütünlüğünü korur.
Etkili LINQ Sorguları Yazmak
DTO'ya Projeksiyon
Entity'leri asla doğrudan API endpoint'lerinden döndürmeyin. Her zaman projeksiyon kullanın:
public async Task<List<OrderSummaryDto>> GetRecentOrdersAsync(
int customerId, CancellationToken ct)
{
return await _context.Orders
.Where(o => o.CustomerId == customerId)
.OrderByDescending(o => o.CreatedAt)
.Take(class="code-number">20)
.Select(o => new OrderSummaryDto
{
OrderNumber = o.OrderNumber,
Total = o.TotalAmount,
Status = o.Status,
ItemCount = o.OrderItems.Count,
CreatedAt = o.CreatedAt
})
.ToListAsync(ct);
}`Select` ile yapılan projeksiyon, EF Core'un yalnızca ihtiyaç duyulan sütunları getiren hedefli bir SQL sorgusu oluşturmasını sağlar. En basit ve en etkili performans iyileştirmelerinden biridir.
Read-Only Sorgularda AsNoTracking
class=class="code-string">"code-comment">// DbContext varsayılan olarak tracking yapıyorsa, açıkça devre dışı bırakın
var products = await _context.Products
.AsNoTracking()
.Where(p => p.IsActive)
.ToListAsync(ct);Change tracker, yüklediği her entity'nin referansını tutar. Sadece veri okuyan endpoint'lerde bu referanslar tamamen gereksiz bir yüktür.
Sık Çalışan Sorgular için Compiled Queries
Dakikada binlerce kez çalışan sorgularda, compiled query'ler her çağrıda expression tree çevirisini atlar:
private static readonly Func<AppDbContext, int, CancellationToken, Task<Product?>>
GetProductById = EF.CompileAsyncQuery(
(AppDbContext ctx, int id, CancellationToken ct) =>
ctx.Products.FirstOrDefault(p => p.Id == id));
class=class="code-string">"code-comment">// Kullanım
var product = await GetProductById(_context, productId, ct);Yoğun trafikli API'lerde çalışırken, en çok istek alan endpoint'lerdeki compiled query'ler her istekten 2-3ms tasarruf sağladı. Tek başına küçük görünebilir ama toplam trafikte devasa bir fark yaratıyor.
N+1 Problemi ve Çözümleri
N+1 problemi, ORM kullanan uygulamalarda muhtemelen en yaygın performans katilidir. Bir liste çekilirken her ilişkili entity için ayrı sorgu tetiklenmesiyle oluşur.
Problem
class=class="code-string">"code-comment">// KÖTÜ: Siparişler için class="code-number">1 sorgu + her müşteri için N ayrı sorgu
var orders = await _context.Orders.ToListAsync();
foreach (var order in orders)
{
class=class="code-string">"code-comment">// order.Customer'a her erişim ayrı bir SQL sorgusu tetikler
Console.WriteLine($class="code-string">"Sipariş {order.OrderNumber} - {order.Customer.Name}");
}100 siparişiniz varsa bu 101 SQL sorgusu üretir. Lazy loading açıksa bu sessizce gerçekleşir.
Çözüm 1: Include ile Eager Loading
class=class="code-string">"code-comment">// İYİ: Her şeyi yükleyen class="code-number">1 sorgu (veya split query ile class="code-number">2)
var orders = await _context.Orders
.Include(o => o.Customer)
.Include(o => o.OrderItems)
.ToListAsync();Çözüm 2: Geniş Include'larda Split Query
Birden fazla collection include ettiğinizde, tek bir sorgu kartezyen çarpım patlamasına yol açabilir. Split query'ler bunu çözer:
var orders = await _context.Orders
.Include(o => o.Customer)
.Include(o => o.OrderItems)
.ThenInclude(oi => oi.Product)
.AsSplitQuery()
.ToListAsync();Bu, birden fazla SQL ifadesi üretir ama veri tekrarı sorununu ortadan kaldırır.
Çözüm 3: Projeksiyon (Salt Okunur Veriler için En İyisi)
class=class="code-string">"code-comment">// API yanıtları için EN İYİSİ: sadece ihtiyacınız olanı getirin
var orders = await _context.Orders
.Select(o => new OrderDetailDto
{
OrderNumber = o.OrderNumber,
CustomerName = o.Customer.Name,
Items = o.OrderItems.Select(oi => new OrderItemDto
{
ProductName = oi.Product.Name,
Quantity = oi.Quantity,
Price = oi.UnitPrice
}).ToList()
})
.ToListAsync();Projeksiyon N+1'i tamamen ortadan kaldırır, çünkü EF Core veritabanı seviyesinde JOIN'li tek bir SQL sorgusu oluşturur.
Migration En İyi Uygulamaları
Migration'lar, veritabanı şemanızın versiyon kontrol sistemidir. Yanlış yönetim; deployment hataları, veri kaybı ve gece yarısı olay müdahalelerine yol açar.
Migration Oluşturma ve İnceleme
# Migration'ları her zaman açıklayıcı isimlerle oluşturun
dotnet ef migrations add AddOrderStatusIndex
# Uygulamadan önce üretilen kodu mutlaka inceleyin
dotnet ef migrations script --idempotentÜretilen migration dosyasını her zaman gözden geçirin. EF Core'un diff algoritması iyi çalışır ama mükemmel değildir -- bazen rename işlemlerini sil-ve-yeniden-oluştur olarak yorumlayabilir.
Güvenli Migration Kalıpları
public partial class AddOrderStatusIndex : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
class=class="code-string">"code-comment">// Tablo kilitlenmesini önlemek için concurrent index (PostgreSQL)
migrationBuilder.Sql(
class="code-string">"CREATE INDEX CONCURRENTLY IF NOT EXISTS " +
class="code-string">"\"IX_Orders_Status\" ON \"Orders\" (\"Status\");");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: class="code-string">"IX_Orders_Status",
table: class="code-string">"Orders");
}
}Uyguladığım Migration Kuralları
Performans Optimizasyonu
Toplu İşlemler (Batching)
EF Core 7+ sürümlerinde `SaveChanges` çağrıları otomatik olarak gruplanır, ancak batch boyutunu kontrol edebilirsiniz:
options.UseSqlServer(connectionString, sqlOptions =>
{
sqlOptions.MaxBatchSize(class="code-number">100);
});Bulk İşlemler
Binlerce satır eklerken `SaveChanges` çok yavaştır. EF Core 7+ ile gelen `ExecuteUpdate` ve `ExecuteDelete` kullanın:
class=class="code-string">"code-comment">// Tüm inaktif ürünleri tek bir SQL ifadesiyle güncelle
await _context.Products
.Where(p => !p.IsActive && p.LastModified < cutoffDate)
.ExecuteUpdateAsync(s =>
s.SetProperty(p => p.IsArchived, true)
.SetProperty(p => p.ArchivedAt, DateTime.UtcNow));
class=class="code-string">"code-comment">// Entity'leri belleğe yüklemeden toplu silme
await _context.Products
.Where(p => p.IsArchived && p.ArchivedAt < retentionDate)
.ExecuteDeleteAsync();Bunlar doğrudan SQL `UPDATE` ve `DELETE` ifadelerine dönüşür. Hiçbir entity belleğe yüklenmez.
Soft Delete için Query Filter'lar
class=class="code-string">"code-comment">// Entity konfigürasyonunda
builder.HasQueryFilter(p => !p.IsDeleted);
class=class="code-string">"code-comment">// Bu filtre her sorguya otomatik uygulanır
var activeProducts = await _context.Products.ToListAsync();
class=class="code-string">"code-comment">// Silinen kayıtlara erişmeniz gerektiğinde filteri atlatın
var allProducts = await _context.Products
.IgnoreQueryFilters()
.ToListAsync();Bağlantı Dayanıklılığı (Connection Resiliency)
Bulut ortamlarında geçici hatalar kaçınılmazdır:
options.UseSqlServer(connectionString, sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(
maxRetryCount: class="code-number">5,
maxRetryDelay: TimeSpan.FromSeconds(class="code-number">30),
errorNumbersToAdd: null);
});Query Tag'leri ile İzleme
var orders = await _context.Orders
.TagWith(class="code-string">"GetRecentOrders - OrdersController")
.Where(o => o.CreatedAt > cutoff)
.ToListAsync();Bu, üretilen SQL'e bir yorum ekler. Yavaş sorguları, onları üreten C# koduna kadar izlemeyi kolaylaştırır.
Sık Yapılan EF Core Hataları
1. Entity'leri API Endpoint'lerinden Doğrudan Döndürmek
class=class="code-string">"code-comment">// KÖTÜ: İç yapıyı açığa çıkarır, lazy loading tetikler,
class=class="code-string">"code-comment">// circular reference nedeniyle serialization hataları verir
[HttpGet]
public async Task<List<Order>> GetOrders()
=> await _context.Orders.Include(o => o.Customer).ToListAsync();
class=class="code-string">"code-comment">// İYİ: DTO'ya projeksiyon yapın
[HttpGet]
public async Task<List<OrderDto>> GetOrders()
=> await _context.Orders
.Select(o => new OrderDto { class=class="code-string">"code-comment">/* ... */ })
.ToListAsync();2. CancellationToken Kullanmamak
class=class="code-string">"code-comment">// KÖTÜ: İstemci bağlantıyı keserse sorgu çalışmaya devam eder
await _context.Products.ToListAsync();
class=class="code-string">"code-comment">// İYİ: HTTP isteği iptal edildiğinde sorgu da iptal olur
await _context.Products.ToListAsync(cancellationToken);3. Tüm Tabloyu Belleğe Yüklemek
class=class="code-string">"code-comment">// KÖTÜ: Her şeyi yükler, sonra bellekte filtreler
var expensive = (await _context.Products.ToListAsync())
.Where(p => p.Price > class="code-number">100);
class=class="code-string">"code-comment">// İYİ: Filtreleme veritabanı seviyesinde yapılır
var expensive = await _context.Products
.Where(p => p.Price > class="code-number">100)
.ToListAsync();4. Uzun Ömürlü DbContext Kullanımı
`DbContext` kısa ömürlü olacak şekilde tasarlanmıştır. Web uygulamalarında tek bir request'e scope'lanmalıdır. Birden fazla request boyunca aynı DbContext'i tutmak; change tracker'ın şişmesine, eski verilerin kalmasına ve eşzamanlılık sorunlarının çoğalmasına neden olur.
5. Üretilen SQL'i Görmezden Gelmek
class=class="code-string">"code-comment">// Geliştirme ortamında EF Core'un gerçekte ne gönderdiğini görün
options.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging()
.EnableDetailedErrors();Yoğun trafikli API'lerde çalışırken, geliştirme aşamasında SQL loglamayı açmak incelediğim her code base'de en az bir gizli N+1 problemi yakalamamı sağladı. Bunu bir alışkanlık haline getirin.
Sonuç
EF Core, disiplinli kullanıldığında güçlü bir araçtır. Sorunlu bir veri katmanı ile performanslı bir veri katmanı arasındaki fark neredeyse her zaman şuna bağlıdır: entity yüklemek yerine projeksiyon yapmak, loading stratejisini bilinçli seçmek, üretilen SQL'i incelemek ve migration'ları birinci sınıf deployment artifactı olarak ele almak.
Burada paylaştığım kalıplar, milyonlarca isteği karşılayan gerçek üretim sistemlerinden geliyor. Bunlar teorik değil -- savaş alanında test edilmiş yöntemler.
Veritabanı mimariniz ve EF Core katmanınız için danışmanlık sağlayabilirim.
İlgili Makaleler
.NET Nedir? Modern Backend Geliştirme Rehberi
.NET platformunun ne olduğunu, nasıl çalıştığını ve neden kurumsal projelerde tercih edildiğini öğrenin.
ASP.NET Core ile RESTful API Geliştirme
ASP.NET Core ile production-ready REST API geliştirmenin temellerini öğrenin. Controller, routing ve best practice'ler.
.NET'te Clean Architecture: Ölçeklenebilir Proje Yapısı
.NET projelerinde Clean Architecture uygulayın. Katmanlar, bağımlılık yönetimi ve test edilebilir kod için rehber.
Flutter Projeniz mi Var?
iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.
İletişime Geç