How does Entity Framework implement and handle concurrency conflicts using optimistic locking ? Question For - Mid Level Developer
Question
CDOTNET Entity Framework Q21 – How does Entity Framework implement and handle concurrency conflicts using optimistic locking ? Question For – Mid Level Developer
Brief Answer
Optimistic locking in Entity Framework is a concurrency control strategy that assumes data conflicts are rare. Instead of locking data immediately, it allows multiple users to read and potentially modify data concurrently. Conflict detection occurs only at the moment a user attempts to save changes.
How Entity Framework Implements Optimistic Locking:
-
Versioning Mechanism: EF relies on a “concurrency token” property in your entity.
- Commonly, this is a
byte[]property mapped to SQL Server’srowversion(formerlytimestamp) column, which is automatically updated by the database on every row modification. - Alternatively, you can use a custom property (e.g.,
DateTime,int) that you manually update.
When an entity is retrieved, its version value is read. Upon saving, EF compares this original version with the current version in the database.
- Commonly, this is a
-
Conflict Detection: If the versions differ during
SaveChanges()orSaveChangesAsync(), Entity Framework throws aDbUpdateConcurrencyException. This signals that another user modified the data since it was loaded.- The exception provides access to
Entry.OriginalValues(what you loaded),Entry.CurrentValues(what you tried to save), and crucially,Entry.GetDatabaseValues()(what’s currently in the DB) to help resolve conflicts.
- The exception provides access to
Handling Concurrency Conflicts:
It is critical to catch and handle DbUpdateConcurrencyException. Common strategies include:
- Reloading (Database Wins): Discarding client changes and reloading the latest database data. The user can then re-apply their changes.
- Merging (User Resolution): Presenting both client and database values to the user for manual reconciliation.
- Overwriting (Client Wins): Forcing client changes, discarding others’ modifications (use with extreme caution).
Configuration:
You configure a property as a concurrency token using:
- Data Annotations: Apply
[ConcurrencyCheck]to the property (e.g.,public byte[] RowVersion { get; set; }). - Fluent API: In
OnModelCreating, use.IsConcurrencyToken()or specifically.IsRowVersion()forrowversionproperties.
Key Takeaway:
Optimistic locking is highly scalable and generally preferred for web/distributed applications when conflicts are infrequent. However, its effectiveness hinges entirely on robust exception handling of DbUpdateConcurrencyException and a well-defined conflict resolution strategy to prevent data loss or application crashes.
Super Brief Answer
Entity Framework implements optimistic locking by assuming conflicts are rare and detecting them at save time.
- It uses a “concurrency token” (e.g., a
rowversioncolumn) to track changes. When saving, EF compares the original token value with the current database value. - If they differ, a
DbUpdateConcurrencyExceptionis thrown, signaling a conflict. - You must catch this exception and implement a resolution strategy (e.g., reloading database values with
GetDatabaseValues(), merging, or overwriting). - Configure the token using
[ConcurrencyCheck]orIsRowVersion().
It’s crucial for scalability but requires explicit conflict handling.
Detailed Answer
Key Takeaway: Optimistic locking in Entity Framework detects data conflicts at save time using a versioning mechanism (like a `rowversion` column) and throws a `DbUpdateConcurrencyException` if conflicts are found, requiring explicit handling and resolution strategies.
What is Optimistic Locking in Entity Framework?
Optimistic locking is a concurrency control strategy that assumes data conflicts are rare. Instead of locking data immediately upon retrieval (like pessimistic locking), it allows multiple users to read and potentially modify the same data concurrently. Conflict detection only occurs at the moment a user attempts to save their changes to the database. If a conflict is detected, meaning the data was modified by another user since it was initially retrieved, Entity Framework will prevent the save operation and signal the conflict.
How Entity Framework Implements Optimistic Locking
The core of Entity Framework’s optimistic locking implementation relies on a versioning mechanism:
1. Versioning Mechanism
To detect if data has been modified externally, Entity Framework uses a specific column or property in your entity to track its version. When an entity is retrieved, its version value is also read. When the entity is saved, EF compares the original version value (the one read when the entity was first loaded) with the current version value in the database. If they differ, it indicates that another user has modified the data.
- Database Column (`rowversion`/`timestamp`): SQL Server’s `rowversion` (formerly `timestamp`) is a common choice. This column is automatically updated by the database every time the row is modified, providing a unique, incrementing binary number. Entity Framework can automatically map to this type.
- Custom Property (e.g., `DateTime`, `int`): You can also use a custom property (like a `LastUpdated` `DateTime` or an `int` counter) that you manually update in your application logic before saving. However, `rowversion` is generally preferred for its automatic, reliable management by the database.
2. Conflict Detection: `DbUpdateConcurrencyException`
If Entity Framework detects a version mismatch during `SaveChanges()` or `SaveChangesAsync()`, it throws a `DbUpdateConcurrencyException`. This exception is your application’s signal that a concurrency conflict has occurred, and the save operation has failed.
The `DbUpdateConcurrencyException` provides access to the `Entries` collection, which contains the `DbEntityEntry` objects for the entities that caused the conflict. From these entries, you can access three sets of values:
- `Entry.OriginalValues`: The values of the entity when it was first queried from the database.
- `Entry.CurrentValues`: The values that your application attempted to save to the database.
- `Entry.GetDatabaseValues()`: The current values of the entity as they exist in the database at the time of the exception. This is crucial for resolving conflicts.
Handling Concurrency Conflicts
It is crucial to catch and handle `DbUpdateConcurrencyException` gracefully. Failure to do so can lead to a crashed application or, worse, unintended data overwrites. Common strategies include:
- Overwriting (Client Wins): The client’s changes are forced onto the database, discarding any changes made by other users. This approach is generally discouraged as it can lead to data loss and should be used with extreme caution.
- Reloading (Database Wins): The client’s changes are discarded, and the latest data from the database is reloaded into the entity. The user is then presented with the most current data, and they can re-apply their changes if desired.
- Merging (User Resolution): Both the client’s proposed changes and the current database values are presented to the user. The user then decides which values to keep, effectively merging the changes manually. This is often the most user-friendly approach but also the most complex to implement.
- Custom Logic: Implement a specific resolution strategy based on your application’s business rules. For example, for an inventory count, you might add or subtract quantities instead of overwriting the total.
Configuring Optimistic Concurrency in EF Core
You can configure a property to be a concurrency token using Data Annotations or the Fluent API:
1. Data Annotations
Use the `[ConcurrencyCheck]` attribute on the property in your entity. For SQL Server’s `rowversion`, the property type should be `byte[]`.
public class Product
{
public int ProductId { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
[ConcurrencyCheck] // Marks this property as a concurrency token
public byte[] RowVersion { get; set; } // Ideal for SQL Server's rowversion
}
2. Fluent API
In your `DbContext`’s `OnModelCreating` method, use `IsConcurrencyToken()` for a general concurrency token, or `IsRowVersion()` specifically for `rowversion`/`timestamp` properties (which also implies `IsConcurrencyToken()` and `ValueGeneratedOnAddOrUpdate()`):
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity()
.Property(p => p.RowVersion)
.IsRowVersion(); // Configures for rowversion/timestamp and marks as concurrency token
}
Code Sample: Handling `DbUpdateConcurrencyException`
The following example demonstrates how to catch and handle a `DbUpdateConcurrencyException` when updating an entity, showing a common “reload and inform” strategy:
public async Task UpdateProductPrice(int productId, decimal newPrice)
{
using (var context = new YourDbContext())
{
var product = await context.Products.FindAsync(productId);
if (product == null)
{
Console.WriteLine($"Product with ID {productId} not found.");
return;
}
product.Price = newPrice;
try
{
await context.SaveChangesAsync();
Console.WriteLine($"Product ID {productId} price updated successfully to {newPrice}.");
}
catch (DbUpdateConcurrencyException ex)
{
Console.WriteLine($"Concurrency conflict detected for Product ID {productId}.");
// Log the exception details for debugging purposes
// logger.LogError(ex, "Concurrency conflict during product update.");
foreach (var entry in ex.Entries)
{
var databaseValues = entry.GetDatabaseValues(); // Get current values from DB
if (databaseValues == null)
{
// The row was deleted in the database by another user
Console.WriteLine($"Product ID {productId} was deleted by another user.");
}
else
{
// Get the property values from the database
var databaseProduct = (Product)databaseValues.ToObject();
Console.WriteLine($"Your attempted price: {product.Price}");
Console.WriteLine($"Current database price: {databaseProduct.Price}");
Console.WriteLine("Please review the changes and try again if necessary.");
// Option 1: Reload the entity with database values (Database Wins)
// This updates the 'product' object in memory with the latest DB values
await entry.ReloadAsync();
// Now 'product' entity has the latest database values.
// You might re-present this to the user in a UI.
// Option 2: Overwrite (Client Wins - use with extreme caution!)
// entry.OriginalValues.SetValues(databaseValues); // Update original values to current DB values
// await context.SaveChangesAsync(); // Try saving again with the updated original values
}
}
// Depending on your strategy, you might re-throw the exception,
// or handle it completely here (e.g., by presenting a UI message).
throw; // Re-throw to propagate the error or for further handling up the call stack
}
catch (Exception ex)
{
Console.WriteLine($"An unexpected error occurred: {ex.Message}");
// Log other exceptions
throw;
}
}
}
Key Considerations and Best Practices
-
Optimistic vs. Pessimistic Locking
Optimistic locking is generally preferred in web applications and distributed systems due to its scalability. It offers better performance when conflicts are rare because it doesn’t hold locks, allowing higher concurrency. However, it requires robust error handling for conflicts.
Pessimistic locking (e.g., database table locks) prevents conflicts by locking data as soon as it’s accessed, ensuring data integrity but significantly impacting performance and scalability, especially in high-concurrency environments. The choice between them is a trade-off between performance and guaranteed immediate data integrity.
-
When is Optimistic Locking Most Suitable?
Optimistic locking is ideal for scenarios where data conflicts are infrequent. This includes:
- Low-Concurrency Applications: Where multiple users are unlikely to modify the same data record simultaneously.
- Read-Heavy Systems: Applications with significantly more read operations than write operations, as locking overhead is minimized.
- Content Management Systems (CMS): Where authors usually work on different articles, making simultaneous edits to the exact same article less common.
- E-commerce (Product Updates): Updates to different product details are common, but concurrent updates to the *same* product by different administrators are less frequent.
-
Importance of Exception Handling
While optimistic locking offers significant performance benefits, you absolutely must handle `DbUpdateConcurrencyException`. Failure to do so can lead to application crashes, data loss, or data corruption if unhandled conflicts are ignored. A robust concurrency handling strategy, including clear user feedback mechanisms, is essential for any application using optimistic locking.

