← Back to all posts

.NET 8'de Middleware Yazımı — Pipeline'ı Özelleştirmek

.NET 8'de Middleware Yazımı — Pipeline'ı Özelleştirmek

.NET 8'de Middleware Yazımı — Pipeline'ı Özelleştirmek

Pipeline Nedir?

Bir HTTP isteği uygulamanıza ulaştığında doğrudan controller'a gitmez. Bir dizi katmandan geçer — her katman isteği inceleyebilir, değiştirebilir, yanıtı şekillendirebilir ya da zinciri tamamen durdurabilir. Bu katmanlar dizisine request pipeline, her bir katmana da middleware denir.

İstek Geldi
    ↓
┌─────────────────────┐
│   HTTPS Yönlendirme │ ← app.UseHttpsRedirection()
└──────────┬──────────┘
           ↓
┌─────────────────────┐
│   Authentication    │ ← app.UseAuthentication()
└──────────┬──────────┘
           ↓
┌─────────────────────┐
│   Authorization     │ ← app.UseAuthorization()
└──────────┬──────────┘
           ↓
┌─────────────────────┐
│   Özel Middleware   │ ← app.UseRequestLogging()
└──────────┬──────────┘
           ↓
┌─────────────────────┐
│   Routing           │ ← app.MapControllers()
└──────────┬──────────┘
           ↓
       Controller
           ↓
       (Yanıt pipeline'dan geriye doğru akar)

Her middleware bir sonrakini çağırmak zorunda değildir. Zinciri kırarsa yanıt o noktada şekillenir ve geriye doğru akar. Bu hem güç hem de sorumluluk demektir.


Middleware Yazma Yöntemleri

Yöntem 1 — Inline Middleware (Hızlı Prototip)

// Program.cs
app.Use(async (context, next) =>
{
    // İstek geldiğinde — öncesi
    Console.WriteLine($"→ {context.Request.Method} {context.Request.Path}");

    await next(context); // Bir sonraki middleware'e geç

    // Yanıt döndüğünde — sonrası
    Console.WriteLine($"← {context.Response.StatusCode}");
});

Basit ve hızlıdır ama test edilemez, yeniden kullanılamaz. Prototip dışında kullanmayın.

Yöntem 2 — Conventional Middleware (Standart)

// RequestLoggingMiddleware.cs
public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestLoggingMiddleware> _logger;

    // Singleton scope — constructor'da sadece singleton servisleri enjekte edin
    public RequestLoggingMiddleware(
        RequestDelegate next,
        ILogger<RequestLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    // Her istek için çağrılır — scoped ve transient servisler buraya enjekte edilir
    public async Task InvokeAsync(HttpContext context, ICurrentUserService currentUser)
    {
        var requestId = Guid.NewGuid().ToString("N")[..8];

        using (_logger.BeginScope(new Dictionary<string, object>
        {
            ["RequestId"] = requestId,
            ["UserId"] = currentUser.UserId?.ToString() ?? "anonymous"
        }))
        {
            _logger.LogInformation(
                "→ {Method} {Path}",
                context.Request.Method,
                context.Request.Path);

            var stopwatch = Stopwatch.StartNew();

            await _next(context);

            stopwatch.Stop();

            _logger.LogInformation(
                "← {StatusCode} | {ElapsedMs}ms",
                context.Response.StatusCode,
                stopwatch.ElapsedMilliseconds);
        }
    }
}

// Extension method — temiz kayıt için
public static class RequestLoggingMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestLogging(
        this IApplicationBuilder app)
        => app.UseMiddleware<RequestLoggingMiddleware>();
}

// Program.cs
app.UseRequestLogging();

Yöntem 3 — IMiddleware Interface (Strongly Typed)

// Scoped servis olarak DI container'a kaydedilebilir
public class TenantResolutionMiddleware : IMiddleware
{
    private readonly ITenantRepository _tenantRepo;
    private readonly ILogger<TenantResolutionMiddleware> _logger;

    // IMiddleware ile scoped servisler constructor'a enjekte edilebilir
    public TenantResolutionMiddleware(
        ITenantRepository tenantRepo,
        ILogger<TenantResolutionMiddleware> logger)
    {
        _tenantRepo = tenantRepo;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();

        if (!string.IsNullOrEmpty(tenantId))
        {
            var tenant = await _tenantRepo.GetByIdAsync(tenantId);
            if (tenant is not null)
            {
                context.Items["Tenant"] = tenant;
                _logger.LogDebug("Tenant tespit edildi: {TenantId}", tenantId);
            }
        }

        await next(context);
    }
}

