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
Orderetkileşimi - Filtreye bağımlılık enjekte etmek:
ServiceFilter,TypeFilter - Kısa devre:
context.Resultatamanın sonucu - Exception filtresinin yakalayamadığı istisnalar
- Global hata yönetiminin doğru yeri ve
IExceptionHandlerzinciri - 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ür | Ne görür | Tipik 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:
| Middleware | Filtre | |
|---|---|---|
| Kapsam | Tüm istekler (statik dosya, 404 dahil) | Yalnız MVC uç noktaları |
| Ne zaman | Yönlendirmeden önce de çalışabilir | Uç nokta seçildikten sonra |
| Gördüğü veri | Ham HttpContext | Argümanlar, ModelState, sonuç nesnesi |
| Seçicilik | UseWhen ile yola göre | Öznitelikle uç nokta bazında |
| DI | Kurucuda yalnız singleton | Fabrika 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:
| Yol | Nasıl | Not |
|---|---|---|
[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 filtresi | Uç nokta filtresi | |
|---|---|---|
| Tür sayısı | Beş ayrı tür | Tek arayüz |
| Sıra | Order + kapsam | Ekleme sırası |
| Argümanlara erişim | ActionArguments sözlüğü | ctx.Arguments listesi |
| Kısa devre | ctx.Result ataması | next çağırmadan sonuç döndürmek |
| Uygulanır | Denetleyici uçlarına | Minimal 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)
- 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.
- Kapsam: aynı action filtresini global, denetleyici ve action düzeyinde
kaydet. Hangi sırayla çalıştılar? Sonra birine
Order = -1ver ve değişimi gözle. - Kısa devre: action filtresinde
ctx.Resultata venext'i çağırma. Action çalıştı mı? Sonuç filtresi çalıştı mı? - DI: filtreye scoped bir servis enjekte et. Önce
[ServiceFilter]ile DI kaydı yapmadan dene, hatayı oku; sonra[TypeFilter]ile dene. - İ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ı?
- Doğru yer: aynı üç hatayı
IExceptionHandler+UseExceptionHandlerile ele al ve üçünün de tek biçimdeProblemDetailsdöndüğünü doğrula. - Zincir: iki
IExceptionHandlerkaydet; ilki yalnız kendi istisna tipini işlesin, diğeri kalanları.falsedöndürmenin etkisini gör. - Minimal taraf: aynı süre ölçen filtreyi
IEndpointFilterolarak yaz ve birMapGroup'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."
