.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 —
InvokeAsyncparametresi 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 —
DefaultHttpContextile 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.