Concurrency control in Entity Framework Core
Max Bazhenov
Senior Full Stack Developer | 8+ Years in Software Engineering | .NET (C#), Angular, TypeScript, Azure | Enterprise Web Applications | Real-time UI, Performance Optimization
Concurrency control ensures data integrity when multiple users or processes interact with the database simultaneously.
The concurrency problem occurs when multiple users or processes attempt to read, write, or update the same data in a database simultaneously. Without concurrency control the data could be inconsistent, incorrect, or lost and it can lead to the famours problems as Lost updates, Dirty reads, Non-repeatable reads, Phantom reads and others.
EF Core supports concurrency control to ensure data integrity when multiple users or processes interact with the database simultaneously with several ways.
EF Core transactions
EF Core transactions ensure that a sequence of operations is treated as a single atomic unit. If any operation in the transaction fails, all operations are rolled back to maintain data consistency.
There is three different approaches:
1. Implicit Transactions
EF Core implicitly creates a transaction for database commands when SaveChanges() is called. If SaveChanges() fails, the changes are rolled back.
using (var context = new ApplicationDbContext())
{
context.Add(new Product { Name = "Product A", Price = 100 });
context.SaveChanges(); // Implicit transaction
}
2. Explicit Transactions
Explicit transactions provide more control, allowing multiple SaveChanges() calls within the same transaction.
using (var transaction = context.Database.BeginTransaction())
{
try
{
context.Add(new Product { Name = "Product B", Price = 150 });
context.SaveChanges();
context.Add(new Product { Name = "Product C", Price = 200 });
context.SaveChanges();
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
3. TransactionScope
Provides a higher-level API to manage transactions across multiple contexts or resources.
using (var scope = new TransactionScope())
{
using (var context = new ApplicationDbContext())
{
context.Add(new Product { Name = "Product D", Price = 250 });
context.SaveChanges();
}
using (var anotherContext = new AnotherDbContext())
{
anotherContext.Add(new Order { OrderNumber = "ORD123" });
anotherContext.SaveChanges();
}
scope.Complete(); // Commit both operations
}
Concurrency token
Concurrency token is a field used to detect conflicting updates when multiple users attempt to modify the same record. EF Core ensures that the original value of the token matches the current value in the database when SaveChanges() is called. If they don't match, a DbUpdateConcurrencyException will be thrown. This token could setup by [ConcurrencyCheck] attribute or Fluent API.
modelBuilder.Entity<Product>()
.Property(p => p.Stock)
.IsConcurrencyToken();
By marking a property as a concurrency token, EF Core ensures that the property is included in the WHERE clause of the UPDATE or DELETE SQL commands. This allows EF Core to compare the original value of the property (at the time of data retrieval) with its current value in the database. If the value of the concurrency token in the database has changed since it was last read, EF Core detects a conflict and throw a DbUpdateConcurrencyException is thrown when SaveChanges() is called.
领英推荐
RowVersion
EF Core supports versioning using a byte[] field. RowVersion property is the most automatic way to detect concurrency conflicts in EF Core. This designed to handle conflicts for the entire row, not just a specific property. When a RowVersion property introduced, EF Core automatically manages conflict detection and resolution at the database level using this property.
public byte[] RowVersion { get; set; }
modelBuilder.Entity<Product>()
.Property(p => p.RowVersion)
.IsRowVersion();
The database generates and updates the value for each modification, and EF Core compares it to detect concurrency conflicts. Every time a row is updated, the database automatically generates a new RowVersion value. When EF Core sends an UPDATE or DELETE command, it includes the RowVersion value in the WHERE clause to ensure the row has not been modified by another transaction.
When to use this approaches
Transactions, Concurrency Tokens, and RowVersion each have different characteristics regarding reliability and performance, and their suitability depends on the application's requirements.
Transactions
Reliability - transactions provide the highest reliability for data integrity across multiple operations.
Performance - trnsactions introduce performance overhead, especially if they are long-running, as they may lock resources in the database, blocking other operations.
When to use - when a series of operations must succeed or fail together, or for critical operations where partial updates could corrupt data.
Concurrency tokens
Reliability - provide medium reliability for detecting conflicts at the property level.
Performance - concurrency tokens are lightweight since only the specified properties are checked for conflicts.
When to use - when you only need to track changes to specific properties.
RowVersion
Reliability - provides high reliability for detecting conflicts at the row level.
Performance - efficient. RowVersion is automatically managed by the database and optimized for performance
When to use: when you need to track changes to the entire row.