Skip to content

Bölüm 10 / 14

Filter'lar

Filter türleri ve global hata yönetimi.

11 dakikalık okuma

7. dersin sonunda middleware ile filtre ayrımının ölçütünü söylemiş, ayrıntısını bu derse bırakmıştık: "tüm isteklerde X" middleware'dir, "bu denetleyicideki action'larda X" filtredir. Şimdi o cümlenin arkasındaki mekanizmayı kuruyoruz. Filtreler, uç nokta çalıştırılırken açılan ikinci bir boru hattıdır; middleware gibi sararlar ama fark ettikleri şey farklıdır — model bağlama sonucunu, action argümanlarını ve dönen sonuç nesnesini görürler.

01Ne öğreneceksin

  • Beş filtre türü ve çalışma sıraları
  • Middleware ile filtrenin somut farkı ve seçim ölçütü
  • Kapsam (global / denetleyici / action) ve Order etkileşimi
  • Filtreye bağımlılık enjekte etmek: ServiceFilter, TypeFilter
  • Kısa devre: context.Result atamanın sonucu
  • Exception filtresinin yakalayamadığı istisnalar
  • Global hata yönetiminin doğru yeri ve IExceptionHandler zinciri
  • Minimal API'de uç nokta filtreleri ve karşılaştırması

02Beş filtre türü ve sıra

Yönlendirme uç noktayı seçtikten sonra, action çalışmadan önce ve sonra bir dizi kanca devreye girer. Her biri farklı bir aşamada durur:

Uç nokta seçildi
   │
   ├─ 1. Yetkilendirme filtresi     (Authorization)  → erken reddeder
   ├─ 2. Kaynak filtresi            (Resource)       → model bağlamadan ÖNCE ve sonra
   │        ├─ model bağlama + doğrulama
   │        ├─ 3. Action filtresi   (Action)         → argümanları görür
   │        │       └─ ACTION GÖVDESİ
   │        ├─ 3. Action filtresi   (sonra)          → dönen sonucu görür
   │        └─ 4. İstisna filtresi  (Exception)      → yukarıdaki adımlarda hata olursa
   ├─ 5. Sonuç filtresi             (Result)         → sonuç yazılmadan önce/sonra
   └─ Yanıt yazıldı
TürNe görürTipik kullanım
IAuthorizationFilter Kimlik ve uç nokta üst verisi Özel yetki kuralı (genelde yerleşik politika yeter)
IResourceFilter İsteği, model bağlama öncesinde Yanıt önbelleği, kısa devre, bağlama öncesi kısıt
IActionFilter Bağlanmış argümanlar, ModelState Doğrulama, denetim kaydı, süre ölçümü, işlem sınırı
IExceptionFilter Action'dan çıkan istisna Denetleyiciye özgü hata çevirisi
IResultFilter Dönen sonuç nesnesi Yanıt zarfı, ek başlık, sonucu değiştirme

Kapsam ve Order: bir filtre üç kapsamda uygulanabilir — global (AddControllers(o => o.Filters.Add<T>())), denetleyici ([T] sınıf üstünde) ve action ([T] metot üstünde). Sıralama iki aşamalıdır: önce Order değeri (küçükten büyüğe), eşitse kapsam (global → denetleyici → action). "Öncesi" adımları bu sırayla, "sonrası" adımları ters sırayla çalışır — tıpkı middleware'de olduğu gibi, çünkü yapı yine dekoratördür.

[TypeFilter<DenetimFiltresi>(Order = -10)]     // negatif Order → daha erken, en dışta
public sealed class SiparislerController : ControllerBase

03Middleware mi, filtre mi

7. dersteki ölçütün gerekçesi budur:

MiddlewareFiltre
KapsamTüm istekler (statik dosya, 404 dahil)Yalnız MVC uç noktaları
Ne zamanYönlendirmeden önce de çalışabilirUç nokta seçildikten sonra
Gördüğü veriHam HttpContextArgümanlar, ModelState, sonuç nesnesi
SeçicilikUseWhen ile yola göreÖznitelikle uç nokta bazında
DIKurucuda yalnız singletonFabrika ile scoped alabilir