// Program.cs — IMiddleware scoped olarak kaydedilmeli
builder.Services.AddScoped<TenantResolutionMiddleware>();
app.UseMiddleware<TenantResolutionMiddleware>();

Gerçek Senaryolar

1. Global Exception Handling Middleware

Production'da işlenmeyen her exception'ı yakalayın, loglayin ve kullanıcıya anlamlı yanıt döndürün.

// GlobalExceptionHandlingMiddleware.cs
public class GlobalExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<GlobalExceptionHandlingMiddleware> _logger;

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

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        var (statusCode, title, detail) = exception switch
        {
            NotFoundException notFound =>
                (StatusCodes.Status404NotFound,
                 "Kaynak Bulunamadı",
                 notFound.Message),

            ValidationException validation =>
                (StatusCodes.Status400BadRequest,
                 "Doğrulama Hatası",
                 string.Join(", ", validation.Errors.Select(e => e.ErrorMessage))),

            UnauthorizedAccessException =>
                (StatusCodes.Status401Unauthorized,
                 "Yetkisiz Erişim",
                 "Bu işlem için yetkiniz bulunmuyor."),

            ForbiddenException =>
                (StatusCodes.Status403Forbidden,
                 "Erişim Reddedildi",
                 "Bu kaynağa erişim izniniz yok."),

            ConflictException conflict =>
                (StatusCodes.Status409Conflict,
                 "Çakışma",
                 conflict.Message),

            _ =>
                (StatusCodes.Status500InternalServerError,
                 "Sunucu Hatası",
                 "Beklenmedik bir hata oluştu.")
        };

        // 500'leri logla — diğerleri beklenen durum
        if (statusCode == StatusCodes.Status500InternalServerError)
        {
            _logger.LogError(exception,
                "İşlenmeyen hata: {Method} {Path}",
                context.Request.Method,
                context.Request.Path);
        }
        else
        {
            _logger.LogWarning(
                "İş hatası [{StatusCode}]: {Message}",
                statusCode,
                exception.Message);
        }

        // RFC 7807 — Problem Details formatı
        var problemDetails = new ProblemDetails
        {
            Status = statusCode,
            Title = title,
            Detail = detail,
            Instance = context.Request.Path,
            Extensions =
            {
                ["traceId"] = context.TraceIdentifier,
                ["timestamp"] = DateTime.UtcNow
            }
        };

        context.Response.StatusCode = statusCode;
        context.Response.ContentType = "application/problem+json";

        await context.Response.WriteAsJsonAsync(problemDetails);
    }
}

public static class GlobalExceptionHandlingExtensions
{
    public static IApplicationBuilder UseGlobalExceptionHandling(
        this IApplicationBuilder app)
        => app.UseMiddleware<GlobalExceptionHandlingMiddleware>();
}

Not: .NET 8'de IExceptionHandler interface'i de kullanılabilir:

// .NET 8 — IExceptionHandler
public class AppExceptionHandler : IExceptionHandler
{
    private readonly ILogger<AppExceptionHandler> _logger;

    public AppExceptionHandler(ILogger<AppExceptionHandler> logger)
    {
        _logger = logger;
    }

    public async ValueTask<bool> TryHandleAsync(
        HttpContext context,
        Exception exception,
        CancellationToken ct)
    {
        if (exception is NotFoundException notFound)
        {
            context.Response.StatusCode = StatusCodes.Status404NotFound;
            await context.Response.WriteAsJsonAsync(new ProblemDetails
            {
                Status = 404,
                Title = "Kaynak Bulunamadı",
                Detail = notFound.Message
            }, ct);

            return true; // Hata işlendi
        }

        return false; // Bu handler ilgilenmedi, sonrakine geç
    }
}

// Program.cs
builder.Services.AddExceptionHandler<AppExceptionHandler>();
builder.Services.AddProblemDetails();
app.UseExceptionHandler();

2. Request/Response Loglama Middleware

Tam istek ve yanıt içeriğini loglayan middleware — debugging ve audit için:

