Lidar com erros e mensagens

Microsoft.Data.SqlClient relata falhas do servidor e do provedor por meio de SqlException. Uma operação de banco de dados pode devolver múltiplos erros, então inspecione a coleção da exceção Errors em vez de depender apenas da Message combinada.

Avisos e mensagens informativas do mecanismo de banco de dados são entregues por meio de InfoMessage. Assine antes de abrir a conexão ou executar o comando que pode produzir a mensagem.

Lidar com uma falha de banco de dados nesta ordem:

  1. Intercepte SqlException em um ponto de tratamento que possa adicionar contexto de operação ou escolher uma ação de recuperação.
  2. Inspecione cada SqlError na coleção Errors.
  3. Grave campos diagnósticos estruturados sem registrar credenciais ou valores sensíveis.
  4. Verifique o estado da conexão após uma falha relacionada à conexão.
  5. Tente novamente somente quando o erro for transitório e a operação for segura para repetir.

Os exemplos deste artigo presumem que uma conexão connection aberta chamada SqlConnection tem como destino o banco de dados de exemplo 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ída de PRINT. Um evento pode conter múltiplos SqlError objetos. Inscreva-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 trecho imprime:

Info 0, class 0: Connected and ready.

Catch SqlException

Capture SqlException na operação de banco de dados que você possa gerenciar ou enriquecer. Não use uma captura ampla para transformar uma operação de banco de dados fracassada em um resultado bem-sucedido.

Uma operação de banco de dados pode retornar mais de um erro. Cada SqlError em Errors inclui campos estruturados:

Property Meaning
Number Número do erro do motor de banco de dados ou do provedor.
Class Severidade.
State Estado definido pelo servidor que pode distinguir localizações para o mesmo número de erro.
LineNumber Número da linha informada.
Procedure Procedimento ou nome do gatilho quando fornecido.
Server Nome do servidor foi reportado para o erro.
Message Texto de erro legível para humanos.

As propriedades de conveniência em SqlException, incluindo Number, Class, State, LineNumber, Procedure, e Server, refletem apenas o primeiro erro. O trecho a seguir imprime uma mensagem e depois gera um erro no mesmo batch. Ele 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 trecho 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 saída PRINT não aciona InfoMessage. Quando um lote retorna um erro que gera SqlException, o provedor adiciona as mensagens informativas desse lote ao final de SqlException.Errors.

Este trecho detecta o erro controlado apenas para mostrar seus campos. No código da aplicação, relança com throw; a menos que o bloco catch 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 faça análise de textos localizados de mensagens.

Verificar o estado da conexão

O mecanismo de banco de dados atribui uma classe de gravidade 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 conexã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 presuma que uma conexão ainda é utilizável porque a exceção foi capturada. Um erro de gravidade 16 normalmente mantém a conexão aberta, mas falhas que quebram a conexão podem fechá-la. Descarte uma conexão fechada ou quebrada e obtenha outra conexão do pool de conexões. Não codifique a política de retentativa apenas pela gravidade. A operação também deve ser segura para ser repetida.

Identificar um comando lote que falhou

Quando um SqlBatch falha, SqlException.BatchCommand pode referenciar o SqlBatchCommand associado. A propriedade é null quando o provedor não consegue atribuir a falha a um comando em lote. O trecho a seguir executa dois comandos em lote. A segunda opção 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 trecho imprime:

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

O texto de comando pode conter literais sensíveis em aplicações mal parametrizadas. Prefira nomes estáveis de operações de aplicação em logs de produção e não registre valores de parâmetros por padrão.

Encaminhar erros do usuário para o InfoMessage

FireInfoMessageEventOnUserErrors usa como padrão false. Quando você define para true, erros com gravidade 16 ou inferior vão para o manipulador InfoMessage em vez de gerar 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 é gerada. O trecho imprime:

Info 50000, class 16: Routed to InfoMessage.

Use essa opção apenas quando a aplicação possui um modelo de erro deliberado baseado em eventos, pois o código que chama não vê mais uma exceção. Erros com gravidade 17 ou superior ainda acionam SqlException. Defina a propriedade uma vez como parte da configuração da conexão, em vez de ativá-la/desativá-la em comandos individuais.

Contexto de diagnóstico de logs

Capture contexto estruturado suficiente para correlacionar a falha sem expor os dados. O trecho a seguir registra um nome de operação de 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 de acordo com cada conexão. O trecho imprime resultados semelhantes a:

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

Também registre se uma transação estava ativa e, se BatchCommand estiver disponível, qual comando batch falhou. Inclua identificadores de servidores e bancos de dados sanitizados apenas se a política organizacional permitir. Não registre strings de conexão, tokens de acesso, senhas ou valores sensíveis de parâmetros. Use contadores de diagnóstico, rastreamento de fontes de eventos e o guia de solução de problemas do SqlClient para uma investigação mais aprofundada.