Skip to content

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, next ve 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, UseWhen farkı 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öntemNe yaparAna 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)

  1. Sırayı gör: üç app.Use ekle; her biri next'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?
  2. 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?
  3. 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.
  4. Esir bağımlılık: bu sınıfın kurucusuna scoped bir servis enjekte etmeyi dene. Hata ne diyor? Sonra aynı servisi InvokeAsync parametresi yapıp çalıştır.
  5. Yanlış sıra deneyi: UseAuthorization'ı UseAuthentication'ın önüne al ve [Authorize] işaretli bir uç noktayı geçerli jetonla çağır. Ne oluyor?
  6. Hata işleyici: global hata middleware'ini ProblemDetails dö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.
  7. 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.
  8. Dallanma: UseWhen ile yalnız /api yolunda çalışan bir başlık ekleyici yaz; /saglik ucunda 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."