更好的 TypeInitializationException(innerException 也为 null)

Better TypeInitializationException (innerException is also null)

简介

当用户在 NLog 配置中创建错误(如无效 XML)时,我们 (NLog) 会抛出一个 NLogConfigurationException。异常包含错误的描述。

但有时如果对 NLog 的第一次调用来自静态 field/property.

,则此 NLogConfigurationExceptionSystem.TypeInitializationException "eaten"

例子

例如如果用户有这个程序:

using System;
using System.Collections.Generic;
using System.Linq;
using NLog;

namespace TypeInitializationExceptionTest
{
    class Program
    {
        //this throws a NLogConfigurationException because of bad config. (like invalid XML)
        private static Logger logger = LogManager.GetCurrentClassLogger();

        static void Main()
        {
            Console.WriteLine("Press any key");
            Console.ReadLine();
        }
    }
}

并且配置中存在错误,NLog 抛出:

throw new NLogConfigurationException("Exception occurred when loading configuration from " + fileName, exception);

但是用户会看到:

"Copy exception details to the clipboard":

System.TypeInitializationException was unhandled Message: An unhandled exception of type 'System.TypeInitializationException' occurred in mscorlib.dll Additional information: The type initializer for 'TypeInitializationExceptionTest.Program' threw an exception.

所以消息不见了!

问题

  1. 为什么 innerException 不可见? (在 Visual Studio 2013 年测试)。
  2. 我可以向 TypeInitializationException 发送更多信息吗?喜欢留言?我们已经发送了一个 innerException。
  3. 我们可以使用另一个例外吗?或者 Exception 上是否有属性以便报告更多信息?
  4. 是否有其他方式向用户提供(更多)反馈?

注释

编辑:

请注意,我是图书馆的维护者,不是图书馆的用户。 我无法更改调用代码!

我现在看到的唯一解决办法是:

将静态初始化(字段)移动到具有 try catch

的静态构造函数

System.TypeInitializationException 总是或几乎总是由于未正确初始化 class 的静态成员而发生。

您必须通过调试器检查 LogManager.GetCurrentClassLogger()。我确定错误发生在该部分代码中。

//go into LogManager.GetCurrentClassLogger() method
private static Logger logger = LogManager.GetCurrentClassLogger();

此外,我建议您仔细检查 app.config 并确保您没有包含任何错误内容。

System.TypeInitializationException 每当静态构造函数抛出异常时,或者每当您尝试访问静态构造函数抛出异常的 class 时抛出。

.NET 加载类型时,它必须在 您第一次 使用类型。有时,初始化需要 运行 代码。当该代码失败时,您会得到 System.TypeInitializationException.

根据 docs

When a class initializer fails to initialize a type, a TypeInitializationException is created and passed a reference to the exception thrown by the type's class initializer. The InnerException property of TypeInitializationException holds the underlying exception. TypeInitializationException uses the HRESULT COR_E_TYPEINITIALIZATION, that has the value 0x80131534. For a list of initial property values for an instance of TypeInitializationException, see the TypeInitializationException constructors.

1) InnerException 不可见,这是此类异常的典型表现。 Visual Studio 的版本无关紧要(确保选中 "Enable the exception assistant" 选项 - Tools> Options>Debugging>General

2) 通常TypeInitializationException隐藏了真正的异常,可以通过InnerException查看。但是下面的测试示例显示了如何填充内部异常信息:

public class Test {
    static Test() {
        throw new Exception("InnerExc of TypeInitializationExc");
    }
    static void Main(string[] args) {
    }
}

But sometimes this NLogConfigurationException is "eaten" by an System.TypeInitializationException if the first call to NLog is from a static field/property.

没什么奇怪的。有人在某处错过了 try catch 块。

我看到的原因是入口点class的类型初始化失败。由于没有初始化任何类型,因此类型加载器没有任何关于 TypeInitializationException 中失败类型的报告。

但是如果你把logger的Static initializer改成其他的class然后在Entry方法中引用那个class。你会得到 InnerException on TypeInitialization 异常。

static class TestClass
{
    public static Logger logger = LogManager.GetCurrentClassLogger();  
}

class Program
{            
    static void Main(string[] args)
    {
        var logger = TestClass.logger;
        Console.WriteLine("Press any key");
        Console.ReadLine();
    }
}

现在您将得到 InnerException,因为加载了 Entry 类型以报告 TypeInitializationException。

希望您现在有了保持入口点干净的想法,Bootstrap 来自 Main() 的应用程序而不是入口点 class.[=20= 的静态 属性 ]

更新 1

您还可以利用 Lazy<> 来避免在声明时执行配置初始化。

class Program
{
    private static Lazy<Logger> logger = new Lazy<Logger>(() => LogManager.GetCurrentClassLogger());

    static void Main(string[] args)
    {
        //this will throw TypeInitialization with InnerException as a NLogConfigurationException because of bad config. (like invalid XML)
        logger.Value.Info("Test");
        Console.WriteLine("Press any key");
        Console.ReadLine();
    }
}

或者,尝试在 LogManager 中 Lazy<> 进行记录器实例化,以便在实际出现第一个日志语句时进行配置初始化。

更新 2

我分析了NLog的源码,好像已经实现了,很有道理。根据 属性 "NLog should not throw exception unless specified by property LogManager.ThrowExceptions in LogManager.cs" 的评论。