public class RequestResponseLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestResponseLoggingMiddleware> _logger;

    // Loglanmayacak path'ler
    private static readonly HashSet<string> ExcludedPaths =
    [
        "/health",
        "/metrics",
        "/favicon.ico"
    ];

    // Loglanmayacak content type'lar
    private static readonly HashSet<string> ExcludedContentTypes =
    [
        "multipart/form-data",
        "application/octet-stream"
    ];

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

    public async Task InvokeAsync(HttpContext context)
    {
        // Hariç tutulan path'leri atla
        if (ExcludedPaths.Contains(context.Request.Path.Value ?? string.Empty))
        {
            await _next(context);
            return;
        }

        // İstek gövdesini logla
        var requestBody = await ReadRequestBodyAsync(context.Request);

        // Yanıt gövdesini okuyabilmek için stream'i değiştir
        var originalBodyStream = context.Response.Body;
        using var responseBodyStream = new MemoryStream();
        context.Response.Body = responseBodyStream;

        var stopwatch = Stopwatch.StartNew();

        await _next(context);

        stopwatch.Stop();

        // Yanıt gövdesini oku
        var responseBody = await ReadResponseBodyAsync(context.Response);

        // Orijinal stream'e geri yaz
        await responseBodyStream.CopyToAsync(originalBodyStream);
        context.Response.Body = originalBodyStream;

        _logger.LogInformation(
            "HTTP {Method} {Path} | {StatusCode} | {ElapsedMs}ms\nRequest: {RequestBody}\nResponse: {ResponseBody}",
            context.Request.Method,
            context.Request.Path,
            context.Response.StatusCode,
            stopwatch.ElapsedMilliseconds,
            requestBody,
            responseBody);
    }

    private static async Task<string> ReadRequestBodyAsync(HttpRequest request)
    {
        var contentType = request.ContentType ?? string.Empty;

        if (ExcludedContentTypes.Any(ct => contentType.Contains(ct)))
            return "[binary content]";

        request.EnableBuffering();

        using var reader = new StreamReader(
            request.Body,
            Encoding.UTF8,
            leaveOpen: true);

        var body = await reader.ReadToEndAsync();
        request.Body.Position = 0;

        return string.IsNullOrEmpty(body) ? "[empty]" : body;
    }

    private static async Task<string> ReadResponseBodyAsync(HttpResponse response)
    {
        response.Body.Seek(0, SeekOrigin.Begin);
        var body = await new StreamReader(response.Body).ReadToEndAsync();
        response.Body.Seek(0, SeekOrigin.Begin);

        return string.IsNullOrEmpty(body) ? "[empty]" : body;
    }
}

public static class RequestResponseLoggingExtensions
{
    // Sadece development ortamında kullanın — production'da performans etkisi var
    public static IApplicationBuilder UseRequestResponseLogging(
        this IApplicationBuilder app)
        => app.UseMiddleware<RequestResponseLoggingMiddleware>();
}

3. API Key Authentication Middleware

Header'dan API key okuyup doğrulayan middleware:

public class ApiKeyAuthenticationMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ApiKeyAuthenticationMiddleware> _logger;
    private const string ApiKeyHeaderName = "X-Api-Key";

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

    public async Task InvokeAsync(HttpContext context, IApiKeyValidator validator)
    {
        // Public endpoint'leri atla
        var endpoint = context.GetEndpoint();
        if (endpoint?.Metadata.GetMetadata<AllowAnonymousAttribute>() is not null)
        {
            await _next(context);
            return;
        }

        if (!context.Request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKey))
        {
            _logger.LogWarning(
                "API key eksik. Path: {Path} | IP: {IP}",
                context.Request.Path,
                context.Connection.RemoteIpAddress);

            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            await context.Response.WriteAsJsonAsync(new ProblemDetails
            {
                Status = 401,
                Title = "Yetkisiz Erişim",
                Detail = $"'{ApiKeyHeaderName}' header'ı zorunludur."
            });
            return;
        }

        var validationResult = await validator.ValidateAsync(apiKey!);

        if (!validationResult.IsValid)
        {
            _logger.LogWarning(
                "Geçersiz API key. Path: {Path} | IP: {IP}",
                context.Request.Path,
                context.Connection.RemoteIpAddress);

            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            await context.Response.WriteAsJsonAsync(new ProblemDetails
            {
                Status = 401,
                Title = "Geçersiz API Key",
                Detail = "Sağlanan API key geçerli değil veya süresi dolmuş."
            });
            return;
        }

        // API key bilgilerini context'e ekle
        context.Items["ApiKeyOwner"] = validationResult.Owner;
        context.Items["ApiKeyScopes"] = validationResult.Scopes;

        await _next(context);
    }
}

4. Rate Limiting Middleware

.NET 8'de yerleşik rate limiting desteği geldi:

