.NET:如何将异常转换为字符串?

55

当在IDE中进行调试时,如果抛出异常,我有机会查看异常的详细信息:

enter image description here

但是,在代码中,如果我调用exception.ToString(),我将无法看到这些有用的详细信息:

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

但是Visual Studio有一些魔法,它可以将异常复制到剪贴板

输入图像描述

这提供了有用的细节:

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是否有将其转换为字符串的秘密方法?


5
你需要进行反射;我不知道任何内置的代码可以做到这一点。 - SLaks
你想将IDE调试器窗口详细信息输出到某个地方吗?还是只是自定义异常对象输出,在其中只需附加一些异常对象的成员即可。你的问题仍然不是很清楚。 - Zenwalker
1
请记住,异常可能包含异常。有时真正的错误被埋在另一个异常中。因此,无论什么代码都应该在某种循环中,比如 while (innerException != null),这样所有异常都将被报告,而不仅仅是顶层异常。 - Jon Raynor
@zenwalker 我想要一个 ExceptionToString 函数,它具有微软自己的 ExceptionToString 在将字符串复制到剪贴板时所具有的所有有用功能。 - Ian Boyd
1
嗯...你可能已经知道了,但是-2146232060和异常消息中报告的0x80131904是相同的数字。因此,这些信息并没有丢失,只是以不同的方式表示。(在这种情况下,十六进制表示法对于大多数目的来说更加“正确”.) - Daniel Pryden
@DanielPryden 我确实这样做了。但是对于其他通过谷歌搜索错误信息的人来说,指出这一点是有用的——知道这个错误是 HRESULT 的带符号十进制表示形式。 - Ian Boyd
11个回答

56
ErrorCode 是与 ExternalException 相关的,而不是 ExceptionLineNumberNumber 是与 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());
}

3
+1 这种方法比我之前建议的覆盖 ToString() 要好得多。我喜欢扩展方法,因为我们真的想扩展基本异常类的功能,这似乎是一种非常好的方法。 - Matt
@Ian Boyd:反射部分不需要更改。请查看我的编辑,以获取.NET 2.0兼容的答案。 - jason
1
为什么不直接检查所涉及的“异常”是否是“SqlException”,如果是,则将其转换为“SqlException”并读取属性,而不是使用反射?这样会更快。虽然在这种情况下额外的属性很有用,但我认为您不能对每种类型的异常都做出“总是”有用的一般性陈述。 - Daniel Pryden
因为我想要能够处理各种异常。这个SqlException只是这个问题中使用的示例。我还想处理Win32ExceptionSocketExceptionXmlException以及任何ApplicationException的子类。基本上,任何地方ToString()Message会导致有用信息的丢失。幸运的是,在FCL中只有2或3打异常具有附加属性,因此硬编码的异常集将使我只依赖于几十个.NET dlls。 - Ian Boyd
3
你忘记了一些内容。许多时候,异常会有内部异常,这些内部异常实际上几乎比你在最高级别获得的异常更重要。如果InnerException不为null,你需要使用一种递归方法来深入挖掘。 - Pepito Fernandez
显示剩余5条评论

21

我最初尝试了Jason的答案(在顶部),效果还不错,但我还想要:

  • 循环迭代内部异常并对它们进行缩进。
  • 忽略空属性,并增加输出的可读性。
  • 它包括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();

4
我喜欢你的解决方案@Gerben Rampaart,但它需要些许维护。为了避免异常,我用var val = propInfo!=null?propInfo.GetValue(exception, null):null;替换了var val = propInfo.GetValue(exception);。将其更改为私有方法,并使用扩展来调用它:public static string ToStringAllExceptionDetails(this Exception exception) { StringBuilder builderToFill = new StringBuilder(); WriteExceptionDetails(exception, builderToFill, 0); return builderToFill.ToString(); } - Kraken101
嗨@Kraken101,感谢您的扩展。请随意编辑我的答案以反映您所说的内容。 - Gerben Rampaart
1
@GerbenRampaart 哇!这是一个非常棒的解决方案!向您致敬!我太喜欢它了,为您制作了一个新的简化和压缩版本!请查看我的回答!:) 谢谢您!:D (是的,我知道这已经过去10年了!哈哈!) - MaxOvrdrv

