Search code examples
.nettransactionscope

How to enlist with a TransactionScope?


Short Version

How do i enlist in an ongoing TransactionScope?

Long Version

If you use a TransactionScope, you can create an "ambient" transaction:

using (TransactionScope scope = new TransactionScope())
{
   //...stuff happens, then you complete...

   // The Complete method commits the transaction. 
   scope.Complete();
}

What is that good for? Well, there are some classes in the .NET framework that know how to check if there is an ambient ongoing transaction by checking the static:

This lets them know that there is a transaction in progress.

For example, if you had some arbitrary database operation that otherwise didn't consider transactions:

void DropStudents()
{
   using (var cmd = connection.CreateCommand())
   {
      cmd.CommandText = "DROP TABLE Students;";
      cmd.ExecuteNonQuery();
   }
}

If you plop that in the middle of a TransactionScope:

using (TransactionScope scope = new TransactionScope())
{
   DropStudents();

   // The Complete method commits the transaction. 
   scope.Complete();
}

Suddnely your ADO.net operations are in a transaction; and can be rolled back.

Automatic BeginTranasction, Commit, and Rollback

The SqlClient library knows to check:

  • System.Transactions.Current

and automatically internally:

  • Begin a database transaction
  • Commit the database transaction when you call Scope.Complete
  • Rollback the database transaction when the scope is rolled back

And it's all just magic.

  • somehow the DbConnection gets a notification to call .Commit
  • and somehow the DbConnection gets a notification to call .Rollback

How do i do that?

I have a class that also has transactions. And rather than forcing the caller to call:

using (IContosoTransaction tx = turboEncabulator.BeginTransaction())
{
   try
   {
      turboEncabulator.UpendCardinalGrammeters();
   }
   catch (Exception ex)
   {
      tx.Rollback();
      throw;
   }
   tx.Commit();
}

It would be nice if they could just call:

turboEncabulator.UpendCardinalGrammeters();

And my code will simply check:

  • System.Transactions.Current

And if there is a transaction in progress, i will:

  • begin a transaction
  • wait for the notification to Commit
  • or wait for the notification to Rollback

But how do i do that?

How do i register myself with an ongoing TransactionScope to get these notifications?


Solution

  • It's not that bad actually.

    1. Check if there is a transaction in progress by seeing if System.Transactions.Transaction.Current is assigned. If there is, Enlist in the transaction:

      //Enlist in any current transactionScope if one is active
      if (System.Transactions.Transaction.Current != null)
         System.Transactions.Transaction.Current.EnlistVolatile(this, EnlistmentOptions.None);
      

      I pass this, as the object that will get the notification callbacks through the IEnlistmentNotification interface that it must implement.

    2. And then you need to implement the four notification methods of IEnlistmentNotification:

      • void Prepare(PreparingEnlistment preparingEnlistment);
      • void Commit(Enlistment enlistment);
      • void InDoubt(Enlistment enlistment);
      • void Rollback(Enlistment enlistment);

    The actual implementations are trivial boilerplate notifications:

    • Prepare: Transaction manager is asking for your vote on whether the transaction is good to be committed:
      • To vote Yes: call preparingEnlistment.Prepared();
      • To vote No: call preparingEnlistment.ForceRollback();
    • Commit: Do your stuff to commit and declare that you are done with your enlistment:
      • enlistment.Done();
    • InDoubt: Notification that the transaction manager lost communication with someone else involved in the transaction. Respond by letting them know you are done with the enlistment:
      • enlistment.Done();
    • Rollback: Notification that the transaction is being rolled back. Do any sort of rollback work, and let them know you're done with the enlistment:
      • enlistment.Done();

    Or more fully

    public void Prepare(PreparingEnlistment preparingEnlistment)
    {
       //The transaction manager is asking for our vote if the transaction
       //can be committed
    
       //Vote "yes" by calling .Prepared:
       preparingenlistment.Prepared();
    
       //Vote "no" by calling .ForceRollback:
       //preparingEnlistment.ForceRollback();
    }
    
    public void Commit(Enlistment enlistment)
    {
       //The transaction is being committed - do whatever it is we do to commit.
    
       //Let them know we're done with the enlistment.
       enlistment.Done();
    }
    
    public void InDoubt(Enlistment enlistment)
    {
       //Do any work necessary when indoubt notification is received.
       //This method is called if the transaction manager loses contact with one or more participants, 
       //so their status is unknown.
       //If this occurs, you should log this fact so that you can investigate later whether any of the 
       //transaction participants has been left in an inconsistent state.
    
       //Let them know we're done with the enlistment.
       enlistment.Done();
    }
    
    public void Rollback(Enlistment enlistment)
    {
       //If any resource manager reported a failure to prepare in phase 1, the transaction manager invokes 
       //the Rollback method for each resource manager and indicates to the application the failure of the commit.
    
       //Let them know we're done with the enlistment.
       enlistment.Done();
    }