Перехват исключения внутри IDisposable.Dispose - PullRequest
27 голосов
/ 21 октября 2008

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

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

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

Ответы [ 11 ]

16 голосов
/ 14 января 2013

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

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

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

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

Такой шаблон реализован в TransactionScope классе.

15 голосов
/ 21 октября 2008

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

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

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

5 голосов
/ 20 апреля 2012

невозможно , чтобы получить исключение в методе Dispose().

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

Если вам не нужен класс и вы хотите просто захватить исключение, вы можете создать функцию, которая принимает лямбда-выражения, выполняемые в блоке 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;
    }
}
3 голосов
/ 22 октября 2008

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

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

Если вы посмотрите на него с точки зрения класса-обёртки, почему его должно заботить, чтобы он присутствовал внутри блока using и было исключение? Какие знания это приносит? Есть ли угроза безопасности, когда сторонний код становится доступным к деталям исключений и трассировке стека? Что может 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-е издание .. 1-е издание .


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

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

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

2 голосов
/ 16 марта 2012

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

public void Dispose()
{
    bool ExceptionOccurred = Marshal.GetExceptionPointers() != IntPtr.Zero
                             || Marshal.GetExceptionCode() != 0;
    if(ExceptionOccurred)
    {
        System.Diagnostics.Debug.WriteLine("We had an exception");
    }
}
1 голос
/ 05 апреля 2018

Теперь, в 2017 году, это общий способ сделать это, включая обработку отката для исключений.

    public static T WithinTransaction<T>(this IDbConnection cnn, Func<IDbTransaction, T> fn)
    {
        cnn.Open();
        using (var transaction = cnn.BeginTransaction())
        {
            try
            {
                T res = fn(transaction);
                transaction.Commit();
                return res;
            }
            catch (Exception)
            {
                transaction.Rollback();
                throw;
            }
            finally
            {
                cnn.Close();
            }
        }
    }

и вы называете это так:

        cnn.WithinTransaction(
            transaction =>
            {
                var affected = ..sqlcalls..(cnn, ...,  transaction);
                return affected;
            });
1 голос
/ 13 ноября 2010

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

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

1 голос
/ 21 октября 2008

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

try
{
  MyWrapper wrapper = new MyWrapper();

}
catch (Exception e)
{
  wrapper.CaughtException = true;
}
finally
{
   if (wrapper != null)
   {
      wrapper.Dispose();
   }
}
0 голосов
/ 08 октября 2015

В моем случае я хотел сделать это, чтобы регистрировать, когда происходит сбой микросервиса. У меня уже есть 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);
       }
  }

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

0 голосов
/ 08 августа 2011

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

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

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

...