Entity Framework Core: Veritabanı İşlemlerinin Modern Yolu

15 dakika okuma9 Şubat 2026Güncellendi: 9 Mar 2026
Entity Framework CoreEF Core tutorialORM .NET.NET databaseCode-first EF CoreEF Core migrationsLINQ queriesEF Core performance

# 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.

csharp
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:

csharp
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.

csharp
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

csharp
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:

csharp
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

csharp
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:

csharp
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

csharp
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

csharp
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:

csharp
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)

csharp
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

bash
# 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ı

csharp
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ı

  • **Her migration tek bir konuya odaklansın.** Şema değişikliklerini ve veri migration'larını karıştırmayın.
  • **Herhangi bir ortama uygulanmış migration'ı asla düzenlemeyin.** Bunun yerine yeni bir migration oluşturun.
  • **`Down` metodunu her zaman yazın.** Bir gün mutlaka geri almak zorunda kalacaksınız.
  • **Üretim ortamı için idempotent script kullanın.** `--idempotent` ile oluşturulan scriptler tekrar çalıştırıldığında güvenlidir.
  • **Veri migration'larını şema migration'larından ayırın.** Veri dönüşümleri geri almak daha zordur ve ayrı bir deployment adımı olarak ele alınmalıdır.
  • Performans Optimizasyonu

    Toplu İşlemler (Batching)

    EF Core 7+ sürümlerinde `SaveChanges` çağrıları otomatik olarak gruplanır, ancak batch boyutunu kontrol edebilirsiniz:

    csharp
    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:

    csharp
    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

    csharp
    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:

    csharp
    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

    csharp
    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

    csharp
    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

    csharp
    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

    csharp
    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

    csharp
    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

    Flutter Projeniz mi Var?

    iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.

    İletişime Geç