How do you handle concurrency in Entity Framework?
Quick Answer
EF Core handles concurrency optimistically: mark a property as a concurrency token (e.g., a `RowVersion`/`[Timestamp]` column or `IsConcurrencyToken`), and EF includes it in the `WHERE` clause on updates. If the row changed since it was read, zero rows are affected and EF throws `DbUpdateConcurrencyException`, which you handle by reloading, merging, or surfacing the conflict (client-wins/store-wins).
Detailed Answer
Concurrency Control prevents data conflicts when multiple users update the same record simultaneously.
1. Optimistic Concurrency with RowVersion (Timestamp)
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
}
// Or using Fluent API
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity()
.Property(p => p.RowVersion)
.IsRowVersion();
}
How It Works:
try
{
var product = context.Products.Find(1);
product.Price = 99.99m;
context.SaveChanges(); // Checks RowVersion
}
catch (DbUpdateConcurrencyException ex)
{
// Handle conflict
var entry = ex.Entries.Single();
var databaseValues = entry.GetDatabaseValues();
var currentValues = entry.CurrentValues;
Console.WriteLine("Conflict detected!");
Console.WriteLine($"Current: {currentValues["Price"]}");
Console.WriteLine($"Database: {databaseValues["Price"]}");
}
2. Concurrency Token (Any Property)
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
[ConcurrencyCheck]
public DateTime LastModified { get; set; }
}
// Or Fluent API
modelBuilder.Entity()
.Property(p => p.LastModified)
.IsConcurrencyToken();
3. Complete Concurrency Handling Strategy
public async Task UpdateProductAsync(Product product)
{
using var transaction = await context.Database.BeginTransactionAsync();
try
{
context.Entry(product).State = EntityState.Modified;
await context.SaveChangesAsync();
await transaction.CommitAsync();
return true;
}
catch (DbUpdateConcurrencyException ex)
{
await transaction.RollbackAsync();
foreach (var entry in ex.Entries)
{
if (entry.Entity is Product)
{
var proposedValues = entry.CurrentValues;
var databaseValues = entry.GetDatabaseValues();
if (databaseValues == null)
{
// Entity was deleted
Console.WriteLine("Entity was deleted by another user");
}
else
{
// Conflict resolution strategies:
// 1. Client Wins (overwrite database)
entry.OriginalValues.SetValues(databaseValues);
// 2. Database Wins (discard client changes)
// entry.CurrentValues.SetValues(databaseValues);
// 3. Merge (selective update)
// foreach (var property in proposedValues.Properties)
// {
// var proposed = proposedValues[property];
// var database = databaseValues[property];
// // Custom merge logic
// }
}
}
}
// Retry the save operation
try
{
await context.SaveChangesAsync();
return true;
}
catch (DbUpdateConcurrencyException)
{
return false; // Give up after retry
}
}
}
4. Disconnected Entity Scenario (Web API)
[HttpPut("products/{id}")]
public async Task UpdateProduct(int id, ProductDto dto)
{
var product = new Product
{
Id = id,
Name = dto.Name,
Price = dto.Price,
RowVersion = dto.RowVersion // From client
};
context.Products.Attach(product);
context.Entry(product).Property(p => p.Name).IsModified = true;
context.Entry(product).Property(p => p.Price).IsModified = true;
try
{
await context.SaveChangesAsync();
return Ok();
}
catch (DbUpdateConcurrencyException)
{
return Conflict(new { message = "This record was modified by another user" });
}
}
5. Manual Concurrency Check
modelBuilder.Entity()
.Property(p => p.Name)
.IsConcurrencyToken();
// SQL Server generates:
// UPDATE Products
// SET Price = @price
// WHERE Id = @id AND Name = @originalName
Concurrency Resolution Strategies:
| Strategy | Description | Use Case |
|---|---|---|
| Client Wins | Overwrite database with client values | User is always right |
| Database Wins | Discard client changes | Latest change wins |
| Merge | Combine non-conflicting changes | Collaborative editing |
| User Decides | Show both versions, let user choose | Critical data |
Best Practices:
- Always use RowVersion/Timestamp for SQL Server
- Handle DbUpdateConcurrencyException appropriately
- Inform users about conflicts
- Use optimistic concurrency for web applications
- Consider pessimistic locking (database locks) for critical sections
- Log concurrency conflicts for monitoring