Gérer les erreurs et les messages

Microsoft.Data.SqlClient rapporte les défaillances du serveur et du fournisseur via SqlException. Une opération de base de données peut retourner plusieurs erreurs, donc inspectez la collection Errors de l’exception au lieu de vous fier uniquement au message combiné Message.

Les avertissements et messages d’information provenant du moteur de base de données sont transmis via InfoMessage. Souscrivez avant d’ouvrir la connexion ou d’exécuter la commande qui peut produire le message.

Gérez une défaillance de base de données dans cet ordre :

  1. Interceptez SqlException au niveau d’une limite permettant d’ajouter un contexte d’opération ou de choisir une action de récupération.
  2. Inspectez chaque SqlError élément de la Errors collection.
  3. Enregistrez des champs de diagnostic structurés sans enregistrer les identifiants ni les valeurs sensibles.
  4. Vérifiez l’état de la connexion après une défaillance liée à la connexion.
  5. Réessayez seulement lorsque l’erreur est transitoire et que l’opération peut être répétée sans danger.

Les exemples de cet article supposent qu’une connexion connection ouverte nommée SqlConnection cible la base de données d’échantillons AdventureWorks. Les exemples de C# nécessitent using Microsoft.Data.SqlClient;.

Recevoir des messages d’information

L’événement InfoMessage reçoit des messages serveur de faible gravité, y compris des sorties de PRINT. Un événement peut contenir plusieurs SqlError objets. Abonnez-vous avant d’exécuter la commande qui peut produire un message :

connection.InfoMessage += (_, eventArgs) =>
{
    foreach (SqlError message in eventArgs.Errors)
    {
        Console.WriteLine(
            $"Info {message.Number}, class {message.Class}: {message.Message}");
    }
};

using var command = new SqlCommand(
    "PRINT N'Connected and ready.';",
    connection);
command.ExecuteNonQuery();

L’extrait affiche :

Info 0, class 0: Connected and ready.

Catch SqlException

Interceptez SqlException autour de l’opération sur la base de données que vous pouvez gérer ou enrichir. N’utilisez pas un bloc catch général pour transformer une opération de base de données ratée en un résultat réussi.

Une opération de base de données peut renvoyer plus d’une erreur. Chaque SqlError dans Errors comprend des champs structurés :

Propriété Meaning
Number Numéro d’erreur du moteur de base de données ou du fournisseur.
Class Gravité.
State État défini par le serveur qui peut distinguer les emplacements pour le même numéro d’erreur.
LineNumber Numéro de ligne signalé.
Procedure Procédure ou nom du déclencheur lorsqu’il est fourni.
Server Nom du serveur signalé pour l’erreur.
Message Texte d’erreur compréhensible par l’utilisateur.

Les propriétés utilitaires sur SqlException, y compris Number, Class, State, LineNumber, Procedure et Server, ne reflètent que la première erreur. L’extrait suivant affiche un message puis déclenche une erreur dans le même lot. Il lit exception.Number puis itère Errors pour rapporter chaque entrée :

using (var failing = new SqlCommand("""
    PRINT N'Starting controlled operation.';
    RAISERROR (N'The controlled operation failed.', 16, 1);
    """, connection))
{
    try
    {
        await failing.ExecuteNonQueryAsync();
    }
    catch (SqlException exception)
    {
        Console.WriteLine($"First error number: {exception.Number}");

        foreach (SqlError error in exception.Errors)
        {
            Console.WriteLine(
                $"Number={error.Number}; Class={error.Class}; " +
                $"State={error.State}; Message={error.Message}");
        }
    }
}

L’extrait affiche :

First error number: 50000
Number=50000; Class=16; State=1; Message=The controlled operation failed.
Number=0; Class=0; State=1; Message=Starting controlled operation.

La sortie PRINT ne génère pas InfoMessage. Lorsqu’un lot renvoie une erreur qui déclenche SqlException, le fournisseur ajoute les messages d’information de ce lot à la fin de SqlException.Errors.

Cet extrait détecte l’erreur contrôlée uniquement pour en montrer les champs. Dans le code applicatif, rethrow avec throw; sauf si le bloc catch choisit une véritable action de récupération. Prenez des décisions à partir des numéros d’erreur documentés et du contexte de l’opération. N'analysez pas les textes localisés des messages.

Vérifiez l’état de la connexion

Le moteur de base de données attribue une classe de sévérité à chaque message. Les classes dérivées sont informatives. Les classes traitées comme des erreurs produisent SqlException. Des erreurs de gravité élevée peuvent laisser la connexion fermée ou rompue.

Après une erreur, vérifiez l’état de la connexion avant de réutiliser la connexion :

using var command = new SqlCommand(
    "RAISERROR (N'Controlled failure.', 16, 1);",
    connection);

