Exceptions en .NET : principes, gestion et centralisation

Comprendre comment gérer correctement les exceptions est essentiel pour garantir la robustesse de vos applications. Cet article explore les principes fondamentaux de la gestion des exceptions en .NET et vous montre une façon de tout centraliser.
alexJournet.jpg
Alexandre JOURNETMis à jour le 30 Oct 2023
exceptions C# .net conseils

La gestion des exceptions est une composante cruciale de tout développement logiciel de qualité. En tant que développeur .NET, vous avez la possibilité d'utiliser des mécanismes puissants pour capturer, gérer et traiter les exceptions.

Comprendre comment gérer correctement les exceptions est essentiel pour garantir la robustesse de vos applications.

Cet article explore les principes fondamentaux de la gestion des exceptions en .NET et vous montre une façon de tout centraliser.

Qu’est-ce qu'une exception en informatique ?

Une exception est une condition anormale ou une erreur qui se produit pendant l'exécution de votre application. Elle peut être causée par divers facteurs, tels qu'une division par zéro, une référence nulle (Les fameux NPE), une mauvaise syntaxe, une erreur d'E/S, une connexion réseau perdue, ou d'autres situations inattendues.

Sans une gestion appropriée, une exception non gérée peut provoquer la fermeture brutale de votre application.

Les types d’exceptions en informatique

Il existe deux grandes familles d’exceptions:

  • Exceptions gérées : Ce sont les exceptions que l’on peut prévoir et traiter, souvent celles que notre IDE nous remonte pour qu’on les traite dans un bloc try-catch en général. Elles dérivent toute de la classe System.Exception.
  • Exceptions non gérées : Ce sont les exceptions critiques, souvent celles qui bloquent ou arrêtent notre application. Elles apparaissent généralement lors du résultat d’un processus effectué différent de celui développé, qui peut mener à nos fameuses NPE ou encore une division par zéro.

Les exceptions personnalisées

On va séparer ça en deux fichiers:

  • La CustomErrorEnum : C’est l’énumération qui va centraliser tous nos messages d’erreurs.
  • La CustomException : C’est celle qui va pouvoir être throw lors de nos tests de non-null, par exemple :
if (toto is not null) 
{
  // Do something
} 
else 
{
  throw new CustomException("message")
  OU
  throw CustomException.Format(...)
  OU
  throw CustomException.Error400()
}

Enumération d’erreurs

Voici un exemple d’énumération, elle contient certains types d’erreurs et peut retourner un message personnalisé à la suite.

public enum CustomErrorEnum
{
    CUSTOM_400,
    CUSTOM_400_WITH_CAUSE,
    CUSTOM_401,
    CUSTOM_401_WITH_CAUSE,
    CUSTOM_403,
    CUSTOM_403_WITH_CAUSE,
    CUSTOM_404,
    CUSTOM_404_WITH_CAUSE,
    CUSTOM_500,
    CUSTOM_500_WITH_CAUSE,
}

public static class CustomErrorEnumExtension
{
    public static string ShowError(this CustomErrorEnum customErrorEnum)
    {
        return customErrorEnum switch
        {
            CustomErrorEnum.CUSTOM_400 => "Le paramètre est erroné.",
            CustomErrorEnum.CUSTOM_400_WITH_CAUSE => "Le paramètre est erroné : {0}",
            CustomErrorEnum.CUSTOM_401 => "Accès non autorisé.",
            CustomErrorEnum.CUSTOM_401_WITH_CAUSE => "Accès non autorisé : {0}",
            CustomErrorEnum.CUSTOM_403 => "Accès interdit",
            CustomErrorEnum.CUSTOM_403_WITH_CAUSE => "Accès interdit : {0}",
            CustomErrorEnum.CUSTOM_404 => "Aucun résultat trouvé.",
            CustomErrorEnum.CUSTOM_404_WITH_CAUSE => "Aucun résultat trouvé: {0}",
            CustomErrorEnum.CUSTOM_500 => "Une erreur serveur est survenue.",
            CustomErrorEnum.CUSTOM_500_WITH_CAUSE => "Une erreur serveur est survenue : {0}",
            _ => "",
        };
    }
}

Exception personnalisée

Voici un exemple d’exception personnalisée, grâce à nos fonctions Format nous allons pouvoir générer nos exceptions bien plus rapidement.

public class CustomException : Exception
{
    public int? StatusCode { get; set; }
    public CustomException(string? message) : base(message)
    {
    }

    public CustomException(int statusCode, string? message) : base(message)
    {
        StatusCode = statusCode;
    }

    public CustomException(int statusCode, string? message, Exception? innerException) : base(message, innerException)
    {
        StatusCode = statusCode;
    }

    public CustomException(string? message, Exception innerException) : base(message, innerException)
    {
    }

    public static CustomException Format(CustomErrorEnum CustomErrorEnum, params object[] values)
    {
        return new CustomException(string.Format(CustomErrorEnum.ShowError(), values));
    }

    public static CustomException Format(int statusCode, CustomErrorEnum CustomErrorEnum, params object[] values)
    {
        return new CustomException(statusCode, string.Format(CustomErrorEnum.ShowError(), values));
    }
    public static CustomException Format(Exception ex, CustomErrorEnum CustomErrorEnum, params object[] values)
    {
        return new CustomException(string.Format(CustomErrorEnum.ShowError(), values), ex);
    }

    public static CustomException Format(Exception ex, int statusCode, CustomErrorEnum CustomErrorEnum, params object[] values)
    {
        return new CustomException(statusCode, string.Format(CustomErrorEnum.ShowError(), values), ex);
    }