Somut kararlar: istek/yanıt loglaması tüm istekleri kapsamalıdır → middleware. Kimlik doğrulama boru hattı düzeyindedir → middleware. Belirli action'larda denetim kaydı → filtre. Model doğrulama sonucunu görmek gereken her iş → filtre, çünkü middleware model bağlamadan önce çalışır ve ModelState henüz yoktur. Dönen sonucu değiştirmek (zarf, ek alan) → sonuç filtresi; middleware'de aynı işi yapmak için yanıt akışını yakalamak gerekir ve HasStarted tuzağına girilir.

04Filtre yazmak

Her tür için senkron ve asenkron iki arayüz vardır (IActionFilter / IAsyncActionFilter). Aynı sınıfta ikisini birden uygulama — çerçeve yalnız asenkron olanı çağırır, senkron metotların gövdesi sessizce ölü kod olur. Asenkron biçim next alır ve yine dekoratör gibi görünür:

public sealed class IslemFiltresi(IUnitOfWork uow, ILogger<IslemFiltresi> log) : IAsyncActionFilter
{
    public async Task OnActionExecutionAsync(ActionExecutingContext ctx, ActionExecutionDelegate next)
    {
        // ÖNCE: argümanlar bağlanmış, ModelState dolu
        if (!ctx.ModelState.IsValid)
        {
            ctx.Result = new BadRequestObjectResult(new ValidationProblemDetails(ctx.ModelState));
            return;                                   // kısa devre: action hiç çalışmaz
        }

        var sonuc = await next();                     // action çalışır

        // SONRA: sonuç nesnesi ve varsa istisna elimizde
        if (sonuc.Exception is null)
            await uow.KaydetAsync(ctx.HttpContext.RequestAborted);
        else
            log.LogWarning("Action hata verdi, işlem geri alındı: {Action}",
                           ctx.ActionDescriptor.DisplayName);
    }
}

İki kritik ayrıntı bu örnekte: (1) Kısa devre — ctx.Result atanıp next çağrılmazsa action ve sonraki filtreler atlanır, doğrudan o sonuç yazılır. (2) İşlem sınırını filtreye koymak yaygın bir kalıptır ama tartışmalıdır: 4. hattaki Unit of Work dersinde "işlem sınırı iş akışının kararıdır" demiştik. Filtreye konursa her action'a görünmez bir işlem eklenir; okuma uçlarında gereksiz, çok adımlı akışlarda yanlış yerde olur. Küçük ve tek biçimli CRUD servislerinde pratik, karmaşık akışlarda uygulama servisine ait bir karardır.

Bağımlılık enjeksiyonu. Öznitelikler C# kısıtı gereği yalnız sabit argüman alır, bu yüzden filtreye servis vermek için üç yol vardır:

YolNasılNot
[ServiceFilter<T>] Filtre DI'ya kaydedilir, oradan çözülür Kayıt zorunlu; unutulursa çalışma anında hata
[TypeFilter<T>] Kurucu argümanları verilir, kalanı DI'dan Kayıt gerekmez; her istekte örnek üretilir
Global kayıt o.Filters.Add<T>() Tüm denetleyicilere uygulanır

Filtre örnekleri istek başına oluşturulur (global kaydedilen tip örnekleri hariç), bu yüzden middleware'deki esir bağımlılık sorunu burada yaşanmaz — filtrenin kurucusuna scoped servis alınabilir. Buna karşılık filtre nesnesinde durum tutmak yine yanlıştır: bir alan olarak tutulan sayaç ya da liste, eşzamanlı isteklerde beklenmedik davranır. İsteğe özgü veri HttpContext.Items'a konur.

Performans notu: filtre, uç noktaya iliştirilmiş bir sarmalayıcıdır ve her çağrıda çalışır. Global bir filtreye konan senkron dosya erişimi ya da ek veritabanı sorgusu, uygulamanın tüm uçlarına eklenen sabit bir maliyettir. Ölçmeden global filtre eklenmez.

05Hata yönetimi: filtre mi middleware mi

IExceptionFilter vardır ama global hata yönetiminin doğru yeri değildir. Sebebi kapsamıdır: istisna filtresi yalnız action ve action filtreleri aşamasındaki istisnaları yakalar. Yakalayamadıkları:

  • Middleware'lerde oluşan istisnalar (kimlik doğrulama, sıkıştırma, özel bileşenler)
  • Yönlendirme ve model bağlama aşamasındaki hatalar
  • Sonuç çalıştırılırken oluşan hatalar (seri hâle getirme sırasında patlayan bir nesne)
  • Filtre boru hattının dışında kalan her şey

