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, чтобы сообщалось больше информации?
  • Есть ли другой способ дать (более) обратную связь с пользователем?

Примечания

  • Конечно, мы не имеем никакого влияния на программу, написанную пользователем.
  • Я один из разработчиков NLog.
  • Вам нравится тестировать его самостоятельно? Оформить заказ https://github.com/NLog/NLog/tree/TypeInitializationException-tester и запустить NLog/src/NLog.netfx45.sln

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>