Back to: ASP.NET Core Tutorials For Beginners and Professionals
Transactions in Entity Framework Core (EF Core)
In this article, I will discuss Transactions in Entity Framework Core (EF Core) with Examples. Please read our previous article discussing Entity Framework Core Inheritance (TPH, TPT, and TPC) with Examples. At the end of this article, you will understand the following pointers:
- What are Transactions in Entity Framework Core?
- Different Ways to Implement Transactions in Entity Framework Core
- Example to Understand Transactions in Entity Framework Core
- Automatic Transactions with SaveChanges in EF Core
- Manual Transactions in Entity Framework Core using Database.BeginTransaction()
- Asynchronous Transactions in Entity Framework Core
- Distributed Transaction using TransactionScope in EF Core
- Using Existing Transactions in EF Core
- Choosing The Right Transaction in Entity Framework Core
- Transaction Challenges in Entity Framework Core
What are Transactions in Entity Framework Core?
Transactions in Entity Framework Core (EF Core) are crucial for maintaining data integrity and consistency, especially when performing multiple operations that need to be treated as a single unit of work. EF Core provides several ways to handle transactions, giving us more control over executing and committing database operations.
A Transaction is a set of operations (multiple DML Operations) that ensures that all database operations succeed or fail to ensure data consistency. This means the job is never half done; either all of it is done, or nothing is done.
Different Ways to Implement Transactions in Entity Framework Core:
In Entity Framework Core (EF Core), there are several ways to implement transactions to ensure that a series of operations on a database are executed as a single unit of work. They are as follows:
- Automatic Transaction: The SaveChanges() method automatically wraps the changes in a transaction. If any command fails, it throws an exception, and all changes are rolled back.
- Manual Transaction: You can manually begin, commit, or rollback transactions using the Database property of the DbContext.Database.BeginTransaction().
- Distributed Transaction: The TransactionScope automatically manages transaction lifetimes, allowing operations across multiple contexts or databases to participate in a single transaction.
- Using Existing Transaction: You can attach an existing transaction to the DbContext using DbContext.Database.UseTransaction():
- Asynchronous Transactions: You can also manage transactions asynchronously, which is particularly useful in web applications where you don’t want to block threads.
Example to Understand Transactions in Entity Framework Core:
Let us see examples to understand transactions in Entity Framework Core. So, first, create the following model class:
namespace EFCoreCodeFirstDemo.Entities { public class Student { public int StudentId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } }
Next, modify the context class as follows:
using Microsoft.EntityFrameworkCore; namespace EFCoreCodeFirstDemo.Entities { public class EFCoreDbContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { //Configuring the Connection String optionsBuilder.UseSqlServer(@"Server=LAPTOP-6P5NK25R\SQLSERVER2022DEV;Database=EFCoreDB;Trusted_Connection=True;TrustServerCertificate=True;"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { } public DbSet<Student> Students { get; set; } } }
Migrate and Apply Database Changes:
After defining your entities and configuring the model, you can use EF Core’s migration commands to create or update the database schema based on your model. So, open the Package Manager Console and Execute the add-migration and update-database commands as follows. You can give any name to your migration. Here, I am giving Mig1. The name that you are giving it should not be given earlier.
Automatic Transactions with SaveChanges in EF Core:
By default, when we call the SaveChanges() method, EF Core will automatically wrap our changes in a transaction. This transaction is committed or rolled back automatically if an exception occurs. That means every time we call the SaveChanges() method, EF Core wraps that operation in a transaction. This transaction includes all the changes made to the database context since the last time SaveChanges()was called. The syntax is given below:
The SaveChanges methods automatically begin and commit a transaction if one is not already in progress. For a better understanding, please modify the Program class as follows:
using EFCoreCodeFirstDemo.Entities; namespace EFCoreCodeFirstDemo { public class Program { static async Task Main(string[] args) { try { using var context = new EFCoreDbContext(); Student std1 = new Student() { FirstName = "Pranaya", LastName = "Rout" }; context.Students.Add(std1); // This will automatically use a transaction. context.SaveChanges(); Student std2 = new Student() { FirstName = "Tarun", LastName = "Kumar" }; context.Students.Add(std2); // This will automatically use a new transaction. context.SaveChanges(); Console.WriteLine("Entities are Saved"); Console.Read(); } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); ; } } } }
In this example, the SaveChanges method is used to persist the changes to the database, and the transaction management ensures that all operations are committed or none if an error occurs. It is important to handle exceptions properly to avoid unintended writes to the database and ensure that transactions are always properly completed or rolled back.
Manual Transactions in Entity Framework Core using Database.BeginTransaction()
If you need more control over the transaction, for instance, if you want to include several SaveChanges() calls in one transaction, you can start a transaction manually using Database.BeginTransaction() or Database.BeginTransactionAsync(). That means you can begin a transaction manually using the BeginTransaction method on the database connection or the Database property of your context object. If the transaction is completed successfully, call the Commit method. If any exception occurs, invoke the Rollback method using the transaction object. The following is the syntax:
In Entity Framework Core (EF Core), you can manually control transactions using the Database.BeginTransaction() or Database.BeginTransactionAsync() methods on your DbContext. This is useful when you want to execute multiple operations as a single unit of work, ensuring that all operations succeed or fail. You can roll back all the changes if any part of the transaction fails. Please look at the following example for a better understanding:
using EFCoreCodeFirstDemo.Entities; namespace EFCoreCodeFirstDemo { public class Program { static async Task Main(string[] args) { try { using (var context = new EFCoreDbContext()) { // Begin a new transaction using (var transaction = context.Database.BeginTransaction()) { try { // Perform database operations within the transaction Student std1 = new Student() { FirstName = "Pranaya", LastName = "Rout" }; context.Students.Add(std1); // SaveChanges() but do not commit yet, this will persist changes but hold the commit until // we are sure that all operations succeed context.SaveChanges(); Student std2 = new Student() { FirstName = "Tarun", LastName = "Kumar" }; context.Students.Add(std2); // SaveChanges() but do not commit yet, this will persist changes but hold the commit until // we are sure that all operations succeed context.SaveChanges(); // If everything is fine until here, commit the transaction transaction.Commit(); Console.WriteLine("Entities are Saved"); } catch (Exception ex) { // If an exception is thrown, roll back the transaction transaction.Rollback(); // Handle or throw the exception as needed Console.WriteLine(ex.Message); throw; } } } Console.Read(); } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); ; } } } }
In the above code:
- BeginTransaction() is called on the Database property of the context to start a new transaction.
- Database operations are then performed normally, such as adding an entity and saving changes.
- Commit() is called on the transaction object to commit all the operations as an atomic unit if all operations succeed without throwing exceptions.
- If there is an exception, Rollback() is called to undo all the operations that were part of this transaction.
- It is important to wrap the transaction code within a try-catch block to ensure exceptions are handled, and resources are cleaned up properly.
Asynchronous Transactions in Entity Framework Core
In EF Core 2.1 and later, you can begin a transaction explicitly using the BeginTransactionAsync method for asynchronous operations. That means you can also manage transactions asynchronously, which is particularly useful in web applications where you don’t want to block threads. The syntax is given below:
It’s also a good practice to use async and await for database operations to avoid blocking calls, especially in a web application environment. For a better understanding, please have a look at the following example.
using EFCoreCodeFirstDemo.Entities; namespace EFCoreCodeFirstDemo { public class Program { static async Task Main(string[] args) { try { using (var context = new EFCoreDbContext()) { // Begin a transaction asynchronously await using (var transaction = await context.Database.BeginTransactionAsync()) { try { // Perform database operations within the transaction Student std1 = new Student() { FirstName = "Pranaya", LastName = "Rout" }; context.Students.Add(std1); // SaveChangesAsync() but do not commit yet, this will persist changes but hold the commit until // we are sure that all operations succeed await context.SaveChangesAsync(); Student std2 = new Student() { FirstName = "Tarun", LastName = "Kumar" }; context.Students.Add(std2); // SaveChangesAsync() but do not commit yet, this will persist changes but hold the commit until // we are sure that all operations succeed await context.SaveChangesAsync(); // If everything is fine until here, commit the transaction await transaction.CommitAsync(); Console.WriteLine("Entities are Saved"); } catch (Exception ex) { // If there is any error, roll back all changes await transaction.RollbackAsync(); // Handle or throw the exception as needed Console.WriteLine(ex.Message); throw; } } } Console.Read(); } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); ; } } } }
Distributed Transaction using TransactionScope in EF Core:
In Entity Framework Core, the TransactionScope class can manage transaction boundaries across multiple database contexts or different types of databases. It’s a .NET Framework feature that allows you to run a code block within a transaction without interacting directly with the transaction itself. The syntax to use Transaction Scope in EF Core is given below:
When working with TransactionScope, it is important to note that it relies on the open database connection within the scope. Transactions are automatically promoted to a distributed transaction if necessary (for example, when the operations span multiple databases). For a better understanding, please have a look at the following example:
using EFCoreCodeFirstDemo.Entities; using System.Transactions; namespace EFCoreCodeFirstDemo { public class Program { static async Task Main(string[] args) { try { // Define the scope of the transaction var options = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted, Timeout = TransactionManager.DefaultTimeout // Default is 1 minute }; // Start a new TransactionScope using (var scope = new TransactionScope(TransactionScopeOption.Required, options)) { try { using (var context1 = new EFCoreDbContext()) { // Perform data access using context1 here Student std1 = new Student() { FirstName = "Pranaya", LastName = "Rout" }; context1.Students.Add(std1); context1.SaveChanges(); } // The actual INSERT command is sent to the database here // You can even use another context or database operation here using (var context2 = new EFCoreDbContext()) { // Perform data access using context2 here Student std2 = new Student() { FirstName = "Rakesh", LastName = "Kumar" }; context2.Students.Add(std2); context2.SaveChanges(); } // The actual INSERT command is sent to the database here // Complete the scope here, if everything succeeded // If all operations complete successfully, commit the transaction scope.Complete(); Console.WriteLine("Entities are Saved"); } catch (Exception ex) { // Handle errors and the transaction will be rolled back Console.WriteLine(ex.Message); // The TransactionScope is disposed without calling Complete(), so the transaction will be rolled back // Handle the exception as needed Console.WriteLine(ex.Message); } } // The TransactionScope is disposed here, committing or rolling back the transaction Console.Read(); } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); ; } } } }
In this Example:
- The TransactionScope is created with a specified isolation level and timeout period.
- The TransactionScope is created using a “using” block, ensuring that it is disposed of properly, committing or rolling back the transaction depending on whether the scope.Complete() is called.
- Within the transaction scope, multiple database operations are performed across different contexts.
- Database operations are performed within different contexts but are covered by the same transaction scope.
- If everything executes successfully, call scope.Complete() commits all the operations.
- scope.Complete() indicates that all operations within the scope are completed successfully. If this method is not called, the transaction will be rolled back when the scope is disposed.
- If an exception is thrown, or if scope.Complete() is not called, the transaction will be rolled back, and none of the operations will be persisted.
Using Existing Transaction in EF Core:
You can attach an existing transaction to the DbContext using DbContext.Database.UseTransaction(). The syntax is given below:
In Entity Framework Core (EF Core), if you want to use an existing database transaction across multiple operations or contexts, you can pass the existing DbTransaction to another DbContext instance. This allows you to share the same transaction across multiple contexts, ensuring that all operations commit or rollback together. This can be helpful in scenarios where a transaction is started using the underlying database connection, and you want EF Core to enlist in that transaction.
First, let’s assume you have a method that starts a transaction using the database connection:
public static DbTransaction BeginDatabaseTransaction(EFCoreDbContext context) { var connection = context.Database.GetDbConnection(); if (connection.State != ConnectionState.Open) { connection.Open(); } return connection.BeginTransaction(); }
Now, you can use this DbTransaction in your operations with EF Core:
public static void UseExistingTransaction(EFCoreDbContext context, DbTransaction transaction) { // Enlist in an existing transaction context.Database.UseTransaction(transaction); try { // Perform operations within the transaction Student std1 = new Student() { FirstName = "Rakesh", LastName = "Kumar" }; context.Students.Add(std1); context.SaveChanges(); // You can perform more operations here, and they will all be part of the same transaction // ... // Commit or rollback is controlled outside this method since the transaction was passed in } catch (Exception ex) { // Handle exception // ... Console.WriteLine($"Error: {ex.Message}"); // Note that the responsibility for rolling back the transaction, if needed, lies outside this method } finally { // Detach the context from the transaction after the work is done context.Database.UseTransaction(null); } }
And here is how you can call UseExistingTransaction:
using (var context = new EFCoreDbContext()) { using (var transaction = BeginDatabaseTransaction(context)) { try { // Perform data access using context2 here Student std1 = new Student() { FirstName = "Pranaya", LastName = "Rout" }; context.Students.Add(std1); UseExistingTransaction(context, transaction); // If everything was successful, commit the transaction transaction.Commit(); Console.WriteLine("Entities Added"); } catch (Exception ex) { // If there was an error, roll back the transaction transaction.Rollback(); throw; } } }
The complete example code is given below:
using EFCoreCodeFirstDemo.Entities; using Microsoft.EntityFrameworkCore; using System.Data.Common; using System.Data; namespace EFCoreCodeFirstDemo { public class Program { public static DbTransaction BeginDatabaseTransaction(EFCoreDbContext context) { var connection = context.Database.GetDbConnection(); if (connection.State != ConnectionState.Open) { connection.Open(); } return connection.BeginTransaction(); } public static void UseExistingTransaction(EFCoreDbContext context, DbTransaction transaction) { // Enlist in an existing transaction context.Database.UseTransaction(transaction); try { // Perform operations within the transaction Student std1 = new Student() { FirstName = "Rakesh", LastName = "Kumar" }; context.Students.Add(std1); context.SaveChanges(); // You can perform more operations here, and they will all be part of the same transaction // ... // Commit or rollback is controlled outside this method since the transaction was passed in } catch (Exception ex) { // Handle exception // ... Console.WriteLine($"Error: {ex.Message}"); // Note that the responsibility for rolling back the transaction, if needed, lies outside this method } finally { // Detach the context from the transaction after the work is done context.Database.UseTransaction(null); } } static async Task Main(string[] args) { try { using (var context = new EFCoreDbContext()) { using (var transaction = BeginDatabaseTransaction(context)) { try { // Perform data access using context2 here Student std1 = new Student() { FirstName = "Pranaya", LastName = "Rout" }; context.Students.Add(std1); UseExistingTransaction(context, transaction); // If everything was successful, commit the transaction transaction.Commit(); Console.WriteLine("Entities Added"); } catch (Exception ex) { // If there was an error, roll back the transaction transaction.Rollback(); throw; } } } Console.Read(); } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); } } } }
In the above Example:
- You begin by starting a transaction with the database directly.
- You pass this transaction to the UseExistingTransaction method, which tells EF Core to use this transaction for all operations within the current context.
- You perform your database operations within the try block.
- After the operations, you either commit or roll back the transaction. Note that this decision is taken outside of the UseExistingTransaction method.
- It is important to handle exceptions and ensure the transaction is rolled back in case of an error.
- Finally, make sure to detach the context from the transaction once you’re done by passing null to UseTransaction.
Choosing The Right Transaction in Entity Framework Core:
It’s important to choose the right approach based on the scope and requirements of your transaction. The implicit transaction provided by SaveChanges() might be sufficient for small, single-operation transactions, whereas more complex scenarios might require BeginTransaction() or even TransactionScope. Always ensure that any manual transactions are properly disposed of to avoid resource leaks, and remember that transactions should be as short as possible to avoid locking resources for longer than necessary.
- Scalability and Performance: Nested or distributed transactions can impact performance. TransactionScope may escalate to a distributed transaction if it spans multiple connections, which is heavier on resources.
- Ease of Use: SaveChanges() within a DbContext is the simplest and most recommended for basic operations.
- Flexibility: BeginTransaction() gives you more control and is recommended when you need to span a transaction across multiple operations within a single context or multiple instances of DbContext.
- Cross-Resource Transactions: TransactionScope is suitable for transactions spanning multiple databases or resources like file systems.
- Compatibility: Not all database providers support TransactionScope or distributed transactions.
- Error Handling: Make sure to handle exceptions properly to avoid partial commits and ensure that transactions are rolled back in the event of an error.
Transaction Challenges in Entity Framework Core:
- Avoid Long-Running Transactions: Keeping transactions open for long periods can lock resources and impact the performance of your application and database.
- Handling Exceptions: Always ensure you handle exceptions properly to avoid uncommitted transactions that can cause locks.
- Avoiding Ambient Transactions If Not Needed: TransactionScope can be convenient but can lead to escalated transactions if multiple resource managers get involved.
- Concurrency Considerations: Be aware of the database’s concurrency model (optimistic/pessimistic) and how transactions are handled concurrently.
In the next article, I will discuss Seed Data in Entity Framework Core (EF Core) with Examples. In this article, I try to explain Transactions in Entity Framework Core (EF Core) with Examples. I hope you enjoy this Transactions in EF Core article.
About the Author: Pranaya Rout
Pranaya Rout has published more than 3,000 articles in his 11-year career. Pranaya Rout has very good experience with Microsoft Technologies, Including C#, VB, ASP.NET MVC, ASP.NET Web API, EF, EF Core, ADO.NET, LINQ, SQL Server, MYSQL, Oracle, ASP.NET Core, Cloud Computing, Microservices, Design Patterns and still learning new technologies.