// Program.cs
builder.Services.AddRateLimiter(options =>
{
    // Global politika
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
    {
        var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value
            ?? context.Connection.RemoteIpAddress?.ToString()
            ?? "anonymous";

        return RateLimitPartition.GetFixedWindowLimiter(userId, _ => new FixedWindowRateLimiterOptions
        {
            PermitLimit = 100,           // 100 istek
            Window = TimeSpan.FromMinutes(1), // 1 dakikada
            QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
            QueueLimit = 0
        });
    });

    // Named politika — belirli endpoint'ler için
    options.AddFixedWindowLimiter("strict", policy =>
    {
        policy.PermitLimit = 5;
        policy.Window = TimeSpan.FromMinutes(1);
    });

    options.AddSlidingWindowLimiter("api", policy =>
    {
        policy.PermitLimit = 30;
        policy.Window = TimeSpan.FromMinutes(1);
        policy.SegmentsPerWindow = 6; // 10 saniyelik dilimler
    });

    // Rate limit aşıldığında
    options.OnRejected = async (context, ct) =>
    {
        context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;

        if (context.Lease.TryGetMetadata(
            MetadataName.RetryAfter, out var retryAfter))
        {
            context.HttpContext.Response.Headers.RetryAfter =
                retryAfter.TotalSeconds.ToString();
        }

        await context.HttpContext.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Status = 429,
            Title = "Çok Fazla İstek",
            Detail = "İstek limitinizi aştınız. Lütfen bekleyip tekrar deneyin."
        }, ct);
    };
});

app.UseRateLimiter();
// Controller'da named politika kullanımı
[ApiController]
[Route("api/auth")]
public class AuthController : ControllerBase
{
    [HttpPost("login")]
    [EnableRateLimiting("strict")]   // 5 deneme / dakika
    public async Task<IActionResult> Login(LoginRequest request) { ... }

    [HttpGet("profile")]
    [EnableRateLimiting("api")]      // 30 istek / dakika
    public async Task<IActionResult> Profile() { ... }

    [HttpGet("health")]
    [DisableRateLimiting]            // Rate limit yok
    public IActionResult Health() => Ok();
}

5. Correlation ID Middleware

Dağıtık sistemlerde istekleri izlemek için her isteğe benzersiz ID atayın:

public class CorrelationIdMiddleware
{
    private readonly RequestDelegate _next;
    private const string CorrelationIdHeader = "X-Correlation-Id";

    public CorrelationIdMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context, ICorrelationIdProvider provider)
    {
        // Gelen header'dan al ya da yeni üret
        var correlationId = context.Request.Headers[CorrelationIdHeader].FirstOrDefault()
            ?? Guid.NewGuid().ToString("N");

        // Context'e kaydet
        provider.Set(correlationId);
        context.Items["CorrelationId"] = correlationId;

        // Yanıt header'ına ekle — client takip edebilsin
        context.Response.OnStarting(() =>
        {
            context.Response.Headers[CorrelationIdHeader] = correlationId;
            return Task.CompletedTask;
        });

        // Serilog ile her log'a otomatik ekle
        using (LogContext.PushProperty("CorrelationId", correlationId))
        {
            await _next(context);
        }
    }
}

// Servis kayıtları
public interface ICorrelationIdProvider
{
    string? Get();
    void Set(string correlationId);
}

public class CorrelationIdProvider : ICorrelationIdProvider
{
    private string? _correlationId;

    public string? Get() => _correlationId;
    public void Set(string correlationId) => _correlationId = correlationId;
}

// Program.cs
builder.Services.AddScoped<ICorrelationIdProvider, CorrelationIdProvider>();
app.UseMiddleware<CorrelationIdMiddleware>();

6. Response Caching Middleware

Yanıtları middleware seviyesinde cache'leyin:

