Подтвердить что ты не робот

Обнаружение объекта Dispose() из исключения внутри блока 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);
}

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

Ответ 2

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

Что вы хотите сделать, так это обнаружить во время вызова Dispose, что этот метод вызывается в контексте исключения. Когда вы сможете это сделать, разработчикам не нужно будет явно вызывать Commit. Однако проблема заключается в том, что нет надежного способа обнаружить это в .NET. Хотя существуют механизмы для запроса последней порожденной ошибки (например, HttpServerUtility.GetLastError), эти механизмы специфичны для хоста (поэтому у ASP.NET есть другой механизм как формы окон, например). И хотя вы могли бы написать реализацию для конкретной реализации хоста, например реализацию, которая будет работать только в 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 возможен только при полном доверии

Ответ 3

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

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

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

Ответ 4

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

Что вы можете сделать, так это установить флаг в вашем классе базы данных со значением по умолчанию "false" (это нехорошо идти), а в последней строке внутри с помощью блока вы вызываете метод, который устанавливает его в "true", затем в методе Dispose() вы можете проверить, имеет ли флаг "исключение" или нет.

using (var db = new Database())
{
    // Do stuff

    db.Commit(); // Just set the flag to "true" (it 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();
    }
}

Ответ 6

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

Ответ 7

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

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

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

Ответ 8

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