Parte 2 — Middleware: logging și exception handling
Pasul 10 — Structura unui middleware
Un middleware ASP.NET Core e o clasă cu trei piese:
- Un constructor care primește
RequestDelegate next(referința către restul pipeline-ului) + alte dependențe injectate - O metodă
InvokeAsync(HttpContext context)care primește request-ul curent - Un apel
await _next(context)care dă mai departe pipeline-ului
Tot ce scrieți înainte de await _next(context) se execută la request. Tot ce scrieți după se execută la response. Pe orice diagonală, e o oportunitate.
public class FoiMiddleware
{
private readonly RequestDelegate _next;
public FoiMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
// PRE: rulat la request
await _next(context);
// POST: rulat la response
}
}
Pasul 11 — LoggingMiddleware cu timing
Scopul: la fiecare request loga method + path + status code + durată.
În Lab10_start/Middleware/LoggingMiddleware.cs aveți scheletul. Completați:
using System.Diagnostics;
namespace Lab10.Middleware;
public class LoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<LoggingMiddleware> _logger;
public LoggingMiddleware(RequestDelegate next, ILogger<LoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var stopwatch = Stopwatch.StartNew();
var method = context.Request.Method;
var path = context.Request.Path;
_logger.LogInformation("-> {Method} {Path}", method, path);
await _next(context);
stopwatch.Stop();
var statusCode = context.Response.StatusCode;
_logger.LogInformation("<- {Method} {Path} -> {StatusCode} ({Duration}ms)",
method, path, statusCode, stopwatch.ElapsedMilliseconds);
}
}
Câteva detalii:
Stopwatch>DateTime.UtcNowpentru durate.Stopwatchfolosește contoare de înaltă rezoluție.- Structured logging —
{Method},{Path},{StatusCode},{Duration}sunt placeholder-e nominale, nu format string-uri C#.ILoggerle stochează ca proprietăți separate (vedeți efectul în Partea 3). - Citim status code-ul DUPĂ
await _next— controller-ul nu a rulat încă pe partea „PRE".
Pasul 12 — ExceptionHandlingMiddleware
Scopul: prindem orice excepție care iese din pipeline, logăm și răspundem cu JSON + status code corespunzător.
În Lab10_start/Middleware/ExceptionHandlingMiddleware.cs:
namespace Lab10.Middleware;
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception on {Method} {Path}",
context.Request.Method, context.Request.Path);
await HandleExceptionAsync(context, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
var (statusCode, message) = exception switch
{
KeyNotFoundException => (StatusCodes.Status404NotFound, "Resursa nu a fost gasita."),
UnauthorizedAccessException => (StatusCodes.Status403Forbidden, "Acces interzis."),
ArgumentException => (StatusCodes.Status400BadRequest, "Cerere invalida."),
_ => (StatusCodes.Status500InternalServerError, "A aparut o eroare interna.")
};
context.Response.ContentType = "application/json";
context.Response.StatusCode = statusCode;
return context.Response.WriteAsJsonAsync(new
{
success = false,
message
});
}
}
switch expression pe tipul excepției e cel mai compact mod de a face mapping-ul. Dacă vreți mai multe tipuri custom (NotFoundException, ValidationException, etc.), adăugați rânduri.
Ce aruncă pipeline-ul - convenții pentru servicii
ExceptionHandlingMiddleware e consumatorul. Dar cine produce excepțiile? Controller-ele MVC standard returnează NotFound() / BadRequest() direct, fără să arunce. Serviciile pot arunca dacă proiectați așa:
KeyNotFoundException- resursa cerută cuidspecific nu există (middleware mapează la 404)ArgumentException- input invalid (400)UnauthorizedAccessException- acces nepermis (403)- orice altceva neașteptat - 500
Exemplu: GetByIdAsync care aruncă în loc să returneze null
// Varianta cu throw - semnatura non-nullable = "garantez ca returnez ceva valid"
public async Task<Article> GetByIdAsync(int id, CancellationToken cancellationToken = default)
{
var article = await _unitOfWork.ArticleRepository.GetByIdWithDetailsAsync(id, cancellationToken);
if (article is null)
throw new KeyNotFoundException($"Articolul cu id={id} nu a fost gasit.");
return article;
}
Null vs throw - când alegem ce
| Situație | Abordare recomandată | Cine răspunde cu 404 |
|---|---|---|
| Căutare cu filtru - pot fi 0 rezultate | Returnează null / listă goală |
Controller-ul (explicit NotFound()) |
Lookup după id cunoscut - trebuie să existe |
Aruncă KeyNotFoundException |
Middleware-ul (automat 404 JSON) |
În proiectul nostru, controller-ele MVC verifică null și returnează NotFound(). Endpoint-urile de API beneficiază mai mult de throw + middleware, pentru că răspunsul JSON e consistent automat fără cod suplimentar în controller.
Excepții custom
Pentru erori de domeniu specifice, putem defini excepții proprii:
public class ArticleNotFoundException : Exception
{
public ArticleNotFoundException(int id)
: base($"Articolul cu id={id} nu a fost gasit.") { }
}
Avantaj: putem face catch (ArticleNotFoundException) fără să prindem orice KeyNotFoundException din sistem (de ex. dintr-un Dictionary care lipsește).
Result Pattern - o alternativă
O alternativă la excepții pentru erori așteptate este Result Pattern: returnezi un obiect Result<T> care poate fi succes sau eroare, fără să arunci.
// pseudocod - nu facem asa in proiectul nostru
Result<Article> result = await service.GetByIdAsync(id);
if (result.IsSuccess)
return Ok(result.Value);
else
return NotFound(result.Error);
Avantaje: eroarea e explicită în semnătura metodei, compilatorul te forțează să o tratezi.
Dezavantaje: boilerplate suplimentar, incompatibil cu middleware-ul standard ASP.NET Core de error handling.
Nu îl folosim în proiect - preferăm convențiile standard (null + 404, sau excepție + middleware). Există librării care îl implementează (FluentResults, ErrorOr) dacă îl vreți într-un proiect viitor.
Pasul 13 — Înregistrare în Program.cs
În Lab10_start/Program.cs aveți TODO-ul. Înlocuiți-l cu:
// Custom middleware - ordinea conteaza
app.UseMiddleware<ExceptionHandlingMiddleware>();
app.UseMiddleware<LoggingMiddleware>();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
Ordinea — de ce ExceptionHandling primul? Pentru că vrea să prindă inclusiv ce se întâmplă în
LoggingMiddleware, înUseRouting, în controller-e și în EF Core. Dacă LoggingMiddleware ar fi înainte și ar arunca, ExceptionHandling nu l-ar vedea — pipeline-ul l-ar fi depășit deja.
Pasul 14 — Test rapid: provocăm un 404 și un 500
Cel mai ușor mod să verificați că ExceptionHandlingMiddleware funcționează — un endpoint care aruncă pe purpose. Adăugați temporar într-un controller existent (ex. HomeController):
[HttpGet("/test/notfound")]
public IActionResult TestNotFound()
{
throw new KeyNotFoundException("Test 404");
}
[HttpGet("/test/boom")]
public IActionResult TestBoom()
{
throw new InvalidOperationException("Test 500");
}
Porniți aplicația și navigați la /test/notfound — ar trebui să vedeți JSON cu success: false și status 404. La /test/boom, status 500. În consolă apar log-urile din LoggingMiddleware și ExceptionHandlingMiddleware.
După ce ați validat, ștergeți cele două endpoint-uri de test (sau lăsați-le într-un #if DEBUG — alegerea voastră).
Exercițiul 2 — Middleware pipeline complet (1p)
- [ ]
LoggingMiddlewaremăsoară durata cuStopwatchși logează method/path/status/durata (Pasul 11) - [ ]
ExceptionHandlingMiddlewareprinde excepții și răspunde cu JSON + status code mapat din tip (Pasul 12) - [ ] Ambele înregistrate în
Program.csîn ordinea corectă (Pasul 13) - [ ] Verificat manual cu cel puțin două scenarii: un request normal (vedeți log-urile pereche în consolă) și un request care aruncă (
KeyNotFoundException→ 404 JSON) (Pasul 14)
Validare: în consolă trebuie să vedeți log-uri de forma:
-> GET /Articles
<- GET /Articles -> 200 (47ms)
Și pentru request-ul care aruncă:
-> GET /test/notfound
fail: Lab10.Middleware.ExceptionHandlingMiddleware[0]
Unhandled exception on GET /test/notfound
System.Collections.Generic.KeyNotFoundException: Test 404
<- GET /test/notfound -> 404 (12ms)