EFEF Core Handbook

İLERİ

Global Query Filters

Model düzeyinde tanımlanan, o entity'ye yapılan tüm sorgulara otomatik eklenen WHERE koşulları. Soft delete (IsDeleted), multi-tenancy (TenantId) gibi her sorguda tekrar tekrar yazılması gereken filtreleri tek noktada tanımlarsın.

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

Tanımlama

// Soft delete filtresi
builder.HasQueryFilter(p => !p.IsDeleted);

// Multi-tenant filtresi (DbContext'e inject edilen tenantId ile)
builder.HasQueryFilter(p => p.TenantId == _tenantId);

// Filtreli sorguyu devre dışı bırakma
var allProducts = context.Products.IgnoreQueryFilters().ToList();

Soft Delete filtresi nasıl çalışır:

// Normal sorgu
context.Products.Where(p => p.Price > 100).ToList();
-- EF'in ürettiği SQL (filtre otomatik eklenir):
SELECT [p].[Id], [p].[Name], [p].[Price]
FROM [Products] AS [p]
WHERE [p].[Price] > 100.0 
  AND [p].[IsDeleted] = CAST(0 AS bit);    -- ← Otomatik eklendi!
-- EF'in ürettiği SQL (filtre otomatik eklenir):
SELECT p.id, p.name, p.price
FROM products AS p
WHERE p.price > 100.0 
  AND p.is_deleted = FALSE;                -- ← Otomatik eklendi!
// Filtreyi devre dışı bırak (admin paneli, raporlama vb.)
context.Products.IgnoreQueryFilters().Where(p => p.Price > 100).ToList();
-- Filtresiz:
SELECT [p].[Id], [p].[Name], [p].[Price]
FROM [Products] AS [p]
WHERE [p].[Price] > 100.0;                 -- Filtre YOK
-- Filtresiz:
SELECT p.id, p.name, p.price
FROM products AS p
WHERE p.price > 100.0;                     -- Filtre YOK

Örnek veri — Products (IsDeleted ile):

Id Name Price IsDeleted Silinen Ürünü Gören
1 Laptop 84999.99 0 Normal sorgu
2 Eski Model 29999.00 1 Sadece IgnoreQueryFilters()
3 iPhone 54999.00 0 Normal sorgu

Birden Fazla Filter Kombinasyonu

// ⚠️ EF Core 9 ve öncesi: Tek bir entity'ye sadece BİR HasQueryFilter atanabilir.
// Birden fazla koşul AND ile birleştirilir:
builder.HasQueryFilter(p => !p.IsDeleted && p.TenantId == _tenantId);

// ❌ İkinci HasQueryFilter ilkini EZİP yazar (override) — EF9 ve öncesi:
builder.HasQueryFilter(p => !p.IsDeleted);
builder.HasQueryFilter(p => p.TenantId == _tenantId);  // ← Sadece bu geçerli!

Named Query Filters — EF Core 10+ 🆕

EF Core 10 ile artık birden fazla isimli filter tanımlanabilir ve seçici olarak devre dışı bırakılabilir:

// 🆕 EF Core 10 (GA — .NET 10): İsimli query filter'lar
modelBuilder.Entity<Product>()
    .HasQueryFilter("SoftDelete", p => !p.IsDeleted)
    .HasQueryFilter("Tenant", p => p.TenantId == _tenantId);

// Sorguda sadece belirli filter'ları devre dışı bırakma:
var allProducts = await context.Products
    .IgnoreQueryFilters(["SoftDelete"])   // Sadece soft-delete kaldırıldı, Tenant hâlâ aktif
    .ToListAsync();

// Tüm filter'ları kaldırma (eski davranış):
var everything = await context.Products
    .IgnoreQueryFilters()   // Hepsi devre dışı
    .ToListAsync();
-- IgnoreQueryFilters(["SoftDelete"]) ile:
SELECT * FROM [Products] WHERE [TenantId] = @tenantId;  -- IsDeleted filtresi YOK

-- IgnoreQueryFilters() ile:
SELECT * FROM [Products];  -- Hiçbir filter yok
-- IgnoreQueryFilters(["SoftDelete"]) ile:
SELECT * FROM products WHERE tenant_id = @tenantId;  -- is_deleted filtresi YOK

-- IgnoreQueryFilters() ile:
SELECT * FROM products;  -- Hiçbir filter yok

Migration notu: EF Core 9'dan 10'a geçerken mevcut HasQueryFilter çağrıların çalışmaya devam eder. İsimli filter'lar opsiyonel — yalnızca seçici disable ihtiyacın olunca kullan.

İlişkili Entity'lerde Filter Davranışı

// Category → HasQueryFilter(c => !c.IsDeleted)
// Product → HasQueryFilter(p => !p.IsDeleted)

// Include ile: her iki filter DA uygulanır
var categories = context.Categories
    .Include(c => c.Products)
    .ToList();
-- EF her iki tabloya da filter uygular:
SELECT [c].*, [p].*
FROM [Categories] AS [c]
LEFT JOIN [Products] AS [p] ON [p].[CategoryId] = [c].[Id] AND [p].[IsDeleted] = 0
WHERE [c].[IsDeleted] = 0;
-- EF her iki tabloya da filter uygular:
SELECT c.*, p.*
FROM categories AS c
LEFT JOIN products AS p ON p.category_id = c.id AND p.is_deleted = FALSE
WHERE c.is_deleted = FALSE;

Required navigation + filter = silent data loss!
Eğer Product.CategoryId required ise ve Category soft-deleted ise → Product join'da kaybolur.
Çözüm: İlişkiyi optional yap veya IgnoreQueryFilters() kullan.