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