This was originally posted on my blog.
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 the 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.
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()
):
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:
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:
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
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:
// 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:
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:
// 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 is 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.
Top comments (4)
IMHO one should always be careful with transactions that involve multiple resources. They may not scale so well and lead to synchronization/blocking issues. If the scope grows too big, it's better to split it across small scopes and allow intermediate states in your application.
Hi João,
You are spot on here.
I have another post that’s in the pipeline that specifically talks about some of the issues, caveats, and just things anyone should be cognizant of before trying to handle these type of things (or if they should at all). Thanks again for bringing this up, it’s an important one.
One of the best posts about TransactionScope that I ever seen.
Very well explained, fast reading and with some code example.
Congratulations
First let me say I love TransactionScope. But keep in mind database stuff should be done in the database.