Lidar com erros e mensagens

Microsoft.Data.SqlClient reporta falhas do servidor e do fornecedor através de SqlException. Uma operação de base de dados pode devolver múltiplos erros, por isso inspeciona a coleção da exceção Errors em vez de confiar apenas na mensagem combinada Message.

Avisos e mensagens informativas do motor de base de dados são entregues através de InfoMessage. Subscreva-se antes de abrir a conexão ou executar o comando suscetível de produzir a mensagem.

Trate de uma falha de base de dados por esta ordem:

  1. Detete SqlException num limite que possa adicionar contexto da operação ou escolher uma ação de recuperação.
  2. Inspeciona cada SqlError na coleção Errors.
  3. Gravar campos de diagnóstico estruturados sem registar credenciais ou valores sensíveis.
  4. Verifique o estado da ligação após uma falha relacionada com a ligação.
  5. Tente novamente apenas quando o erro for transitório e a operação for segura para repetir.

Os exemplos neste artigo assumem que um connection aberto com o nome SqlConnection tem como alvo a base de dados de exemplo do AdventureWorks. Os exemplos de C# requerem using Microsoft.Data.SqlClient;.

Receber mensagens informativas

O InfoMessage evento recebe mensagens de servidor de baixa gravidade, incluindo saídas de PRINT. Um evento pode conter vários SqlError objetos. Subscreva-se antes de executar o comando que pode produzir uma mensagem:

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

O excerto apresenta:

Info 0, class 0: Connected and ready.

Catch SqlException

Capture SqlException à volta da operação da base de dados que possa gerir ou enriquecer. Não use uma captura genérica de exceções para transformar uma operação da base de dados que falhou num resultado bem-sucedido.

Uma operação de base de dados pode devolver mais do que um erro. Cada SqlError em Errors inclui campos estruturados:

Property Meaning
Number Número do erro do motor da base de dados ou do fornecedor.
Class Gravidade.
State Estado definido pelo servidor que pode distinguir localizações para o mesmo número de erro.
LineNumber Número da linha comunicado.
Procedure Procedimento ou nome do gatilho quando fornecido.
Server Nome do servidor indicado para o erro.
Message Texto de erro legível por humanos.

As propriedades de conveniência em SqlException, incluindo Number, Class, State, LineNumber, Procedure, , e Server, refletem apenas o primeiro erro. O excerto seguinte imprime uma mensagem e depois gera um erro no mesmo lote. Lê exception.Number e depois itera Errors para reportar cada entrada:

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

O excerto imprime:

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.

A PRINT saída não gera InfoMessage. Quando um lote devolve um erro que gera SqlException, o fornecedor acrescenta as mensagens informativas desse lote ao final de SqlException.Errors.

Este excerto deteta o erro controlado apenas para mostrar os seus campos. No código da aplicação, relança com throw; a menos que o bloco de captura escolha uma ação real de recuperação. Tome decisões a partir dos números de erro documentados e do contexto da operação. Não analises o texto localizado da mensagem.

Verificar o estado da ligação

O motor de base de dados atribui uma classe de severidade a cada mensagem. As classes inferiores são informativas. Classes tratadas como erros produzem SqlException. Erros de alta gravidade podem deixar a conexão fechada ou quebrada.

Após um erro, verifique State antes de reutilizar a ligação:

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

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

Não assumas que uma ligação continua a ser utilizável só porque a exceção foi apanhada. Um erro de gravidade 16 normalmente deixa-a aberta, mas falhas que quebram a ligação podem fechá-la. Elimine uma conexão fechada ou interrompida e obtenha outra conexão do pool. Não codifique a política de repetição apenas com base na gravidade. A operação também deve ser segura quando repetida.

Identificar um comando batch falhado

Quando um SqlBatch falha, SqlException.BatchCommand pode referenciar o SqlBatchCommand. A propriedade é null quando o fornecedor não consegue atribuir a falha a um comando batch. O excerto seguinte executa dois comandos em lote. O segundo falha:

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

O excerto imprime:

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

O texto de comandos pode conter literais sensíveis em aplicações mal parametrizadas. Prefira nomes estáveis de operações de aplicação nos registos de produção e não registe valores de parâmetros por defeito.

Encaminhar erros do utilizador para o InfoMessage

FireInfoMessageEventOnUserErrors assume como padrão false. Quando defines para true, erros com gravidade 16 ou inferior vão para o processador InfoMessage em vez de gerarem 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();

Nenhuma exceção é lançada. O excerto imprime:

Info 50000, class 16: Routed to InfoMessage.

Use esta opção apenas quando a aplicação tiver um modelo de erro deliberadamente baseado em eventos, porque o código de chamada já não vê uma exceção. Erros com gravidade 17 ou superior ainda geram SqlException. Defina a propriedade uma vez como parte da configuração da ligação em vez de a alternar em comandos individuais.

Contexto de diagnóstico do registo

Captura contexto estruturado suficiente para correlacionar a falha sem expor dados. O excerto seguinte regista o nome de operação da aplicação, o ClientConnectionId, e os campos de cada SqlError. ClientConnectionId Ajuda a correlacionar uma falha do cliente com diagnósticos de servidor e rede. Não é uma credencial.

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

O ClientConnectionId valor varia em cada ligação. O excerto imprime resultados semelhantes a:

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

Regista também se uma transação estava ativa e, se BatchCommand estiver disponível, qual comando batch falhou. Incluir identificadores de servidores e bases de dados sanitizados apenas se a política organizacional o permitir. Não registe cadeias de ligação, tokens de acesso, palavras-passe ou valores de parâmetros sensíveis. Utilize contadores de diagnóstico, rastreio de fontes de eventos e o guia de resolução de problemas do SqlClient para uma investigação mais aprofundada.