public class ResponseCachingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ICacheService _cache;
    private readonly ILogger<ResponseCachingMiddleware> _logger;

    public ResponseCachingMiddleware(
        RequestDelegate next,
        ICacheService cache,
        ILogger<ResponseCachingMiddleware> logger)
    {
        _next = next;
        _cache = cache;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Sadece GET isteklerini cache'le
        if (context.Request.Method != HttpMethods.Get)
        {
            await _next(context);
            return;
        }

        // Cache attribute kontrolü
        var endpoint = context.GetEndpoint();
        var cacheAttr = endpoint?.Metadata.GetMetadata<CacheResponseAttribute>();

        if (cacheAttr is null)
        {
            await _next(context);
            return;
        }

        var cacheKey = BuildCacheKey(context.Request);
        var cached = await _cache.GetAsync<CachedResponse>(cacheKey);

        if (cached is not null)
        {
            _logger.LogDebug("Cache hit: {CacheKey}", cacheKey);

            context.Response.StatusCode = cached.StatusCode;
            context.Response.ContentType = cached.ContentType;
            context.Response.Headers["X-Cache"] = "HIT";

            await context.Response.WriteAsync(cached.Body);
            return;
        }

        // Cache miss — normal akış
        var originalBody = context.Response.Body;
        using var memoryStream = new MemoryStream();
        context.Response.Body = memoryStream;

        await _next(context);

        // Yanıtı cache'e yaz
        memoryStream.Seek(0, SeekOrigin.Begin);
        var responseBody = await new StreamReader(memoryStream).ReadToEndAsync();

        if (context.Response.StatusCode == StatusCodes.Status200OK)
        {
            await _cache.SetAsync(cacheKey, new CachedResponse
            {
                StatusCode = context.Response.StatusCode,
                ContentType = context.Response.ContentType ?? "application/json",
                Body = responseBody
            }, TimeSpan.FromSeconds(cacheAttr.DurationSeconds));

            context.Response.Headers["X-Cache"] = "MISS";
        }

        memoryStream.Seek(0, SeekOrigin.Begin);
        await memoryStream.CopyToAsync(originalBody);
        context.Response.Body = originalBody;
    }

    private static string BuildCacheKey(HttpRequest request)
    {
        var keyBuilder = new StringBuilder();
        keyBuilder.Append(request.Path);

        if (request.QueryString.HasValue)
            keyBuilder.Append(request.QueryString.Value);

        return $"response:{keyBuilder}";
    }
}

// Custom attribute
[AttributeUsage(AttributeTargets.Method)]
public class CacheResponseAttribute : Attribute
{
    public int DurationSeconds { get; }

    public CacheResponseAttribute(int durationSeconds = 60)
    {
        DurationSeconds = durationSeconds;
    }
}

// Controller'da kullanım
[HttpGet("categories")]
[CacheResponse(durationSeconds: 300)]  // 5 dakika cache
public async Task<IActionResult> GetCategories() { ... }

Middleware Sırası — Kritik Detay

Middleware sırası son derece önemlidir. Yanlış sıra güvenlik açığına veya beklenmedik davranışa yol açar:

// Program.cs — Doğru sıra
var app = builder.Build();

// 1. Exception handling — en dışta olmalı, her şeyi yakalamalı
app.UseGlobalExceptionHandling();

// 2. HTTPS yönlendirmesi
app.UseHttpsRedirection();

// 3. Correlation ID — en erken atanmalı, loglar için
app.UseMiddleware<CorrelationIdMiddleware>();

// 4. Request loglama — correlation ID atandıktan sonra
app.UseRequestLogging();

// 5. Static files — auth gerektirmez
app.UseStaticFiles();

// 6. Routing
app.UseRouting();

// 7. Rate limiting — auth öncesi
app.UseRateLimiter();

// 8. Authentication — kim olduğunu belirle
app.UseAuthentication();

// 9. Authorization — ne yapabileceğini belirle
app.UseAuthorization();

// 10. Tenant resolution — auth sonrası, user bilgisi gerekebilir
app.UseMiddleware<TenantResolutionMiddleware>();

// 11. Response caching — auth sonrası, kullanıcıya özgü cache
app.UseMiddleware<ResponseCachingMiddleware>();

// 12. Endpoint mapping
app.MapControllers();
app.MapHealthChecks("/health");

app.Run();

Short-Circuit — Zinciri Kırmak

Bazen sonraki middleware'lere geçmek istemezsiniz. Yanıtı doğrudan döndürürsünüz:

public class MaintenanceModeMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IConfiguration _config;

    public MaintenanceModeMiddleware(RequestDelegate next, IConfiguration config)
    {
        _next = next;
        _config = config;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var maintenanceMode = _config.GetValue<bool>("App:MaintenanceMode");

        if (maintenanceMode)
        {
            // Health check'e her zaman izin ver
            if (context.Request.Path.StartsWithSegments("/health"))
            {
                await _next(context);
                return;
            }

            // Zinciri kır — sonraki middleware'ler çalışmaz
            context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
            context.Response.Headers["Retry-After"] = "3600";

            await context.Response.WriteAsJsonAsync(new ProblemDetails
            {
                Status = 503,
                Title = "Bakım Modu",
                Detail = "Sistem şu anda bakımdadır. Lütfen daha sonra tekrar deneyin."
            });

            return; // _next(context) çağrılmıyor — zincir durdu
        }

        await _next(context);
    }
}

