Nguon: Microsoft Learn · .NET 8.0

Phần 8: Razor Pages với EF Core trong ASP.NET Core - Xử lý xung đột đồng thời (Concurrency)

Nguồn: Part 8: Razor Pages with EF Core in ASP.NET Core - Concurrency

Trang này là một phần trong chuỗi hướng dẫn tạo ứng dụng web Razor Pages sử dụng Entity Framework Core. Trang này trình bày cách xử lý xung đột đồng thời (concurrency conflicts) khi nhiều người dùng cập nhật cùng một entity cùng lúc.

Tổng quan

Xung đột đồng thời (concurrency conflicts) xảy ra khi:

Nếu không có cơ chế phát hiện đồng thời, người dùng lưu sau cùng sẽ ghi đè lên mọi thay đổi trước đó.

Các phương pháp xử lý đồng thời

Pessimistic Concurrency (Khóa dữ liệu)

Optimistic Concurrency (Đồng thời lạc quan)

Cho phép xung đột xảy ra, sau đó xử lý phù hợp. Có ba chiến lược:

  1. Theo dõi từng thuộc tính (Property-level tracking): Theo dõi thuộc tính nào thay đổi và chỉ cập nhật các cột đó
  2. Nhược điểm: Không thể ngăn xung đột trên cùng một thuộc tính
  3. Client Wins (Người dùng thắng): Bản cập nhật cuối cùng ghi đè mọi thay đổi trước đó
  4. Hành vi mặc định nếu không có xử lý đồng thời
  5. Store Wins (Cơ sở dữ liệu thắng): Giá trị trong database ưu tiên hơn giá trị từ client
  6. Được dùng trong hướng dẫn này
  7. Ngăn mất dữ liệu bằng cách thông báo cho người dùng về xung đột

Triển khai với Concurrency Tokens

Thêm thuộc tính theo dõi

Thêm thuộc tính ConcurrencyToken vào model:

Với SQL Server (Visual Studio):

csharp
[Timestamp]
public byte[] ConcurrencyToken { get; set; }

Với SQLite/Non-SQL Server (Visual Studio Code):

csharp
public Guid ConcurrencyToken { get; set; } = Guid.NewGuid();

Sau đó cấu hình trong DbContext.OnModelCreating():

csharp
modelBuilder.Entity<Department>()
    .Property(d => d.ConcurrencyToken)
    .IsConcurrencyToken();

Cách hoạt động

Ví dụ SQL được tạo ra:

sql
UPDATE [Departments] SET [Name] = @p0
WHERE [DepartmentID] = @p1 AND [ConcurrencyToken] = @p2;
SELECT [ConcurrencyToken]
FROM [Departments]
WHERE @@ROWCOUNT = 1 AND [DepartmentID] = @p1;

Tạo Migration (Di chuyển schema)

Visual Studio:

powershell
Add-Migration RowVersion
Update-Database

Visual Studio Code:

bash
dotnet ef migrations add RowVersion
dotnet ef database update

Xử lý xung đột trong trang Edit

Page Model cho Edit

Đặt giá trị concurrency token gốc trước khi cập nhật:

csharp
public async Task<IActionResult> OnPostAsync(int id)
{
    // Lấy entity hiện tại từ DB
    var departmentToUpdate = await _context.Departments
        .Include(i => i.Administrator)
        .FirstOrDefaultAsync(m => m.DepartmentID == id);

    if (departmentToUpdate == null)
    {
        return HandleDeletedDepartment();
    }

    // Đặt giá trị token gốc để phát hiện xung đột
    _context.Entry(departmentToUpdate)
        .Property(d => d.ConcurrencyToken)
        .OriginalValue = Department.ConcurrencyToken;

    if (await TryUpdateModelAsync<Department>(
        departmentToUpdate, "Department",
        s => s.Name, s => s.StartDate, s => s.Budget, s => s.InstructorID))
    {
        try
        {
            await _context.SaveChangesAsync();
            return RedirectToPage("./Index");
        }
        catch (DbUpdateConcurrencyException ex)
        {
            var exceptionEntry = ex.Entries.Single();
            var clientValues = (Department)exceptionEntry.Entity;
            var databaseEntry = exceptionEntry.GetDatabaseValues();
            
            if (databaseEntry == null)
            {
                ModelState.AddModelError(string.Empty, 
                    "Unable to save. The department was deleted by another user.");
                return Page();
            }

            var dbValues = (Department)databaseEntry.ToObject();
            await SetDbErrorMessage(dbValues, clientValues, _context);

            // Cập nhật token với giá trị database hiện tại
            Department.ConcurrencyToken = dbValues.ConcurrencyToken;
            ModelState.Remove($"{nameof(Department)}.{nameof(Department.ConcurrencyToken)}");
        }
    }

    return Page();
}

Razor Page Edit

Thêm concurrency token vào hidden field:

cshtml
@page "{id:int}"
<form method="post">
    <input type="hidden" asp-for="Department.DepartmentID" />
    <input type="hidden" asp-for="Department.ConcurrencyToken" />
    
    <!-- Các trường form khác -->
    
    <input type="submit" value="Save" class="btn btn-primary" />
</form>

Hiển thị thông báo lỗi

Khi phát hiện xung đột, hiển thị các giá trị hiện tại trong database:

csharp
private async Task SetDbErrorMessage(Department dbValues,
    Department clientValues, SchoolContext context)
{
    if (dbValues.Name != clientValues.Name)
    {
        ModelState.AddModelError("Department.Name",
            $"Current value: {dbValues.Name}");
    }
    if (dbValues.Budget != clientValues.Budget)
    {
        ModelState.AddModelError("Department.Budget",
            $"Current value: {dbValues.Budget:c}");
    }
    
    ModelState.AddModelError(string.Empty,
        "The record you attempted to edit was modified by another user. " +
        "The current values have been displayed. " +
        "If you still want to edit, click Save again.");
}

Xử lý xung đột trong trang Delete

Page Model cho Delete

csharp
public async Task<IActionResult> OnPostAsync(int id)
{
    try
    {
        if (await _context.Departments.AnyAsync(m => m.DepartmentID == id))
        {
            _context.Departments.Remove(Department);
            await _context.SaveChangesAsync();
        }
        return RedirectToPage("./Index");
    }
    catch (DbUpdateConcurrencyException)
    {
        return RedirectToPage("./Delete",
            new { concurrencyError = true, id = id });
    }
}

Razor Page Delete

cshtml
@page "{id:int}"
<form method="post">
    <input type="hidden" asp-for="Department.DepartmentID" />
    <input type="hidden" asp-for="Department.ConcurrencyToken" />
    <input type="submit" value="Delete" class="btn btn-danger" />
</form>

Kiểm tra đồng thời

  1. Mở hai tab trình duyệt để chỉnh sửa cùng một entity
  2. Thực hiện các thay đổi khác nhau ở mỗi tab
  3. Lưu ở tab đầu tiên (thành công)
  4. Lưu ở tab thứ hai (hiển thị lỗi đồng thời với các giá trị hiện tại trong database)
  5. Cập nhật các trường bị xung đột và lưu lại

Sự khác biệt giữa SQL Server và SQLite

Khía cạnhSQL ServerSQLite
Kiểu Tokenbyte[]Guid
Attribute[Timestamp]Thủ công IsConcurrencyToken()
Tự động cập nhậtTự độngThủ công hoặc dùng trigger

Các điểm quan trọng