Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

EF Core - Giao dịch & Đồng thời

Transactions

Basic Transaction

using var transaction = await context.Database.BeginTransactionAsync();

try
{
    var order = new Order { CustomerId = 1 };
    context.Orders.Add(order);
    await context.SaveChangesAsync();
    
    var orderItem = new OrderItem { OrderId = order.Id, ProductId = 1 };
    context.OrderItems.Add(orderItem);
    await context.SaveChangesAsync();
    
    await transaction.CommitAsync();
}
catch (Exception ex)
{
    await transaction.RollbackAsync();
    throw;
}

Transaction với Isolation Level

var transaction = await context.Database.BeginTransactionAsync(
    System.Data.IsolationLevel.Serializable);

try
{
    // Operations với Serializable isolation
    await transaction.CommitAsync();
}
finally
{
    await transaction.DisposeAsync();
}

Transaction với Database Transaction

await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();

await using var transaction = await connection.BeginTransactionAsync();
try
{
    var command = connection.CreateCommand();
    command.Transaction = transaction;
    command.CommandText = "INSERT INTO Products VALUES (@name, @price)";
    
    var param1 = command.CreateParameter();
    param1.ParameterName = "@name";
    param1.Value = "Product";
    
    var param2 = command.CreateParameter();
    param2.ParameterName = "@price";
    param2.Value = 9.99m;
    
    command.Parameters.Add(param1);
    command.Parameters.Add(param2);
    
    await command.ExecuteNonQueryAsync();
    
    await transaction.CommitAsync();
}
catch
{
    await transaction.RollbackAsync();
    throw;
}

Concurrency (Xung đột dữ liệu)

Vấn đề Concurrency

User A reads product (Version = 1)
User B reads product (Version = 1)
User A updates price to 50, saves (Version = 1 → 2)
User B updates price to 60, saves (Version = 1 → ?)
                              ↓
                    CONFLICT - User B should fail!

Giải pháp: 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; }
}

Cấu hình Fluent API

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>()
        .Property(p => p.RowVersion)
        .IsRowVersion(); // SQL Server会自动使用 rowversion
    
    // Hoặc cho các database khác
    modelBuilder.Entity<Product>()
        .Property(p => p.RowVersion)
        .IsConcurrencyToken();
}

Xử lý DbUpdateConcurrencyException

public async Task<bool> UpdateProduct(Product product)
{
    try
    {
        context.Products.Update(product);
        await context.SaveChangesAsync();
        return true;
    }
    catch (DbUpdateConcurrencyException ex)
    {
        // Get entry để xử lý
        var entry = ex.Entries.Single();
        
        // Lấy database values
        var databaseValues = await entry.GetDatabaseValuesAsync();
        
        // Option 1: Reload từ database
        await entry.ReloadAsync();
        
        // Option 2: Merge với client values
        var clientValues = entry.CurrentValues;
        var databaseValues = await entry.GetDatabaseValuesAsync();
        
        // Log hoặc thông báo cho user
        Console.WriteLine("Concurrency conflict detected!");
        
        return false;
    }
}

Retry on Concurrency

public async Task<bool> UpdateProductWithRetry(Product product)
{
    var retryCount = 0;
    const int maxRetries = 3;
    
    while (retryCount < maxRetries)
    {
        try
        {
            context.Products.Update(product);
            await context.SaveChangesAsync();
            return true;
        }
        catch (DbUpdateConcurrencyException)
        {
            retryCount++;
            if (retryCount >= maxRetries) throw;
            
            // Reload và retry
            var entry = context.Entry(product);
            await entry.ReloadAsync();
        }
    }
    
    return false;
}

Client-Side Concurrency Token

Custom concurrency token

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    
    // Sử dụng property khác làm concurrency token
    public DateTime UpdatedAt { get; set; }
}

// Cấu hình
modelBuilder.Entity<Product>()
    .Property(p => p.UpdatedAt)
    .IsConcurrencyToken();

Optimistic vs Pessimistic Concurrency

Optimistic (EF Core default)

  • Không lock records
  • Kiểm tra version khi save
  • Nếu conflict → exception
  • Phù hợp cho low-contention scenarios

Pessimistic

  • Lock records trước khi update
  • Sử dụng explicit transactions
  • Phù hợp cho high-contention scenarios
// Pessimistic lock example
await using var transaction = await context.Database.BeginTransactionAsync();

var product = await context.Products
    .FromSqlRaw("SELECT * FROM Products WITH (UPDLOCK) WHERE Id = {0}", id)
    .FirstAsync();

// Update product
product.Price = newPrice;
await context.SaveChangesAsync();

await transaction.CommitAsync();