ASP.NET Core'un kalbi bir pipeline'dır. Gelen her HTTP isteği bu pipeline boyunca akar; her middleware katmanı isteği işler, bir sonrakine devredebilir veya pipeline'ı sonlandırabilir. Bu yapıyı anlamadan ASP.NET Core uygulamalarını gerçek anlamda yönetmek mümkün değil.

Pipeline Nasıl Çalışır?

Middleware'ler bir zincir oluşturur. Her halka iki kez çalışır: istek inerken ve yanıt çıkarken. Bu yapıya çift yönlü pipeline veya Matruşka modeli de denir.

app.Use(async (context, next) =>
{
    Console.WriteLine("[A] İstek geldi");
    await next(context);                 // Bir sonraki middleware'e geç
    Console.WriteLine("[A] Yanıt döndü");
});

app.Use(async (context, next) =>
{
    Console.WriteLine("[B] İstek geldi");
    await next(context);
    Console.WriteLine("[B] Yanıt döndü");
});

app.Run(async context =>
{
    Console.WriteLine("[C] Terminal — yanıt üretiliyor");
    await context.Response.WriteAsync("Merhaba!");
});

// Çıktı sırası:
// [A] İstek geldi
// [B] İstek geldi
// [C] Terminal — yanıt üretiliyor
// [B] Yanıt döndü
// [A] Yanıt döndü

Use, Run ve Map Farkları

app.Use — Zinciri Devam Ettiren

app.Use(async (context, next) =>
{
    // next() çağrılırsa zincir devam eder
    // next() çağrılmazsa zincir burada kısa devre yapar
    if (context.Request.Method == "OPTIONS")
    {
        context.Response.StatusCode = 200;
        return; // next() çağrılmıyor — short circuit
    }
    await next(context);
});

app.Run — Terminal (Zinciri Sonlandıran)

// Run her zaman son halka olmalı; next parametresi yoktur
app.Run(async context =>
{
    await context.Response.WriteAsync("Bu son yanıt");
    // Buradan sonra başka middleware çalışmaz
});

app.Map — URL'e Göre Dal Açma

// /health path'ine gelen istekler ayrı pipeline'a gider
app.Map("/health", branch =>
{
    branch.Run(async ctx =>
    {
        ctx.Response.ContentType = "application/json";
        await ctx.Response.WriteAsync("{\"status\":\"healthy\"}");
    });
});

// /api prefix'li istekler için
app.Map("/api", apiApp =>
{
    apiApp.Use(async (ctx, next) =>
    {
        ctx.Response.Headers["X-API-Version"] = "1.0";
        await next(ctx);
    });
    apiApp.MapControllers();
});

// app.MapWhen — koşula göre dal açma
app.MapWhen(
    ctx => ctx.Request.Headers.ContainsKey("X-Legacy-Client"),
    branch => branch.UseMiddleware<LegacyClientMiddleware>());

Sıra Kritik Önem Taşır

Middleware sırası yanlış olduğunda güvenlik açıkları veya beklenmedik davranışlar oluşur:

// Doğru sıra — gerekçeleriyle
var app = builder.Build();

// 1. Exception handler EN DIŞTA olmalı — tüm hataları yakalar
app.UseExceptionHandler("/error");

// 2. HSTS ve HTTPS yönlendirme erken gelmeli
app.UseHsts();
app.UseHttpsRedirection();

// 3. Static dosyalar — auth gerektirmez, erken dön
app.UseStaticFiles();

// 4. Routing hangi endpoint'in çağrılacağını belirler
app.UseRouting();

// 5. CORS, routing sonrası gelir (endpoint bilgisi gerekebilir)
app.UseCors();

// 6. Authentication kullanıcının kim olduğunu belirler
app.UseAuthentication();

// 7. Authorization authentication sonrası olmalı
app.UseAuthorization();

// 8. Session ve cache sonraya bırakılabilir
app.UseSession();
app.UseResponseCaching();

