在 using 块中,如何从异常中检测到 Dispose()?

22

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

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()方法中检测它是因为异常而被调用,而不是常规的关闭块结束,这样我就可以自动回滚了呢?

我不想手动添加try ... catch块,我的目标是使用using块作为一个逻辑安全的事务管理器,如果执行正常则提交到数据库,如果出现任何异常则回滚。

你对此有什么想法吗?

8个回答

30

正如其他人所说,您使用Dispose模式的方式导致了问题。如果该模式对您产生问题,则应更改该模式。通过使提交成为using块的默认行为,您假定每次使用数据库都会提交,这显然不是情况-特别是如果发生错误。明确的提交,可能与try/catch块相结合,会更好。

但是,如果您确实想按照原来的模式使用,可以使用:

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

在您的 Dispose 实现中,要确定是否已引发异常(更多细节请参见此处)。


8
Adrian,你明白了,我不需要有关模式使用方面的辅导,只是需要一个技术解决方案,它非常成功地起作用了。 - Edwin Jarvis
1
请看一下我的回答。我试着解释为什么在 Dispose 方法中检查异常是一个不好的想法。 - Steven
2
@Steven:正如大多数答案(包括你的)所认同的,这不是一个明智的使用模式。即便如此,原问题仍然询问是否有可能在“Dispose”方法中知道是否已抛出异常,这就是我的答案展示的内容——尽管它确实存在相关问题。 - adrianbanks
我正在寻找类似的东西来使用,实际上与您链接的博客在处理事务方面几乎做了相同的事情。这个方法完美地运行,在生产中测试过,并且我很喜欢它。在我看来,最好在一个地方测试回滚事务的需要,而不是要求每个上下文实例都检查回滚的需要或显式调用回滚。我尝试了其他方法,包括反射、线程跟踪和应用程序域事件,但这个方法效果最好 - 实际上是唯一有效的方法。谢谢! :D - Travis J

11

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

你想在调用Dispose时检测到,在异常上下文中调用此方法。如果您能够做到这一点,开发人员就不必显式调用Commit。然而,问题在于在.NET中没有可靠的检测方式。虽然有机制查询最后一个抛出的错误(例如HttpServerUtility.GetLastError),但这些机制是特定于主机的(因此ASP.NET具有与Windows窗体不同的机制)。而且,尽管您可以编写特定于主机实现的实现,例如只能在ASP.NET下运行的实现,但还有另一个更重要的问题:如果您的Database类在异常上下文中使用或创建怎么办?以下是一个示例:

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期间,您可以检查它是否在异常上下文中调用,如果是,则存储该异常。在调用Dispose时,您检查最后一个抛出的异常是否与缓存的异常不同。如果不同,则应回滚。如果相同,则提交。

虽然这似乎是一个不错的解决方案,但以下代码示例呢?

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

在这个例子中,你可以看到在try块之前创建了一个Database实例。因此,它无法正确检测到不应该回滚。虽然这可能是一个人为的例子,但它显示了当试图以不需要显式调用Commit的方式设计类时所面临的困难。
最终,你将会使你的Database类难以设计,难以维护,并且永远无法真正做到完美。
正如其他人已经说过的那样,需要显式调用Commit或Complete的设计将更容易实现,更容易正确使用,更容易维护,并且提供的使用代码更加可读(例如,因为它看起来像开发人员所期望的)。
最后注意一点,如果你担心开发人员忘记调用这个Commit方法:你可以在Dispose方法中进行一些检查,看看是否调用了它而没有调用Commit,并写入控制台或在调试时设置断点。编写这样的解决方案仍然比尝试完全摆脱Commit要容易得多。
更新: Adrian编写了一个有趣的替代方法来使用HttpServerUtility.GetLastError。正如Adrian所指出的,你可以使用Marshal.GetExceptionPointers(),这是一个通用的方法,在大多数主机上都可以工作。请注意,这种解决方案具有上述相同的缺点,并且在完全信任下才能调用Marshal类。

