2

The same question has been asked for Java, but I'm interested in a .NET answer.

Consider the following code:

class Program
{
    static void Main()
    {
        try
        {
            RunTransaction();
            // If there was an exception within the transaction,
            // I won't be here anymore. But if the transaction was
            // cancelled without an exception being thrown, I really
            // need to know because I must stop here anyway.
            OtherCode();
        }
        catch (Excexption ex)
        {
            // Log the exception...
            // If an exception was thrown in the transaction scope,
            // this must be logged here. If a "helper" exception was
            // created in the Dispose method, this may be logged, but
            // it won't say much. It just made sure that nothing else
            // was executed in this try block.
        }
    }

    static void RunTransaction()
    {
        using (var trans = new Transaction())
        {
            // An error may occur here and it should be logged.
            throw new Exception();
            // Maybe the scope is simply left without an exception.
            return;
            // Otherwise, the transaction is committed.
            trans.Commit();
        }
    }
}

class Transaction : IDisposable
{
    bool isCommitted;

    public void Commit()
    {
        isCommitted = true;
    }

    public void Dispose()
    {
        if (!isCommitted)
        {
            // Was an exception thrown before this is called?
            // If not, I might consider throwing one here.
            // I can't always throw an exception here because if
            // another exception is already propagated, it would
            // be dropped and the real error cause would not be
            // visible anymore.
        }
    }
}

In the Transaction.Dispose method, how can I know whether an exception was already thrown?

Note that the finally block is not explicitly shown here, but hidden in the using statement which calls the IDisposable.Dispose method, which is shown here.

Update: My background is that I have a transaction wrapper class that behaves a bit like TransactionScope. But TransactionScope is too much magic and doesn't work as expected so I went back to real database transactions. Some methods need a transaction but if they're called from another method that already needed a transaction, the inner "transaction" must "join" the outer transaction instead of requesting a new, nested transaction from the database, which is not supported anywhere I know of. The real code is a bit more complex than my sample, where the inner transaction may be cancelled, effectively ending the transaction. Then, if anything continues to run in the outer transaction, which does not exist anymore, it cannot be rolled back, but will effectively run outside of any transaction! This must be prevented by all means. Thoring an exception in the first place would do it, but the inner transaction can also be cancelled without that. This is what I want to detect in my scope helper class.

ygoe
  • 18,655
  • 23
  • 113
  • 210
  • possible duplicate of [Intercepting an exception inside IDisposable.Dispose](http://stackoverflow.com/questions/220234/intercepting-an-exception-inside-idisposable-dispose) – Chostakovitch Aug 27 '15 at 15:10
  • Press ctrl+alt+e and mark the checkbox for clr exception – Gilad Aug 27 '15 at 15:12
  • 1
    If you want to know if the transaction was cancelled why not have a return value from the method that indicates if it was? – juharr Aug 27 '15 at 15:14
  • 1
    What exactly are you trying to achieve? Some sort of guard to ensure that no code returns from a transaction block without committing it? – odyss-jii Aug 27 '15 at 15:15
  • I have a hard time understanding what you're trying to do with this. You can log the exception outside, if one happens. You shouldn't throw inside `Dispose()`. I may be missing something but what would this buy you? – xxbbcc Aug 27 '15 at 15:17
  • Further you shouldn't care if an exception was thrown before `Dispose` was called. You should only care about the current state of your object and what you need to do to clean up unmanaged resources. – juharr Aug 27 '15 at 15:17
  • I'll update my question to explain what I'm trying to achieve with this. – ygoe Aug 27 '15 at 15:23
  • @LonelyPixel After your update, I'm pretty sure you have an X/Y problem question here. Fiddling with Dispose() won't solve your actual problem which is: what happens to the whole transaction when part of that transaction did work but hasn't been committed? Since you don't have nested transaction, you need to define this outcome (which will be pretty hard, in my opinion). – xxbbcc Aug 27 '15 at 15:31
  • @LonelyPixel Dispose doesn't matter here - I see no difference between a transaction not being committed upon return because someone cancelled it or because an exception interrupted it. (Cancelling is any code path where `Commit` is not called.) Any time when `Commit` is not called, you have the issue of what to do with the "outside" transaction (which is really the same since you don't actually have a nested transaction). – xxbbcc Aug 27 '15 at 15:33
  • @xxbbcc But even when the transaction is cancelled in the inner scope, the outer scope will happily continue to run database queries _outside of a transaction_ if it's not stopped by an exception, which can totally happen if somebody chooses to cancel the scope but not throw an exception. I need to determine this without manually validating the database transaction in every second line. – ygoe Aug 27 '15 at 15:36
  • @LonelyPixel `Dispose` is not the right tool for this - you should have `try..finally` blocks around your code so when a transaction isn't committed, the finally block can throw. That would disrupt the caller in a chain. – xxbbcc Aug 27 '15 at 15:42
  • Still, the finally block should not throw if something else is already flying around, which will get lost if I'm throwing something new. That's what I want to find out in this question. – ygoe Aug 27 '15 at 20:49

