Перехват исключения внутри IDisposable.

В методе IDisposable.Dispose есть ли способ выяснить, генерируется ли исключение?

using (MyWrapper wrapper = new MyWrapper())
{
    throw new Exception("Bad error.");
}

Если в выражении using возникает исключение, я хочу знать об этом, когда объект IDisposable расположен.

Ответ 1

Нет, в .NET Framework нет способа сделать это, вы не можете определить текущее-исключение-которое-бытие-брошено в предложении finally.

См. эту статью в своем блоге, для сравнения с аналогичной моделью в Ruby, она выделяет пробелы, которые, как я думаю, существуют с шаблоном IDisposable,

У Ayende есть трюк, который позволит вам обнаружить, что произошло исключение, однако он не скажет вам, какое это исключение.

Ответ 2

Вы можете расширить IDisposable с помощью метода Complete и использовать шаблон следующим образом:

using (MyWrapper wrapper = new MyWrapper())
{
    throw new Exception("Bad error.");
    wrapper.Complete();
}

Если исключение выбрано внутри оператора using, Complete не будет вызываться до Dispose.

Если вы хотите узнать, какое именно исключение выбрано, подпишитесь на событие AppDomain.CurrentDomain.FirstChanceException и сохраните последнее исключенное исключение в переменной ThreadLocal<Exception>.

Такая модель реализована в классе TransactionScope.

Ответ 3

Невозможно зафиксировать исключение в методе Dispose().

Тем не менее, можно проверить Marshal.GetExceptionCode() в Dispose, чтобы определить, произошло ли исключение, но я бы не стал полагаться на это.

Если вам не нужен класс и вы хотите просто захватить исключение, вы можете создать функцию, которая принимает лямбда, которая выполняется в блоке try/catch, примерно так:

HandleException(() => {
    throw new Exception("Bad error.");
});

public static void HandleException(Action code)
{
    try
    {
        if (code != null)
            code.Invoke();
    }
    catch
    {
        Console.WriteLine("Error handling");
        throw;
    }
}

В качестве примера вы можете использовать метод, который автоматически выполняет транзакцию Commit() или Rollback() транзакции и выполняет некоторые протоколирования. Таким образом, вам не всегда нужен блок try/catch.

public static int? GetFerrariId()
{
    using (var connection = new SqlConnection("..."))
    {
        connection.Open();
        using (var transaction = connection.BeginTransaction())
        {
            return HandleTranaction(transaction, () =>
            {
                using (var command = connection.CreateCommand())
                {
                    command.Transaction = transaction;
                    command.CommandText = "SELECT CarID FROM Cars WHERE Brand = 'Ferrari'";
                    return (int?)command.ExecuteScalar();
                }
            });
        }
    }
}

public static T HandleTranaction<T>(IDbTransaction transaction, Func<T> code)
{
    try
    {
        var result = code != null ? code.Invoke() : default(T);
        transaction.Commit();
        return result;
    }
    catch
    {
        transaction.Rollback();
        throw;
    }
}

Ответ 4

Джеймс, все wrapper могут делать это собственные исключения. Вы не можете заставить пользователя wrapper регистрировать собственные исключения. Это не то, для чего IDisposable. IDisposable предназначен для полудетерминированного выделения ресурсов для объекта. Написание правильного IDisposable кода не является тривиальным.

На самом деле, потребителю класса даже не требуется вызывать метод размещения классов, и они не должны использовать используемый блок, поэтому все это скорее ломается.

Если вы посмотрите на это с точки зрения класса-оболочки, почему он должен заботиться о том, чтобы он присутствовал внутри блока использования, и было исключение? Какие знания это приносит? Является ли угрозой безопасности иметь секретный код третьей стороны для деталей исключения и трассировки стека? Что может wrapper делать, если в расчете есть деление на ноль?

Единственный способ регистрировать исключения, независимо от IDisposable, - это try-catch, а затем повторить бросок в catch.

try
{
    // code that may cause exceptions.
}
catch( Exception ex )
{
   LogExceptionSomewhere(ex);
   throw;
}
finally
{
    // CLR always tries to execute finally blocks
}

Вы упомянули, что вы создаете внешний API. Вам нужно будет обернуть каждый вызов на вашей публичной границе API с помощью try-catch, чтобы зарегистрировать, что исключение исходило из вашего кода.

Если вы пишете открытый API, вы действительно должны читать Руководство по дизайну рамок: соглашения, идиомы и шаблоны для многоразовых библиотек .NET(Microsoft.NET Development Series) - 2-е издание. 1st Edition.


Пока я не защищаю их, я видел, что IDisposable используется для других интересных шаблонов:

  • Семантика транзакций автоматического отката. Класс транзакции отменит транзакцию в Dispose, если она еще не была выполнена.
  • Временные кодовые блоки для ведения журнала. Во время создания объекта была записана метка времени, а на Dispose TimeSpan была рассчитана и записано событие журнала.

* Эти шаблоны могут быть достигнуты с помощью другого слоя косвенных и анонимных делегатов легко и без необходимости перегружать IDisposable семантику. Важно отметить, что ваша обложка IDisposable бесполезна, если вы или член команды забыли использовать ее правильно.

Ответ 5

Вы можете сделать эту покупку, реализуя метод Dispose для класса "MyWrapper". В методе удаления вы можете проверить, есть ли следующее исключение.

