拦截IDisposable.Dispose中的异常

时间:2008-10-20 22:56:35

标签: c# .net idisposable

IDisposable.Dispose方法中有没有办法弄清楚是否抛出异常?

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

如果在using语句中抛出异常,我希望在处理IDisposable对象时知道它。

11 个答案:

答案 0 :(得分:16)

您可以使用方法IDisposable扩展Complete并使用以下模式:

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

如果在using语句Complete中引发异常,则Dispose之前不会调用AppDomain.CurrentDomain.FirstChanceException

如果您想知道抛出的确切异常,请订阅ThreadLocal<Exception>事件并将最后抛出的异常存储在TransactionScope变量中。

此类模式在{{1}}类中实现。

答案 1 :(得分:15)

,在.Net框架中无法做到这一点,你无法弄清楚finally子句中正在抛出的当前异常。

请参阅此post on my blog,与Ruby中的类似模式进行比较,它突出了我认为存在于IDisposable模式中的差距。

Ayende有一个技巧可以让你detect an exception happened,但是,它不会告诉你它是哪个例外。

答案 2 :(得分:5)

不可能Dispose()方法中捕获异常。

但是,可以检查Dispose中的Marshal.GetExceptionCode()以检测是否确实发生了异常,但我不会依赖它。

如果你不需要一个类并且只想捕获异常,你可以创建一个接受在try / catch块中执行的lambda的函数,如下所示:

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 :(得分:3)

詹姆斯,所有wrapper能做的就是记录它自己的异常。您无法强制wrapper的使用者记录他们自己的异常。这不是IDisposable的用途。 IDisposable用于对象的资源的半确定性释放。编写正确的IDisposable代码并非易事。

事实上,类的使用者甚至不需要调用你的类dispose方法,也不需要使用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。您必须使用try-catch在API的公共边界处包装每个调用,以便记录该异常来自您的代码。

如果你正在撰写公共API,那么你真的应该阅读Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries (Microsoft .NET Development Series) - 2nd Edition .. 1st Edition


虽然我不提倡它们,但我已经看到IDisposable用于其他有趣的模式:

  1. 自动回滚事务语义。如果尚未提交,事务类将在Dispose上回滚事务。
  2. 用于记录的定时代码块。在对象创建期间记录了一个时间戳,并且在Dispose上计算了TimeSpan并写入了一个日志事件。
  3. *这些模式可以通过另一层间接和匿名委托轻松实现,而无需重载IDisposable语义。重要的是,如果您或团队成员忘记正确使用它,您的IDisposable包装器将毫无用处。

答案 4 :(得分:2)

你可以这样做为“MyWrapper”类购买实现Dispose方法。在dispose方法中,您可以检查是否存在如下异常

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

答案 5 :(得分:1)

为什么不为此实现自己的逻辑,而不是using语句的语法糖。类似的东西:

try
{
  MyWrapper wrapper = new MyWrapper();

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

答案 6 :(得分:1)

不仅可以找出在处理一次性对象时是否抛出了异常,你甚至可以用一点魔法来触及在finally子句中抛出的异常。 ApiChange工具的My Tracing库使用此方法跟踪using语句中的异常。更多信息如何找到它here

此致,    Alois Kraus

答案 7 :(得分:1)

现在,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;
            });

答案 8 :(得分:0)

这将捕获直接或在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的实现中处理这个问题 - 无论如何你应该在Disposal中处理任何问题。

如果您正在占用某些资源,而您需要API的用户以某种方式释放它,请考虑使用Close()方法。您的处置也应该调用它(如果它还没有),但是如果需要更好的控制,API的用户也可以自己调用它。

答案 9 :(得分:0)

如果你想完全保留在.net中,我建议的两种方法是编写一个“try-catch-finally”包装器,它将接受不同部分的委托,或者写一个“使用样式”的包装器,接受一个要调用的方法,以及一个或多个IDisposable对象,它们应在完成后处理。

“using-style”包装器可以处理try-catch块中的处理,如果在处理中抛出任何异常,则将它们包装在CleanupFailureException中,这将保留处理失败以及发生的任何异常。主委托,或者使用原始异常向异常的“Data”属性添加内容。我赞成在CleanupFailureException中包装东西,因为清理期间发生的异常通常表明比主线处理中出现的问题大得多;此外,可以编写一个CleanupFailureException以包含多个嵌套异常(如果有'n'个IDisposable对象,则可能有n + 1个嵌套异常:一个来自主线,一个来自每个Dispose)。

在vb.net中编写的“try-catch-finally”包装器,可以从C#调用,可以包含一些在C#中不可用的功能,包括将其扩展为“try-filter-catch-fault”的功能-finally“block”,在从异常中解除堆栈之前执行“filter”代码并确定是否应该捕获异常,“fault”块将包含仅在发生异常时才运行的代码,但是实际上并没有捕获它,“fault”和“finally”块都会收到参数,指示在执行“try”期间发生了什么异常(如果有的话),以及“try”是否成功完成(注意,顺便说一句,即使主线完成,异常参数也可能是非空的;纯C#代码无法检测到这种情况,但vb.net包装器可以)。

答案 10 :(得分:0)

就我而言,我想在微服务崩溃时进行记录。我已经有一个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);
       }
  }

根据您的需要,这可能过于严格,但它可以满足我的需求。