.NET: Как преобразовать исключение в строку?

Когда генерируется исключение (при отладке в среде IDE), у меня есть возможность просмотреть сведения об исключении:

enter image description here

Но в коде, если я вызываю exception.ToString(), я не вижу этих полезных деталей:

System.Data.SqlClient.SqlException (0x80131904): Could not find stored procedure 'FetchActiveUsers'.
  [...snip stack trace...]

Но Visual Studio имеет некоторую магию, где она может копировать исключение в буфер обмена:

enter image description here

Что дает полезные сведения:

System.Data.SqlClient.SqlException was unhandled by user code
  Message=Could not find stored procedure 'FetchActiveUsers'.
  Source=.Net SqlClient Data Provider
  ErrorCode=-2146232060
  Class=16
  LineNumber=1
  Number=2812
  Procedure=""
  Server=vader
  State=62
  StackTrace:
       [...snip stack trace...]
  InnerException:

Ну, я хочу этого!

Каким будет содержимое:

String ExceptionToString(Exception ex)
{ 
    //todo: Write useful routine
    return ex.ToString();
}

который может совершить ту же магию. Есть ли где-то встроенная функция .NET? Есть ли у Exception секретный метод, чтобы преобразовать его в строку?

Ответ 1

ErrorCode специфичен для ExternalException, а не Exception и LineNumber и Number характерны для SqlException, а не Exception. Таким образом, единственный способ получить эти свойства из общего метода расширения на Exception - использовать отражение для итерации по всем общедоступным свойствам.

Итак, вам нужно сказать что-то вроде:

public static string GetExceptionDetails(this Exception exception) {
    var properties = exception.GetType()
                            .GetProperties();
    var fields = properties
                     .Select(property => new { 
                         Name = property.Name,
                         Value = property.GetValue(exception, null)
                     })
                     .Select(x => String.Format(
                         "{0} = {1}",
                         x.Name,
                         x.Value != null ? x.Value.ToString() : String.Empty
                     ));
    return String.Join("\n", fields);
}

(Не проверен на проблемы с помехами.)

Ответ на совместимость с .NET 2.0:

public static string GetExceptionDetails(this Exception exception) 
{
    PropertyInfo[] properties = exception.GetType()
                            .GetProperties();
    List<string> fields = new List<string>();
    foreach(PropertyInfo property in properties) {
        object value = property.GetValue(exception, null);
        fields.Add(String.Format(
                         "{0} = {1}",
                         property.Name,
                         value != null ? value.ToString() : String.Empty
        ));    
    }         
    return String.Join("\n", fields.ToArray());
}

Ответ 2

Я сначала попробовал ответить Джейсону (наверху), который работал очень хорошо, но я также хотел:

  • Чтобы циклически перебирать внутренние исключения и отступать от них.
  • Игнорировать свойства null и повышать читаемость выходных данных.
  • Он включает метаданные в свойстве Data. (если есть), но исключает само свойство Data. (его бесполезно).

Теперь я использую это:

    public static void WriteExceptionDetails(Exception exception, StringBuilder builderToFill, int level)
    {
        var indent = new string(' ', level);

        if (level > 0)
        {
            builderToFill.AppendLine(indent + "=== INNER EXCEPTION ===");                
        }

        Action<string> append = (prop) =>
            {
                var propInfo = exception.GetType().GetProperty(prop);
                var val = propInfo.GetValue(exception);

                if (val != null)
                {
                    builderToFill.AppendFormat("{0}{1}: {2}{3}", indent, prop, val.ToString(), Environment.NewLine);
                }
            };

        append("Message");
        append("HResult");
        append("HelpLink");
        append("Source");
        append("StackTrace");
        append("TargetSite");

        foreach (DictionaryEntry de in exception.Data)
        {
            builderToFill.AppendFormat("{0} {1} = {2}{3}", indent, de.Key, de.Value, Environment.NewLine);
        }

        if (exception.InnerException != null)
        {
            WriteExceptionDetails(exception.InnerException, builderToFill, ++level);
        }
    }

Вызов:

        var builder = new StringBuilder();
        WriteExceptionDetails(exception, builder, 0);
        return builder.ToString();

Ответ 3

