Better TypeInitializationException (innerException также равно null)
Введение
Когда пользователь создает ошибку в конфигурации NLog (например, недопустимый XML), мы (NLog) бросаем NLogConfigurationException
. Исключение содержит описание, что не так.
Но иногда этот NLogConfigurationException
"съедается" System.TypeInitializationException
, если первый вызов NLog происходит из статического поля/свойства.
Пример
например. если у пользователя есть эта программа:
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);
Но пользователь увидит:
![Throw exception]()
"Скопировать детали исключения в буфер обмена":
Исключение System.TypeInitializationException было необработанным Сообщение: Необработанное исключение типа "Исключение System.TypeInitializationException" произошло в файле mscorlib.dll Дополнительная информация: инициализатор типа для "TypeInitializationExceptionTest.Program" сделал исключение.
Итак, сообщение ушло!
Вопросы
- Почему внутреннее исключение не видно? (тестируется в Visual Studio 2013).
- Можно ли отправить дополнительную информацию в
TypeInitializationException
? Как сообщение? Мы уже отправляем внутреннее исключение.
- Можно ли использовать другое исключение или есть свойства на
Exception
, чтобы сообщалось больше информации?
- Есть ли другой способ дать (более) обратную связь с пользователем?
Примечания
Edit
обратите внимание, что я являюсь хранителем библиотеки, а не пользователем библиотеки. Я не могу изменить код вызова!
Ответы
Ответ 1
Я просто укажу на основную проблему, с которой вы имеете дело. Вы боретесь с ошибкой в отладчике, у него очень простое решение. Используйте "Инструменты" > "Параметры" > "Отладка" > "Основные" > установите флажок "Использовать управляемый режим совместимости". Также отключите Just My Code для наиболее информативного отчета об отладке:
![введите описание изображения здесь]()
Если "Только мой код" отмечен галочкой, отчет об исключении менее информативен, но его можно легко просверлить, щелкнув ссылку "Подробнее".
Имя опции является излишне загадочным. Фактически это говорит о том, что Visual Studio использует более старую версию механизма отладки. Любой, кто использует VS2013 или VS2015, будет иметь эту проблему с новым движком, возможно, VS2012. Также основная причина, по которой этот вопрос не был рассмотрен в NLog раньше.
Хотя это очень хороший способ обхода, это не совсем легко обнаружить. Также программистам не понравилось бы использовать старый движок, блестящие новые функции, такие как отладка возвращаемого значения и E + C для 64-битного кода, не поддерживаются старым движком. Действительно ли это ошибка, надзор или техническое ограничение в новом двигателе трудно догадаться. Это слишком уродливо, поэтому не стесняйтесь называть его "ошибкой", я настоятельно рекомендую вам воспользоваться этим на connect.microsoft.com. Все будут впереди, когда они будут исправлены, я поцарапал голову над этим, по крайней мере, однажды, что я помню. Сверло его с помощью Debug > Windows > Исключения > отмеченные исключения CLR в то время.
Обходной путь для этого очень неудачного поведения обязательно уродлив. Вам нужно отложить повышение исключения до тех пор, пока выполнение программы не будет продвигаться достаточно далеко. Я не очень хорошо знаю вашу кодовую базу, но задерживаю синтаксический разбор конфигурации до тех пор, пока первая команда журнала не позаботится об этом. Или сохраните объект исключения и выбросите его на первую команду журнала, возможно, проще.
Ответ 2
Причина, по которой я вижу, заключается в том, что инициализация типа класса Entry point завершилась неудачно. Поскольку ни один тип не был инициализирован, поэтому загрузчик Type не имеет права сообщать о неудавшемся типе в TypeInitializationException
.
Но если вы измените статический инициализатор регистратора на другой класс, а затем обратитесь к этому классу в методе Entry. вы получите исключение InnerException при исключении 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.
![введите описание изображения здесь]()
Надеюсь, теперь у вас возникнет идея сохранить точку входа чистой и загрузите приложение из Main() вместо статического свойства класса точки входа.
Обновление 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();
}
}
В качестве альтернативы попробуйте Lazy<>
в LogManager для создания экземпляра журнала, чтобы инициализация конфигурации происходила, когда фактически выполнялся первый оператор журнала.
Обновление 2
Я проанализировал исходный код NLog и, похоже, уже реализован и имеет смысл. Согласно комментариям по свойству "NLog не следует бросать исключение, если не указано свойство LogManager.ThrowExceptions
в LogManager.cs".
Исправить. В классе LogFactory частный метод GetLogger() имеет оператор инициализации, который вызывает исключение. Если вы вводите try catch с проверкой свойства ThrowExceptions
, вы можете предотвратить исключение инициализации.
if (cacheKey.ConcreteType != null)
{
try
{
newLogger.Initialize(cacheKey.Name, this.GetConfigurationForLogger(cacheKey.Name, this.Configuration), this);
}
catch (Exception ex)
{
if(ThrowExceptions && ex.MustBeRethrown())
throw;
}
}
Также было бы здорово, чтобы эти исключения/ошибки хранились где-то, чтобы можно было проследить, почему инициализация Logger завершилась неудачно, потому что они были проигнорированы из-за ThrowException
.
Ответ 3
Проблема в том, что статическая инициализация происходит, когда класс сначала ссылается. В Program
это происходит еще до метода Main()
. Так как эмпирическое правило - избегайте любого кода, который может выйти из строя при статическом методе инициализации. Что касается вашей конкретной проблемы - вместо этого используйте ленивый подход:
private static Lazy<Logger> logger =
new Lazy<Logger>(() => LogManager.GetCurrentClassLogger());
static void Main() {
logger.Value.Log(...);
}
Таким образом, инициализация регистратора произойдет (и, возможно, сбой), когда вы впервые получите доступ к регистратору - не в каком-то сумасшедшем статическом контексте.
UPDATE
В конечном итоге бремя пользователя вашей библиотеки придерживаться лучших практик. Так что, если бы это был я, я бы сохранил это как есть. Есть несколько вариантов, хотя, если вам действительно нужно решить это с вашей стороны:
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
(я знаю, что ваш класс Logger
намного сложнее, для этой проблемы допустим, что он имеет только один метод 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);
}
Вы избавитесь от этой проблемы (реальная инициализация произойдет, когда вызывается первый метод журнала, и это подход с обратной совместимостью); только проблема заключается в том, что вы добавили еще один слой (я не ожидаю резкого снижения производительности, но некоторые механизмы ведения журнала действительно в микро-оптимизации).
Ответ 4
В предыдущем комментарии я сказал, что не могу воспроизвести вашу проблему. Тогда мне пришло в голову, что вы искали его только во всплывающем окне исключения, которое не отображает ссылку на детали шоу.
Итак, как вы можете получить в InnerException
в любом случае, потому что он определенно существует, за исключением того, что Visual Studio не сообщает об этом по какой-либо причине (вероятно, потому, что он в точке входа типа как vendettamit понял).
Итак, когда вы запускаете ветвь тестера, вы получаете следующий диалог:
![Диалог исключений]()
И он не показывает ссылку View Details.
Детали исключения копирования в буфер обмена не особенно полезны:
Исключение System.TypeInitializationException было необработанным
Сообщение: Необработанное исключение типа "Исключение System.TypeInitializationException" произошло в mscorlib.dll
Дополнительная информация: инициализатор типа для "TypeInitializationExceptionTest.Program" сделал исключение.
Теперь откройте диалоговое окно с кнопкой ОК и идите в окно отладки локалей. Вот что вы увидите:
![Вид локалей]()
См? InnerException
определенно есть, и вы нашли VS quirk: -)
Ответ 5
Единственное решение, которое я вижу сейчас:
переместить статические инициализации (поля) в статический конструктор с помощью try catch
Ответ 6
System.TypeInitializationException всегда или почти всегда происходит из-за неправильной инициализации статических членов класса.
Вы должны проверить LogManager.GetCurrentClassLogger()
через отладчик. Я уверен, что ошибка произошла внутри этой части кода.
//go into LogManager.GetCurrentClassLogger() method
private static Logger logger = LogManager.GetCurrentClassLogger();
Также я предлагаю вам дважды проверить ваш app.config и убедиться, что вы не включили ничего плохого.
System.TypeInitializationException
генерируется всякий раз, когда статический конструктор генерирует исключение или всякий раз, когда вы пытаетесь получить доступ к классу, в котором статический конструктор создал исключение.
Когда .NET
загружает тип, он должен подготовить все его статические поля до в первый раз, которые вы используете для этого типа. Иногда инициализация требует запуска кода. Когда этот код выходит из строя, вы получаете System.TypeInitializationException
.
В соответствии с docs
Когда инициализатор класса не инициализирует тип, создается TypeInitializationException и передается ссылка на исключение, созданное инициализатором типа. Исключением является свойство InnerException для TypeInitializationException. TypeInitializationException использует HRESULT COR_E_TYPEINITIALIZATION, которая имеет значение 0x80131534. Список исходных значений свойств экземпляра TypeInitializationException см. В конструкторах TypeInitializationException.
1) InnerException
не видно, что это очень типично для этого типа исключения. Версия Visual Studio не имеет значения (убедитесь, что опция "Включить помощник исключения" отмечена - Tools> Options>Debugging>General
)
2) Обычно TypeInitializationException
скрывает реальное исключение, которое можно просмотреть через InnerException. Но приведенный ниже пример теста показывает, как вы можете заполнить внутреннюю информацию об исключении:
public class Test {
static Test() {
throw new Exception("InnerExc of TypeInitializationExc");
}
static void Main(string[] args) {
}
}
Но иногда это исключение NLogConfigurationException "съедается" System.TypeInitializationException, если первый вызов NLog из статическое поле/свойство.
Ничего странного. Кто-то пропустил блок try catch
где-то.
Ответ 7
В моем случае, я добавляю некоторый контент ниже в файл config
, затем NLog дает мне исключение
<entityFramework>
<providers>
<provider invariantName="MySql.Data.MySqlClient" type="MySql.Data.MySqlClient.MySqlProviderServices, MySql.Data.Entity.EF6, Version=6.2.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d" />
</providers>
</entityFramework>
наконец, я обнаружил, что забыл добавить <entityFramework>
в верхний элемент <configSection>
, после добавления в <configSection>
проблема решена.
Ссылочная статья
<configSections>
<!--This is your EF section definition that was added-->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
...
</configSections>