EFEF Core Handbook

UZMAN

Multi-Tenancy Patterns

Tek uygulama ile birden fazla kiracıya (müşteri/organizasyon) hizmet vermek. Üç yaklaşım: aynı tabloda discriminator sütunu (basit), kiracı başına ayrı schema (orta), kiracı başına ayrı veritabanı (tam izolasyon).

Pattern 1: Discriminator (Aynı Tablo — Global Filter)

// Tenant bilgisi
public interface ITenantEntity
{
    string TenantId { get; set; }
}

// Entities
public class Product : ITenantEntity
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string TenantId { get; set; } = null!;  // Her satırda tenant bilgisi
}

// Tenant servis
public interface ITenantService
{
    string GetCurrentTenantId();
}

// DbContext
public class MultiTenantDbContext : DbContext
{
    private readonly string _tenantId;

    public MultiTenantDbContext(
        DbContextOptions options,
        ITenantService tenantService) : base(options)
    {
        _tenantId = tenantService.GetCurrentTenantId();
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Tüm tenant entity'lerine global filter
        modelBuilder.Entity<Product>()
            .HasQueryFilter(p => p.TenantId == _tenantId);

        modelBuilder.Entity<Order>()
            .HasQueryFilter(o => o.TenantId == _tenantId);
    }

    public override async Task<int> SaveChangesAsync(CancellationToken ct = default)
    {
        // Yeni kayıtlara otomatik tenant ata
        foreach (var entry in ChangeTracker.Entries<ITenantEntity>()
            .Where(e => e.State == EntityState.Added))
        {
            entry.Entity.TenantId = _tenantId;
        }
        return await base.SaveChangesAsync(ct);
    }
}

Pattern 2: Ayrı Veritabanı (Database per Tenant)

// Her tenant'ın kendi connection string'i var
public class TenantDbContextFactory
{
    private readonly ITenantService _tenantService;
    private readonly IConfiguration _config;

    public AppDbContext CreateContext()
    {
        var tenantId = _tenantService.GetCurrentTenantId();
        var connectionString = _config.GetConnectionString($"Tenant_{tenantId}");
        
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseSqlServer(connectionString)
            .Options;

        return new AppDbContext(options);
    }
}

// Program.cs
builder.Services.AddDbContext<AppDbContext>((sp, options) =>
{
    var tenantService = sp.GetRequiredService<ITenantService>();
    var tenantId = tenantService.GetCurrentTenantId();
    var connectionString = configuration.GetConnectionString($"Tenant_{tenantId}");
    options.UseSqlServer(connectionString);
});

Pattern 3: Ayrı Schema (Schema per Tenant)

// Her tenant farklı schema kullanır
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.HasDefaultSchema(_tenantId); // "tenant_abc" schema
}

Karşılaştırma

Kriter Discriminator Ayrı DB Ayrı Schema
Veri izolasyonu Yazılımsal Tam DB-seviye
Maliyet Düşük Yüksek Orta
Migration Tek sefer Her tenant Her schema
Ölçekleme Tek DB limiti Bağımsız Tek DB limiti
Backup/Restore Hepsi birlikte Bağımsız Schema bazlı