В этом всеобъемлющем ответе написано:

  • Свойство коллекции Data найдено во всех исключениях (принятый ответ этого не делает).
  • Любые другие настраиваемые свойства, добавленные в исключение.
  • Рекурсивно выписывает InnerException (принятый ответ не делает этого).
  • Записывает коллекцию исключений, содержащихся в AggregateException.

Он также записывает свойства исключений в более хорошем порядке. Он использует С# 6.0, но вам должно быть очень легко конвертировать в более старые версии, если это необходимо.

public static class ExceptionExtensions
{
    public static string ToDetailedString(this Exception exception)
    {
        if (exception == null)
        {
            throw new ArgumentNullException(nameof(exception));
        }

        return ToDetailedString(exception, ExceptionOptions.Default);
    }

    public static string ToDetailedString(this Exception exception, ExceptionOptions options)
    {
        var stringBuilder = new StringBuilder();

        AppendValue(stringBuilder, "Type", exception.GetType().FullName, options);

        foreach (PropertyInfo property in exception
            .GetType()
            .GetProperties()
            .OrderByDescending(x => string.Equals(x.Name, nameof(exception.Message), StringComparison.Ordinal))
            .ThenByDescending(x => string.Equals(x.Name, nameof(exception.Source), StringComparison.Ordinal))
            .ThenBy(x => string.Equals(x.Name, nameof(exception.InnerException), StringComparison.Ordinal))
            .ThenBy(x => string.Equals(x.Name, nameof(AggregateException.InnerExceptions), StringComparison.Ordinal)))
        {
            var value = property.GetValue(exception, null);
            if (value == null && options.OmitNullProperties)
            {
                if (options.OmitNullProperties)
                {
                    continue;
                }
                else
                {
                    value = string.Empty;
                }
            }

            AppendValue(stringBuilder, property.Name, value, options);
        }

        return stringBuilder.ToString().TrimEnd('\r', '\n');
    }

    private static void AppendCollection(
        StringBuilder stringBuilder,
        string propertyName,
        IEnumerable collection,
        ExceptionOptions options)
        {
            stringBuilder.AppendLine($"{options.Indent}{propertyName} =");

            var innerOptions = new ExceptionOptions(options, options.CurrentIndentLevel + 1);

            var i = 0;
            foreach (var item in collection)
            {
                var innerPropertyName = $"[{i}]";

                if (item is Exception)
                {
                    var innerException = (Exception)item;
                    AppendException(
                        stringBuilder,
                        innerPropertyName,
                        innerException,
                        innerOptions);
                }
                else
                {
                    AppendValue(
                        stringBuilder,
                        innerPropertyName,
                        item,
                        innerOptions);
                }

                ++i;
            }
        }

    private static void AppendException(
        StringBuilder stringBuilder,
        string propertyName,
        Exception exception,
        ExceptionOptions options)
    {
        var innerExceptionString = ToDetailedString(
            exception, 
            new ExceptionOptions(options, options.CurrentIndentLevel + 1));

        stringBuilder.AppendLine($"{options.Indent}{propertyName} =");
        stringBuilder.AppendLine(innerExceptionString);
    }

    private static string IndentString(string value, ExceptionOptions options)
    {
        return value.Replace(Environment.NewLine, Environment.NewLine + options.Indent);
    }

    private static void AppendValue(
        StringBuilder stringBuilder,
        string propertyName,
        object value,
        ExceptionOptions options)
    {
        if (value is DictionaryEntry)
        {
            DictionaryEntry dictionaryEntry = (DictionaryEntry)value;
            stringBuilder.AppendLine($"{options.Indent}{propertyName} = {dictionaryEntry.Key} : {dictionaryEntry.Value}");
        }
        else if (value is Exception)
        {
            var innerException = (Exception)value;
            AppendException(
                stringBuilder,
                propertyName,
                innerException,
                options);
        }
        else if (value is IEnumerable && !(value is string))
        {
            var collection = (IEnumerable)value;
            if (collection.GetEnumerator().MoveNext())
            {
                AppendCollection(
                    stringBuilder,
                    propertyName,
                    collection,
                    options);
            }
        }
        else
        {
            stringBuilder.AppendLine($"{options.Indent}{propertyName} = {value}");
        }
    }
}

