If multiple users can edit the same record, developers need to decide how your system handles that. If you don’t, EF Core decides for you.
Designing Intentional Conflict Handling in .NET Applications
By default, EF Core uses last write wins. The final call to SaveChanges() overwrites whatever came before it. There’s no warning and no exception.
That behavior isn’t necessarily wrong. But in many business systems, it isn’t what anyone expects.
Preventing lost updates starts with being explicit about concurrency instead of accepting the default.
EF Core gives you two ways to do that. You can detect conflicts after they happen (optimistic concurrency), or you can prevent them up front (pessimistic concurrency). The right choice depends on how your system behaves when two things want the same data at the same time.
The Default: Silent Overwrites
Picture a common workflow. User A loads a record. User B loads the same record. User A saves a change. User B saves a different change.
By default, the second save overwrites the first. No error. No indication that anything unusual happened.
If that record represents workflow state, approvals, or financial data, that overwrite may change the meaning of the data. And that’s usually not something you want happening silently.
If concurrent updates matter, the default behavior isn’t enough.
Optimistic Concurrency: Detect and Respond
Optimistic concurrency works best when conflicts are possible but not constant. Instead of locking a row when it’s read, EF Core checks whether it changed when you save.
In practice, that usually means adding a RowVersion column:
public class Case
{
public int Id { get; set; }
public string Status { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
}
When SaveChanges() runs, EF Core includes the original RowVersion value in the WHERE clause of the generated UPDATE. If someone else modified the row first, the values no longer match. The update affects zero rows, and EF Core throws a DbUpdateConcurrencyException.
Instead of losing data quietly, you get a signal that something changed.
From there, you decide what happens next:
try
{
await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
// resolve conflict
}
In many applications, the right move is simple: reload the current values and ask the user to review the latest state before trying again. That alone prevents silent data loss.
You can go further and compare original, attempted, and current values to merge changes selectively. But that only makes sense when the rules are clear and specific to your domain.
Optimistic concurrency doesn’t prevent collisions. It makes them explicit and gives you control over the outcome.
It also fits naturally with web applications. Requests are stateless. Users may leave a form open for minutes. Holding a database lock that entire time doesn’t scale well. Detecting conflicts at save time usually strikes the right balance between safety and performance.
Pessimistic Concurrency: Coordinate Up Front
Pessimistic concurrency prevents conflicts by making other updates wait. Instead of letting two actors work independently and resolving conflicts later, the system coordinates access so only one update can proceed at a time.
In practice, this usually means holding locks while a transaction is in progress. That can be a good fit for short, system-driven operations, like background workers competing for the same work. It’s usually a poor fit for user-driven workflows, where someone might keep an edit form open for minutes.
The trade-off is straightforward: pessimistic concurrency reduces the chance of conflicts, but it does it by reducing concurrency. You’re choosing coordination over throughput.
Choosing Deliberately
The decision between optimistic and pessimistic concurrency isn’t about which one is “better.” It’s about how your system should respond when two users or processes try to update the same data.
If conflicts are infrequent and user-driven, optimistic concurrency is usually the right starting point. If contention is frequent and system-driven, pessimistic locking may be the safer option.
What’s rarely correct is letting silent overwrites happen simply because that’s the default.
If concurrent updates matter in your system, it’s worth deciding how they should be handled instead of relying on the default behavior.