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