public struct ExceptionOptions
{
    public static readonly ExceptionOptions Default = new ExceptionOptions()
    {
        CurrentIndentLevel = 0,
        IndentSpaces = 4,
        OmitNullProperties = true
    };

    internal ExceptionOptions(ExceptionOptions options, int currentIndent)
    {
        this.CurrentIndentLevel = currentIndent;
        this.IndentSpaces = options.IndentSpaces;
        this.OmitNullProperties = options.OmitNullProperties;
    }

    internal string Indent { get { return new string(' ', this.IndentSpaces * this.CurrentIndentLevel); } }

    internal int CurrentIndentLevel { get; set; }

    public int IndentSpaces { get; set; }

    public bool OmitNullProperties { get; set; }
}

Верхний Совет - Исключения регистрации

Большинство пользователей будут использовать этот код для ведения журнала. Рассмотрите возможность использования Serilog с моим Serilog.Exceptions Пакет NuGet, который также регистрирует все свойства исключения, но делает это быстрее и без отражения в большинстве случаев. Serilog - это очень продвинутая система ведения журнала, которая на момент написания книги была всего лишь яростью.

Верхний совет - отслеживаемые трассировки стека

Вы можете использовать Ben.Demystifier пакет NuGet, чтобы получить отслеживаемые пользователем трассировки стека за ваши исключения или serilog-enrichers-demystify Пакет NuGet, если вы используете Serilog.

Ответ 4

Нет секретного метода. Вероятно, вы можете просто переопределить метод ToString() и построить нужную строку.

Такие вещи, как ErrorCode и Message, являются только свойствами исключения, которые вы можете добавить в нужный вывод строки.


Обновление:. После повторного чтения вашего вопроса и более подробного обсуждения Jason answer, скорее всего, что вы хотите. Переопределение метода ToString() было бы полезно только для созданных вами исключений, а не для уже реализованных. Для добавления этой функции не имеет смысла использовать существующие исключения для класса.

Ответ 5

Для отображения некоторых деталей пользователю вы должны использовать ex.Message. Для отображения разработчикам вам, вероятно, понадобится ex.Message и ex.StackTrace.

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

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

Ответ 6

Каждое левое имя является свойством в Exception. Если вы хотите отобразить поле Сообщение, вы можете сделать

return ex.Message;

Довольно просто. Аналогично, StackTrace может отображаться как ссылка ниже.

Полный пример StackTrace: http://msdn.microsoft.com/en-us/library/system.exception.stacktrace.aspx

и класс исключения: http://msdn.microsoft.com/en-us/library/system.exception.aspx

Ответ 7

Вам, вероятно, придется вручную построить эту строку, объединив различные интересующие вас поля.

Ответ 8

Для людей, которые не хотят вмешиваться в переопределение, этого простого неинтрузивного метода может быть достаточно:

    public static string GetExceptionDetails(Exception exception)
    {
        return "Exception: " + exception.GetType()
            + "\r\nInnerException: " + exception.InnerException
            + "\r\nMessage: " + exception.Message
            + "\r\nStackTrace: " + exception.StackTrace;
    }

Он не отображает детали, специфичные для SQLException, но...

Ответ 9

В визуальной студии такая информация может выводиться визуализатором отладчика.

Я предполагаю, что, поскольку можно написать собственный визуализатор отладчика: http://msdn.microsoft.com/en-us/library/e2zc529c.aspx

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

EDIT:

Вот сообщение о том, где хранятся визуализаторы отладчика: Где найти Microsoft.VisualStudio.DebuggerVisualizers?

Возможно, вы сможете использовать его для своих целей.

Ответ 10

Если вы вызываете объект ToString в Exception, вы получаете имя класса, добавленное сообщением, за которым следует внутреннее исключение, а затем трассировка стека.

className + message + InnerException + stackTrace

Учитывая, что InnerException и StackTrace добавляются только в том случае, если они не являются нулевыми. Кроме того, поля, которые вы упомянули на скриншоте, не являются частью стандартного класса Exception. Да, исключение предлагает публичное свойство под названием "Данные", которое содержит дополнительную информацию, определяемую пользователем об исключении.