Introduction
In enterprise-grade applications, data consistency and integrity are non-negotiable.
When multiple users or services interact with the same data simultaneously, it’s crucial to manage transactions and handle concurrency conflicts effectively.
With Entity Framework Core 9 (EF Core 9), Microsoft has refined transaction handling and introduced improved features for optimistic concurrency, savepoints, and distributed transactions—making it more powerful for real-world, multi-user systems.
This article explains how to handle transactions and concurrency in EF Core 9, using simple, step-by-step explanations, diagrams, and practical code examples.
1. Understanding Transactions in EF Core
A transaction is a sequence of database operations that must be treated as a single logical unit of work.
If any operation fails, all operations within that transaction should be rolled back to maintain data consistency.
ACID Properties
EF Core transactions follow ACID principles:
Atomicity: All operations complete or none do.
Consistency: The database remains valid after the transaction.
Isolation: One transaction’s changes are invisible to others until committed.
Durability: Once committed, data is permanent even after system failures.
2. Technical Workflow (Flowchart)
+---------------------------+
| Begin Transaction |
+---------------------------+
|
v
+---------------------------+
| Perform DB Operations |
| (Insert, Update, Delete) |
+---------------------------+
|
v
+---------------------------+
| Error Occurred? |
+-------------+-------------+
| Yes
v
+-------------------+
| Rollback Changes |
+-------------------+
|
v
+-------------+-------------+
| No |
v |
+---------------------------+
| Commit Transaction |
+---------------------------+
3. Implementing Transactions in EF Core 9
EF Core automatically creates a transaction when SaveChanges() is called.
However, for more control, you can manually manage transactions using BeginTransaction().
Example 1: Manual Transaction Handling
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
var customer = new Customer { Name = "Rajesh", Email = "[email protected]" };
_context.Customers.Add(customer);
await _context.SaveChangesAsync();
var order = new Order { CustomerId = customer.Id, TotalAmount = 2500 };
_context.Orders.Add(order);
await _context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch (Exception)
{
await transaction.RollbackAsync();
throw;
}
Explanation
A transaction is explicitly started using BeginTransactionAsync().
If any operation fails, we rollback to ensure no partial data is saved.
On success, CommitAsync() finalizes all operations.
4. Nested Transactions with Savepoints
EF Core 9 supports Savepoints, allowing partial rollbacks inside a transaction—useful for complex workflows.
Example 2: Using Savepoints
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
_context.Customers.Add(new Customer { Name = "Amit" });
await _context.SaveChangesAsync();
await transaction.CreateSavepointAsync("BeforeOrder");
_context.Orders.Add(new Order { CustomerId = 1, TotalAmount = 5000 });
await _context.SaveChangesAsync();
// Simulate failure
throw new Exception("Order creation failed!");
await transaction.CommitAsync();
}
catch (Exception)
{
await transaction.RollbackToSavepointAsync("BeforeOrder");
await transaction.CommitAsync();
}
Key Takeaway
Savepoints help preserve partial progress and rollback specific sections without discarding the entire transaction.
5. Distributed Transactions
When multiple databases or microservices are involved, EF Core 9 supports distributed transactions through TransactionScope.
Example 3: Using TransactionScope
using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
try
{
_dbContext1.Products.Add(new Product { Name = "Laptop" });
await _dbContext1.SaveChangesAsync();
_dbContext2.Inventory.Add(new Inventory { ProductName = "Laptop", Quantity = 10 });
await _dbContext2.SaveChangesAsync();
scope.Complete();
}
catch
{
// If an exception occurs, the transaction rolls back automatically
}
Use Case
Ideal for multi-database synchronization scenarios or integrating microservices that share transactional consistency.
6. Handling Concurrency in EF Core
Concurrency conflicts occur when two or more users attempt to update the same record simultaneously.
EF Core handles this using Optimistic Concurrency Control—it doesn’t lock the data but detects conflicts when saving.
How It Works
A user fetches a record for editing.
Another user updates and saves that same record.
When the first user tries to save, EF Core detects that the record has changed since it was fetched and throws a DbUpdateConcurrencyException.
7. Implementing Optimistic Concurrency
Step 1: Add a Concurrency Token
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
}
Here, the [Timestamp] attribute adds a RowVersion column that EF Core checks during updates.
Step 2: Handle Concurrency Exceptions
try
{
var product = await _context.Products.FirstAsync(p => p.Id == 1);
product.Price = 1200;
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
foreach (var entry in ex.Entries)
{
if (entry.Entity is Product)
{
var databaseValues = await entry.GetDatabaseValuesAsync();
entry.OriginalValues.SetValues(databaseValues);
}
}
Console.WriteLine("Concurrency conflict detected. Data reloaded from database.");
}
Explanation
EF Core compares the current RowVersion with the database version.
If different, it throws an exception.
You can reload the current database values or prompt the user to reapply their changes.
8. Pessimistic Concurrency (Optional)
If optimistic concurrency isn’t enough, EF Core supports pessimistic concurrency (using explicit locks).
Example
await _context.Database.ExecuteSqlRawAsync("SELECT * FROM Products WITH (UPDLOCK, ROWLOCK) WHERE Id = 1");
This approach locks the record until the transaction completes — preventing others from modifying it.
Note
Pessimistic locking should be used carefully, as it can cause deadlocks and reduce performance in high-concurrency systems.
9. Best Practices
✅ Use Optimistic Concurrency by default — lighter and scalable.
✅ Handle DbUpdateConcurrencyException gracefully — don’t just fail silently.
✅ Use Transactions for multiple related operations.
✅ Log Transaction Failures for debugging and auditing.
✅ Avoid Long Transactions — keep them short and specific.
✅ Use Savepoints when multiple steps are interdependent.
| Concept | Description |
|---|
| Transaction | Ensures all operations succeed or fail together |
| Savepoint | Allows partial rollback without losing all progress |
| Distributed Transaction | Coordinates multiple databases |
| Optimistic Concurrency | Detects conflicting updates using RowVersion |
| Pessimistic Concurrency | Prevents conflicts by locking records |
Conclusion
EF Core 9 makes managing transactions and concurrency more powerful and developer-friendly than ever.
By combining transactions for data integrity with concurrency control for multi-user safety, you can build robust and reliable enterprise applications that handle real-world data challenges gracefully.
If your system involves critical financial operations, inventory management, or multi-user collaboration, mastering these techniques ensures data accuracy, reliability, and user trust.