1
如果在异常情况下想要写入数据库,该怎么办? - cjk
1
这取决于模式的用途。例如,我经常使用“using”作为嵌入性能计数器开始/停止事件的便捷方式。但根据异常指针的状态提交或回滚数据库事务通常会引发问题,这没有必要。 - James Dingle
我真的希望.NET包含一个IDisposableEx接口,其中包括一个Dispose(Exception Ex)方法,并且using会将任何发生的异常传递给Dispose方法,以便处理导致程序流转出该方法的任何异常。这将允许在其他情况下难以实现正确语义的情况下进行实现(例如,如果意外异常将程序流从保护读写锁的“写入令牌”的using块中移出,则安全行为不应该是清除锁定,也不应该使其悬空,而是... - supercat
@supercat,这种编程风格应该很少见。如果您的应用程序中有许多catch语句,那么您可能做错了什么。因此,我认为拥有一个IDisposableEx接口不是一个好主意。 - Steven
@Steven:代码应该很少捕获意外异常,但这并不意味着它应该忽略它们并希望调用堆栈中的代码知道该怎么做。如果维护对象不变量需要在执行某个步骤序列时保持锁定,并且执行这些步骤的代码通过异常退出,则无论为什么抛出异常,都不能指望期望的序列已经完全运行,并且应该假定受保护的对象无效,即使调用堆栈中的代码捕获异常并认为一切正常。 - supercat
显示剩余7条评论

7

看一下System.Transactions中TransactionScope的设计。他们的方法要求你在事务范围上调用Complete()来提交事务。我建议你设计Database类时也遵循这个模式。

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

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


2
简而言之,我认为这是不可能的,但是你可以在数据库类上设置一个标志,默认值为“false”(不好进行),并在using块内的最后一行调用一个将其设置为“true”的方法。然后,在Dispose()方法中,您可以检查标志是否“有异常”。
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();
    }
}

不幸的是,这不是一个类的正确设计。Dispose 应该尽可能地少做事情。当你在 Dispose 方法中除了释放资源之外还做其他事情时,Dispose 方法抛出异常的可能性会更高。Dispose 也在 Exception 的上下文中被调用,在这种情况下,你将用新的异常替换原来的异常,使得调试和查找异常的原因更加困难。同样的原因,也不要在 Dispose 中主动回滚数据库事务。只需释放底层事务即可。 - Steven
我猜这个评论应该放在问题上,而不是我的答案上,但无论如何... “Dispose也在异常的上下文中调用”他知道这一点,这就是为什么他想找出是否有异常,而不是再次抛出异常,他会在CommitToDatabase和Rollback周围加上try catch之类的东西 - BrunoLM
我评论了你答案中的代码,但你说得对:我的评论确实更适合放在问题本身上。顺便说一句,如果没有显式的“Commit”方法,尝试这样做时很难正确设计,就像我在我的答案中所解释的那样:https://dev59.com/qHE85IYBdhLWcg3wVR6g#2830501 - Steven

1

你应该在 using 块的内容中包含 try/catch,并在 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();
}

我不想这样做,我想使用“using”作为该操作的辅助工具,并添加其他必要的样板代码。 - Edwin Jarvis
@Augusto - 回滚与 "using" 配合得很好。请查看我的更新答案。 - Joel Coehoorn
3
使用“using”只是一个try{} finally{ o.Dispose() }的简写语法。 将“using”替换为单个try{} catch{ rollback } finally{ dispose }才是正确的做法。 - chilltemp
我在我的回答中尝试解释为什么一个没有显式的CompleteCommit方法的类设计是可能的:https://dev59.com/qHE85IYBdhLWcg3wVR6g#2830501 - Steven


1
如Anthony在上面指出的那样,问题在于您在此场景中使用using子句时存在错误。IDisposable范例旨在确保对象的资源在场景的结果无论如何都会被清理(因此异常、返回或其他使using块仍然触发Dispose方法的事件)。但是,您已经重新定义了它的含义,以提交事务。
我的建议是像其他人所说的那样使用与TransactionScope相同的范例。开发人员应该在事务结束之前(在using块关闭之前)显式调用Commit或类似的方法,以明确表示事务已经准备好提交。因此,如果异常导致执行离开using块,则在这种情况下的Dispose方法可以执行回滚操作。这仍然符合范例,因为执行回滚操作是一种“清理”Database对象的方式,使其不处于无效状态。
这种设计转变也将使您更容易实现想要实现的功能,因为您不必试图“检测”异常来对抗.NET的设计。

0
您可以从 Database 类继承并覆盖 Dispose() 方法(确保关闭 db 资源),然后可以引发自定义事件,您可以在代码中订阅该事件。

为什么Dispose会是虚拟的? - Steven Sudit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接