// 9. Endpoint mapping en sona
app.MapControllers();
app.MapRazorPages();

Sınıf Tabanlı Middleware: IMiddleware

Lambda middleware'ler basit senaryolar için iyidir. Karmaşık logic ve DI gerektiren durumlar için sınıf tabanlı middleware tercih edilmeli:

// Yöntem 1: Convention tabanlı (InvokeAsync metodu)
public class RequestTimingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestTimingMiddleware> _logger;

    public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context, IMetricsService metrics) // Scoped servis!
    {
        var sw = Stopwatch.StartNew();
        var path = context.Request.Path;
        var method = context.Request.Method;

        try
        {
            await _next(context);
        }
        finally
        {
            sw.Stop();
            var statusCode = context.Response.StatusCode;
            metrics.RecordRequest(method, path, statusCode, sw.ElapsedMilliseconds);

            if (sw.ElapsedMilliseconds > 1000)
                _logger.LogWarning("Yavaş istek: {Method} {Path} = {Ms}ms",
                    method, path, sw.ElapsedMilliseconds);
        }
    }
}

// Yöntem 2: IMiddleware arayüzü — DI container tarafından yönetilir
public class ApiKeyMiddleware : IMiddleware
{
    private readonly IApiKeyValidator _validator;

    public ApiKeyMiddleware(IApiKeyValidator validator) => _validator = validator;

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        if (!context.Request.Headers.TryGetValue("X-API-Key", out var key)
            || !await _validator.IsValidAsync(key!))
        {
            context.Response.StatusCode = 401;
            await context.Response.WriteAsJsonAsync(new { error = "Geçersiz API anahtarı" });
            return;
        }

        await next(context);
    }
}

// Program.cs
builder.Services.AddScoped<ApiKeyMiddleware>(); // IMiddleware: DI'dan alınır
app.UseMiddleware<RequestTimingMiddleware>();
app.UseMiddleware<ApiKeyMiddleware>();

Gerçek Dünya Örneği: Correlation ID Middleware

Her isteğe benzersiz bir ID atayan ve bunu log ile response header'larına ekleyen middleware:

public class CorrelationIdMiddleware(RequestDelegate next)
{
    private const string HeaderName = "X-Correlation-ID";

    public async Task InvokeAsync(HttpContext context, ILogger<CorrelationIdMiddleware> logger)
    {
        // Gelen header'da varsa kullan, yoksa yeni üret
        var correlationId = context.Request.Headers[HeaderName].FirstOrDefault()
                            ?? Guid.NewGuid().ToString("N");

        // Response header'a ekle
        context.Response.OnStarting(() =>
        {
            context.Response.Headers[HeaderName] = correlationId;
            return Task.CompletedTask;
        });

        // Log scope'a ekle — tüm loglar bu ID ile etiketlenir
        using (logger.BeginScope(new Dictionary<string, object>
               { ["CorrelationId"] = correlationId }))
        {
            context.Items["CorrelationId"] = correlationId;
            await next(context);
        }
    }
}

Middleware Test Etmek

// Middleware'i izole test etme
[Fact]
public async Task ApiKeyMiddleware_MissingKey_Returns401()
{
    var validator = Substitute.For<IApiKeyValidator>();
    var middleware = new ApiKeyMiddleware(validator);

    var context = new DefaultHttpContext();
    context.Response.Body = new MemoryStream();

    var nextCalled = false;
    await middleware.InvokeAsync(context, ctx =>
    {
        nextCalled = true;
        return Task.CompletedTask;
    });

    context.Response.StatusCode.Should().Be(401);
    nextCalled.Should().BeFalse();
}

Middleware sistemi ASP.NET Core'un en güçlü taraflarından biri. Authentication'dan logging'e, rate limiting'den response compression'a kadar her çapraz kesişen concern (cross-cutting concern) buraya ait. Önemli olan: her middleware tek bir şeyi yapmalı, sıraya dikkat edilmeli ve performans kritik path'lerde gereksiz iş yapılmamalı.