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.
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" });
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'
COMMIT TRANSACTION SomeTransaction
END TRY
BEGIN CATCH
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())
{
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:
using (var scope = new TransactionScope())
{
try
{
GetSomeDataFromSqlServer();
GetSomeDataFromAzure();
GetSomeDataFromEntityFramework();
GetSomeDataFromMongoDB();
}
catch (TransactionAbortedException ex)
{
}
}
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())
{
SomeTransactedCall();
SomeOtherTransactedCall();
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:
using (var ambientTransaction = new TransactionScope())
{
using (var sqlConnection = new SqlConnection(...))
{
}
}
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.
CodeProject