Хорошее, подробное исключение — мощный инструмент, который помогает найти и исправить проблему. Поэтому в исключения стоит вносить больше деталей.
Один из способов — добавить в текст сообщения исключения дополнительные сведения. Как в коде ниже.
try
{
return await _ordersRepository.Get(id, cancellationToken);
}
catch (Exception exception)
{
throw new Exception($"Unable to get order info, user {userName}, order id {id}", exception);
}
Но при таком подходе затруднительно создавать сообщения. И небезопасно: сообщение может быть отправлено пользователю в случаях, когда исключение не было корректно обработано. Нам следует избегать отправки пользователю такой информации, как идентификаторы. Все, что нужно, чтобы исправить ошибку, — залогировать дополнительные сведения и информацию об оригинальном исключении на стороне сервера.
Другой подход — создать свое исключение с определенными свойствами. Но создать множество классов исключений и так достаточно сложно, а главная проблема — настроить логирование свойств этих классов.
Класс .NET Framework Exception имеет свойство Data, которое уже обеспечивает хранение дополнительных сведений в виде коллекции заданных пар «ключ — значение». Так как это свойство базового класса, я уверен, вы сможете настроить логгер. В наших примерах ниже мы будем использовать NLog, который настраивается достаточно легко. Чтобы избежать конфликта ключей и обработать ошибку наиболее эффективным способом, советую также создать свое исключение.
Половина классов исключений .NET Framework имеет свойства, которые не логируются, — вы можете добавить эти данные в свойство Exception.Data вашего нового исключения. Свойство InnerException будет содержать ссылку на оригинальное исключение.
Код ниже демонстрирует, как использовать свойство Exception.Data.
try
{
return await _ordersRepository.Get(id, cancellationToken);
}
catch (Exception exception)
{
const string message = "Unable to get order info";
var yourException = new YourAppException(message, exception);
yourException.Data[nameof(userName)] = userName;
yourException.Data[nameof(id)] = id;
throw yourException;
}
Упс. Похоже, мы добавили больше кода — но можем исправить это, создав расширения, которые позволят задействовать паттерн Fluent interface. Пример ниже демонстрирует, как сделать код более читабельным и простым в использовании.
try
{
return await _ordersRepository.Get(id, cancellationToken);
}
catch (Exception exception)
{
throw exception.With("Unable to get order info")
.DetailData(nameof(userName), userName)
.DetailData(nameof(id), id);
}
Давайте рассмотрим пример настройки NLog layout для логирования свойства Exception.Data.
${shortdate} ${time} [${level:uppercase=true}]: ${message:withException=true}${when:when=length('${exception:format=Data}')>0:Inner=${newline}--- Exception Data ---${newline}${exception:format=Data:exceptionDataSeparator=,\r\n}}
Как это выглядит в консоли.
Этот пример работает замечательно, если добавлять простые структуры: NLog вызывает ToString(), чтобы записать значения в targets. Поэтому мы можем правильно залогировать объекты, только если они корректно переопределяют метод ToString().
Но переопределить ToString для всех классов практически невозможно. Наиболее простой способ представить объект в виде строки — сериализовать его в JSON. Код ниже добавляет C#-класс, который это делает.
/// <summary>
/// Defines a value/json pair to represent an exception data value as JSON
/// </summary>
public record ExceptionDataEntry
{
private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = {new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)},
WriteIndented = true
};
private ExceptionDataEntry(in object value, in string json)
{
Value = value;
Json = json;
}
public object Value { get; }
public string Json { get; }
public static ExceptionDataEntry FromValue(in object value)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
var json = JsonSerializer.Serialize(value, SerializerOptions);
return new ExceptionDataEntry(value, json);
}
/// <summary>
/// Represents an exception data value as JSON
/// </summary>
/// <returns></returns>
public override string ToString()
{
return Json;
}
}
А этот код добавляет расширение.
public static YourAppException DetailData(this YourAppException exception, in string key, in object value)
{
try
{
exception.Data[key] = ExceptionDataEntry.FromValue(value);
}
catch
{
// ignored, because we use it inside another exception catch block
// so, we should avoid throwing a new exception to keep the original exception
}
return exception;
}
Как это будет выглядеть в консоли в нашем примере.
Я надеюсь, этот подход к использованию Exception.Data для логирования дополнительных сведений поможет вам в поддержке приложений. Если у вас есть идеи, как улучшить предложенный подход, пожалуйста, пишите в комментариях. Спасибо =)