TransactionScope in Multi-Threaded Applications

Using the TransactionScope class is a terrific way to do implicit transactional programming. It’s incredibly simple, and transactions can span multiple operations across multiple connections.

Basic usage couldn’t be simpler:

using (var ts = new TransactionScope())
{
    // do transactional stuff

    ts.Complete();
}

I was working on an application that accepted a batch of items to be processed asynchronously. The desired behavior was to rollback everything if any individual items failed. I figured this would be a snap with TransactionScope. I wrapped the multi-threaded processing code in a TransactionScope, but it didn’t work. The problem is that the TransactionScope does not transcend application and thread boundaries, so the operations launched on a separate thread were executed outside the scope of the ambient transaction.

There is a relatively simple solution to this problem, though. A DependentTransaction can be created from the current transaction and passed into the sub-processes. Here is a simple example:

public void Parent()
{
    using (var ts = new TransactionScope())
    {
        ThreadPool.QueueUserWorkItem(
            Child,
            Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete);
        ts.Complete();
    }
}

public void Child(object o)
{
    var dtx = o as DependentTransaction;
    using (var ts = new TransactionScope(dtx))
    {
        // do transactional stuff

        ts.Complete();
    }
    dtx.Complete();
}

Author: Adam Prescott

I'm enthusiastic and passionate about creating intuitive, great-looking software. I strive to find the simplest solutions to complex problems, and I embrace agile principles and test-driven development.