修复 - 在 LogFactory class 中,私有方法 GetLogger() 具有导致异常发生的初始化语句。如果你引入一个带有 属性 ThrowExceptions 检查的 try catch 那么你可以防止初始化异常。

      if (cacheKey.ConcreteType != null)
            {
                try
                {
                    newLogger.Initialize(cacheKey.Name, this.GetConfigurationForLogger(cacheKey.Name, this.Configuration), this);
                }
                catch (Exception ex)
                {
                    if(ThrowExceptions && ex.MustBeRethrown())
                    throw;
                }
            }

此外,将这些 exceptions/errors 存储在某个地方会很棒,这样就可以跟踪记录器初始化失败的原因,因为它们由于 ThrowException 而被忽略。

问题是在首次引用 class 时发生静态初始化。在您的 Program 中,它甚至发生在 Main() 方法之前。因此,根据经验 - 避免任何可能在静态初始化方法中失败的代码。至于您的特定问题 - 请改用惰性方法:

private static Lazy<Logger> logger = 
  new Lazy<Logger>(() => LogManager.GetCurrentClassLogger());

static void Main() {
  logger.Value.Log(...);
}

所以当您第一次访问记录器时,记录器的初始化将会发生(并且可能会失败)——而不是在一些疯狂的静态上下文中。

更新

坚持最佳实践最终是图书馆用户的负担。所以如果是我,我会保持原样。如果你真的必须自己解决它,那么选择很少:

1) 不要抛出异常 - 永远 - 这是日志引擎中的有效方法,以及 log4net 的工作原理 - 即

static Logger GetCurrentClassLogger() {
    try {
       var logger = ...; // current implementation
    } catch(Exception e) {
        // let the poor guy now something is wrong - provided he is debugging
        Debug.WriteLine(e);
        // null logger - every single method will do nothing 
        return new NullLogger();
    }
}

2) 围绕 Logger class 的实现包装懒惰的方法(我知道你的 Logger class 要复杂得多,为了这个问题让我们假设它只有一种方法 Log 并且需要 string className 来构建 Logger 实例。

class LoggerProxy : Logger {
  private Lazy<Logger> m_Logger;
  // add all arguments you need to construct the logger instance
  public LoggerProxy(string className) {
    m_Logger = new Lazy<Logger>(() => return new Logger(className)); 
  }
  public void Log(string message) {
   m_Logger.Value.Log(message);
  }
}

static Logger GetCurrentClassLogger() {
  var className = GetClassName();
  return new LoggerProxy(className);
}

你将摆脱这个问题(真正的初始化将在调用第一个日志方法时发生,并且它是向后兼容的方法);唯一的问题是您添加了另一层(我不希望性能大幅下降,但一些日志记录引擎确实进行了微优化)。

我在之前的评论中说过我无法重现您的问题。然后我想到你 只是 在弹出的异常对话框中寻找它,它不显示 显示详细信息 link .

所以这里是您可以到达 InnerException 的方法,因为 它肯定在那里 ,除了 Visual Studio 由于某种原因没有报告它(可能是因为它是 vendettamit 发现的入口点类型)。

所以,当你 运行 tester 分支时,你会得到以下对话框:

并且它不显示 查看详细信息 link。

将异常详细信息复制到剪贴板也不是特别有用:

System.TypeInitializationException was unhandled
Message: An unhandled exception of type 'System.TypeInitializationException' occurred in mscorlib.dll
Additional information: The type initializer for 'TypeInitializationExceptionTest.Program' threw an exception.

现在,使用 OK 按钮关闭对话框并前往 Locals 调试 window。您将看到以下内容:

看到了吗? InnerException 绝对 那里,你发现了一个 VS 怪癖:-)

我只是指出你在这里处理的潜在问题。您正在与调试器中的错误作斗争,它有一个非常简单的解决方法。使用工具 > 选项 > 调试 > 常规 > 勾选 "Use Managed Compatibility Mode" 复选框。同时取消勾选“仅我的代码”以获得最有用的调试报告:

如果勾选“仅我的代码”,则异常报告的信息量较少,但仍可通过单击 "View Detail" link.

轻松深入了解

选项名称含糊不清。它真正 所做的是告诉Visual Studio 使用旧版本的调试引擎。任何使用 VS2013 或 VS2015 的人都会遇到新引擎的问题,可能是 VS2012。这也是之前 NLog 中没有解决这个问题的基本原因。

虽然这是一个很好的解决方法,但并不容易发现。程序员也不会特别喜欢使用旧引擎,旧引擎不支持闪亮的新功能,如 return 值调试和 64 位代码的 E+C。很难猜测这是新引擎中的一个真正的错误、疏忽还是技术限制。这太丑了所以不要犹豫把它标记为"bug",我强烈建议你把它带到connect.microsoft.com。当它得到修复时,每个人都会领先,我记得至少有一次为此挠头。使用 Debug > Windows > Exceptions > 当时打勾的 CLR Exceptions 对其进行了深入研究。

这种非常不幸的行为的解决方法肯定是丑陋的。您必须延迟引发异常,直到程序执行进展到足够远为止。我不太了解你的代码库,但延迟解析配置直到第一个日志命令应该处理它。或者存储异常对象并在第一个日志命令上抛出它,可能更容易。