Обработка ошибок и сообщений

Microsoft. Data.SqlClient сообщает о сбоях сервера и провайдера через SqlException. Операция базы данных может возвращать несколько ошибок, поэтому проверяйте коллекцию Errors исключения, а не полагайтесь только на объединённый Message.

Предупреждения и информационные сообщения от движка базы данных доставляются через InfoMessage. Подпишитесь до открытия соединения или выполнения команды, которая может создать сообщение.

Действия при сбое базы данных в следующем порядке:

  1. Перехватите SqlException на границе, которая может добавить контекст операции, или выбрать действие восстановления.
  2. Проверьте все SqlError в коллекции Errors.
  3. Записывайте структурированные диагностические поля без записи учетных данных или чувствительных значений.
  4. Проверьте состояние соединения после сбоя, связанного с подключением.
  5. Повторяйте только тогда, когда ошибка временная и операция безопасна для повторения.

Примеры в этой статье предполагают, что открытое соединение SqlConnection с именем connection нацелено на образцовую базу данных AdventureWorks. Примеры C# требуют using Microsoft.Data.SqlClient;.

Получайте информационные сообщения

Событие InfoMessage принимает сообщения сервера низкой степени, включая вывод из PRINT. Одно событие может содержать несколько SqlError объектов. Оформите подписку перед выполнением команды, которая может создать сообщение:

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

Выводится:

Info 0, class 0: Connected and ready.

Catch SqlException

Перехватывайте SqlException исключения при операции с базой данных, которые вы можете обработать или дополнить контекстом. Не используйте слишком общий обработчик исключений, чтобы превратить неудачную операцию базы данных в успешный результат.

Операция с базой данных может вернуть несколько ошибок. Каждый SqlError в Errors включает структурированные поля:

Property Meaning
Number Номер ошибки ядра базы данных или поставщика данных.
Class Степень серьезности.
State Состояние, определённое сервером, которое может различать местоположения для одного и того же номера ошибки.
LineNumber Указанный номер строки.
Procedure Процедура или название триггера, если указано.
Server Имя сервера, указанное для ошибки.
Message Понятный человеку текст ошибки.

Вспомогательные свойства на SqlException, включая Number, Class, State, LineNumber, Procedure, и Server, отражают только первую ошибку. Следующий отрывок печатает сообщение и затем вызывает ошибку в том же пакете. Он читает exception.Number, а затем перебирает Errors, чтобы вывести каждую запись:

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

Выводится фрагмент:

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.

Выход PRINT не вызывает InfoMessage. Когда пакет возвращает ошибку, которая вызывает SqlException, провайдер добавляет информационные сообщения этой партии в конец SqlException.Errors.

Этот отрывок фиксирует контролируемую ошибку только для того, чтобы показать её поля. В коде приложения повторно выбросьте исключение с помощью throw;, если блок catch не выполняет реальное восстановление. Принимайте решения, исходя из задокументированных номеров ошибок и операционного контекста. Не разбирайте локализованный текст сообщения.

Проверьте состояние подключения

Движок базы данных назначает каждому сообщению класс серьёзности. Нижележащие классы — информационные. Классы, рассматриваемые как ошибки, приводят к SqlException. Ошибки высокой серьезности могут оставить соединение закрытым или разорванным.

После ошибки проверьте State перед повторным использованием соединения:

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

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

Не думайте, что соединение остаётся пригодным только потому, что исключение было перехвачено. Ошибка уровня серьезности 16 обычно оставляет его открытым, но при разрывах соединения он может закрыться. Удалите закрытое или сломанное соединение и возьмите другое соединение из пула. Не кодируйте политику повторных попыток только на основе уровня серьёзности. Операция также должна быть безопасной для повторения.

Идентифицировать пакетную команду, завершившуюся с ошибкой

Когда SqlBatch не выполняется, SqlException.BatchCommand может ссылаться на соответствующее SqlBatchCommand. Свойство имеет значение null, когда провайдер не может связать сбой с одной командой batch. Следующий отрывок выполняет две пакетные команды. Второе не удаётся:

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

Вывод отрывка:

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

Текст команд может содержать чувствительные литералы в плохо параметризованных приложениях. Предпочитайте стабильные имена операций приложений в производственных логах и не записывайте значения параметров по умолчанию.

Направляйте ошибки пользователя в InfoMessage

FireInfoMessageEventOnUserErrors по умолчанию использует false. Когда вы устанавливаете это значение в true, ошибки с уровнем серьезности 16 или ниже передаются обработчику InfoMessage вместо вызова 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();

Исключение не возникает. Вывод фрагмента:

Info 50000, class 16: Routed to InfoMessage.

Используйте этот вариант только если приложение намеренно использует модель ошибки на основе событий, поскольку вызывающий код больше не видит исключения. Ошибки с серьёзностью 17 и выше по-прежнему вызывают SqlException. Задайте свойство один раз в конфигурации соединения, а не переключайте его для каждой команды.

Контекст диагностики журнала

Зафиксировать достаточно структурированного контекста, чтобы сопоставить с ним сбой, не раскрывая данные. Следующий отрывок регистрирует имя операции приложения, ClientConnectionId, и поля каждого SqlError. ClientConnectionId помогает связать сбой клиента с диагностикой серверов и сети. Это не учетные данные.

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

Значение ClientConnectionId отличается при каждом соединении. Отрывок печатает результат, похожий на следующий:

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

Также запишите, была ли транзакция активна и, если доступен BatchCommand, какая команда batch не сработала. Включайте очищенные идентификаторы серверов и баз данных только в том случае, если это позволяет организационная политика. Не фиксируйте строки соединения, токены доступа, пароли или чувствительные значения параметров. Используйте диагностические счётчики, отслеживание источников событий и руководство по устранению неполадок SqlClient для более глубокого изучения.