Обнаружение Dispose () из исключения внутри блока using - PullRequest
21 голосов
/ 14 мая 2010

В моем приложении указан следующий код:

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);
}

Этот код запускает метод Dispose () класса Database, как обычно, и этот метод автоматически фиксирует транзакцию в базе данных, но это оставляет мою базу данных в несогласованном состоянии, поскольку ответы удаляются, а вопрос и опрос - нет.

Есть ли какой-нибудь способ, которым я могу обнаружить в методе Dispose (), что он вызывается из-за исключения вместо обычного завершения закрывающего блока, поэтому я могу автоматизировать откат?

Я не хочу вручную добавлять блок try ... catch, моя цель - использовать блок using в качестве логического безопасного менеджера транзакций, чтобы он фиксировал базу данных, если выполнение было чистым, или откат, если возникло исключение произошло.

У вас есть какие-нибудь мысли по этому поводу?

Ответы [ 8 ]

26 голосов
/ 14 мая 2010

Как уже говорили другие, использование вами шаблона Disposable для этой цели является причиной проблем. Если шаблон работает против вас, то я бы изменил шаблон. Делая фиксацию поведением по умолчанию для блока using, вы предполагаете, что каждое использование базы данных приводит к фиксации, что явно не так, особенно если происходит ошибка. Явный коммит, возможно, в сочетании с блоком try / catch будет работать лучше.

Однако, , если вы действительно хотите сохранить использование шаблона как , вы можете использовать:

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

в вашей реализации Displose, чтобы определить, было ли выброшено исключение (подробнее здесь ).

10 голосов
/ 14 мая 2010

Все остальные уже написали, каким должен быть правильный дизайн вашего класса 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();
}

В примере вы видите, что экземпляр Database создается перед блоком try. Поэтому он не может правильно определить, что он не может откатиться. Хотя это может быть надуманным примером, он показывает трудности, с которыми вы столкнетесь при попытке создать свой класс таким образом, чтобы не требовался явный вызов Commit.

В конце концов, вы сделаете свой класс Database трудным для разработки, сложным в обслуживании, и вы никогда не сделаете это правильно.

Как уже говорили другие, дизайн, который требует явного вызова Commit или Complete, будет проще в реализации, легче понять, проще в обслуживании и даст код использования, который будет более читабельным (например, , потому что это выглядит так, как ожидают разработчики).

Последнее замечание, если вы беспокоитесь о том, что разработчики забыли вызвать этот метод Commit: вы можете выполнить некоторую проверку в методе Dispose, чтобы увидеть, вызывается ли он без Commit, и записать в консоль. или установите точку останова во время отладки. Кодировать такое решение было бы намного проще, чем пытаться вообще избавиться от Commit.

Обновление: Адриан написал интересную альтернативу использованию HttpServerUtility.GetLastError. Как отмечает Адриан, вы можете использовать Marshal.GetExceptionPointers(), который является общим способом, который будет работать на большинстве хостов. Обратите внимание, что это решение имеет те же недостатки, которые описаны выше, и что вызов класса Marshal возможен только при полном доверии

7 голосов
/ 14 мая 2010

Посмотрите на проект для TransactionScope в System.Transactions. Их метод требует, чтобы вы вызывали Complete () в области транзакции, чтобы зафиксировать транзакцию. Я хотел бы рассмотреть вопрос о разработке вашего класса базы данных по той же схеме:

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

Возможно, вы захотите представить концепцию транзакций вне вашего объекта базы данных. Что произойдет, если потребители захотят использовать ваш класс, не захотят использовать транзакции и все автоматически совершат?

2 голосов
/ 14 мая 2010

Короче говоря: я думаю, что это невозможно, НО

Что вы можете сделать, это установить флаг в вашем классе 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();
    }
}
1 голос
/ 14 мая 2010

Как указывает Энтони выше, проблема заключается в том, что вы используете условие использования в этом сценарии. Парадигма IDisposable предназначена для того, чтобы гарантировать, что ресурсы объекта очищаются независимо от результата сценария (таким образом, почему исключение, возврат или другое событие, которое покидает блок using, все еще вызывает метод Dispose). Но вы изменили его значение, чтобы обозначить что-то другое, совершить транзакцию.

Мое предложение будет таким же, как заявили другие, и будет использовать ту же парадигму, что и TransactionScope. Разработчик должен явно вызывать метод Commit или аналогичный метод в конце транзакции (до закрытия блока использования), чтобы явно сказать, что транзакция выполнена и готова к фиксации. Таким образом, если исключение заставляет выполнение покинуть блок using, метод Dispose в этом случае может вместо этого выполнить откат. Это по-прежнему вписывается в парадигму, поскольку выполнение отката было бы способом «очистить» объект базы данных, чтобы он не оставил недопустимое состояние.

Этот сдвиг в дизайне также значительно облегчит выполнение того, что вы хотите сделать, поскольку вы не будете бороться с дизайном .NET, пытаясь "обнаружить" исключение.

1 голос
/ 14 мая 2010

Вы должны обернуть содержимое используемого вами блока в 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();
}
1 голос
/ 14 мая 2010
0 голосов
/ 14 мая 2010

Вы можете наследовать от класса Database, а затем переопределить метод Dispose () (убедитесь, что ресурсы db закрыты), это может вызвать пользовательское событие, на которое вы можете подписаться в своем коде.

...