-->

суббота, 22 августа 2015 г.

Exception.ToString() - в чем твоя проблема?

Сегодня я бы хотел поделиться своей болью: архитектурный изъян в реализации метода Exception.ToString() приводит к потере части информации для некоторых вложенных  исключений. Всем известно, что данный метод выводит Message + StackTrace самого исключения и всех вложенных. Например, следующий код:
var exception = new FileNotFoundException("error message", "filename");

Console.WriteLine(exception.ToString());
Выведет на консоль:
System.IO.FileNotFoundException: error message
File name: 'filename'
Теперь вложим наше исключение в другое:
var exception = new FileNotFoundException("error message", "filename");

var outerException = new Exception("outer message", exception);

Console.WriteLine(outerException.ToString());
Результат:
System.Exception: outer message ---> System.IO.FileNotFoundException: error message
 --- End of inner exception stack trace --- 
Заметили? Где имя пропавшего файла?


Это касается не только FileNotFoundException. Любая информация, не содержащаяся в Message или StackTrace как стандартных исключений .NET, так и ваших собственных, будет утеряна - если использовать стандартный ToString() для логирования. Не поможет даже переопределение метода ToString() в вашем исключении (что, впрочем, в любом случае необходимо сделать, если вы храните информацию в полях, отличных от Message). Взгляните на реализацию ToString() у класса Exception (я привожу сокращенный вариант, а полную версию можно найти в официальных исходных кодах .NET):
public override String ToString()
{
 return ToString(true, true);
}

private String ToString(bool needFileLineInfo, bool needMessage) 
{
 String message = (needMessage ? Message : null);
 String s = GetClassName() + ": " + message;

 if (_innerException!=null) 
 {
  s = s + " ---> " 
  + _innerException.ToString(needFileLineInfo, needMessage) 
  + Environment.NewLine 
  + "   " 
  + Environment.GetResourceString("Exception_EndOfInnerExceptionStack");
 }

 string stackTrace = GetStackTrace(needFileLineInfo);
 if (stackTrace != null)
 {
  s += Environment.NewLine + stackTrace;
 }

 return s;
}
Итак, проблема в том, что для _innerException вызывается не публичный ToString(), а приватный ToString(bool, bool), естественно недоступный для переопределения. Таким образом, логику обработки вложенных исключений метода ToString() изменить невозможно. Как будем решать проблему?

  • отказаться от FileNotFoundException и им подобных исключений - невозможно, они могут быть брошены в неподвластном нам коде;
  •  записывать всю необходимую информацию в Message - невозможно по той же причине;
  • пробрасывать FileNotFoundException на самый верх приложения - перечеркнуть всю идею структурной обработки исключений, поломать архитектуру и в перспективе переписать кучу кода. 

Похоже, единственный надежный на 100% вариант - гарантировать вызов публичного ToString() для каждого вложенного исключения через метод-расширение:
public static string ToFullString(this Exception exception)
{
 if (exception == null) return string.Empty;

 return exception
  + Environment.NewLine
  + "-----------------------"
  + Environment.NewLine
  + ToFullString(exception.InnerException);
}
Результат:
System.Exception: outer message ---> System.IO.FileNotFoundException: error message
   --- End of inner exception stack trace ---
-----------------------
System.IO.FileNotFoundException: error message
File name: 'filename'
----------------------- 
Осторожно! Логируемый текст увеличится многократно, так как каждый вызов ToString() печатает информацию о всех вложенных исключениях, включая все StackTrace'ы. Но полная информация в логах куда важнее их объема во многих сценариях!

Полезные ссылки:
Why doesn't System.Exception.ToString call virtual ToString for inner exceptions? - пост на StackOverflow с похожей болью;
О логировании исключений и Exception.ToString - аналогичные проблемы при логировании AggregationException.

Комментариев нет:

Отправить комментарий