Bu yüzden 7. derste kurduğumuz yapı geçerlidir: global hata yönetimi boru hattının en dışındadır. .NET 8 ile bunun düzenli biçimi IExceptionHandler zinciridir — her istisna ailesi için ayrı bir sınıf yazılır, sırayla denenir, ilk true döndüren işi bitirir (davranışsal desenler dersindeki Chain of Responsibility):

public sealed class SiparisHataIsleyici(IProblemDetailsService sorunlar) : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(HttpContext ctx, Exception hata, CancellationToken ct)
    {
        var (kod, baslik) = hata switch
        {
            SiparisBulunamadi   => (StatusCodes.Status404NotFound, "Kayıt bulunamadı"),
            GecersizDurumGecisi => (StatusCodes.Status409Conflict, "İşlem bu durumda yapılamaz"),
            _                   => (0, "")                        // bizim işimiz değil
        };
        if (kod == 0) return false;                               // sıradaki işleyiciye devret

        ctx.Response.StatusCode = kod;
        return await sorunlar.TryWriteAsync(new ProblemDetailsContext
        {
            HttpContext = ctx,
            ProblemDetails = { Status = kod, Title = baslik, Instance = ctx.Request.Path }
        });
    }
}

builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler<SiparisHataIsleyici>();      // sıra: kayıt sırası
builder.Services.AddExceptionHandler<GenelHataIsleyici>();        // en sona düşen
app.UseExceptionHandler();

Peki istisna filtresi ne işe yarar? Dar bir işe: belirli bir denetleyiciye ya da alana özgü istisna çevirisi. Örneğin yalnız ödeme uçlarında sağlayıcı istisnasını özel bir gövdeye çevirmek. Genel kural: uygulama geneli hata → middleware/IExceptionHandler, yerel çeviri → istisna filtresi. İkisi birlikte kullanılabilir; filtre ele almadığı istisnayı ExceptionHandled = false bırakarak dışarıya geçirir.

Sonuç filtresiyle zarf üretmek de sık görülen bir istektir ("tüm yanıtlar { veri, hata, basarili } olsun"). Uyarı: bu, HTTP durum kodunu ikinci plana atar ve önbellek, istemci kütüphaneleri ve OpenAPI araçlarıyla uyumu bozar. Modern yaklaşım, başarıda düz gövde ve doğru durum kodu, hatada ProblemDetails kullanmaktır. Zarf yine de isteniyorsa IAsyncResultFilter içinde ObjectResult'ın değeri sarılır — ve IAlwaysRunResultFilter, kısa devre edilmiş sonuçlarda da çalışmasını sağlar.

06Minimal API'de uç nokta filtreleri

Minimal API'nin karşılığı IEndpointFilter'dır. Tek bir kanca vardır — türler yoktur — ve zincir yine dekoratördür:

public sealed class SureFiltresi(ILogger<SureFiltresi> log) : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext ctx,
                                                EndpointFilterDelegate next)
    {
        var basla = Stopwatch.GetTimestamp();
        var sonuc = await next(ctx);                  // uç nokta delegesi
        log.LogInformation("{Yol} {SureMs} ms", ctx.HttpContext.Request.Path,
                           Stopwatch.GetElapsedTime(basla).TotalMilliseconds);
        return sonuc;                                 // sonucu değiştirebilirsin
    }
}

app.MapGroup("/api/siparisler")
   .AddEndpointFilter<SureFiltresi>()                 // gruptaki tüm uçlara
   .AddEndpointFilter<DogrulamaFiltresi<SiparisOlusturDto>>();
MVC filtresiUç nokta filtresi
Tür sayısıBeş ayrı türTek arayüz
SıraOrder + kapsamEkleme sırası
Argümanlara erişimActionArguments sözlüğüctx.Arguments listesi
Kısa devrectx.Result atamasınext çağırmadan sonuç döndürmek
UygulanırDenetleyici uçlarınaMinimal uçlara ve gruplara