14

这个全面的答案处理了以下内容:

  1. 在所有异常中找到的Data集合属性(被接受的答案没有做到这一点)。
  2. 任何其他自定义属性添加到该异常中。
  3. 递归写出InnerException(被接受的答案没有做到这一点)。
  4. 写出包含在AggregateException中的异常集合。

它还以更好的顺序写出异常的属性。它使用C# 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)。如果您正在使用.NET Core 2.1,则此功能已内置。


9

对于不想使用覆盖方法的人来说,这种简单的非侵入式方法可能已经足够了:

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

但是它不会显示你想要的特定于SQLException的细节...


6
没有秘密方法。您可以重写ToString()方法并构建所需的字符串。像ErrorCodeMessage这样的东西只是异常的属性,您可以将其添加到所需的字符串输出中。
更新:重新阅读您的问题并仔细思考后,Jason的答案更有可能是您想要的。回答。重写ToString()方法只对您创建的异常有用,而不是已经实现的异常。对现有异常进行子类化以添加此功能没有意义。

жҳҜзҡ„пјҢжҲ‘ж— жі•зңҹжӯЈиҰҶзӣ–System.Data.SqlClient.SqlException并дҪҝSystem.Data.SqlConnectionдҪҝз”Ёе®ғгҖӮ - Ian Boyd

4

要向用户显示一些细节,您应该使用ex.Message。对于向开发人员显示,您可能需要ex.Messageex.StackTrace

没有什么“秘密”方法,您可以考虑使用Message属性来获得用户友好的消息。

还要注意,在某些情况下,您可能会在捕获的异常中有内部异常,这也很有用于日志记录。


2

2
你可能需要手动构建字符串,通过连接你感兴趣的各个字段来完成。

1

我认为将异常序列化为JSON是一个不错的选择。以下是示例结果:

{
"Errors": [{
        "Source": ".Net SqlClient Data Provider",
        "Number": -1,
        "Class": 20,
        "Server": "111.168.222.70",
        "Message": "A transport-level error has occurred when receiving results from the server. (provider: Session Provider, error: 19 - Physical connection is not usable)"
    }
],
"ClientConnectionId": "b1854037-51e4-4943-94b4-72b7bb4c6ab7",
"ClassName": "System.Data.SqlClient.SqlException",
"Message": "A transport-level error has occurred when receiving results from the server. (provider: Session Provider, error: 19 - Physical connection is not usable)",
"Data": {
    "HelpLink.ProdName": "Microsoft SQL Server",
    "HelpLink.EvtSrc": "MSSQLServer",
    "HelpLink.EvtID": "-1",
    "HelpLink.BaseHelpUrl": "http://go.microsoft.com/fwlink",
    "HelpLink.LinkId": "20476"
},
"InnerException": null,
"HelpURL": null,
"StackTraceString": "   at System.Data.SqlClient.SqlConnection.OnError ... DbExecutionStrategy.Execute[TResult](Func`1 operation)",
"RemoteStackTraceString": null,
"RemoteStackIndex": 0,
"ExceptionMethod": "8\nOnError\nSystem.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\nSystem.Data.SqlClient.SqlConnection\nVoid OnError(System.Data.SqlClient.SqlException, Boolean, System.Action`1[System.Action])",
"HResult": -2146232060,
"Source": ".Net SqlClient Data Provider",
"WatsonBuckets": null
}

0
如果你在 Exception 对象上调用 ToString 方法,你将会得到类名后跟着消息,随后是内部异常和堆栈跟踪的信息。
className + message + InnerException + stackTrace

鉴于此,只有在InnerException和StackTrace不为空时才会添加它们。此外,您在截图中提到的字段不是标准异常类的一部分。是的,异常确实提供了一个名为“Data”的公共属性,其中包含有关异常的其他用户定义信息。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接