Gestire errori e messaggi

Microsoft. Data.SqlClient segnala i guasti di server e provider tramite SqlException. Un'operazione nel database può restituire più errori, quindi ispeziona la collezione dell'eccezione Errors invece di affidarti solo al messaggio combinato Message.

Gli avvisi e i messaggi informativi dal motore di database sono consegnati tramite InfoMessage. Sottoscriversi prima di aprire la connessione o di eseguire il comando che può produrre il messaggio.

Gestire un guasto al database in quest'ordine:

  1. Intercetta SqlException in un punto limite dove puoi aggiungere il contesto dell'operazione o scegliere un'azione di ripristino.
  2. Controlla ogni SqlError nella raccolta Errors.
  3. Registra campi diagnostici strutturati senza registrare credenziali o valori sensibili.
  4. Controlla lo stato della connessione dopo un errore legato alla connessione.
  5. Riprova solo quando l'errore è transitorio e l'operazione è sicura da ripetere.

Gli esempi in questo articolo presuppongono che un elemento aperto connection denominato SqlConnection faccia riferimento al database di esempio di AdventureWorks. Gli esempi di C# richiedono using Microsoft.Data.SqlClient;.

Ricevere messaggi informativi

L'evento InfoMessage riceve messaggi del server a bassa gravità, incluso l'output da PRINT. Un evento può contenere più SqlError oggetti. Iscriviti prima di eseguire il comando che può produrre un messaggio:

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();

Il brano viene stampato:

Info 0, class 0: Connected and ready.

Catch SqlException

Intercetta SqlException intorno all'operazione del database che puoi gestire o arricchire. Non usare un catch generico per trasformare un'operazione di database fallita in un risultato di successo.

Un'operazione nel database può restituire più di un errore. Ogni SqlError in Errors include campi strutturati:

Property Meaning
Number Numero di errore del motore di database o del fornitore.
Class Gravità.
State Stato definito dal server che può distinguere le origini per lo stesso numero di errore.
LineNumber Numero di linea segnalato.
Procedure Nome della procedura o del trigger, se fornito.
Server Nome del server riportato per l'errore.
Message Testo di errore leggibile dall'uomo.

Le proprietà di convenienza su SqlException, inclusi Number, Class, State, LineNumber, Procedure e Server, riflettono solo il primo errore. L'estratto seguente stampa un messaggio e poi genera un errore nello stesso batch. Legge exception.Number e poi itera Errors per riportare ogni voce:

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}");
        }
    }
}

Il brano viene stampato:

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.

L'output PRINT non genera InfoMessage. Quando un batch restituisce un errore che genera SqlException, il provider aggiunge i messaggi informativi di quel lotto alla fine di SqlException.Errors.

Questo estratto rileva l'errore controllato solo per mostrarne i campi. Nel codice applicativo, rilanciare con throw; a meno che il blocco di cattura non scelga un'azione di recupero reale. Prendi decisioni in base ai numeri di errore documentati e al contesto operativo. Non analizzare il testo del messaggio localizzato.

Controllare lo stato della connessione

Il motore del database assegna una classe di gravità a ogni messaggio. Le classi derivate sono informative. Le classi trattate come errori producono SqlException. Errori ad alta gravità possono lasciare la connessione chiusa o interrotta.

Dopo un errore, controlla State prima di riutilizzare la connessione:

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

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

Non dare per scontato che una connessione rimanga utilizzabile solo perché l'eccezione è stata intercettata. Un errore di gravità 16 normalmente lo lascia aperto, ma guasti che interrompono la connessione possono chiuderlo. Elimina una connessione chiusa o rotta e prendi un'altra connessione dal pool. Non definire il criterio dei tentativi di ripetizione basandoti solo sulla gravità. L'operazione deve anche essere sicura da ripetere.

Identifica un comando batch fallito

Quando un SqlBatch fallisce, SqlException.BatchCommand può fare riferimento all'associato SqlBatchCommand. La proprietà è null quando il provider non può attribuire l'errore a un comando batch. Il seguente estratto esegue due comandi batch. Il secondo fallisce:

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}");
        }
    }
}

Il brano viene stampato:

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

Il testo di comando può contenere letterali sensibili in applicazioni poco parametrizzate. Preferire nomi stabili delle operazioni applicative nei log di produzione e non registrare per impostazione predefinita i valori dei parametri.

Instrada gli errori dell'utente a InfoMessage

Il valore predefinito di FireInfoMessageEventOnUserErrors è false. Quando lo imposti su true, errori di gravità 16 o inferiore vanno al gestore InfoMessage invece di generare 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();

Non viene generata alcuna eccezione. Viene stampato il brano:

Info 50000, class 16: Routed to InfoMessage.

Usa questa opzione solo quando l'applicazione ha un modello di errore deliberatamente basato sugli eventi, perché il codice chiamante non vede più un'eccezione. Gli errori con gravità 17 o superiore generano comunque SqlException Imposta la proprietà una volta come parte della configurazione della connessione invece di alternarla tra i singoli comandi.

Contesto diagnostico di log

Raccogli abbastanza contesto strutturato da correlare il guasto senza esporre i dati. L'estratto seguente registra il nome di un'operazione applicativa, il ClientConnectionId, e i campi di ogni SqlError. ClientConnectionId aiuta a correlare un guasto del client con la diagnostica di server e rete. Non è una credenziale.

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}");
        }
    }
}

Il valore ClientConnectionId è diverso per ogni connessione. Il frammento stampa un output simile a:

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

Registra anche se una transazione era attiva e, se BatchCommand disponibile, quale comando batch è fallito. Includere identificatori di server e database sanificati solo se la politica organizzativa lo consente. Non registrare stringhe di connessione, token di accesso, password o valori di parametri sensibili. Usa contatori diagnostici, tracciamento delle sorgenti eventi e la guida alla risoluzione dei problemi SqlClient per un'indagine più approfondita.