dateo. Coding Blog

Coding, Tech and Developers Blog

.NET
transaction
entity framework

A primer on transaction management in Entity Framework

Dennis Frühauff on March 7th, 2024

Whenever you are doing work within Entity Framework, there is (almost) no chance that you are doing it without transactions - whether you are aware of it or not. If you are aware of it, you'll likely not need this introduction. If you are not, stay with me for this short primer on that topic.


Transactions are a fundamental part of almost any big database technology that are are using in our day-to-day business. With abstraction layers like Entity Framework though, many developers do not have to deal with them or need not know how they work. And oftentimes this is totally fine. It is when you actually need to leverage their potential that you need some basic knowledge.


This article will not be an in-depth tutorial on transactions and the inner workings of databases. It is merely meant as a brief introduction to the topic, giving you a certain amount of superficial knowledge.


What is a transaction?

A transaction, in a database sense, is an atomic set of operations that is being executed against a database. For example, adding a single product to a table can be part of one transaction. But also adding one product and then another one might be captured in a single transaction. The term atomic refers to the fact that a single transaction is supposed to complete either as a whole or not at all. There cannot be an in-between state where only the first product was successfully inserted and the second was not. It's an all-or-nothing approach.


Transactions in Entity Framework

Entity Framework is nothing more than an abstraction layer to everything that the database is doing for us, trying to make it look easy to perform database operations. In that sense, it can be a blessing to software developers, while being a nightmare to database admins.


Because it is an abstraction, we are usually not made aware of what happens behind the scenes. Now, when it comes to transactions, the implementation details of how they are implemented in the database are provider-specific. Most of the common relational database providers do support transactions. For example, SQLServer, MySQL, SQLite, Oracle, and Postgres support transactions. Entity Framework's in-memory database and the in-memory version of SQLite do not (which can give us a headache in testing scenarios).


In any case, the thing that developers should at least be aware of is that whenever we perform an operation like the following, Entity Framework (or, rather, the provider-specific implementation of it) will implicitly create and commit a transaction for this:


dDbContext.Set<Product>().Add(product1);
dbContext.Set<Product>().Add(product2);
        
await dbContext.SaveChangesAsync();

The result will be that you can not end up with only one of these products in the database but only with both or nothing at all (in case something fails).


Why use transactions then?

As I mentioned, we are usually not made aware that a transaction is being committed in the background, and oftentimes we don't need to know. But there will be some situations in which actively doing work in a transactional work will be helpful:


  • You want to group certain change operations, making sure that only the full change is committed, or nothing in case of failure.
  • You want to have full control over what is happening in case of failure, e.g., roll back your changes or restore a certain save point in between.
  • You want to be able to both change entities and execute stored procedures atomically.
  • You want to link transactions across two or more databases, making sure they succeed or fail together.

Transaction management is your tool in those cases. So let's get you started.


TransactionScope in Entity Framework

The simplest (and recommended) way to get started with transactions in Entity Framework are transaction scopes. They provide an abstraction layer to the actual begin and commit operations of the underlying database:


using (var scope = new TransactionScope(
    TransactionScopeOption.Required,
    transactionOptions: new TransactionOptions() 
    {
       IsolationLevel = IsolationLevel.ReadCommitted
    }))
{
    var product1 = new Product(name1);
    dbContext.Set<Product>().Add(product1);
    await dbContext.SaveChangesAsync();
    scope.Complete();
    var product2 = new Product(name2);
    dbContext.Set<Product>().Add(product2);
}

Okay, what are we looking at? We are using the modern methods of Entity Framework to instruct it to consider the operations a) within the using statement and b) until the call to scope.Complete() to be part of a single transaction.
In this very specific example, please be aware that the second product will not actually end up in the database because the call to complete the operation is made before that. This is essentially the same as not calling SaveChangesAsync after adding the second product.


Please note also that the call to SaveChangesAsync will still be necessary. It is just that because we are within an ambient transaction SaveChangesAsync will not actually go and commit the changes to the database. Instead, it will just mark all of your changes up to this point as a snapshot for later commitment. How does Entity Framework know that we are within an ambient transaction? There is the following property that provides this exact information:


Transaction? currentTransaction = Transaction.Current;

The ambient transaction will also help you to enlist to an already ongoing transaction if that is needed.


The transaction scope itself does not provide you with many more methods.
In case an error happens during the commit operation, this is the place to put your try-catch, though. So please be aware of that.


The highest level of customization of how the underlying transaction should work can be done during the creation of the transaction scope itself. TransactionScopeOption and TransactionOptions give you a set of options and properties that control the behavior of this operation.


Manually starting and committing transactions

If you want (or need) very fine-grained control over the actual transactional work, Entity Framework provides a low-level abstraction for this as well:


await using var transaction = await writeDbContext.Database.BeginTransactionAsync();
var product1 = new Product(name1);
dbContext.Set<Product>().Add(product1);
await dbContext.SaveChangesAsync();
            
var product2 = new Product(name2);
dbContext.Set<Product>().Add(product2);
            
await transaction.CommitAsync();

Here, besides using the using declaration style of C# (no explicit scope by using curly brackets) just to mix things up a little, we are explicitly beginning a transaction on the database level.
Again, to mark things as done, we both need to call SaveChangesAsync on everything that we consider final and then CommitAsync to actually write the values to the database.


Whether you are within an IDbContextTransaction or not, can be checked via


IDbContextTransaction? currentTransaction = dbContext.Database.CurrentTransaction;

It also gives you a few more methods and properties to choose from, e.g.:


bool SupportsSavepoints;
Task RollbackAsync(...);
Task CreateSavePointAsync(...);
Task ReleaseSavepointAsync(...);

The IDbContextTransaction gives you much more control, especially when it comes to error handling and rollback operations, but it is also the most complex to use. Be advised to use it carefully.


Summary

A little bit of knowledge about database transactions and transaction management can be very helpful in certain situations. While we can get away with the simple SaveChangesAsync in many cases, some business processes require performing a set of operations atomically in an all-or-nothing fashion.


While this article is not meant to give you a full-fledged tutorial on this topic, I hope it provides a starting point for future research on this topic.



Please share on social media, stay in touch via the contact form, and subscribe to our post newsletter!

Be the first to know when a new post was released

We don’t spam!
Read our Privacy Policy for more info.

We use cookies on our website to give you the most relevant experience by remembering your preferences and repeat visits. By clicking “Accept All”, you consent to the use of ALL the cookies. However, you may visit "Cookie Settings" to provide a controlled consent.