Branch — Koşullu Pipeline

Farklı path'ler için farklı pipeline dalları oluşturabilirsiniz:

// /api path'i için ayrı middleware zinciri
app.Map("/api", apiApp =>
{
    apiApp.UseMiddleware<ApiKeyAuthenticationMiddleware>();
    apiApp.UseMiddleware<RequestResponseLoggingMiddleware>();
    apiApp.UseRateLimiter();
});

// /webhook path'i için ayrı zincir
app.Map("/webhook", webhookApp =>
{
    webhookApp.UseMiddleware<WebhookSignatureValidationMiddleware>();
});

// Koşullu — sadece development'ta
app.MapWhen(
    context => context.Request.Headers.ContainsKey("X-Debug"),
    debugApp =>
    {
        debugApp.UseMiddleware<DebugInfoMiddleware>();
    });

Middleware Test Etmek

// RequestLoggingMiddlewareTests.cs
public class RequestLoggingMiddlewareTests
{
    [Fact]
    public async Task Middleware_ShouldLog_RequestAndResponse()
    {
        // Arrange
        var logger = new Mock<ILogger<RequestLoggingMiddleware>>();
        var nextCalled = false;

        RequestDelegate next = ctx =>
        {
            nextCalled = true;
            ctx.Response.StatusCode = 200;
            return Task.CompletedTask;
        };

        var middleware = new RequestLoggingMiddleware(next, logger.Object);

        var context = new DefaultHttpContext();
        context.Request.Method = "GET";
        context.Request.Path = "/api/products";

        // Act
        await middleware.InvokeAsync(context);

        // Assert
        nextCalled.Should().BeTrue();

        logger.Verify(
            x => x.Log(
                LogLevel.Information,
                It.IsAny<EventId>(),
                It.Is<It.IsAnyType>((v, _) => v.ToString()!.Contains("GET")),
                null,
                It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
            Times.AtLeastOnce);
    }

    [Fact]
    public async Task ExceptionMiddleware_ShouldReturn404_WhenNotFoundException()
    {
        // Arrange
        var logger = new Mock<ILogger<GlobalExceptionHandlingMiddleware>>();

        RequestDelegate next = _ => throw new NotFoundException("Ürün bulunamadı.");

        var middleware = new GlobalExceptionHandlingMiddleware(next, logger.Object);

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

        // Act
        await middleware.InvokeAsync(context);

        // Assert
        context.Response.StatusCode.Should().Be(404);
    }
}

Denetim Listesi

✅ Exception handling middleware en dışta tanımlandı
✅ Middleware sırası — authentication → authorization → özel middleware
✅ Singleton middleware constructor'ında sadece singleton servisler var
✅ Scoped servisler IMiddleware veya InvokeAsync parametresi ile enjekte ediliyor
✅ Her middleware için extension method yazıldı — UseXxx()
✅ Health check endpoint rate limiting ve auth dışında bırakıldı
✅ Short-circuit noktalarda _next çağrılmıyor
✅ Problem Details formatı tutarlı kullanıldı
✅ Correlation ID her log satırında mevcut
✅ Request loglama production'da performans etkisi değerlendirildi
✅ Her middleware birim testlerle kapsandı
✅ Maintenance mode mekanizması hazır

Temel Çıkarımlar

  • Middleware pipeline'ı HTTP'nin kalbidir — her istek bu kanaldan geçer
  • Sıra kritiktir — authentication'dan önce authorization çalışmaz
  • Singleton middleware scoped servis enjekte edemez — InvokeAsync parametresi kullanılır
  • Exception handling en dışta olmalıdır — her şeyi yakalamalı
  • Her middleware tek bir sorumluluğa odaklanmalıdır — request loglama, auth, cache ayrı ayrı
  • Short-circuit güçlü bir araçtır — gereksiz yere pipeline'ı ilerletmeyin
  • Her middleware test edilebilir olmalıdır — DefaultHttpContext ile izole test yapılabilir
  • Problem Details formatını tutarlı kullanın — client'lar tek formata alışır

Middleware pipeline'ı uygulamanızın bağışıklık sistemidir. Doğru katmanlar doğru sırada yerleştirildiğinde kötü istekler hiçbir zaman iş mantığınıza ulaşmaz.