9 thoughts on “TransactionScope in Multi-Threaded Applications”

  1. i am also looking for same thing but wont able to roll back

    using (var _transactionScope = new TransactionScope())
    {

    try
    {
    var tt = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete);
    var taskArray =
    wbisid.Select(query =>
    Task.Factory.StartNew(() => //Task.Factory.StartNew for .NET 4.0
    ExecuteQuery(connectionString, query, tt)))
    .ToArray();

    Task.WaitAll(taskArray);

    _transactionScope.Complete();

    }

    catch (Exception)
    {
    _transactionScope.Dispose();
    //tran.Rollback();
    throw;

    }
    }

    public void ExecuteQuery(string connectionString, int w, DependentTransaction t)
    {

    try
    {
    using (TransactionScope scope = new TransactionScope(t))
    {

    DataTable dataTable = null;
    if (w == 5)
    {
    throw new Exception(“F”);
    }
    using (var connection = new SqlConnection(connectionString))
    {
    connection.Open();

    SqlCommand com = new SqlCommand(“dbo.T1”, connection);
    com.ExecuteNonQuery();
    connection.Close();

    }
    scope.Complete();
    }

    }
    catch (Exception)
    {
    //t.Dispose();
    throw;
    }

    }

    how to use transactionscoe using task

  2. What you have looks generally correct to me. The flaw that I see is you’re creating one DependentClone to be passed to all of your tasks. Each separate thread (Task) should have its own clone.

    Try changing this:

    var t = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete);
    var taskArray = wbisid.Select(
    	q => Task.Factory.StartNew(() => ExecuteQuery(connectionString, q, t)))
    	.ToArray();
    

    To this:

    var taskArray = wbisid.Select(
    	q => {
    		var t = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete);
    		return Task.Factory.StartNew(() => ExecuteQuery(connectionString, q, t));
    	}).ToArray();
    
  3. thanks but now i am getting another error

    at System.Transactions.TransactionStateAborted.CreateAbortingClone(InternalTransaction tx)
    at System.Transactions.DependentTransaction..ctor(IsolationLevel isoLevel, InternalTransaction internalTransaction, Boolean blocking)
    at System.Transactions.Transaction.DependentClone(DependentCloneOption cloneOption)
    at System.Transactions.TransactionScope.SetCurrent(Transaction newCurrent)
    at System.Transactions.TransactionScope.PushScope()
    at System.Transactions.TransactionScope.Initialize(Transaction transactionToUse, TimeSpan scopeTimeout, Boolean interopModeSpecified)
    at System.Transactions.TransactionScope..ctor(Transaction transactionToUse)
    at Scheduler.Form1.ExecuteQuery(String connectionString, Int32 w, DependentTransaction t) in D:\Test Projects\Scheduler\Scheduler\Form1.cs:line 106

    1. Yes, this error makes sense. You cannot create a new dependent clone from a transaction that’s been aborted, and the transaction becomes aborted as soon as one of your queries fails.

      You have a choice for how to handle the creation of dependent clones. One option is to create all the clones upfront and then pass one to each new Task. Another is to check the state of the transaction before creating each new clone.

      Something like this might work:

      var clones = wbisid.Select(x => Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete)).ToArray();
      var i = 0;
      var tasks = wbisid.Select(q => Task.Factory.StartNew(() => ExecuteQuery(connectionString, q, clones[i++]))).ToArray();
      
      1. I am having Transaction Scope on top of Tasks. Here is my code: I am unable to rollback. Please see you can help. Thanks.

        private const string connectionString = @”Data Source=xxxxxx;Initial Catalog=xxxxx;Integrated Security=SSPI”;
        static void Main(string[] args)
        {
        try
        {
        // This method is a new one uses ‘DependentCloneOption.BlockCommitUntilComplete’, it’s working fine in positive test case
        // In negative test case, it fails and aborting the transaction but I am unable to rollback it.
        CloneTxnMthod();
        }
        catch (Exception ex)
        {
        Console.WriteLine(ex.Message);
        }
        Console.WriteLine(Environment.NewLine + “End”);
        Console.ReadLine();
        }

        private static void CloneTxnMthod()
        {
        List tasks = new List();
        using (var scope = new TransactionScope())
        {
        try
        {
        ThreadPool.QueueUserWorkItem(Child, Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete));
        }
        catch (AggregateException ae)
        {
        Transaction.Current.Rollback();
        string exceptionMessage = string.Empty;
        foreach (var e in ae.Flatten().InnerExceptions)
        {
        exceptionMessage = $”{exceptionMessage}{Environment.NewLine}{e.Message}”;
        }
        throw new Exception(exceptionMessage);
        }
        catch(Exception ex)
        {
        Transaction.Current.Rollback();
        throw new Exception(ex.Message);
        }
        scope.Complete();
        }
        }

        rivate static void Child(object o)
        {
        var dtx = o as DependentTransaction;
        using (var ts = new TransactionScope(dtx))
        {
        try
        {
        CallMethods();
        ts.Complete();
        dtx.Complete();
        }
        catch
        {
        ts.Dispose();
        //string exceptionMessage = string.Empty;
        //foreach (var e in ae.Flatten().InnerExceptions)
        //{
        // exceptionMessage = $”{exceptionMessage}{Environment.NewLine}{e.Message}”;
        //}
        //throw;
        }

        }
        }

        private static void CallMethods()
        {
        try
        {
        int totalWorkers = 25;
        List workers = new List(totalWorkers);
        for (int i = 0; i runOne(id));

        }

        private static void runOne(string id)
        {
        //try
        //{
        using (SqlConnection conn1 = new SqlConnection(connectionString))
        {
        SqlCommand cmd = null;
        if (id.Equals(“20”))
        {
        // This if statement is a negative test case with an invalid table. This ‘TRANSTESTING1’ doesn’t exist
        cmd = new SqlCommand(“INSERT INTO TRANSTESTING1 VALUES(” + id + “,’test” + id + “‘)”);
        }
        else
        {
        cmd = new SqlCommand(“INSERT INTO TRANSTESTING VALUES(” + id + “,’test” + id + “‘)”);
        }
        cmd.Connection = conn1;
        cmd.Connection.Open();
        cmd.ExecuteNonQuery();
        }
        //}
        //catch
        //{
        // throw;
        //}
        }

Leave a comment