EF Core (Entity Framework Core) global query filter, bir entity tipine bağlanan ve o tip her sorgulandığında otomatik olarak eklenen bir Where koşuludur. OnModelCreating içinde HasQueryFilter ile tanımlanır; en yaygın kullanımları soft delete (silinmiş satırları varsayılan olarak gizlemek) ve çok kiracılı (multi-tenant) uygulamalarda her sorguyu geçerli kiracıya daraltmaktır. Belirli bir sorguda filtreyi kapatmak için IgnoreQueryFilters kullanılır ve EF Core 10 ile birlikte filtrelere ad verip yalnızca birini kapatmak mümkün hale geldi.
Bu yazıda filtrelerin nasıl tanımlandığını, bağlam (context) verisine nasıl eriştiğini, birden fazla filtrenin EF Core 10 öncesi ve sonrasında nasıl yönetildiğini, required navigation ile birlikte ortaya çıkan beklenmedik sonuçları ve belgelerdeki sınırlamaları anlatıyoruz. Sorgu tarafının genel işleyişi için EF Core LINQ sorguları, silme işleminin kendisi için SaveChanges ile ekleme, güncelleme ve silme yazılarına bakabilirsiniz.
Global query filter ne işe yarar?

Bir filtreyi, ilgili entity tipi her sorgulandığında sorgunun sonuna sessizce eklenen ek bir Where operatörü gibi düşünebilirsiniz. Bu yaklaşımın asıl değeri unutulmaz olmasıdır: uygulamanızda yüzlerce sorgu olsa bile her birine aynı koşulu elle yazmanız gerekmez ve bir geliştiricinin bir yerde koşulu eklemeyi unutması riski ortadan kalkar. Filtre model yapılandırmasının bir parçasıdır, bu yüzden OnModelCreating içinde ya da bir IEntityTypeConfiguration<T> sınıfında tanımlanır. Model yapılandırmasının genel kuralları EF Core model yapılandırma yazısında anlatılıyor.
Filtreler yalnızca sorgu tarafını etkiler. Bir kaydı soft delete ile "silmek" için o kaydın bayrağını sizin ya da sizin yazdığınız bir mekanizmanın ayarlaması gerekir; filtre yalnızca bu bayrağı taşıyan satırların sorgulardan düşmesini sağlar. Kiracı filtrelerinde de durum aynıdır: yeni bir satır eklerken kiracı kimliğini doğru atamak uygulamanın sorumluluğudur.
Temel örnek: soft delete
Soft delete, satırı veritabanından gerçekten silmek yerine bir IsDeleted bayrağıyla silinmiş olarak işaretleme kalıbıdır. Belgeler bunun iki faydasını öne çıkarır: gerekirse silinen satırları geri getirebilirsiniz ve silinen satırlar erişilebilir kaldığı için bir denetim izi (audit trail) korunur. Önce entity tipine bayrak eklenir:
public class Blog
{
public int Id { get; set; }
public bool IsDeleted { get; set; }
public string Name { get; set; }
}Ardından filtre OnModelCreating içinde tanımlanır:
modelBuilder.Entity<Blog>().HasQueryFilter(b => !b.IsDeleted);Bu noktadan sonra context.Blogs üzerinden yazdığınız her sorgu, varsayılan olarak IsDeleted değeri true olan satırları dışarıda bırakır. Ancak belgelerin vurguladığı gibi bu aşamada bir entity'yi soft delete ile silmek için IsDeleted özelliğini elle ayarlamanız gerekir. context.Remove(blog) çağırırsanız satır gerçekten silinir.
Remove çağrısını soft delete'e çevirmek
Daha bütünlüklü bir çözüm için belgeler, bağlam tipinizde SaveChangesAsync metodunu ezmeyi (override) önerir. Bu metot kullanıcının silmek üzere işaretlediği tüm entity'leri dolaşır, durumlarını Modified olarak değiştirir ve IsDeleted değerini true yapar:
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
ChangeTracker.DetectChanges();
foreach (var item in ChangeTracker.Entries<Blog>().Where(e => e.State == EntityState.Deleted))
{
item.State = EntityState.Modified;
item.CurrentValues["IsDeleted"] = true;
}
return await base.SaveChangesAsync(cancellationToken);
}Böylece uygulamanın geri kalanı her zamanki gibi Remove kullanabilir ve kayıtlar fiziksel olarak silinmek yerine işaretlenir. DetectChanges çağrısının neden en başta yapıldığını ve entity durumlarının nasıl çalıştığını EF Core change tracker ve entity state yazısında bulabilirsiniz. Bu örnek yalnızca Blog tipine uygulanmıştır; birden fazla tipte soft delete kullanacaksanız aynı mantığı her tip için tekrarlamanız ya da ortak bir arayüz üzerinden genellemeniz gerekir.
Bağlam verisi kullanmak: çok kiracılı yapı

