使用块从内部exception中检测Dispose()

我的应用程序中有以下代码:

using (var database = new Database()) { var poll = // Some database query code. foreach (Question question in poll.Questions) { foreach (Answer answer in question.Answers) { database.Remove(answer); } // This is a sample line that simulate an error. throw new Exception("deu pau"); database.Remove(question); } database.Remove(poll); } 

此代码像往常一样触发Database类Dispose()方法,并且此方法自动将事务提交到数据库,但这会使我的数据库处于不一致状态,因为答案将被删除,但问题和轮询不会。

我可以在Dispose()方法中检测到它因为exception而不是结束块的常规结束而被调用,所以我可以自动化回滚吗?

我不想手动添加try … catch块,我的目标是使用using块作为逻辑安全事务管理器,因此如果执行是干净的则提交到数据库,如果发生任何exception则提交回滚。

你有什么想法吗?

正如其他人所说,为此目的使用一次性模式是导致问题的原因。 如果模式对你不利,那么我会改变模式。 通过提交使用块的默认行为,您假设每次使用数据库都会导致提交,但事实并非如此 – 尤其是在发生错误时。 显式提交(可能与try / catch块结合使用)可以更好地工作。

但是, 如果您确实希望按原样使用该模式,则可以使用:

 bool isInException = Marshal.GetExceptionPointers() != IntPtr.Zero || Marshal.GetExceptionCode() != 0; 

在您的Displose实现中,以确定是否已抛出exception( 此处有更多详细信息)。

所有其他人已经写了你的Database类的正确设计应该是什么,所以我不会重复。 但是,我没有看到任何解释为什么你想要的是不可能的。 所以这就是它。

您要做的是在调用Dispose期间检测在exception的上下文中调用此方法。 如果能够这样做,开发人员就不必明确地调用Commit 。 但是,这里的问题是在.NET中无法可靠地检测到这一点。 虽然有查询最后抛出错误的机制(如HttpServerUtility.GetLastError ),但这些机制是特定于主机的(因此ASP.NET有另一种机制,例如Windows窗体)。 虽然您可以为特定的主机实现编写实现,例如只能在ASP.NET下工作的实现,但还有另一个更重要的问题: 如果使用了Database类,或者在exception的上下文中创建了什么? 这是一个例子:

 try { // do something that might fail } catch (Exception ex) { using (var database = new Database()) { // Log the exception to the database database.Add(ex); } } 

当您的Database类在Exception的上下文中使用时,如上例所示,您的Dispose方法如何知道它仍然必须提交? 我可以想办法解决这个问题,但它会非常脆弱且容易出错。 举个例子。

在创建Database期间,您可以检查是否在exception的上下文中调用它,如果是这种情况,则存储该exception。 在调用Dispose期间,检查最后抛出的exception是否与缓存的exception不同。 如果它不同,您应该回滚。 如果没有,请提交。

虽然这似乎是一个很好的解决方案,但这个代码示例呢?

 var logger = new Database(); try { // do something that might fail } catch (Exception ex) { logger.Add(ex); logger.Dispose(); } 

在该示例中,您会看到在try块之前创建了一个Database实例。 因此无法正确检测到它无法回滚。 虽然这可能是一个人为的例子,但它显示了在尝试以不需要显式调用Commit的方式设计类时将面临的困难。

最后,您将使您的Database类难以设计,难以维护,并且您永远不会真正做到正确。

正如所有其他人已经说过的那样,需要显式CommitComplete调用的设计将更容易实现,更容易实现,更易于维护,并提供更具可读性的使用代码(例如,因为它看起来像开发人员期望的那样)。

最后请注意,如果您担心开发人员忘记调用此Commit方法:您可以在Dispose方法中进行一些检查以查看是否在未调用Commit情况下调用它并在调试时写入控制台或设置断点。 编写这样的解决方案仍然比试图摆脱Commit更容易。

更新: Adrian写了一个使用HttpServerUtility.GetLastError的替代方法。 正如Adrian所说,你可以使用Marshal.GetExceptionPointers() ,这是一种适用于大多数主机的通用方法。 请注意,此解决方案具有上述相同的缺点,并且只有完全信任才能调用Marshal

查看System.Transactions中TransactionScope的设计。 他们的方法要求您在事务范围上调用Complete()来提交事务。 我会考虑设计您的数据库类以遵循相同的模式:

 using (var db = new Database()) { ... // Do some work db.Commit(); } 

您可能希望在Database对象之外引入事务的概念。 如果消费者想要使用您的类并且不想使用事务并且所有内容都自动提交,会发生什么?

简而言之:我认为这是不可能的,但是

你可以做的是在你的Database类上设置一个标志,默认值为“false”(这不好),在使用块的最后一行你调用一个方法将它设置为“true”,然后在Dispose中()方法你可以检查标志是否“有exception”。

 using (var db = new Database()) { // Do stuff db.Commit(); // Just set the flag to "true" (it's good to go) } 

和数据库类

 public class Database { // Your stuff private bool clean = false; public void Commit() { this.clean = true; } public void Dispose() { if (this.clean == true) CommitToDatabase(); else Rollback(); } } 

您应该在try / catch中包装using块的内容并在catch块中回滚事务:

 using (var database = new Database()) try { var poll = // Some database query code. foreach (Question question in poll.Questions) { foreach (Answer answer in question.Answers) { database.Remove(answer); } // This is a sample line that simulate an error. throw new Exception("deu pau"); database.Remove(question); } database.Remove(poll); } catch( /*...Expected exception type here */ ) { database.Rollback(); } 

正如Anthony指出的那样,问题是在这种情况下你使用using子句的错误。 IDisposable范例旨在确保无论方案的结果如何都清理对象的资源(因此,为什么exception,返回或其他事件使得使用块仍然触发Dispose方法)。 但是你已经将它重新用于表示不同的东西,以提交交易。

我的建议将与其他人一样,并使用与TransactionScope相同的范例。 开发人员必须在事务结束时(在使用块关闭之前)显式调用Commit或类似方法,以明确说明事务是好的并准备好提交。 因此,如果exception导致执行离开使用块,则此情况下的Dispose方法可以执行回滚。 这仍然适用于范例,因为执行回滚将是一种“清理”Database对象的方式,以使其不会处于无效状态。

设计的这种转变也会使你更容易做你想做的事情,因为你不会为了试图“检测”exception而与.NET的设计作斗争。

您可以从Database类inheritance,然后重写Dispose()方法(确保关闭数据库资源),然后可以引发您可以在代码中订阅的自定义事件。