There is one problem that every developer eventually experiences, even if the application is small. Everything works perfectly during testing, but once multiple users start using the system at the same time, strange things begin to happen. Someone updates a record, another person updates the same record a few seconds later, and suddenly the first person’s changes disappear.
I remember struggling with this when I started building business applications. At first, I thought it was a database issue. Later I realized the database was actually doing exactly what it was supposed to do. The real problem was that my application never checked whether someone else had already modified the same data.
This is where concurrency handling in Entity Framework Core became very important for me.
In this article, I want to share what I learned while building business systems. The examples are simplified to make the concepts easier to understand. The code snippets shown in this article are not from my actual projects. They were created specifically to simplify the demonstrations while explaining the concepts.
I am not trying to explain every single feature available in EF Core. Instead, I want to explain the parts that actually helped me solve real business problems.
Understanding the Problem
Imagine an inventory management system.
Two warehouse employees open the same product page at almost the same time.
Employee A sees the stock quantity as 120.
Employee B also sees the stock quantity as 120.
Employee A updates the quantity to 115 because some items were damaged.
A few seconds later Employee B updates it to 140 after receiving new stocks.
Without any protection, whoever saves last wins. Employee A’s update simply disappears.
This is commonly called a lost update.
The application does not throw an error.
The database does not complain.
Everything looks successful.
Unfortunately, the business data is now incorrect.
That may sound like a small issue, but imagine this happening in payroll, banking, inventory, purchasing, or customer management systems.
A Simple EF Core Update
A normal update in EF Core looks very straightforward.
var product = await context.Products
.FirstOrDefaultAsync(p => p.Id == id);
product.Quantity = 115;
await context.SaveChangesAsync();There is nothing wrong with this code.
The problem appears when another user changes the same record before SaveChangesAsync() executes.
EF Core has no way of knowing that another user already modified the row unless we tell it how.
How I First Experienced This
In one of my projects, we had an approval process.
Several approvers could open the same request.
One approver might approve it.
Another approver might reject it a few seconds later because they were still looking at the old information on their screen.
The final result depended entirely on who clicked Save last.
From the user’s perspective, everything looked normal.
From the business perspective, it created confusion because users were asking why their decisions suddenly changed.
That experience pushed me to learn more about optimistic concurrency.
What Is Optimistic Concurrency?
Optimistic concurrency assumes that conflicts are uncommon.
Instead of locking records while users are editing them, EF Core allows everyone to work normally.
When someone finally saves, EF Core checks whether the data has changed since it was originally loaded.
If nothing changed, the update succeeds.
If someone already modified the record, EF Core detects the conflict and throws an exception.
This approach works very well for most business applications because users usually edit different records.
Using a Row Version
The most common approach is adding a version column.
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public int Quantity { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
}The RowVersion value changes automatically whenever the row is updated.
When EF Core saves the record, it includes that version in the update statement.
If the version no longer matches, EF Core knows another update already happened.
Instead of silently overwriting data, EF Core throws a DbUpdateConcurrencyException.
Handling the Exception
The exception can be handled like this.
try
{
await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
Console.WriteLine("The record was modified by another user.");
}This looks simple, but the real work begins after catching the exception.
The application now needs to decide what should happen.
Should it retry?
Should it reload the latest data?
Should it ask the user which version they want to keep?
The answer depends entirely on the business requirements.
Different Business Rules
One thing I learned is that there is no universal solution.
Every project has different rules.
For example, in an inventory system, we may force the user to reload before making another update.
In an employee profile page, maybe the latest change wins because small profile updates are less critical.
In a financial system, we definitely cannot allow silent overwrites.
Business requirements always come before technical preferences.
Loading Data
A very common query starts with filtering.
var product = await context.Products
.Where(p => p.Id == id)
.FirstOrDefaultAsync();Later, as the project grows, more conditions are usually added.
var product = await context.Products
.Where(p => p.Id == id)
.Where(p => p.IsActive)
.Where(p => p.Quantity > 0)
.FirstOrDefaultAsync();Nothing complicated yet.
But eventually those simple queries become part of larger business workflows.
Projection Helps
Sometimes we do not need the entire entity.
Instead, we only load the information required by the screen.
var product = await context.Products
.Select(p => new
{
p.Id,
p.Name,
p.Quantity
})
.FirstOrDefaultAsync();This reduces unnecessary data transfer.
It also keeps queries easier to understand.
Joining Related Data
Business screens often need information from multiple tables.
var orders = await context.Orders
.Select(o => new
{
o.OrderNumber,
Customer = o.Customer.Name,
Total = o.TotalAmount
})
.ToListAsync();As applications become larger, these queries slowly evolve into more complex business logic.
That is usually when concurrency issues begin to appear because multiple users start working with the same information.
Why Silent Overwrites Are Dangerous
I remember one discussion with users.
They insisted they clicked Save successfully.
Another employee also insisted they saved correctly.
Both were actually telling the truth.
The application simply allowed the second update to replace the first one.
Nobody realized data was lost until much later.
That experience taught me something important.
Sometimes no error is actually worse than getting an exception.
At least an exception tells us something unexpected happened.
Silent data loss is much harder to discover.
Reloading the Latest Data
Sometimes the simplest solution is asking the user to reload.
catch (DbUpdateConcurrencyException)
{
await context.Entry(product).ReloadAsync();
Console.WriteLine("Please review the latest changes before saving again.");
}The user immediately sees the newest values.
From there they can decide whether another update is still necessary.
Comparing Values
In some applications, users appreciate seeing both versions.
Current database values.
Their attempted changes.
Then they decide what should remain.
Conceptually the flow looks like this.
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var databaseValues =
await entry.GetDatabaseValuesAsync();
var currentValues = entry.CurrentValues;
}This allows the application to compare every property.
Many enterprise systems use this approach because it gives users full visibility before making another decision.
Business Logic Becomes More Important
When I first learned EF Core, I focused heavily on writing clean LINQ queries.
Later I realized queries were only a small part of building business software.
The real challenge was protecting data integrity.
Users expect the application to behave correctly even when dozens or hundreds of people are working simultaneously.
Concurrency handling is one of those topics that many developers ignore until users report inconsistent data.
Updating Related Data
Imagine updating an order.
The order header changes.
Inventory changes.
Audit logs are inserted.
Notifications are created.
Everything happens inside one business operation.
order.Status = "Approved";
inventory.Quantity -= order.Quantity;
audit.Action = "Order Approved";
await context.SaveChangesAsync();Now imagine another user modifying the inventory during the same period.
Without concurrency checks, the inventory numbers may no longer be accurate.
Transactions and Concurrency
People sometimes confuse transactions with concurrency.
A transaction guarantees that a group of operations either all succeed or all fail.
Concurrency ensures that the information being updated has not already changed.
They solve different problems.
Many real applications actually use both together.
Keeping Users Informed
One lesson I learned is that technical error messages should never appear directly to users.
Instead of showing an exception message, I prefer displaying something simple.
“The record was updated by another user. Please reload the information and try again.”
That message immediately tells users what happened and what they should do next.
Logging Conflicts
Whenever concurrency conflicts happen, I also think it is useful to log them.
Not because they are always errors.
Sometimes they simply indicate that many users are actively working with the same records.
Those logs help identify business processes that may need improvement later.
Testing Concurrency
One easy way to test concurrency is opening the same record in two browser windows.
Update it in the first window.
Save.
Then update it in the second window.
Save again.
Without concurrency handling, both operations succeed.
With optimistic concurrency enabled, the second save should detect that the data has already changed.
This simple test helped me understand how EF Core behaves much better than reading documentation alone.
Things I Learned Along the Way
One thing I appreciate about EF Core is that concurrency support is already built into the framework.
We only need to configure it correctly and understand how it fits our business rules.
I also learned that not every table requires concurrency checking.
Reference tables that rarely change may not need it.
Master data edited by many users definitely benefits from it.
Critical business records almost always deserve some level of protection.
Final Thoughts
Handling concurrency conflicts is not just about avoiding exceptions.
It is about protecting business data.
When users trust a system, they expect their changes to remain accurate.
They do not expect someone else’s update to silently replace their work.
Looking back, I probably underestimated this topic when I first started working with EF Core. I spent more time learning LINQ queries, relationships, and performance optimization. Those are all important, but concurrency is equally valuable because it protects the integrity of the information users depend on every day.
Whenever I work on a new business application now, I always ask one question early in the design phase.
“What happens if two people edit this record at exactly the same time?”
That simple question has helped me avoid many production issues before they even happen.
Hopefully this article gives you a practical introduction to concurrency handling in EF Core, not only from the technical side, but also from the perspective of solving real business problems that developers commonly encounter.
Sources
The concepts discussed in this article are based on official documentation and reputable technical references. The explanations and examples have been rewritten in my own words and expanded using my own understanding and experiences.
- Microsoft. Handling Concurrency Conflicts in EF Core.
- Microsoft. Saving Data with Entity Framework Core.
- Microsoft. Entity Framework Core Documentation.
- Microsoft Learn. Optimistic Concurrency in Entity Framework Core.
- Julie Lerman. Articles and presentations about Entity Framework Core.
- Official Entity Framework Core GitHub repository documentation for concurrency related examples.