Global filtrelerin ikinci büyük kullanım alanı çok kiracılı (multi-tenant) uygulamalardır. Bu senaryoda farklı müşterilere ait veriler aynı tabloda tutulur ve her satırı bir kiracıya bağlayan bir kiracı kimliği kolonu bulunur. Global filtre, geçerli kiracının satırlarını otomatik olarak süzer; belgelerin deyişiyle her sorguda kiracıyı düşünme zorunluluğunu ortadan kaldırarak sorgularınız için varsayılan olarak güçlü bir kiracı yalıtımı sağlar.
Soft delete'ten farklı olarak burada geçerli kiracının kimliğini bilmeniz gerekir. Bu değer genellikle kullanıcı web üzerinden kimlik doğruladığında belirlenir. EF Core açısından kiracı kimliğinin bağlam örneği (context instance) üzerinde erişilebilir olması gerekir ki filtre ona başvurabilsin. Belgelerdeki en basit örnek kiracı kimliğini bağlamın yapıcısında almaktır:
public class MultitenancyContext(string tenantId) : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.TenantId == tenantId);
}
}Bu yapı, bir bağlam oluşturan herkesi ilgili kiracı kimliğini belirtmeye zorlar ve sorgulardan varsayılan olarak yalnızca o kimliğe sahip Blog kayıtlarının dönmesini sağlar. Filtre bağlam örneği üzerindeki değere başvurduğu için, sorgu çalışırken o bağlam örneğinin taşıdığı kiracı kimliği kullanılır.
Belgelerdeki kiracı servisi örneği
Çok kiracılık sayfası, geçerli kiracıyı tutan basit bir ITenantService servisi önerir. Servis bağlamın yapıcısına bağımlılık enjeksiyonuyla verilir, kiracı kimliği bir alana kaydedilir ve OnModelCreating içindeki filtre bu alana başvurur:
public ContactContext(
DbContextOptions<ContactContext> opts,
ITenantService service)
: base(opts) => _tenant = service.Tenant;
protected override void OnModelCreating(ModelBuilder modelBuilder)
=> modelBuilder.Entity<MultitenantContact>()
.HasQueryFilter(mt => mt.Tenant == _tenant);Belgeler bu örnekte bağlamı AddDbContextFactory ile ve ServiceLifetime.Scoped yaşam süresiyle kaydeder; scoped yaşam süresi, fabrikanın scoped olan kiracı servisine bağımlı olabilmesini sağlar. Bağımlılıkların her zaman singleton yönüne akması gerektiği, yani singleton bir servisin yalnızca başka singleton servislere bağımlı olabileceği kuralı burada belirleyicidir. AddDbContext, fabrika ve yaşam süreleri DbContext yapılandırma ve yaşam döngüsü yazısında ayrıntılı anlatılıyor.
Çok kiracılık yaklaşımları ve EF Core desteği
Belgeler çok kiracılığı uygulamanın üç yolunu karşılaştırır. Global query filter yalnızca ilk yaklaşımın aracıdır; veritabanı başına kiracı modelinde doğru veritabanına geçmek doğru bağlantı dizesini vermekten ibarettir.
| Yaklaşım | Kiracı kolonu | Kiracı başına şema | Birden çok veritabanı | EF Core desteği |
|---|---|---|---|---|
| Ayırt edici kolon | Evet | Hayır | Hayır | Global query filter |
| Kiracı başına veritabanı | Hayır | Hayır | Evet | Yapılandırma |
| Kiracı başına şema | Hayır | Evet | Hayır | Desteklenmiyor |
Kiracı başına veritabanı modelinde kullanıcı aynı oturumda kiracı değiştirebiliyorsa belgeler fabrika yaşam süresi için şu ayrımı yapar: tek veritabanında her iki durumda Scoped yeterlidir; birden çok veritabanında kullanıcı kiracı değiştirebiliyorsa seçeneklerin her istekte yeniden değerlendirilmesi için Transient gerekir. Blazor Server uygulamalarının bu konuda özel dikkat gerektirdiği de not edilir. Bağlantı dizelerini nerede ve nasıl saklayacağınız connection string yönetimi yazısının konusudur; ASP.NET Core tarafındaki kimlik doğrulama ise ayrı bir konudur.
Birden fazla filtre: EF Core 10 ile adlandırılmış filtreler
HasQueryFilter metodunu ad vermeden çağırdığınızda önceki filtrenin üzerine yazılır. Yani aynı entity tipinde iki ayrı çağrıyla iki filtre tanımlamaya çalışırsanız yalnızca sonuncusu geçerli olur:
modelBuilder.Entity<Blog>().HasQueryFilter(b => !b.IsDeleted);
// Asagidaki cagri bir onceki filtrenin uzerine yazar:
modelBuilder.Entity<Blog>().HasQueryFilter(b => b.TenantId == tenantId);Bu, fark edilmesi zor bir hatadır: soft delete filtresi sessizce devreden çıkar ve silinmiş kayıtlar sorgularda görünmeye başlar. EF Core 10 bu sorunu adlandırılmış filtrelerle çözer. Aynı entity tipinde birden fazla filtre tanımlamak için her birine bir ad verirsiniz:
modelBuilder.Entity<Blog>()
.HasQueryFilter("SoftDeletionFilter", b => !b.IsDeleted)
.HasQueryFilter("TenantFilter", b => b.TenantId == tenantId);Adlandırılmış filtreler her birini ayrı yönetmenize, örneğin bir sorguda yalnızca birini kapatmanıza olanak tanır. EF Core 10 öncesinde birden fazla koşulu tek bir HasQueryFilter çağrısında && ile birleştirmek gerekiyordu: HasQueryFilter(b => !b.IsDeleted && b.TenantId == tenantId). Bu yöntem hâlâ çalışır, ancak koşullardan yalnızca birini seçerek devre dışı bırakmanıza izin vermez. Filtre adlarını sabit (const) alanlarda tutmak, adları hem modelde hem sorgularda aynı yazmanızı kolaylaştırır.
Filtreleri devre dışı bırakmak
Bir yönetim ekranında silinmiş kayıtları listelemek ya da bir bakım işinde bütün kiracıların verisine erişmek gibi durumlarda filtreleri tek bir sorgu için kapatabilirsiniz. Bunun için IgnoreQueryFilters operatörü kullanılır:
var allBlogs = await context.Blogs.IgnoreQueryFilters().ToListAsync();Birden fazla adlandırılmış filtre varsa parametresiz IgnoreQueryFilters hepsini kapatır. EF Core 10 ile birlikte yalnızca belirli filtreleri kapatmak için ad listesini verebilirsiniz:
var allBlogs = await context.Blogs.IgnoreQueryFilters(["SoftDeletionFilter"]).ToListAsync();Bu ayrım çok kiracılı ve soft delete kullanan bir uygulamada kritiktir: "silinmişleri de göster" ekranında yalnızca soft delete filtresini kapatırsanız kiracı yalıtımı korunur. Parametresiz IgnoreQueryFilters kullanırsanız aynı ekranda başka kiracıların verisini de göstermiş olursunuz. Bu yüzden kiracı filtresinin kapatıldığı yerleri sınırlı ve kolay denetlenebilir tutmak iyi bir alışkanlıktır.
IEntityTypeConfiguration ile kiracı filtresi
Yapılandırmanızı IEntityTypeConfiguration<T> sınıflarına ayırdıysanız ek bir güçlük çıkar: OnModelCreating içinde olduğu gibi bağlam örneğine doğrudan erişemezsiniz, oysa kiracı filtresinin bağlamdaki kiracı kimliğine başvurması gerekir. Belgelerin önerdiği geçici çözüm, yapılandırma sınıfına sahte bir bağlam alanı ekleyip filtrede ona başvurmaktır:
private sealed class CustomerEntityConfiguration : IEntityTypeConfiguration<Customer>
{
private readonly SomeDbContext _context = null!;
public void Configure(EntityTypeBuilder<Customer> builder)
{
builder.HasQueryFilter(d => d.TenantId == _context.TenantId);
}
}Bu alan hiçbir zaman gerçek bir değere atanmaz; yalnızca filtre ifadesinde bağlam tipinin TenantId üyesine başvurulabilmesi için vardır.
Mini senaryo: soft delete ve kiracı filtresi birlikte
Birden fazla şirkete hizmet veren bir uygulamada Blog tablosunun hem IsDeleted hem TenantId kolonu taşıdığını düşünün. EF Core 10 ile modelde iki adlandırılmış filtre tanımlarsınız: "SoftDeletionFilter" ve "TenantFilter". Kiracı kimliği, bağlamın yapıcısında bir kiracı servisinden okunup bir alana yazılır. Sıradan ekranlardaki bütün sorgular iki filtreyi birlikte uygular: kullanıcı yalnızca kendi şirketinin silinmemiş bloglarını görür.
Silme işlemi Remove ve SaveChangesAsync ile yapılır; override edilmiş SaveChangesAsync bu çağrıyı IsDeleted değerini true yapan bir güncellemeye çevirir. Bir "çöp kutusu" ekranında silinmiş kayıtları göstermek için yalnızca IgnoreQueryFilters(["SoftDeletionFilter"]) kullanırsınız; kiracı filtresi yerinde kaldığı için bu ekran da yalnızca o şirketin kayıtlarını listeler. Bloglara bağlı yazıların da çöp kutusundaki bloglarla birlikte gizlenmesini istiyorsanız Post tipine de tutarlı bir filtre eklemeyi unutmayın; aksi halde required navigation bölümünde anlatılan beklenmedik sonuçlarla karşılaşabilirsiniz. Kiracılar arası bir bakım işi gerçekten gerekiyorsa parametresiz IgnoreQueryFilters çağrısını tek bir, iyi belgelenmiş yerde tutmak denetimi kolaylaştırır.
Sınırlamalar ve dikkat edilecekler