public void Dispose()
{
    bool ExceptionOccurred = Marshal.GetExceptionPointers() != IntPtr.Zero
                             || Marshal.GetExceptionCode() != 0;
    if(ExceptionOccurred)
    {
        System.Diagnostics.Debug.WriteLine("We had an exception");
    }
}

Ответ 6

Вместо синтаксического сахара используемого оператора, почему бы просто не реализовать свою собственную логику для этого. Что-то вроде:

try
{
  MyWrapper wrapper = new MyWrapper();

}
catch (Exception e)
{
  wrapper.CaughtException = true;
}
finally
{
   if (wrapper != null)
   {
      wrapper.Dispose();
   }
}

Ответ 7

Не только можно узнать, было ли выбрано исключение при размещении одноразового объекта, вы даже можете получить исключение, которое было брошено внутри предложения finally с малой магией. В моей библиотеке трассировки инструмента ApiChange используется этот метод для отслеживания исключений внутри оператора using. Более подробная информация о том, как это работает, можно найти здесь.

С уважением,  Алоис Краус

Ответ 8

Это приведет к сбоям исключений, вызванных либо непосредственно, либо внутри метода dispose:

try
{
    using (MyWrapper wrapper = new MyWrapper())
    {
        throw new MyException("Bad error.");
    }
}
catch ( MyException myex ) {
    //deal with your exception
}
catch ( Exception ex ) {
    //any other exception thrown by either
    //MyWrapper..ctor() or MyWrapper.Dispose()
}

Но это полагается на них, используя этот код - похоже, вы хотите, чтобы MyWrapper сделал это.

Оператор using должен только убедиться, что Dispose всегда вызывается. Это действительно так:

MyWrapper wrapper;
try
{
    wrapper = new MyWrapper();
}
finally {
    if( wrapper != null )
        wrapper.Dispose();
}

Похоже, что вы хотите:

MyWrapper wrapper;
try
{
    wrapper = new MyWrapper();
}
finally {
    try{
        if( wrapper != null )
            wrapper.Dispose();
    }
    catch {
        //only errors thrown by disposal
    }
}

Я бы посоветовал разобраться с этим в вашей реализации Dispose - вы все равно должны решать любые проблемы во время удаления.

Если вы связываете какой-то ресурс, где вам нужны пользователи вашего API, чтобы каким-то образом его освободить, рассмотрите метод Close(). Ваш dispose должен вызывать его (если он еще не был), но пользователи вашего API также могут сами назвать это, если им нужен более тонкий контроль.

Ответ 9

Если вы хотите оставаться чисто внутри .net, два подхода, которые я предложил бы, - это написать оболочку "try-catch-finally", которая будет принимать делегаты для разных частей или писать "обтекающую" оболочку, которые принимают вызываемый метод вместе с одним или несколькими объектами IDisposable, которые должны быть удалены после завершения.

Оболочка "using-style" может обрабатывать удаление в блоке try-catch и, если какие-либо исключения будут выбраны, либо оберните их в исключение CleanupFailureException, которое будет содержать ошибки удаления, а также любые исключения, которые произошли в основного делегата, или добавить что-то к свойству исключений "Данные" с исходным исключением. Я бы предпочел обернуть вещи в исключение CleanupFailureException, поскольку исключение, которое возникает при очистке, обычно указывает на гораздо более серьезную проблему, чем проблема, возникающая при обработке основной строки; кроме того, исключение CleanupFailureException может быть записано для включения нескольких вложенных исключений (если есть "n" IDisposable objects, могут быть n + 1 вложенные исключения: один из основной и один из каждого Dispose).

Оболочка "try-catch-finally", написанная на vb.net, в то время как вызываемая из С#, может включать некоторые функции, которые в противном случае недоступны в С#, включая возможность ее расширения до "try-filter-catch-fault" -finally ", где код" фильтр "будет выполнен до того, как стек будет размотан от исключения и определит, следует ли исключить исключение, блок" ошибка "будет содержать код, который будет работать только в случае возникновения исключения, но на самом деле не поймать его, и оба блока" fault "и" finally "получат параметры, указывающие на то, какое исключение (если оно есть) произошло во время выполнения" try ", и успешно ли" try" (note, btw, что параметр исключения может быть не равным нулю, даже если основная строка завершена, чистый код С# не смог обнаружить такое условие, но оболочка vb.net могла бы).

Ответ 10

В моем случае я хотел сделать это для регистрации при сбое микросервиса. У меня уже есть using для правильной очистки до того, как завершится закрытие экземпляра, но если это из-за исключения, я хочу понять, почему, и я ненавижу нет для ответа.

Вместо того, чтобы заставить его работать в Dispose(), возможно, сделайте делегат для работы, которую вы должны выполнить, а затем завершите захват исключения. Поэтому в моем журнале MyWrapper я добавляю метод, который принимает Action/Func:

 public void Start(Action<string, string, string> behavior)
     try{
        var string1 = "my queue message";
        var string2 = "some string message";
        var string3 = "some other string yet;"
        behaviour(string1, string2, string3);
     }
     catch(Exception e){
       Console.WriteLine(string.Format("Oops: {0}", e.Message))
     }
 }

Для реализации:

using (var wrapper = new MyWrapper())
  {
       wrapper.Start((string1, string2, string3) => 
       {
          Console.WriteLine(string1);
          Console.WriteLine(string2);
          Console.WriteLine(string3);
       }
  }

В зависимости от того, что вам нужно сделать, это может быть слишком ограничительным, но оно работает для того, что мне нужно.