try
{
    command.ExecuteNonQuery();
}
catch (SqlException)
{
    Console.WriteLine($"Connection state: {connection.State}");
}

Ne supposez pas qu’une connexion reste utilisable parce que l’exception a été interceptée. Une erreur de sévérité 16 la laisse normalement ouverte, mais des défaillances de connexion peuvent la fermer. Éliminez une connexion fermée ou cassée et récupérez une autre connexion depuis le pool de connexions. Ne codez pas la politique de nouvelle tentative uniquement en fonction de la gravité. L’opération doit aussi pouvoir être répétée sans danger.

Identifiez une commande batch en échec

Lorsque un SqlBatch échoue, SqlException.BatchCommand peut référencer le SqlBatchCommand La propriété est null lorsque le fournisseur ne peut pas attribuer l’échec à une commande de lot. L’extrait suivant exécute deux commandes par lot. Le deuxième échoue:

using (var batch = new SqlBatch(connection))
{
    batch.BatchCommands.Add(new SqlBatchCommand("SELECT 1;"));
    batch.BatchCommands.Add(new SqlBatchCommand("SELECT 1 / 0;"));

    try
    {
        await batch.ExecuteNonQueryAsync();
    }
    catch (SqlException exception)
    {
        Console.WriteLine($"Error {exception.Number}: {exception.Message}");

        if (exception.BatchCommand is SqlBatchCommand failedCommand)
        {
            Console.WriteLine($"Failed command: {failedCommand.CommandText}");
        }
    }
}

L’extrait affiche :

Error 8134: Divide by zero error encountered.
Failed command: SELECT 1 / 0;

Le texte de commande peut contenir des valeurs littérales sensibles dans des applications mal paramétrées. Préférez des noms d’opérations d’application stables dans les journaux de production, et n’enregistrez pas les valeurs des paramètres par défaut.

Acheminer les erreurs utilisateur vers InfoMessage

FireInfoMessageEventOnUserErrors a la valeur par défaut false. Lorsque vous le mettez sur true, les erreurs de gravité 16 ou inférieures vont au gestionnaire InfoMessage au lieu de déclencher SqlException:

connection.InfoMessage += (_, eventArgs) =>
{
    foreach (SqlError message in eventArgs.Errors)
    {
        Console.WriteLine(
            $"Info {message.Number}, class {message.Class}: {message.Message}");
    }
};
connection.FireInfoMessageEventOnUserErrors = true;

using var command = new SqlCommand(
    "RAISERROR (N'Routed to InfoMessage.', 16, 1);",
    connection);
command.ExecuteNonQuery();

Aucune exception n’est générée. L’extrait affiche :

Info 50000, class 16: Routed to InfoMessage.

Utilisez cette option uniquement lorsque l’application dispose d’un modèle d’erreur basé sur des événements délibéré, car le code appelant ne voit plus d’exception. Les erreurs de gravité 17 ou plus déclenchent toujours SqlException. Définissez la propriété une fois dans la configuration de la connexion plutôt que de l’activer et de la désactiver pour chaque commande.

Contexte de diagnostic du journal

Capturez suffisamment de contexte structuré pour corréler la défaillance sans exposer les données. L’extrait suivant enregistre un nom d’opération d’application, le ClientConnectionId, et les champs de chaque SqlError. ClientConnectionId Aide à corréler une défaillance client avec les diagnostics serveur et réseau. Ce n’est pas un identifiant.

const string operation = "LoadOrders";

using (var load = new SqlCommand("SELECT 1 / 0;", connection))
{
    try
    {
        await load.ExecuteScalarAsync();
    }
    catch (SqlException exception)
    {
        Console.WriteLine(
            $"Operation={operation}; " +
            $"ClientConnectionId={exception.ClientConnectionId}");

        foreach (SqlError error in exception.Errors)
        {
            Console.WriteLine(
                $"Number={error.Number}; Class={error.Class}; " +
                $"State={error.State}; Procedure={error.Procedure}; " +
                $"Line={error.LineNumber}");
        }
    }
}

La valeur ClientConnectionId diffère pour chaque connexion. L’extrait affiche un résultat similaire à :

Operation=LoadOrders; ClientConnectionId=<connection ID>
Number=8134; Class=16; State=1; Procedure=; Line=1

Notez également si une transaction était active et, si BatchCommand est disponible, quelle commande batch a échoué. Inclure des identifiants de serveurs et de bases de données nettoyés uniquement si la politique organisationnelle le permet. Ne journalez pas les chaînes de connexion, les jetons d’accès, les mots de passe ou les valeurs sensibles des paramètres. Utilisez des compteurs de diagnostic, le traçage des sources d’événements et le guide de dépannage SqlClient pour une enquête plus approfondie.