拦截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用于其他有趣的模式:
- 自动回滚事务语义。 如果尚未提交,事务类将在Dispose上回滚事务。
- 用于记录的定时代码块。 在对象创建期间记录了时间戳,并且在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; });