- Yalnızca kök tip: Filtreler bir kalıtım hiyerarşisinin yalnızca kök entity tipi için tanımlanabilir. Kalıtım eşleme stratejileri TPH, TPT ve TPC yazısında anlatılıyor.
- Döngü algılanmaz: EF Core şu an global filtre tanımlarındaki döngüleri algılamaz. Yanlış tanımlanan döngüler sorgu çevirisi sırasında sonsuz döngüye yol açabilir; birbirine navigation üzerinden başvuran filtreleri dikkatle tasarlayın.
- Adsız ikinci çağrı üzerine yazar: Ad vermeden ikinci kez
HasQueryFilterçağırmak öncekini siler. EF Core 10 kullanıyorsanız birden fazla filtre için ad verin. - Toplu işlemler: Soft delete dönüşümü
SaveChangesoverride'ına dayanıyorsa toplu silme işlemleri bu dönüşümden geçmez; veritabanında doğrudanDELETEçalışır. - Pooling ile durum: Bağlam havuzunda aynı bağlam örneği istekler arasında yeniden kullanılır ve
OnConfiguringyalnızca örnek ilk oluşturulduğunda çalışır; bu yüzden kiracı kimliği gibi istekten isteğe değişen bir durumu orada ayarlayamazsınız. Gereken ek adımlar DbContext pooling yazısının kapsamındadır.
Sık Sorulan Sorular
Silinmiş kayıtları belirli bir ekranda nasıl gösteririm?
O sorguda IgnoreQueryFilters() kullanın. Aynı entity üzerinde kiracı filtresi de varsa ve EF Core 10 kullanıyorsanız IgnoreQueryFilters(["SoftDeletionFilter"]) ile yalnızca soft delete filtresini kapatın ki kiracı yalıtımı korunsun.
Kiracı kimliği filtreye nasıl ulaşır?
Filtre model yapılandırmasının bir parçasıdır ve OnModelCreating içinde tanımlanır. Kiracı kimliğinin bağlam örneği üzerinde, örneğin yapıcıda doldurulan bir alanda bulunması gerekir; filtre bu alana başvurur ve sorgu çalışırken o bağlam örneğinin değeri kullanılır. Belgelerdeki örnekte değer, bağımlılık enjeksiyonuyla gelen bir kiracı servisinden okunur.
Include ile bazı kayıtlar neden kayboluyor?
Muhtemelen filtreli bir entity'ye required navigation üzerinden eriştiğiniz için. Required ilişki INNER JOIN ürettiğinden, filtrelenen ana kayda bağlı satırlar da düşer. Navigation'ı optional yapın ya da iki tipe tutarlı filtreler tanımlayın.
Kiracı başına şema kullanabilir miyim?
Belgelere göre bu senaryo EF Core tarafından doğrudan desteklenmez ve önerilen bir çözüm değildir. Migration kullanmıyorsanız ve tablolarınız zaten şemalara ayrılmışsa ToTable ile şemayı kiracıya göre değiştirmek mümkündür, ancak belgeler bunu bir çalışma pratiği olarak sunar, en iyi yöntem olarak değil.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: EF Core öğrenme rehberi · SaveChanges ile silme · Change tracker · DbContext yaşam döngüsü · EF Core 10 yenilikleri