2 Answers2

6
public void Dispose()
{
    if (!isCommitted)
    {
        // Was an exception thrown before this is called?
        // If not, I might consider throwing one here.
        // I can't always throw an exception here because if
        // another exception is already propagated, it would
        // be dropped and the real error cause would not be
        // visible anymore.
    }
}

You say you want to throw an exception from Dispose if one wasn't thrown already.

But Dispose should not throw exceptions. From Implementing a Dispose method:

To help ensure that resources are always cleaned up appropriately, a Dispose method should be callable multiple times without throwing an exception.

Also from Dispose Pattern:

AVOID throwing an exception from within Dispose(bool) except under critical situations where the containing process has been corrupted (leaks, inconsistent shared state, etc.).

Users expect that a call to Dispose will not raise an exception.

If Dispose could raise an exception, further finally-block cleanup logic will not execute. To work around this, the user would need to wrap every call to Dispose (within the finally block!) in a try block, which leads to very complex cleanup handlers. If executing a Dispose(bool disposing) method, never throw an exception if disposing is false. Doing so will terminate the process if executing inside a finalizer context.

Community
  • 1
  • 1
dcastro
  • 66,540
  • 21
  • 145
  • 155
  • OP wants to know if exception was fired _before_ `Dispose()`. – 001 Aug 27 '15 at 15:12
  • 2
    @JohnnyMopp Yes, and he wants to know that *because* he wants to throw an exception from `Dispose` if one wasn't thrown already. But since `Dispose` should not throw exceptions, the point is moot. – dcastro Aug 27 '15 at 15:13
  • While the documentation is correct and reasonable, my Dispose method is not to free resources but to implement the `using IDisposable` pattern. I could write `try/finally` as well, which would work just the same, without the `IDisposable` interface. It's just a helper here to keep the code cleaner. C# sugar. – ygoe Aug 27 '15 at 15:29
  • 1
    @LonelyPixel Then I'd say you're misusing this feature. – dcastro Aug 27 '15 at 15:32
  • Maybe. Could you just imagine I was using try/finally instead? The problem remains the same. – ygoe Aug 27 '15 at 15:36
  • @LonelyPixel No, `Dispose` is to clean up unmanaged resources. No one will expect transaction coordinating code inside `Dispose` - you'd be effectively hiding code to save a few lines of code in call sites that use `Transaction`. – xxbbcc Aug 27 '15 at 15:44
  • Then why is `TransactionScope` from Microsoft designed that way? – ygoe Aug 27 '15 at 20:50
0

The question that you need to ask yourself is "why does this object have any business knowing if there was an exception?". And maybe I'm wrong here, but it seems that it is because you perceive that RunTransaction() has something to do with transaction itself, and that's a wrong assumption here, because the code seems to be located outside of Transaction class.

The way you should refactor your code is:

class Transaction : IDisposable
{
    bool isCommitted;
    public void Commit() { ... }
    public void Dispose() { ... }
    public void RunTransaction() { ... }
}

This way if RunTransaction() throws, you could tell.

EDIT: Alternatively, if the code MUST be located outside Transaction class, you can further refactor Transaction to do:

    public void RunTransaction(Action action) { ... }

and invoke it with:

    trans.RunTransaction(() => RunTransaction());
galets
  • 17,802
  • 19
  • 72
  • 101
  • 2
    I agree with your first paragraph but I think your code sample is misguided. `RunTransaction()` is probably just a placeholder for all functions executing transacted code - it's probably not a general wrapper to run some transaction so it shouldn't be part of `Transaction`. – xxbbcc Aug 27 '15 at 15:20
  • It's sample code, simplified. I can't refactor it because the real code it is derived from does not allow refactoring because your assumptions are not valid there anymore. – ygoe Aug 27 '15 at 15:21