Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Hosted-services / Azure

Smooth Transactions with TransactionScope

3.00/5 (1 vote)
3 May 2019CPOL6 min read 6.2K  
Smooth transactions with TransactionScope

Smooth Transactions with TransactionScope

In the real world, we deal with transactions all the time, even though we rarely think of them as such. Things like driving to work or making dinner for the family require a series of things to all go as expected, otherwise bad things can happen. The sooner we recognize when something goes awry, the sooner we can get things back on track. The same holds true in software, but if we aren’t careful, it can be hard to recognize something went wrong until it’s too late and we end up with eggshells in our omelets.

Transactions can be a way to break up operations that target a database to ensure that everything goes as expected, and if not, allow us to easily rollback the changes, keeping our database in a nice clean state. The problem is that they can be a real pain in the butt to implement properly, or at least, I thought that was the case...

Transactions in a Nutshell

So, what is a transaction in the software sense? It’s really just a single unit of work that generally targets a database. If everything in the transaction succeeds, then the changes are committed, but if one or more steps fail, it’s as if nothing happened at all.

Smooth Transactions with TransactionScope

Consider any multi-stage operation, such as placing an order online. Checking out might require a chain of events to succeed (i.e., verifying credit card information, ensuring items are in stock, etc.) and if any of those fail (i.e., you don’t have any money), then the order isn’t going to go through.

It’s a simple enough concept, but implementing it can usually be a pain.

Transacting All The Things Technologies

Implementing transactions will depend on what your backend/data tier looks like. If you are relying on a framework like Entity Framework, you’ll get it for free (e.g. context.SaveChanges()):

C#
using (var context = new WidgetContext())  
{
      context.Widgets.Add(new Widget(){ Name = "Example" });
      // This statement will commit the transaction 
      // (or blow up and rollback if something goes wrong)
      context.SaveChanges();                
}

Likewise, if you are a SQL-heavy shop, you’ll likely just implement them within your stored procedures or queries:

SQL
BEGIN TRANSACTION SomeTransaction

  BEGIN TRY
      INSERT INTO dbo.Widgets ([Name]) 
      VALUES      ('Example')

      UPDATE dbo.Widgets
      SET    [Name] = CONCAT([Name], '_Updated')
      WHERE  [Name] = 'Example'

      -- This will commit the changes
      COMMIT TRANSACTION SomeTransaction
  END TRY
  BEGIN CATCH
      -- If anything goes wrong, this will rollback the changes to the previous state
      ROLLBACK TRANSACTION SomeTransaction
  END CATCH 

In some situations, you might not be so lucky to have all of your data eggs in one basket. If your application requires a bunch of disparate calls to different procedures, raw SQL queries via Dapper, maybe some calls to SSAS, Azure, etc. and you need all of those to work in conjunction, you’ll need transactions.

Traditionally, transactions are implemented at the connection level and passed around to multiple underlying calls:

C#
using (var connection = new SqlConnection("..."))  
{
     using (var transaction = connection.BeginTransaction())
     {
            // Now pass your specific connection (and transaction) 
            // to your other calls to operate on
            DoSomething(connection, transaction);
            DoSomethingElse(connection, transaction); 
     }
}

