更好的TypeInitializationException(innerException也为null)

介绍

当用户在NLog的配置中创建错误(如无效的XML)时,We(NLog)会抛出NLogConfigurationException 。 该例外包含描述错误的内容。

但是,如果第一次调用NLog来自静态字段/属性,有时这个NLogConfigurationExceptionSystem.TypeInitializationException “吃掉”。

例如,如果用户有此程序:

 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); 

但是用户会看到:

抛出异常

“将exception详细信息复制到剪贴板”:

System.TypeInitializationException未处理消息:mscorlib.dll中发生未处理的类型’System.TypeInitializationException’的exception附加信息:’TypeInitializationExceptionTest.Program’的类型初始值设定项引发exception。

所以消息消失了!

问题

  1. 为什么innerException不可见? (在Visual Studio 2013中测试)。
  2. 我可以向TypeInitializationException发送更多信息吗? 像一条消息? 我们已经发送了一个innerException。
  3. 我们可以使用另一个例外,还是在Exception上有属性,以便报告更多信息?
  4. 还有另一种方法可以向用户提供(更多)反馈吗?

笔记

  • 当然,我们对用户编写的程序没有任何影响。
  • 我是NLog的维护者之一。
  • 你喜欢自己测试一下吗? Checkout https://github.com/NLog/NLog/tree/TypeInitializationException-tester并启动NLog / src / NLog.netfx45.sln

编辑

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

我只想指出你在这里遇到的根本问题。 您正在调试调试器中的错误,它有一个非常简单的解决方法。 使用工具>选项>调试>常规>勾选“使用托管兼容模式”复选框。 同时解开Just My Code以获取最丰富的调试报告:

在此处输入图像描述

如果勾选了“仅我的代码”,则exception报告的信息量较少,但仍可通过单击“查看详细信息”链接轻松钻取。

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

虽然这是一个非常好的解决方法,但它并不容易发现。 程序员也不会特别喜欢使用旧引擎,旧引擎不支持返回值调试和64位代码的E + C等shiny的新function。 这是否真的是一个错误,新引擎的疏忽或技术限制很难猜测。 这太难看了,所以不要犹豫,将它标记为“bug”,我强烈建议你把它带到connect.microsoft.com。 当它被修复时,每个人都会领先,我至少在我记忆中抓过一次。 通过使用Debug> Windows> Exceptions>勾选当时的CLRexception来深入研究它。

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

我看到的原因是因为入口点类的类型初始化失败。 由于没有初始化类型,因此Type loader无法报告TypeInitializationException的失败类型。

但是如果将logger的Static初始化程序更改为其他类,然后在Entry方法中引用该类。 你将获得TypeInitializationexception的InnerException。

 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。

在此处输入图像描述

希望你现在能够保持Entry point的清洁,并从Main()而不是Entry point class的static属性引导应用程序。

更新1

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

 class Program { private static Lazy logger = new Lazy(() => 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的源代码,看起来它已经实现了,这很有道理。 根据对属性的评论“除非由LogManager.ThrowExceptions中的属性LogManager.ThrowExceptions指定,否则NLog不应抛出exception”。

修复 – 在LogFactory类中,私有方法GetLogger()具有导致exception发生的初始化语句。 如果您通过检查属性ThrowExceptions引入try catch,则可以阻止初始化exception。

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

将这些exception/错误存储在某处也是很好的,这样就可以跟踪Logger初始化失败的原因,因为它们因ThrowException而被忽略。

问题是在首次引用类时发生静态初始化。 在您的Program它甚至在Main()方法之前发生。 所以经验法则 – 避免任何可能在静态初始化方法中失败的代码。 至于你的特殊问题 – 使用懒惰的方法:

 private static Lazy logger = new Lazy(() => LogManager.GetCurrentClassLogger()); static void Main() { logger.Value.Log(...); } 

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

UPDATE

您的图书馆用户最终必须坚持最佳实践。 所以,如果是我,我会保持原样。 如果您真的必须在最后解决它,那么几乎没有选择:

1)不要抛出exception – 永远 – 这是记录引擎中的有效方法,以及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类的实现包装惰性方法(我知道你的Logger类要复杂得多,为了解决这个问题,我们假设它只有一个方法Log ,它需要string className来构造Logger实例。

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

你将摆脱这个问题(真正的初始化将在调用第一个log方法时发生,它是向后兼容的方法); 唯一的问题是你已经添加了另一层(我不希望任何大幅度降级的性能,但一些日志引擎真的进入微优化)。

我现在看到的唯一解决方案是:

使用try catch将静态初始化(fields)移动到静态构造函数

我在之前的评论中说过,我无法重现你的问题。 然后我发现你只是在弹出exception对话框中查找它,它不会显示节目详情链接。

因此,无论如何你都可以获得InnerException ,因为它肯定存在 ,除了Visual Studio因为某些原因没有报告它(可能因为它是入口点类型,因为它被认为是出了问题)。

因此,当您运行测试分支时,您将看到以下对话框:

异常对话框

并且它不显示“ 查看详细信息”链接。

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

System.TypeInitializationException未处理
消息:mscorlib.dll中发生了未处理的“System.TypeInitializationException”类型exception
附加信息:’TypeInitializationExceptionTest.Program’的类型初始化程序引发了exception。

现在,使用“ 确定”按钮关闭对话框,然后转向“ 本地”调试窗口。 这是你会看到的:

当地人的观点

看到? InnerException肯定那里,你找到了一个VS怪癖:-)

System.TypeInitializationException总是或几乎总是从不正确的初始化类的静态成员发生。

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

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

另外我建议你仔细检查你的app.config并确保你没有包含任何错误。

每当静态构造函数抛出exception时,或者每当您尝试访问静态构造函数引发exception的类时,都会抛出System.TypeInitializationException

.NET加载类型时,它必须在您第一次使用该类型之前准备它的所有静态字段。 有时,初始化需要运行代码。 当代码失败时,您将获得System.TypeInitializationException

根据文件

当类初始化程序无法初始化类型时,会创建TypeInitializationException并传递对类型的类初始值设定项引发的exception的引用。 TypeInitializationException的InnerException属性保存基础exception。 TypeInitializationException使用HRESULT COR_E_TYPEINITIALIZATION,其值为0x80131534。 有关TypeInitializationException实例的初始属性值列表,请参阅TypeInitializationException构造函数。

1)对于这种类型的exception, InnerException是不可见的。 Visual Studio的版本无关紧要(确保选中“启用例外助手”选项 – Tools> Options>Debugging>General

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

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

但是,如果第一次调用NLog来自静态字段/属性,有时这个NLogConfigurationException会被System.TypeInitializationException“吃掉”。

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