EFEF Core Handbook

TEMEL

DbContext & DbSet Kurulumu

DbContext, uygulama ile veritabanı arasındaki köprüdür. Her sorgu, her kayıt bu köprüden geçer.

Veritabanı sağlayıcısı Bu sayfadaki eşleşen örnekleri seçilen sağlayıcıya göre gösterir.

Temel Kurulum

// NuGet: dotnet add package Microsoft.EntityFrameworkCore.SqlServer

// appsettings.json
{
  "ConnectionStrings": {
    "Default": "Server=.;Database=MyDb;Trusted_Connection=True;TrustServerCertificate=True;"
  }
}

// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

Connection String parametreleri (SQL Server):

Parametre Varsayılan Açıklama
Server Sunucu adı (. = localhost, .\SQLEXPRESS)
Database Veritabanı adı
Trusted_Connection false True → Windows Auth
User Id / Password SQL Auth (Trusted_Connection=False ise)
TrustServerCertificate false Dev'de True (self-signed cert)
MultipleActiveResultSets false Nested reader'lar için (EF Core'da genelde gereksiz)
Encrypt true (.NET 7+) Bağlantı şifreleme
Connection Timeout 15 Bağlantı zaman aşımı (sn)
Max Pool Size 100 Connection pool boyutu
// NuGet: dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
// NuGet: dotnet add package EFCore.NamingConventions  (opsiyonel, şiddetle önerilen)

// appsettings.json
{
  "ConnectionStrings": {
    "Default": "Host=localhost;Port=5432;Database=mydb;Username=app;Password=***;"
  }
}

// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))
           .UseSnakeCaseNamingConvention());  // Products → products, OrderDate → order_date

Connection String parametreleri (Npgsql):

Parametre Varsayılan Açıklama
Host localhost Sunucu adresi
Port 5432 PostgreSQL portu
Database Veritabanı adı
Username Kullanıcı
Password Şifre
SSL Mode Prefer Require → production'da zorunlu
Maximum Pool Size 100 İç havuz boyutu
Connection Idle Lifetime 300 Boş bağlantı ömrü (sn)
Include Error Detail false Dev'de true yap (detaylı hata)
Command Timeout 30 Sorgu zaman aşımı (sn)

Naming Convention:
PostgreSQL dünyasında standart snake_case'dir. UseSnakeCaseNamingConvention() olmadan EF Core, C#'taki PascalCase isimleri doğrudan kullanır → "Products" (tırnaklı). Bu, psql'de her seferinde çift tırnak yazmak zorunda bırakır.

🚨 Güvenlik — Connection String Yönetimi:
Yukarıdaki örneklerde şifreler doğrudan appsettings.json içinde gösterilmiştir — bu sadece geliştirme ortamı içindir. Production'da connection string'leri asla kaynak kodda veya config dosyasında saklamayın:

  • Development: dotnet user-secrets set "ConnectionStrings:Default" "..."
  • Production: Environment variable, Azure Key Vault veya AWS Secrets Manager kullanın
  • appsettings.Development.json dosyasını .gitignore'a ekleyin

DbContext Sınıfı

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<Product> Products { get; set; }
    public DbSet<Category> Categories { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Tüm config sınıflarını assembly'den otomatik yükle
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
    }
}

DbSet<Product> = SQL'deki Products tablosu.
context.Products.Where(...) yazıldığında arka planda SELECT ... FROM Products WHERE ... oluşur.

Lifetime: AddDbContext varsayılan olarak Scoped kayıt yapar — her HTTP request'te bir instance oluşur ve request sonunda dispose edilir.

DbContext Yaşam Döngüsü & IDbContextFactory

DbContext'in ne zaman oluşup ne zaman yok edildiğini anlamak, memory leak ve thread-safety hatalarından korunmanın temelidir. AddDbContext varsayılan olarak Scoped kayıt yapar — her HTTP request'te bir instance oluşur, request bitince dispose edilir.

Yaşam Döngüsü

HTTP Request başlar new AppDbContext() Connection pool'dan bağlantı alınır Change Tracker boş — hiçbir entity izlenmiyor Controller / Service kullanır Sorgular çalışır, entity'ler track edilir, SaveChanges çağrılır Request biter → Dispose() Bağlantı pool'a geri verilir Change Tracker temizlenir — bellek serbest kalır Scoped Lifetime

Kayıt Yöntemleri

Yöntem Ne zaman? Nasıl?
AddDbContext Controller, API endpoint Her request'te otomatik oluşur/yok olur
AddDbContextFactory Background Service, Blazor Server, gRPC factory.CreateDbContextAsync() ile manuel oluşturursun
AddPooledDbContextFactory Yüksek throughput Context nesnesi havuzlanır, allocation azalır
// Scoped (varsayılan — Controller/API için)
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString));

// Factory (Background Service / Blazor Server için)
builder.Services.AddDbContextFactory<AppDbContext>(options =>
    options.UseSqlServer(connectionString));

// Pooled Factory (yüksek throughput için)
builder.Services.AddPooledDbContextFactory<AppDbContext>(options =>
    options.UseSqlServer(connectionString));
// Scoped (varsayılan — Controller/API için)
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(connectionString)
           .UseSnakeCaseNamingConvention());

// Factory (Background Service / Blazor Server için)
builder.Services.AddDbContextFactory<AppDbContext>(options =>
    options.UseNpgsql(connectionString)
           .UseSnakeCaseNamingConvention());

// Pooled Factory (yüksek throughput için)
builder.Services.AddPooledDbContextFactory<AppDbContext>(options =>
    options.UseNpgsql(connectionString)
           .UseSnakeCaseNamingConvention());

Npgsql'in Maximum Pool Size → TCP bağlantılarını havuzlar. EF Core'un Pool'u → DbContext nesnesini havuzlar. İkisi birlikte çalışır.

IDbContextFactory Kullanımı

Request dışı senaryolarda (Background Service, Blazor Server, gRPC) context'i kendin oluşturup kendin dispose etmelisin:

public class OrderProcessingService : BackgroundService
{
    private readonly IDbContextFactory<AppDbContext> _factory;

    public OrderProcessingService(IDbContextFactory<AppDbContext> factory)
        => _factory = factory;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await using var context = await _factory.CreateDbContextAsync(stoppingToken);
            
            var pending = await context.Orders
                .Where(o => o.Status == OrderStatus.Pending)
                .ToListAsync(stoppingToken);

            foreach (var order in pending)
                order.Status = OrderStatus.Processing;

            await context.SaveChangesAsync(stoppingToken);
            await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
        }
    }
}

Dikkat Edilmesi Gerekenler

// ❌ DbContext'i Singleton olarak kaydetme — thread-safe DEĞİL
builder.Services.AddSingleton<AppDbContext>();

// ❌ Aynı DbContext ile paralel sorgu — ÇÖKER
var task1 = context.Products.ToListAsync();
var task2 = context.Orders.ToListAsync();
await Task.WhenAll(task1, task2);  // DbContext thread-safe değil!

// ✅ Paralel iş gerekiyorsa her biri için ayrı context
await using var ctx1 = await factory.CreateDbContextAsync();
await using var ctx2 = await factory.CreateDbContextAsync();
await Task.WhenAll(ctx1.Products.ToListAsync(), ctx2.Orders.ToListAsync());