Her Command/Query, Handler’a ulaşmadan önce bir IPipelineBehavior zincirinden geçer. Zincir, MediatR’da kayıt sırasıyla çalışır. DependencyInjection.cs:
En dış halka. Bilinen exception’ları olduğu gibi yukarı geçirir (throw;), bilinmeyenleri UndefinedApplicationException’a sarar. IBusMessage (Hangfire/bus mesajları) sarılmaz — DLQ/retry kararı altyapıya bırakılır.
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct){ try { return await next(); } catch (Exception ex) when (IsKnownException(ex)) { _logger.LogWarning(ex, "Known exception in {RequestName}", typeof(TRequest).Name); throw; // bilinen exception → olduğu gibi bubble-up } catch (Exception ex) { _logger.LogError(ex, "Unhandled exception in {RequestName}. Request: {@Request}", typeof(TRequest).Name, request); if (request is IBusMessage) // bus mesajlarında wrap yok throw; throw new UndefinedApplicationException("Beklenmeyen bir hata oluştu.", ex); }}private static bool IsKnownException(Exception ex) => ex is FluentValidation.ValidationException || ex is DomainException || ex is ApplicationException || ex is SmsServiceException || ex is EmailServiceException;
Bilinen exception’lar API’deki ProblemDetails katmanına ulaşır ve doğru HTTP koduna çevrilir: DomainException → 400, ValidationException → 422, Sms/EmailServiceException → 503. Yani ForbiddenException (bir DomainException alt sınıfı) bilinen sayılır ve sarılmadan geçer.
RBAC ve multi-tenant erişim kontrolü. Yalnızca IRequirePermissions veya IRequireTenantAccess uygulayan request’lerde devreye girer; aksi halde doğrudan next() çağrılır. Kullanıcı bağlamını ICurrentUserService (BuildingBlocks.Keycloak) üzerinden okur.
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct){ bool requiresPermissions = request is IRequirePermissions; bool requiresTenantAccess = request is IRequireTenantAccess; if (!requiresPermissions && !requiresTenantAccess) return await next(); if (!_currentUser.IsAuthenticated) throw new ForbiddenException(); // Permission kontrolü — tüm izinler mevcut olmalı (AND) if (request is IRequirePermissions permRequest) { var missing = permRequest.RequiredPermissions .Where(p => !_currentUser.HasPermission(p)) .ToList(); if (missing.Count > 0) throw new ForbiddenException(missing); } // Tenant izolasyonu — token tenant'ı ile request tenant'ı örtüşmeli if (request is IRequireTenantAccess tenantRequest) { if (!_currentUser.IsInTenant(tenantRequest.TenantId)) throw new ForbiddenException("Farklı bir organizasyona ait kaynağa erişim engellendi."); } return await next();}
Başarısız her durumda ForbiddenException fırlatılır; bu da HTTP 403’e dönüşür.
İlgili request için kayıtlı tüm IValidator<TRequest>’ları paralel çalıştırır. IBusMessage’ı atlar ve hiç validator yoksa devam eder. Hata varsa tek bir ValidationException (tüm failures ile) fırlatılır.
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct){ if (request is IBusMessage) return await next(); if (!_validators.Any()) return await next(); var context = new ValidationContext<TRequest>(request); var validationResults = await Task.WhenAll( _validators.Select(v => v.ValidateAsync(context, ct))); var failures = validationResults .SelectMany(r => r.Errors) .Where(f => f != null) .ToList(); if (failures.Count != 0) { _logger.LogInformation("Validation failed for {RequestName}. Fields: {Fields}", typeof(TRequest).Name, failures.Select(f => f.PropertyName).Distinct()); throw new ValidationException("Bir veya daha fazla doğrulama hatası oluştu.", failures); } return await next();}
ValidationException API tarafında HTTP 422 + alan bazlı errors listesine çevrilir. Validator’ların nasıl yazıldığı için bkz. Validation & Mapping.
IRequirePermission (tekil) uygulayan request’lerde JWT permissions claim’ini doğrudan IHttpContextAccessor üzerinden okur. Bu, API’deki [RequirePermission] attribute’una ek bir defense-in-depth katmanıdır — özellikle Hangfire job’larından veya event handler’lardan tetiklenen command’lerde değerlidir.
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct){ if (request is not IRequirePermission permissionRequest) return await next(); var user = _httpContextAccessor.HttpContext?.User; if (user?.Identity?.IsAuthenticated != true) throw new DomainException("Bu işlem için kimlik doğrulaması gerekli."); var requiredPermission = permissionRequest.RequiredPermission; bool hasPermission = user.Claims .Where(c => c.Type == "permissions") .Any(c => c.Value == requiredPermission || c.Value == "*:*"); // "*:*" = wildcard süper yetki if (!hasPermission) throw new DomainException($"Bu işlem için '{requiredPermission}' yetkisi gereklidir."); return await next();}
AuthorizationPipelineBehavior (IRequirePermissions, çoğul) ile PermissionPipelineBehavior (IRequirePermission, tekil) iki ayrı marker’dır. İlki ICurrentUserService soyutlamasını ve ForbiddenException (403) üretir; ikincisi ham HttpContext claim’lerini okur ve DomainException (400) fırlatır. Çoğu admin command’i IRequirePermissions’ı tercih eder.