10

I'm trying to work with some ambient transaction scopes (thanks, ), which I haven't really done before, and I'm seeing some ... odd behavior that I'm trying to understand.

I'm trying to enlist in the current transaction scope and do some work after it completes successfully. My enlistment participant implements IDisposable due to some resources it holds. I've got a simple example that exhibits the strange behavior.

For this class,

class WtfTransactionScope : IDisposable, IEnlistmentNotification
{
    public WtfTransactionScope()
    {
        if(Transaction.Current == null)
            return;
        Transaction.Current.EnlistVolatile(this, EnlistmentOptions.None);
    }
    void IEnlistmentNotification.Commit(Enlistment enlistment)
    {
        enlistment.Done();
        Console.WriteLine("Committed");
    }

    void IEnlistmentNotification.InDoubt(Enlistment enlistment)
    {
        enlistment.Done();
        Console.WriteLine("InDoubt");
    }

    void IEnlistmentNotification.Prepare(
        PreparingEnlistment preparingEnlistment)
    {
        Console.WriteLine("Prepare called");
        preparingEnlistment.Prepared();
        Console.WriteLine("Prepare completed");
    }

    void IEnlistmentNotification.Rollback(Enlistment enlistment)
    {
        enlistment.Done();
        Console.WriteLine("Rolled back");
    }

    public void Dispose()
    {
        Console.WriteLine("Disposed");
    }
}

when used as illustrated here

using(var scope = new TransactionScope())
using(new WtfTransactionScope())
{
    scope.Complete();
}

the console output demonstrates the wtf-ness:

Disposed
Prepare called
Committed
Prepare completed

wut.

I'm getting disposed before the transaction completes. This... kind of negates the benefits of hanging with the transaction scope. I was hoping that I'd be informed once the transaction completes successfully (or not) so I could do some work. I was unfortunately assuming that this would happen after scope.Complete() and before I get disposed as we move out of the using scope. This apparently is not the case.

Of course, I could hack it. But I've got other issues which essentially prevent me from doing this. I'll have to scrap and do something else in this eventuality.

Am I doing something wrong here? Or is this expected behavior? Can something be done differently to prevent this from happening??

Community
  • 1
  • 1
  • Is there any way you could remove the Dispose method from your WtfTransactionScope class and instead have it clean up automatically upon scope completion? This makes it more of a 'fire and forget' enlistment and should work as long as you can guarantee that eventually you'll get some notification from the scope. – Dan Bryant Jul 09 '14 at 21:40
  • On a side note, I suspect this is by design; Complete probably marks it as complete, but doesn't actually finish the transaction until exiting the scope. – Dan Bryant Jul 09 '14 at 21:41
  • @DanBryant: The problem is that if there *isn't* an ambient scope, I need to clean up eventually. Can't get rid of it. –  Jul 10 '14 at 12:40
  • if there isn't an ambient scope, can't you just do nothing during your constructor, leaving nothing to be cleaned up? – Dan Bryant Jul 10 '14 at 16:21
  • @DanBryant: Without going into all the details... NO. I *need* to support both. No way around it. Pinky swears. I'm completely not lying. Ten year anniversary coding C#, not a noob mistake. I'm just trying to create the happiest of paths, the widest of pits of success. Was hoping that this would cover situations where there was an ambient transaction, and when there was not. –  Jul 10 '14 at 16:43

1 Answers1

14

This is self-inflicted pain. You violate a very basic rule for IDisposable, an object should only ever be disposed when it is no longer in use anywhere else. It is in use when your using statement calls Dispose(), you handed a reference to your object in the WtfTransactionScope constructor. You cannot dispose it until it is done with it, that necessarily means you have to dispose it after the using statement for the TransactionScope completes and the transaction got committed/rolled-back.

I'll let you fret about making it pretty, but an obvious way is:

using(var wtf = new WtfTransactionScope())
using(var scope = new TransactionScope())
{
    wtf.Initialize();
    scope.Complete();
}

The Prepare completed is merely an unhelpful debug statement. Just delete it.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • 2
    Also, note that Complete does not commit the transaction. It only marks the scope as completed. In fact, the transaction might only be committed when another outer scope completed *and* is disposed. (Ran out of votes for today.) – usr Jul 09 '14 at 22:55
  • Well, I thought about this, however that brings another issue... The scope doesn't exist when I instantiate Wtf, so I can't enlist in it. How, then, would Wtf know that, within its lifetime and its scope, a new transaction is born so that it can enlist with it? –  Jul 10 '14 at 12:42
  • That's what your Initialize() method does :) – Hans Passant Jul 10 '14 at 12:48
  • @HansPassant: Sigh. There goes my pit of success. –  Jul 10 '14 at 12:58