As you imagine, this can get really complicated, especially considering your transactions are tied to your connection, which if you are interacting with multiple connections, could be complicated. Additionally, you are taking on the responsibility for managing all of the individual connections and transactions, which can muddy up your method signatures (especially if some of these methods are being called in multiple places or don't expect a connection to be passed in).

You might try to work around this problem by moving it to the client and relying on promises to chain the individual operations together but again, that’s somewhat smelly if you want to roll things back (e.g., step 3 failed, so rollback 2 and 1 respectively). You might end up implementing some completely separate set of endpoints to call something to undo the previous steps or persist something on the server via the Session/Cookies to keep track of what’s going on.

At this point, we have three “meh” options:

  • Passing a transaction around different methods (fragile if multiple connections involved, using statements, etc.)
  • Chaining client-side promises for each step of our process (doesn’t accomplish transaction on its own, rollbacks will be dirty/smelly)
  • Handle it all in SQL (not always viable, may require major refactoring)

At this point, you’re probably asking yourself “there’s gotta be a better way”, and as usual: there is.

Scoping with Magic

Smooth Transactions with TransactionScope

These can usually be implemented in SQL queries as but in this post, I’ll primarily be talking about interacting with them in C#, which can normally be quite challenging.

In dealing with a scenario that spans across multiple connections, databases, technologies: things can get pretty hairy. Keeping track of a transaction object, ensuring connections aren’t closed early, and God forbid things start going sideways, but it doesn’t have to be that way. The System.Transactions namespace exposes a class called TransactionScope that makes all this just work like magic.

Basically, you just need to wrap your individual disparate calls within a TransactionScope instance, and it will wrap the underlying calls to your data sources with transactions:

C#
// This will create a new transaction and 
// associate all underlying calls to it
using (var scope = new TransactionScope())  
{
       try
       {
           // It doesn't matter where you are pulling your data from, 
           // if transactions are supported there
           // TransactionScope will take care of applying them within 
           // the TransactionScope block if things go south. 
           GetSomeDataFromSqlServer();
           GetSomeDataFromAzure();
           GetSomeDataFromEntityFramework();
           GetSomeDataFromMongoDB();
       }
       catch (TransactionAbortedException ex)
       {
             // At this point - all of the operations that have already been performed 
             // prior to the failure will be rolled back
       }
}

Hitting Azure? SQL Server? Entity Framework? Doesn’t matter, if a construct exists to perform transactions, it’ll just work. Additionally, if there are specific calls within your transaction that you would want to not include (i.e., always execute), you can just suppress those explicitly:

C#
using (var scope = new TransactionScope())  
{
      // Transact all of these things
      SomeTransactedCall();
      SomeOtherTransactedCall();

      // Some readonly operation that we don't care to transact
      using(var suppressedScope = new TransactionScope(TransactionScopeOption.Suppress))  
      {  
           SomeCallThatIsNotTransacted();
      }  
}

That’s it, it seems to work like magic! But magic isn't something that you always want to happen when you have data that may/may not properly be removed when something goes wrong. So let's take a peer behind the curtains and see what is really going on and how this works!

Peering Behind the Curtains

While the TransactionScope might initially just come across as pure magic, which is kind of is, it's important to know what is actually going on behind the scenes should you run into issues.

The instance of TransactionScope itself actually works by managing an ambient static transaction behind the curtains. This works in conjunction with all the classes that actually provide transaction resources (e.g., SqlConnections, Entity Framework Data Contexts, etc.) and allows those resources to enlist their transactions with the ambient one that is created within the transaction scope:

C#
// This is going to create an ambient transaction behind the scenes and manage it
// throughout the scope of this block (i.e., any operations that support transactions
// with be associated with the underlying ambient transaction
using (var ambientTransaction = new TransactionScope())  
{
      // Since the System.Data.SqlClient assembly supports transactions, 
      // any operations performed by it within the scope of the 
      // TransactionScope object will subscribe to the ambient transaction
      // so that if this call "fails", the ambient transaction will "fail" and 
      // thus rollback any previous operations 
      using (var sqlConnection = new SqlConnection(...))
      {
            // Do some SQL Server operations here
      }
}

As you saw in the last point of the previous section, you can define a specific TransactionScopeOption property for a given transaction scope to use. This is the key component that determines if a transaction should/should not be associated with the underlying ambient transaction (i.e., TransactionScopeOption.Suppress will ignore the tracking of transactions for anything within that scope and thus they will not be added to any existing ambient transactions.

Basically - any connections that are opened within a TransactionScope block will automatically use the underlying ambient transaction unless explicitly told not to. The key phrase here is within as any connections that have already been opened prior to the transaction will not automatically participate. This is very important and may require significant refactoring if you commonly share/pass around connections throughout your application already.

There are quite a few caveats and potential issues that can be associated when using TransactionScope, which will be covered in a future post that elaborates on them and discusses solutions/workarounds for many. But for the most part - it should "just work" in most common scenarios.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)