In multi-user applications, one of the most common challenges is data concurrency.

Consider a scenario where multiple users view or edit the same record at the same time.
Without a proper concurrency control mechanism, the last update saved to the database may silently override changes made by others—without anyone noticing.

 

To address this problem, there are generally two approaches:

Pessimistic Concurrency

In this approach, when data is read, it is locked, preventing other users from modifying it.

While this method avoids conflicts, it has notable downsides:

  • It negatively impacts performance
  • It is usually not suitable for UI-driven or web applications

 

Optimistic Concurrency

With optimistic concurrency, we assume that conflicts are rare.

Therefore:

  • No locks are applied when reading data
  • When saving changes, the system checks whether the data has been modified since it was read
  • If a conflict is detected, a DbUpdateConcurrencyException is thrown

Entity Framework Core provides built-in support for optimistic concurrency.

 

Optimistic Concurrency in EF Core

1) Using RowVersion (Timestamp)

In this approach, the database contains a special column (typically of type rowversion or timestamp) that is automatically updated whenever the record changes.

[Timestamp] public byte[] RowVersion { get; set; }

Characteristics:

  • Highly accurate and reliable
  • Independent of business field values
  • Commonly recommended and widely used in production systems

 

2) Using ConcurrencyCheck

In this approach, one or more entity properties are explicitly marked as concurrency tokens:

[ConcurrencyCheck] public string Title { get; set; }

During SaveChanges, EF compares the current value of this property with the original value captured when the entity was loaded.
If the value has changed, the save operation fails with a concurrency exception.

Characteristics:

  • Simple and does not require an additional database column
  • Helpful for understanding optimistic concurrency concepts
  • Useful in specific or educational scenarios

 

When RowVersion Becomes Too Strict

At first glance, RowVersion appears to be the ideal solution for implementing optimistic concurrency.

However, in real-world projects, there are scenarios where this approach can lead to false concurrency conflicts.

 

A Real-World Scenario

Imagine an entity with the following characteristics:

  • Editable by administrators via an admin panel
  • Viewed frequently by regular users, which increments a statistical field such as ViewCount
public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public int ViewCount { get; set; }

    [Timestamp]    public byte[] RowVersion { get; set; }
}

Event Flow

  1. An admin opens the edit form
  2. The current RowVersion is sent to the UI
  3. Meanwhile, users view the book page
  4. Each view increments ViewCount
  5. RowVersion changes
  6. The admin clicks Save
  7. DbUpdateConcurrencyException is thrown

Even though:

  • No business fields such as Title were modified
  • Any change (even statistical) updates RowVersion

The save operation fails.

 

Why Is This a Problem?

RowVersion assumes that every change to the record is concurrency-critical.

In many systems, however, the following types of changes should not block business edits:

  • Statistical updates
  • Log-like data
  • System-maintained fields (LastSeen, ViewCount, HitCount, etc.)

 

Solution: Targeted Use of ConcurrencyCheck

In such cases, it is better to include only meaningful business fields in concurrency control:

public class Book
{
    public int Id { get; set; }
    [ConcurrencyCheck]    public string Title { get; set; }
    public int ViewCount { get; set; }
}

 

With this model:

  • Changes to ViewCount do not cause conflicts
  • A concurrency exception occurs only if the same business field is modified by another user
  • The overall user experience is significantly improved

 

Key Takeaway

Optimistic concurrency should focus on meaningful business changes,
not on every modification made to a record.

While RowVersion is a powerful tool,
in certain real-world scenarios, ConcurrencyCheck provides a more precise and flexible solution.

 

Sample Implementation of Optimistic Concurrency in EF Core

Consider the following entity:

public class Book
{
    public int Id { get; set; }
    [ConcurrencyCheck]    public string Title { get; set; }
}

 

API Example

app.MapPost("/test1", async (int id, string newTitle) =>
{
    using (var context = new SchoolContext())
    {
       var book = context.Books.First(x => x.Id == id);
        book.Title = newTitle;
        context.SaveChanges();
    }
});

If two API calls attempt to update the same book title simultaneously, the second call will fail with a DbUpdateConcurrencyException, preventing silent overwrites.

 

What About UI Scenarios?

In UI-driven applications, there is often a delay between reading data and saving changes.

For example:

  • User A opens an edit form but does not save immediately
  • User B edits and saves the same record
  • User A later clicks Save, unknowingly overriding User B’s changes

To prevent this, we can apply the same concurrency mechanism in the UI.

 

ViewModel

public class BookEditVm
{
    public int BookId { get; set; }
    public string Title { get; set; }

    // Original value for concurrency check
    public string OriginalTitle { get; set; }
}

 

Controller – GET

public async Task<IActionResult> Edit(int id)
{
    var book = await _context.Books.FindAsync(id);

    var vm = new BookEditVm
    {
        BookId = book.Id,
        Title = book.Title,
        OriginalTitle = book.Title
    };


    return View(vm);
}

 

Razor View

<input asp-for="BookId" type="hidden" />
<input asp-for="OriginalTitle" type="hidden" />
<input asp-for="Title" class="form-control" />

 

Controller – POST

[HttpPost]
public async Task<IActionResult> Edit(BookEditVm vm)
{
    var book = new Book
    {
        Id = vm.BookId,
        Title = vm.Title
    };
    _context.Attach(book);

    // Inform EF of the ORIGINAL value
    _context.Entry(book).Property(b => b.Title) .OriginalValue = vm.OriginalTitle;

    try
    {
        await _context.SaveChangesAsync();
        return RedirectToAction("Index");
    }
    catch (DbUpdateConcurrencyException)
    {
        ModelState.AddModelError("", "This record was modified by another user.");
        return View(vm);
    }
}

 

⚠️ Important Note
The following line is essential: _context.Entry(book).Property(b => b.Title).OriginalValue = vm.OriginalTitle; 

Whether you use ConcurrencyCheck or Timestamp, EF requires the original value to be explicitly provided for concurrency tracking.

The same pattern applies to RowVersion, with the difference that you must add:

[Timestamp] public byte[] RowVersion { get; set; }

to both the entity and the ViewModel.