Bölüm 7 / 14
Middleware
Sıralama, özel middleware yazmak.
10 dakikalık okuma
4. hattın yapısal desenler dersinde dekoratörleri iç içe geçirmiş, davranışsal desenler dersinde isteği işleyecek olanı arayan zinciri görmüştün — ve her ikisinde de "middleware boru hattı" sözü verilmişti. Bu ders o sözü ödüyor. Middleware, her isteğin geçtiği katmanlar zinciridir; ASP.NET Core'da kimlik doğrulama, hata yönetimi, sıkıştırma ve loglama hep bu mekanizmadır. Ve sıraları yanlışsa sessizce yanlış çalışırlar — bu dersin en önemli konusu odur.
01Ne öğreneceksin
- Boru hattının mekaniği:
RequestDelegate,nextve geri dönüş yolu - Doğru sıra ve yanlış sıranın somut sonuçları
- Kısa devre (short-circuit) ve terminal middleware
- Özel middleware'in üç yazım biçimi ve yaşam süresi farkları
- Middleware'de scoped servis kullanmanın doğru yolu
- Yanıt başladıktan sonra başlık yazamama (
HasStarted) tuzağı - Global hata yönetimi ve
ProblemDetails Map,MapWhen,UseWhenfarkı ve dallanma
02Boru hattının mekaniği
Middleware, tek bir imzaya indirgenebilir: bir HttpContext alır,
bir sonrakini çağırabilir.
app.Use(async (context, next) =>
{
// 1) İstek yolu: next'ten önce
var basla = Stopwatch.GetTimestamp();
await next(context); // sonraki middleware ve nihayet uç nokta
// 2) Yanıt yolu: next'ten sonra (ters sırada çalışır)
var sure = Stopwatch.GetElapsedTime(basla);
logger.LogInformation("{Yol} {Kod} {SureMs} ms",
context.Request.Path, context.Response.StatusCode, sure.TotalMilliseconds);
});
Bu, 4. hattaki dekoratör yapısının ta kendisidir: her katman bir sonrakini
sarar, öncesinde ve sonrasında iş yapabilir. İsteği kendi başına karşılayıp
next'i hiç çağırmama seçeneği ise Chain of Responsibility tarafıdır
— isteği zincirdeki bir halka karşılar ve diğerlerine hiç ulaşmaz.
İstek ──▶ Hata ──▶ HTTPS ──▶ Statik ──▶ Kimlik ──▶ Yetki ──▶ Uç nokta
Yanıt ◀── Hata ◀── HTTPS ◀── Statik ◀── Kimlik ◀── Yetki ◀────┘
Üç kayıt yöntemi vardır: Use zincire halka ekler ve devam
edebilir; Run terminal bir halka ekler,
next yoktur ve zincir orada biter; Map yol önekine göre
dallandırır.
03Sıra: en çok hata yapılan konu
Boru hattı kayıt sırasıyla çalışır ve yanlış sıra derleme hatası vermez — yalnız yanlış davranış üretir. Yerleşik sıra şudur:
app.UseExceptionHandler("/hata"); // en dışta: her şeyi sarmalı
app.UseHsts(); // (üretimde)
app.UseHttpsRedirection();
app.UseStaticFiles(); // statik dosya: kimlik doğrulamadan önce ucuz yanıt
app.UseRouting(); // uç nokta seçimi burada yapılır
app.UseCors();
app.UseAuthentication(); // sen kimsin
app.UseAuthorization(); // bunu yapabilir misin
app.UseResponseCompression();
app.MapControllers(); // uç noktalar
Yanlış sıranın somut sonuçları:
- Hata işleyici en dışta değilse, ondan önce çalışan middleware'lerde oluşan istisnalar yakalanamaz; istemci ham 500 alır.
UseAuthorization,UseAuthentication'dan önce gelirse yetkilendirme kimliği görmez: kullanıcı doğru jetonla gelse bile yetkisiz sayılır.- Yetkilendirme
UseRouting'den önce gelirse hangi uç noktanın seçileceği henüz bilinmediği için uç noktaya iliştirilmiş yetki politikası (ör.[Authorize(Roles = "yonetici")]) uygulanmaz. - CORS, kimlik doğrulamadan sonraya kalırsa tarayıcının ön kontrol (preflight) isteği kimlik doğrulamaya takılır ve tarayıcı asıl isteği hiç göndermez.
- Statik dosyalar kimlik doğrulamadan sonra sunulursa her CSS/JS isteği gereksiz yere kimlik işine girer; tersi durumda ise korunması gereken dosyalar korumasız sunulur — hangisinin istendiğine göre bilinçli karar verilir.
Kural olarak akılda tutulacak sıra: önce sarmalayanlar (hata, izleme), sonra protokol/ucuz yanıtlar (HTTPS, statik), sonra yönlendirme, sonra kimlik ve yetki, en sonda uç noktalar.
04Özel middleware yazmak
Üç biçim vardır ve aralarındaki fark yaşam süresidir — mülakatta buradan soru gelir.
1) Satır içi — küçük işler, tek kullanım:
app.Use(async (ctx, next) =>
{
ctx.Response.Headers["X-Istek-No"] = ctx.TraceIdentifier;
await next(ctx);
});
2) Uzlaşım tabanlı sınıf — en yaygın biçim. Sınıf DI'dan çözülmez; bir kez oluşturulur ve uygulama boyunca yaşar, yani fiilen singleton'dır:
public sealed class IstekSuresiMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<IstekSuresiMiddleware> _logger;
public IstekSuresiMiddleware(RequestDelegate next, ILogger<IstekSuresiMiddleware> logger)
=> (_next, _logger) = (next, logger); // ← burada yalnız singleton'lar istenebilir
public async Task InvokeAsync(HttpContext ctx, ISiparisDeposu depo) // ← scoped burada istenir
{
var basla = Stopwatch.GetTimestamp();
await _next(ctx);
_logger.LogInformation("{Yol} {SureMs} ms", ctx.Request.Path,
Stopwatch.GetElapsedTime(basla).TotalMilliseconds);
}
}
app.UseMiddleware<IstekSuresiMiddleware>();
Kritik ayrıntı yorum satırlarında: kurucuya scoped servis enjekte edilemez —
bu, 4. dersteki esir bağımlılık hatasıdır ve middleware'de en sık yapılan yanlıştır.
Scoped servisler InvokeAsync parametresi olarak istenir; çerçeve onları
isteğin kapsamından çözer.
3) Fabrika tabanlı (IMiddleware) — sınıf DI'dan çözülür, yani
scoped kaydedilebilir; karşılığında kayıt gerektirir:
public sealed class DenetimMiddleware(IDenetimGunlugu gunluk) : IMiddleware
{
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
{
await gunluk.Yaz(ctx.User.Identity?.Name, ctx.Request.Path);
await next(ctx);
}
}
builder.Services.AddScoped<DenetimMiddleware>(); // kayıt zorunlu
app.UseMiddleware<DenetimMiddleware>();
Yaygın uzlaşım, uzantı metoduyla okunaklı bir ad vermektir:
public static IApplicationBuilder UseIstekSuresi(this IApplicationBuilder app)
=> app.UseMiddleware<IstekSuresiMiddleware>();
05Global hata yönetimi
Middleware'in en değerli kullanımı, hata yönetimini tek yerde toplamaktır: her
denetleyicide try/catch yazmak yerine (DRY ihlali) boru hattının en dışına
bir halka konur.
app.UseExceptionHandler(dal => dal.Run(async ctx =>
{
var hata = ctx.Features.Get<IExceptionHandlerFeature>()?.Error;
var (kod, baslik) = hata switch
{
SiparisBulunamadi => (StatusCodes.Status404NotFound, "Kayıt bulunamadı"),
GecersizDurumGecisi => (StatusCodes.Status409Conflict, "İşlem bu durumda yapılamaz"),
_ => (StatusCodes.Status500InternalServerError, "Beklenmeyen hata")
};
ctx.Response.StatusCode = kod;
await ctx.Response.WriteAsJsonAsync(new ProblemDetails
{
Status = kod,
Title = baslik,
Instance = ctx.Request.Path,
Extensions = { ["traceId"] = ctx.TraceIdentifier } // log ile eşleşsin
});
}));
Üç kural: (1) İstisna mesajı istemciye yazılmaz — iç ayrıntı sızdırır
(yol adları, SQL, tip adları). Mesaj loga, kullanıcıya genel bir açıklama ve
traceId gider; destek kaydında o kimlikle log bulunur. (2) Hata
biçimi tutarlı olmalıdır; ProblemDetails (RFC 7807) bunun standardıdır ve
6. hattaki API hata yönetimi dersinde ayrıntısını göreceksin. (3) .NET 8+
alternatif olarak IExceptionHandler arayüzünü sunar: her istisna türü için
ayrı bir işleyici sınıfı yazılır ve AddExceptionHandler ile sıraya konur —
aynı işin daha düzenli hâli.
Yanıt başladıysa iş biter. Middleware yazarken en sinsi tuzak budur: gövdenin
ilk baytı yazıldıktan sonra durum kodu ve başlıklar değiştirilemez; denemek
InvalidOperationException üretir. Bu yüzden hata işleyicileri
ctx.Response.HasStarted kontrolü yapar ve başlamışsa yalnız loglayıp
bağlantıyı bırakır.
İkinci ileri konu gövdeyi okumaktır. İstek gövdesi ileri yönlü bir akıştır ve
bir kez okunur; denetleyiciden önce loglamak için okursan model bağlama boş gövde
görür. Çözüm ctx.Request.EnableBuffering() ile tamponlamayı açmak, okuyup
konumu Position = 0 ile başa almaktır. Bunun bedeli bellek/disk kullanımıdır
ve gövdede parola gibi hassas veri varsa loglanmaması gerekir (5. ders).
06Dallanma ve seçici uygulama
Bazı middleware'lerin yalnız belirli yollarda çalışması istenir. Üç araç vardır ve farkları sık karıştırılır:
| Yöntem | Ne yapar | Ana hatta döner mi |
|---|---|---|
Map("/yonetim", dal => ...) |
Yol önekine göre ayrı bir dal kurar; önek istek yolundan düşülür | Hayır — dal kendi içinde biter |
MapWhen(kosul, dal => ...) |
Herhangi bir koşula göre ayrı dal (başlık, sorgu, şema) | Hayır |
UseWhen(kosul, dal => ...) |
Koşul sağlanırsa ek middleware ekler | Evet — dal bitince ana hat devam eder |
app.UseWhen(ctx => ctx.Request.Path.StartsWithSegments("/api"),
dal => dal.UseMiddleware<ApiAnahtariMiddleware>()); // yalnız /api altında, sonra devam
Performans notu: her middleware her istekte çalışır. Boru hattına konan
küçük bir gecikme ya da ayırma, istek sayısıyla çarpılır. Bu yüzden pahalı işler
(veritabanı sorgusu, dış çağrı) boru hattına değil, gerçekten gerektiği uç noktaya
konur; gerekiyorsa UseWhen ile yalnız ilgili yollara uygulanır.
Son olarak middleware ile filtre ayrımı 10. dersin konusudur, ama ölçütü şimdiden söylenebilir: middleware tüm istekler için ve HTTP düzeyinde çalışır (yönlendirme öncesini de görür); filtre ise seçilmiş uç nokta bağlamında, model bağlama sonuçlarına erişerek çalışır. "Tüm isteklerde X" middleware'dir, "bu denetleyicideki action'larda X" filtredir.
07Mini alıştırma — Kendi zincirini kur
Yaklaşık 20 dakika · dotnet run (web API)
- Sırayı gör: üç
app.Useekle; her birinext'ten önce ve sonra farklı bir satır yazsın. Bir istek yap ve konsol çıktısının sırasını yaz — gidiş ve dönüş sırası nasıl? - Kısa devre: ikinci middleware'de
nextçağrısını kaldır ve doğrudan yanıt yaz. Üçüncüsü ve uç nokta çalışıyor mu? - Sınıf biçimi: istek süresini ölçen bir middleware'i uzlaşım tabanlı
sınıf olarak yaz, uzantı metodu ekle ve
UseIstekSuresi()ile kaydet. - Esir bağımlılık: bu sınıfın kurucusuna scoped bir servis enjekte
etmeyi dene. Hata ne diyor? Sonra aynı servisi
InvokeAsyncparametresi yapıp çalıştır. - Yanlış sıra deneyi:
UseAuthorization'ıUseAuthentication'ın önüne al ve[Authorize]işaretli bir uç noktayı geçerli jetonla çağır. Ne oluyor? - Hata işleyici: global hata middleware'ini
ProblemDetailsdöndürecek biçimde yaz; bir uç noktada özel bir istisna fırlat ve yanıtın gövdesini incele. İstisna mesajı gövdede görünüyor mu — görünmemeli. - HasStarted: önce
await ctx.Response.WriteAsync("bir şey")yazan, sonra durum kodunu değiştirmeye çalışan bir middleware yaz. Aldığın hatayı bir cümleyle açıkla. - Dallanma:
UseWhenile yalnız/apiyolunda çalışan bir başlık ekleyici yaz;/saglikucunda başlığın gelmediğini doğrula.
08Kendini sına
Middleware boru hattı nasıl çalışır, hangi desenlere karşılık gelir?
Her middleware bir HttpContext alır ve bir sonrakini
(next) çağırabilir; next'ten önceki kod istek yolunda,
sonraki kod yanıt yolunda çalışır — yani yanıt tarafı ters sırada işler. Bu yapı 4.
hattaki dekoratör desenidir: her katman bir sonrakini sarar ve öncesinde/sonrasında iş
yapar. next'i hiç çağırmayıp isteği kendisi karşılama seçeneği ise Chain
of Responsibility tarafıdır — istek zincirdeki bir halkada karşılanır ve diğerlerine
ulaşmaz, buna kısa devre denir. Kayıt yöntemleri: Use devam edebilen halka
ekler, Run terminal halkadır ve zinciri bitirir, Map yol
önekine göre dallandırır. Performans açısından her middleware her istekte çalışır, bu
yüzden pahalı işler boru hattına konmaz.
Middleware sırası neden önemli? Yanlış sıranın somut sonuçları neler?
Boru hattı kayıt sırasıyla çalışır ve yanlış sıra derleme hatası
vermez, yalnız yanlış davranış üretir. Örnekler: hata işleyici en dışta değilse ondan
önceki middleware'lerdeki istisnalar yakalanmaz ve istemci ham 500 alır;
UseAuthorization UseAuthentication'dan önce gelirse
yetkilendirme kimliği göremez, doğru jetonla gelen kullanıcı bile yetkisiz sayılır;
yetkilendirme UseRouting'den önce gelirse hangi uç noktanın seçileceği
bilinmediği için uç noktaya iliştirilmiş politika uygulanmaz; CORS kimlik
doğrulamadan sonraya kalırsa tarayıcının ön kontrol isteği kimliğe takılır ve asıl
istek hiç gönderilmez; statik dosyaların yeri ise bilinçli bir karardır — kimlikten
önce ucuz yanıt verilir ama korunması gereken dosyalar korumasız kalır. Akılda kalıcı
sıra: önce sarmalayanlar (hata, izleme), sonra protokol ve ucuz yanıtlar, sonra
yönlendirme, sonra kimlik ve yetki, en sonda uç noktalar.
Özel middleware'in üç yazım biçimi nedir; scoped servisi nasıl kullanırsın?
Satır içi (app.Use ile lambda) küçük ve tek
kullanımlık işler içindir. Uzlaşım tabanlı sınıf en yaygın biçimdir: kurucusunda
RequestDelegate alır ve bir InvokeAsync metodu vardır; bu
sınıf DI'dan çözülmez, bir kez oluşturulur ve uygulama boyunca yaşar — yani fiilen
singleton'dır, bu yüzden kurucusuna yalnız singleton bağımlılıklar alınabilir. Scoped
servisler InvokeAsync parametresi olarak istenir; çerçeve onları isteğin
kapsamından çözer. Kurucuya scoped servis enjekte etmek esir bağımlılıktır ve
middleware'de en sık yapılan hatadır. Üçüncü biçim fabrika tabanlıdır
(IMiddleware): sınıf DI'dan çözülür ve scoped kaydedilebilir, karşılığında
konteynere kaydedilmesi zorunludur. Okunabilirlik için genelde bir
UseXxx uzantı metodu yazılır.
Global hata yönetimini nasıl kurarsın, hangi tuzaklara dikkat edersin?
Her denetleyiciye try/catch yazmak yerine boru
hattının en dışına UseExceptionHandler konur; istisna türüne göre durum
kodu seçilir ve yanıt tutarlı bir biçimde, tercihen ProblemDetails
(RFC 7807) olarak yazılır. .NET 8+ ile IExceptionHandler arayüzü aynı işi
istisna türü başına ayrı sınıflarla yapmayı sağlar. Kurallar: istisna mesajı istemciye
yazılmaz — yol adları, SQL ve tip adları gibi iç ayrıntı sızdırır; mesaj loga gider,
kullanıcıya genel açıklama ve traceId verilir, böylece destek kaydı log
ile eşleşir. En sinsi tuzak Response.HasStarted'dır: gövdenin ilk baytı
yazıldıktan sonra durum kodu ve başlıklar değiştirilemez, denemek istisna üretir; hata
işleyicileri bunu kontrol eder ve başlamışsa yalnız loglar. İkinci ileri konu istek
gövdesini okumaktır: gövde ileri yönlü akıştır ve bir kez okunur, önceden okumak için
EnableBuffering açılıp konum başa alınır — bedeli bellek kullanımıdır ve
hassas alanlar loglanmamalıdır.
09Sınav köşesi
Yazılıda nasıl sorulur
(1) Tanım: middleware nedir, boru hattı nasıl çalışır. (2) En sık çıkan: verilen
Use... satırlarını doğru sıraya koyunuz; ya da yanlış sıralanmış bir
Program.cs verilip hatanın bulunması. (3) Ayrım:
Use / Run / Map farkı; Map,
MapWhen ve UseWhen farkı. (4) Kavram: kısa devre nedir,
nasıl yapılır. (5) Kod yazma: istek süresini ölçen ya da özel başlık ekleyen bir
middleware yazınız. (6) DI: middleware sınıfının kurucusuna scoped servis neden
enjekte edilemez; nasıl çözülür. (7) IMiddleware ile uzlaşım tabanlı
middleware farkı. (8) Hata: global hata yönetimi nasıl kurulur,
ProblemDetails nedir. (9) Ayrım: middleware ile filtre farkı.
Sözlüde nasıl anlatılır — 2 dakikalık taslak
Düzen: (1) Model — "middleware, her isteğin geçtiği katmanlar zinciridir; her
halka bir sonrakini sarar, next'ten önce istek yolunda, sonra yanıt
yolunda çalışır. Bu tam olarak dekoratör desenidir; next'i hiç
çağırmazsam kısa devre olur ki o da Chain of Responsibility'dir."
(2) Sıra — "sıra kayıt sırasıdır ve hata vermeden yanlış çalışır: hata işleyici en
dışta olmalı, kimlik doğrulama yetkilendirmeden önce gelmeli, ikisi de
UseRouting'den sonra olmalı ki uç noktanın politikası görülsün."
(3) Yazım — "özel middleware'i genelde uzlaşım tabanlı sınıf olarak yazar, bir
UseXxx uzantı metodu eklerim." (4) Tuzak — "o sınıf singleton gibi
yaşar; scoped servisi kurucuya değil InvokeAsync parametresine
koyarım, yoksa esir bağımlılık olur." (5) Kıdem vuruşu — "global hata yönetimini
boru hattının en dışında ProblemDetails ile kurarım; istisna mesajını
istemciye yazmam, traceId veririm ve HasStarted kontrolünü
unutmam — yanıt başladıysa başlık değiştirilemez. Her middleware her istekte
çalıştığı için pahalı işi boru hattına koymam, gerekirse
UseWhen ile yalnız ilgili yollara uygularım."
