How do you handle concurrency in Entity Framework?

4 minadvancedEF-CoreconcurrencyRowVersion

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:

StrategyDescriptionUse Case
Client WinsOverwrite database with client valuesUser is always right
Database WinsDiscard client changesLatest change wins
MergeCombine non-conflicting changesCollaborative editing
User DecidesShow both versions, let user chooseCritical 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