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

Edw*_*vis 21 c# exception-handling using

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

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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

你有什么想法吗?

adr*_*nks 26

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

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

bool isInException = Marshal.GetExceptionPointers() != IntPtr.Zero
                        || Marshal.GetExceptionCode() != 0;
Run Code Online (Sandbox Code Playgroud)

在您的Displose实现中,以确定是否抛出了异常(此处有更多详细信息).

  • 阿德里安,你明白了,我不想做关于模式使用的指导,只是一个技术解决方案,它工作得很漂亮. (8认同)
  • @Steven:正如大多数答案(包括您的答案)都同意的那样,这不是明智的选择。即使这样,最初的问题仍然询问在“ Dispose”方法中是否可以抛出异常,这是我的回答所显示的-即使它确实存在相关的问题。 (2认同)

Ste*_*ven 10

所有其他人已经写了你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);
    } 
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

最后,你将使你的Database课程难以设计,难以维护,你永远不会真正做到正确.

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

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

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


Eri*_*ser 7

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

using (var db = new Database()) 
{
   ... // Do some work
   db.Commit();
}
Run Code Online (Sandbox Code Playgroud)

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