Son bir yön: filtre, uygulamanın enine kesen endişelerini (cross-cutting concerns) toplamak içindir — loglama, denetim, doğrulama, önbellek, yeniden deneme kararı. Ama her tekrar eden kod filtreye taşınmaz. Ölçüt: iş kuralı filtreye konmaz. Filtreye konan iş kuralı görünmezdir; action'ı okuyan biri onu göremez, birim testinde çalışmaz ve aynı kural ikinci bir giriş noktasında (arka plan işi, kuyruk tüketicisi) tekrar yazılmak zorunda kalır — 12. derste bu bedeli somut olarak göreceksin.

07Mini alıştırma — Sırayı ve sınırı gör

Yaklaşık 20 dakika · dotnet run (web API)

  1. Sıra: bir kaynak, bir action ve bir sonuç filtresi yaz; her biri "önce" ve "sonra" satırı loglasın. Bir istek yap ve sekiz satırın sırasını yaz.
  2. Kapsam: aynı action filtresini global, denetleyici ve action düzeyinde kaydet. Hangi sırayla çalıştılar? Sonra birine Order = -1 ver ve değişimi gözle.
  3. Kısa devre: action filtresinde ctx.Result ata ve next'i çağırma. Action çalıştı mı? Sonuç filtresi çalıştı mı?
  4. DI: filtreye scoped bir servis enjekte et. Önce [ServiceFilter] ile DI kaydı yapmadan dene, hatayı oku; sonra [TypeFilter] ile dene.
  5. İstisna filtresinin sınırı: bir istisna filtresi yaz. Sonra hatayı (a) action içinde, (b) bir middleware içinde, (c) model bağlama sırasında (bozuk JSON) üret. Hangilerini yakaladı?
  6. Doğru yer: aynı üç hatayı IExceptionHandler + UseExceptionHandler ile ele al ve üçünün de tek biçimde ProblemDetails döndüğünü doğrula.
  7. Zincir: iki IExceptionHandler kaydet; ilki yalnız kendi istisna tipini işlesin, diğeri kalanları. false döndürmenin etkisini gör.
  8. Minimal taraf: aynı süre ölçen filtreyi IEndpointFilter olarak yaz ve bir MapGroup'a ekle. İki grup uç noktası için de çalıştığını doğrula.

08Kendini sına

Filtre türleri nelerdir, hangi sırayla çalışırlar?

Beş tür vardır: yetkilendirme filtresi en başta çalışır ve erken reddeder; kaynak filtresi model bağlamadan önce ve sonra devreye girer, önbellek ve kısa devre için uygundur; action filtresi bağlanmış argümanları ve ModelState'i görür, action'ın öncesinde ve sonrasında çalışır; istisna filtresi action aşamasındaki hataları yakalar; sonuç filtresi dönen sonuç yazılmadan önce ve sonra çalışır. Her tür için senkron ve asenkron arayüz vardır ve ikisi aynı sınıfta uygulanmaz — çerçeve yalnız asenkron olanı çağırır. Sıralama iki aşamalıdır: önce Order değeri küçükten büyüğe, eşitse kapsam sırası (global → denetleyici → action); "sonra" adımları ters sırada çalışır, çünkü yapı middleware gibi dekoratördür.

Middleware ile filtre arasındaki fark nedir, hangisini ne zaman seçersin?

Middleware tüm istekleri görür — statik dosya, eşleşmeyen yol ve 404 dahil — ve yönlendirmeden önce de çalışabilir; elinde ham HttpContext vardır. Filtre yalnız MVC uç noktalarına uygulanır, uç nokta seçildikten sonra çalışır ve bağlanmış argümanları, ModelState'i ve dönen sonuç nesnesini görür. Seçim ölçütü: "tüm isteklerde X" middleware, "bu denetleyicideki action'larda X" filtredir. İstek loglaması ve kimlik doğrulama middleware'dir; doğrulama sonucunu görmesi gereken ya da dönen sonucu değiştiren işler filtredir — middleware'de aynı işi yapmak yanıt akışını yakalamayı ve HasStarted tuzağını göze almayı gerektirir. DI açısından da fark vardır: uzlaşım tabanlı middleware fiilen singleton'dır, filtre örnekleri istek başına oluşur ve kurucusuna scoped servis alabilir.

