Gestire errori funzionali tramite exception in ASP.NET Core Web API

di Marco De Sanctis, in ASP.NET Core,

Tutte le volte che dobbiamo sollevare un errore in Web API, sappiamo che da un punto di vista semantico, questo si traddurrà in uno status code, e una descrizione per l'utente. Per esempio, se stiamo cercando di modificare una entity Order che non esiste, dovremo ritornare un 404 Not Found, mentre se i dati in input sono errati dovremo restituire un 400 Bad Request, magari con gli errori di validazione.

Il problema è che tipicamente resituiamo questi status code dal controller, mentre questi errori sono sollevati da altri componenti dell'architettura, per esempio un validator, o il nostro OrderRepository:

[HttpPut("{orderId}")]
public IActionResult UpdateOrder(int orderId, [FromBody] Order order)
{
    // fetch the order from the database and
    // return not found if it doesn't exist
    var originalOrder = _orderRepository.Get(orderId);
    if (originalOrder == null)
    {
        return NotFound();
    }

    // update the order based on the input
    // ...

    // validate the updated order
    var validationResults = _orderValidator.Validate(order);
    if (!validationResults.IsValid)
    {
        return BadRequest(validationResults);
    }

    // save the changes to the database
    _orderRepository.Update(order);
    return NoContent();
}

Questo dà origine a codice che è molto verboso, e che inoltre ci espone ad alcuni "rischi", come il metodo Get di OrderRepository che può restituire un null, da cui il rischio di ritrovarci delle NullReferenceException se non effettuiamo i controlli dovuti.

Una possibile alternativa è quella di sollevare un particolare tipo di eccezione dai servizi applicativi, così poi da gestirla all'interno della nostra pipeline. Per esempio possiamo definire una nostra WebApiException come segue:

public class WebApiException : Exception
{
    public HttpStatusCode StatusCode { get; }

    public WebApiException(HttpStatusCode statusCode, string message) : base(message)
    {
        StatusCode = statusCode;
    }
}

e utilizzarla all'interno dei nostri servizi applicativi:

public class OrderRepository
{ 
    public Order Get(int orderId)
    {
        // fetch from the database
        // and throw WebApiException.NotFound if not found
        // ...
        if (order == null)
        {
            throw new WebApiException(HttpStatusCode.NotFound, "Order not found");
        }

        return order;
    }
}

In questo esempio, la nostra classe OrderRepository solleverà una WebApiException, con il relativo status code, per segnalare che l'ordine cercato non è esistente. Si tratta di una scelta architetturale non propriamente "convenzionale", perché un repository non dovrebbe avere "cognizione" di trovarsi in esecuzione in una WebApi, tuttavia un sistema del genere semplifica di molto le cose. Nel controller, infatti, ora possiamo limitarci a chiamare i servizi applicativi, senza doverne controllare l'esito:

[HttpPut("{orderId}")]
public IActionResult UpdateOrder(int orderId, [FromBody] Order order)
{
    // fetch the order from the database and
    // return not found if it doesn't exist
    var originalOrder = _orderRepository.Get(orderId);

    // update the order based on the input
    // ...

    // validate the updated order
    _orderValidator.Validate(order);

    // save the changes to the database
    _orderRepository.Update(order);
    return NoContent();
}

Poi, possiamo impostare un global exception handler nella pipeline di ASP.NET Core, così da intercettare questa particolare eccezione e usarla per ritornarne il contenuto all'utente:

public static void Main(string[] args)
{
    var builder = WebApplication.CreateBuilder(args);

    // .. altro codice qui ..

    app.UseExceptionHandler(handler => 
    {
        handler.Run(context =>
        {
            var exception = context.Features.Get<IExceptionHandlerFeature>().Error;

            var message = "An error occurred while processing your request.";

            if (exception is WebApiException webApiException)
            { 
                context.Response.StatusCode = (int)webApiException.StatusCode;
                message = webApiException.Message;
            }
            else
            {
                context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
            }

            context.Response.ContentType = "application/json";

            return context.Response.WriteAsync(JsonSerializer.Serialize(new
            {
                message
            }));
        });
    });

    ..

    app.Run();
}

Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Approfondimenti

I più letti di oggi