    public static CustomException Error400(params object[] values)
    {
        return values.Length == 0 ? new CustomException(StatusCodes.Status400BadRequest, string.Format(CustomErrorEnum.Custom_400.ShowError(), values)) : new CustomException(StatusCodes.Status400BadRequest, string.Format(CustomErrorEnum.Custom_400_WITH_CAUSE.ShowError(), values));
    }

    Vous pouvez inventer autant de méthodes que de code HTTP pour gérer chaque cas.
}

Gestion des exceptions en .NET

Il existe plusieurs façons de traiter nos exceptions.

La façon simple via try-catch mais qui va nous faire répéter ce pattern autant de fois que l’on a de traitement d’exception, ou via un middleware qui va catch nos exceptions et surtout exceptions personnalisées pour les renvoyer proprement.

Première méthode : Gestion par try-catch

La gestion des exceptions en .NET peut reposer sur les blocs try-catch. Voici comment ils fonctionnent :

  • Le code suspect est placé dans un bloc try.
  • Si une exception se produit à l'intérieur du bloc try, le flux de contrôle est immédiatement dirigé vers le bloc catch.
  • Vous pouvez avoir plusieurs blocs catch pour gérer différents types d'exceptions.
try
{
    // Code susceptible de lancer une exception
}
catch (DivideByZeroException ex)
{
    // Gérer la division par zéro ici
}
catch (Exception ex)
{
    // Gérer toutes les autres exceptions ici
}

NB : on peut aussi rajouter un bloc finally après notre dernier catch si l’on a besoin qu’un bloc de code soit exécuté peu importe si une exception est remontée ou non.

Deuxième méthode : Gestion via middleware

Pour ce faire, nous allons créer deux fichiers :

  • ErrorDetails : C’est le fichier qui contient la structure de notre erreur à renvoyer.
  • ExceptionMiddleware: C’est le middleware qui va être injecté à notre application

Structure de notre erreur

On a créé un fichier assez simple, mais on peut rajouter plus d’informations au besoin :

public class ErrorDetails
{
    public int StatusCode { get; set; }
    public string Message { get; set; }
    public string InnerException { get; set; }
    public string Source { get; set; }
    public string StackTrace { get; set; }
}

Nous avons donc :

  • Le statut HTTP du retour
  • Le message à renvoyer
  • L’exception à remonter
  • La source de l’exception
  • La stacktrace pour localiser rapidement notre erreur

Le middleware

Dans ce fichier, nous allons avoir plusieurs choses :

  • Un attribut RequestDelegate _next qui va nous permettre d’encapsuler l’appel API dans un try-catch.

Et deux méthodes importantes :

  • Celle qui va nous permettre de catch toutes les exceptions personnalisées
  • Celle qui va nous créer notre erreur proprement

Voici la première méthode qui permet d’encapsuler notre appel APIUne API est un programme permettant à deux applications distinctes de communiquer entre elles et d’échanger des données. :

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

NB: Si on ne veut pas utiliser les exceptions personnalisées, on peut tout simplement catch une Exception lambda.

Voici la seconde méthode qui va permettre de transformer l’exception en flux d’erreurs lisibles par un utilisateur :

/// <summary>
/// Permet de transformer l'exception retournée en message d'erreur
/// </summary>
/// <param name="context"></param>
/// <param name="exception"></param>
/// <returns></returns>
private static async Task HandleExceptionAsync(HttpContext context, SdlvException exception)
{
    HttpResponse response = context.Response;
    response.ContentType = "application/json";
    response.StatusCode = exception.StatusCode ?? StatusCodes.Status500InternalServerError;
    ErrorDetails responseModel = new()
    {
        StatusCode = context.Response.StatusCode,
        Message = exception.Message ?? "Internal Server Error",
        InnerException = exception.InnerException?.Message,
        Source = exception.Source,
        StackTrace = exception.StackTrace
    };

    string result = JsonSerializer.Serialize(responseModel, new JsonSerializerOptions
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        WriteIndented = true
    });

    await response.WriteAsync(result);
}

Maintenant que nous avons ces fichiers, il ne reste plus qu’à l’injecter !

On crée un nouvelle classe ExceptionMiddlewareExtensions qui va nous permettre de l’injecter directement dans notre application :

/// <summary>
/// Injecteur du middleware
/// </summary>
public static class ExceptionMiddlewareExtensions
{
    /// <summary>
    /// Permet d'injecter le middleware des exceptions
    /// </summary>
    /// <param name="app"></param>
    public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
    {
        app.UseMiddleware<ExceptionMiddleware>();
    }
}

Il ne reste plus qu’à ajouter la ligne dans notre Program.cs :

app.ConfigureCustomExceptionMiddleware();

Et voilà ! Vous avez une gestion d’erreur centralisée !

En conclusion, la gestion des exceptions joue un rôle vital dans le développement logiciel de qualité, notamment pour les développeurs .NET. Les principes fondamentaux que nous avons explorés dans cet article vous aideront à mieux comprendre comment capturer, gérer et traiter les exceptions de manière efficace. En centralisant cette gestion, vous pouvez renforcer la robustesse de vos applications et offrir une meilleure expérience utilisateur.

Si vous avez des questions, des préoccupations ou si vous souhaitez en savoir plus sur la gestion des exceptions en .NET, n'hésitez pas à nous contacter. Notre équipe d'experts est là pour vous aider à perfectionner vos compétences en développement logiciel. Ne laissez pas les exceptions vous bloquer, faites un pas vers des applications plus fiables et performantes dès aujourd'hui. Contactez-nous pour en savoir plus !