Global hata yönetimini neden istisna filtresiyle yapmazsın?

Çünkü istisna filtresinin kapsamı dardır: yalnız action ve action filtreleri aşamasındaki istisnaları yakalar. Middleware'lerde oluşan hatalar, yönlendirme ve model bağlama hataları, sonuç çalıştırılırken (seri hâle getirme sırasında) oluşan hatalar ve filtre boru hattının dışındaki her şey ona ulaşmaz. Global hata yönetiminin yeri boru hattının en dışıdır: UseExceptionHandler ve .NET 8+ ile IExceptionHandler zinciri. Zincirde her istisna ailesi için ayrı bir sınıf yazılır, sırayla denenir ve ilk true döndüren işi bitirir — Chain of Responsibility deseni. İstisna filtresinin meşru kullanımı dardır: belirli bir denetleyiciye ya da alana özgü hata çevirisi; ele alamadığını dışarı geçirir. Her iki durumda da yanıt ProblemDetails biçiminde, istisna mesajı sızdırılmadan ve traceId ile döner.

Filtreye bağımlılık nasıl verilir, filtreye ne konmaz?

Öznitelikler yalnız sabit argüman alabildiği için üç yol vardır: [ServiceFilter<T>] filtreyi DI'dan çözer ama kaydı zorunludur, unutulursa çalışma anında hata verir; [TypeFilter<T>] kayıt gerektirmez, kurucu argümanlarının bir kısmını sen verir, kalanını DI doldurur; global kayıt (o.Filters.Add<T>()) tüm denetleyicilere uygular. Filtre örnekleri istek başına oluştuğu için kurucuya scoped servis alınabilir. Konmaması gerekenler: filtre nesnesinde durum tutmak (eşzamanlı isteklerde bozulur; isteğe özgü veri HttpContext.Items'a konur), global filtreye ölçülmemiş pahalı iş koymak (tüm uçlara sabit maliyet ekler) ve en önemlisi iş kuralı koymak — filtreye konan kural görünmez olur, action'ın birim testinde çalışmaz ve ikinci bir giriş noktasında tekrar yazılmak zorunda kalır.

09Sınav köşesi

Yazılıda nasıl sorulur

(1) Filtre türlerini ve çalışma sırasını yazınız. (2) Middleware ile filtre farkı — en sık çıkan karşılaştırma sorusu. (3) Kapsam ve Order birlikte verildiğinde çalışma sırasını bulunuz. (4) ServiceFilter ile TypeFilter farkı. (5) Kısa devre nasıl yapılır. (6) İstisna filtresi hangi hataları yakalayamaz. (7) Global hata yönetimi nerede kurulur; IExceptionHandler nedir. (8) Kod yazma: action süresini ölçen ya da denetim kaydı tutan bir filtre yazınız. (9) Senkron ve asenkron arayüzün ikisi birden uygulanırsa ne olur. (10) Minimal API'de filtre karşılığı nedir.

Sözlüde nasıl anlatılır — 2 dakikalık taslak

Düzen: (1) Model — "filtreler, uç nokta çalıştırılırken açılan ikinci bir boru hattıdır; yetkilendirme, kaynak, action, istisna ve sonuç olmak üzere beş türü vardır ve middleware gibi sarmalayarak çalışırlar." (2) Ayrım — "middleware tüm istekleri ve yönlendirme öncesini görür; filtre uç nokta seçildikten sonra çalışır ve argümanları, ModelState'i, dönen sonucu görür. 'Tüm isteklerde X' middleware, 'bu action'larda X' filtredir." (3) Sıra — "önce Order, sonra kapsam: global, denetleyici, action; sonrası adımları ters çalışır." (4) DI — "filtre istek başına oluşur, scoped servisi kurucudan alabilir; ServiceFilter kayıt ister, TypeFilter istemez." (5) Kıdem vuruşu — "global hata yönetimini istisna filtresiyle kurmam: filtre middleware'deki, model bağlamadaki ve sonuç yazımındaki hataları görmez. Onu boru hattının en dışına, IExceptionHandler zinciriyle koyar ve ProblemDetails döndürürüm. Filtreye iş kuralı koymam — görünmez olur ve arka plan işinden çağrıldığında çalışmaz."