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

时间:2010-05-13 20:28:13

标签: 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);
}

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

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

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

你有什么想法吗?

8 个答案:

答案 0 :(得分:26)

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

但是,如果你真的想按照继续使用模式,你可以使用:

bool isInException = Marshal.GetExceptionPointers() != IntPtr.Zero
                        || Marshal.GetExceptionCode() != 0;
在您的Displose实现中

以确定是否抛出了异常(更多详细信息here)。

答案 1 :(得分: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);
    } 
}

当您在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课程难以设计,难以维护,而且你永远不会真正做到正确。

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

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

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

答案 2 :(得分:7)

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

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

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

答案 3 :(得分:2)

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

你可以做的是在你的Database类上设置一个标志,默认值为“false”(这不好),在使用块的最后一行你调用一个方法将它设置为“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();
    }
}

答案 4 :(得分:1)

答案 5 :(得分:1)

您应该在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();
}

答案 6 :(得分:1)

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

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

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

答案 7 :(得分:0)

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