拦截IDisposable.Dispose中的exception

IDisposable.Dispose方法中有一种方法可以判断是否抛出exception?

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

如果在using语句中抛出exception,我想在处理IDisposable对象时知道它。

,在.Net框架中没有办法做到这一点,你无法弄清楚当前exception – 在finally子句中抛出了什么。

在我的博客上看到这篇文章 ,为了与Ruby中的类似模式进行比较,它突出了我认为存在于IDisposable模式中的差距。

Ayende有一个技巧,可以让你检测到发生的exception ,但是,它不会告诉你它是哪个exception。

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

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

如果在using语句中抛出exception,则在Dispose之前不会调用Complete

如果您想知道抛出了什么确切的exception,那么订阅AppDomain.CurrentDomain.FirstChanceException事件并在ThreadLocal变量中存储最后抛出的exception。

这种模式在TransactionScope类中实现。

无法Dispose()方法中捕获Exception。

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

如果您不需要类并且只想捕获Exception,则可以创建一个接受在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(IDbTransaction transaction, Func code) { try { var result = code != null ? code.Invoke() : default(T); transaction.Commit(); return result; } catch { transaction.Rollback(); throw; } } 

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

事实上,该类的消费者甚至不需要调用您的类dispose方法,也不需要使用using块,所以这一切都破坏了。

如果从包装类的角度来看它,它为什么要关心它是否存在于一个使用块中并且有一个例外? 这会带来什么知识? 让第三方代码知道exception细节和堆栈跟踪是否存在安全风险? 如果在计算中存在除零值, wrapper可以做什么?

无论IDisposable如何,记录exception的唯一方法是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的公共边界处包装每个调用,以便记录exception来自您的代码。

如果您正在编写公共API,那么您真的应该阅读框架设计指南:可重用.NET库的约定,惯用法和模式(Microsoft .NET开发系列) – 第2版 。 第1版 。


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

  1. 自动回滚事务语义。 如果尚未提交,事务类将在Dispose上回滚事务。
  2. 用于记录的定时代码块。 在对象创建期间记录了时间戳,并且在Dispose上计算TimeSpan并写入日志事件。

*这些模式可以通过另一层间接和匿名委托轻松实现,而无需重载IDisposable语义。 重要的是,如果您或团队成员忘记正确使用它,您的IDisposable包装器将毫无用处。

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

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

而不是使用using语句的语法糖,为什么不为此实现自己的逻辑。 就像是:

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

不仅可以找出在处理一次性对象时是否抛出exception,您甚至可以通过一点魔法将手伸向finally子句中抛出的exception。 ApiChange工具的My Tracing库使用此方法跟踪using语句中的exception。 更多信息如何工作可以在这里找到。

你的,Alois Kraus

这将捕获直接或在dispose方法内部抛出的exception:

 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的用户也可以自己调用它。

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

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

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

就我而言,我想在微服务崩溃时进行记录。 我已经在实例关闭之前就已经using了正确的清理,但如果这是因为exception,我想知道为什么,我讨厌没有答案。

不要试图让它在Dispose() ,也许为你需要做的工作制作一个委托,然后将exception捕获包装在那里。 所以在我的MyWrapper记录器中,我添加了一个采用Action / Func的方法:

  public void Start(Action 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); } } 

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

现在,在2017年,这是执行此操作的通用方法,包括处理exception的回滚。

  public static T WithinTransaction(this IDbConnection cnn, Func 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; });