Imagine you need to increase the price of 10,000 products by 5%. One way to do this in Entity Framework Core is to load all the products, update their prices in C#, and save the changes.
That approach works, but it can use more memory and take longer than necessary. EF Core has to load the records, keep track of the changes, and send the updates to the database.
There is a simpler option: ExecuteUpdate.
Introduced in EF Core 7, ExecuteUpdate lets you update matching database records directly without loading every entity into your application. It can make bulk updates faster and reduce memory use, especially when you only need to change a few columns.
In this article, we will look at how ExecuteUpdate works, how it differs from the usual EF Core update approach, and when you should use each method.
What Is ExecuteUpdate in EF Core?
ExecuteUpdate allows you to update multiple database records with a single database operation.
For example, suppose your application has a Product table with thousands of records. You want to increase the price of every active product by 5%.
You could load the products, change each price, and call SaveChangesAsync. Or you could let the database perform the update directly.
Here is a simple example:
int affectedRows = await dbContext.Products
.Where(p => p.IsActive)
.ExecuteUpdateAsync(setters =>
setters.SetProperty(
p => p.Price,
p => p.Price * 1.05m));
This code updates the prices of all active products without loading them into memory first.
Let's break it down:
- Where selects the products that should be updated.
- SetProperty defines the new value for the Price column.
- ExecuteUpdateAsync sends the update to the database.
- affectedRows contains the number of rows reported as affected by the operation.
The database does the work, so EF Core does not need to create an entity object for every matching product.
ExecuteUpdate and ExecuteUpdateAsync are available in EF Core 7 and later. The database provider must also support the operation.
The Traditional Approach: Load Entities and Save Changes
Before looking at the advantages of ExecuteUpdate, let's understand the usual approach.
Suppose you want to increase the prices of active products by 5%.
var products = await dbContext.Products
.Where(p => p.IsActive)
.ToListAsync();
foreach (var product in products)
{
product.Price *= 1.05m;
}
await dbContext.SaveChangesAsync();
This code is easy to understand. EF Core loads the matching products, tracks their changes, and generates the necessary database updates when SaveChangesAsync runs.
For a small number of records, this approach is often perfectly fine.
The problem appears when the query returns thousands or hundreds of thousands of records.
EF Core needs memory for the loaded objects and its change-tracking information. The application also has to transfer the records from the database before it can update them.
For an operation that only changes one property, loading every entity can be unnecessary work.
How ExecuteUpdate Improves Performance
With ExecuteUpdate, you can perform the same price update without loading the products.
await dbContext.Products
.Where(p => p.IsActive)
.ExecuteUpdateAsync(setters =>
setters.SetProperty(
p => p.Price,
p => p.Price * 1.05m));
EF Core translates the query into a database update statement. For a relational database, it is conceptually similar to:
UPDATE Products
SET Price = Price * 1.05
WHERE IsActive = 1;
The exact SQL depends on the database provider and how your model is configured.
The main difference is where the work happens.
Traditional approach | ExecuteUpdate |
|---|
Loads matching entities into memory | Does not load matching entities |
Uses change tracking for the updates | Does not use EF Core change tracking |
Updates are applied through SaveChanges | Executes the update immediately |
Useful when business logic needs entity objects | Useful when many rows need a direct update |
Can require more memory for large result sets | Usually uses less application memory for bulk updates |
ExecuteUpdate can reduce the amount of data sent between the application and database. However, actual performance depends on indexes, the number of matching rows, database load, and the complexity of the update.
For a query that affects only five records, the difference may be small. For a query that affects thousands of records, avoiding entity loading can make a meaningful difference.
Update Multiple Columns with ExecuteUpdate
You are not limited to updating a single property. You can set several properties in the same operation.
For example, suppose you want to mark discontinued products as inactive and record when the change happened.
var affectedRows = await dbContext.Products
.Where(p => p.IsDiscontinued)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.IsActive, false)
.SetProperty(p => p.UpdatedAt, DateTime.UtcNow));
This updates both columns for every matching product.
The update happens directly in the database, so EF Core does not need to load the affected product objects.
For timestamp columns, use a consistent time-handling strategy across your application. If your application requires a database-generated timestamp, consider using a database-side expression supported by your provider instead of calculating the value in the application.
Apply Conditions Before Updating Records
One important benefit of ExecuteUpdate is that you can use a LINQ query to select the records that should change.
For example, suppose you want to give a discount to products that cost more than 1,000.
int affectedRows = await dbContext.Products
.Where(p => p.IsActive && p.Price > 1000m)
.ExecuteUpdateAsync(setters =>
setters.SetProperty(
p => p.Price,
p => p.Price * 0.90m));
This applies a 10% discount to active products whose current price is greater than 1,000.
The database evaluates the condition and updates the matching rows. The application does not need to load those products first.
Always review the Where condition carefully before running a bulk update. A missing or incorrect condition can change far more records than intended.
If you are working with important business data, you can first run a matching CountAsync query or inspect the affected rows in a safe test environment. Remember that a separate count and update are not automatically a single atomic operation, so concurrent changes may affect the final result.
Important Limitation: ExecuteUpdate Does Not Update Tracked Entities
This is one of the most important differences between ExecuteUpdate and SaveChanges.
EF Core does not update the values of entities that are already being tracked by the same DbContext.
Consider this example:
var product = await dbContext.Products
.FirstAsync(p => p.Id == 1);
await dbContext.Products
.Where(p => p.Id == 1)
.ExecuteUpdateAsync(setters =>
setters.SetProperty(
p => p.Price,
p => p.Price + 100m));
Console.WriteLine(product.Price);
The value printed by Console.WriteLine may still be the old price because the tracked product object was not refreshed by ExecuteUpdate.
The database has the new price, but the in-memory entity still has its previous value.
This can cause confusing behavior if you continue working with that entity and later save changes.
How to Handle This Problem
If you need the updated value in the current application code, you have a few options.
You can reload the tracked entity:
await dbContext.Entry(product).ReloadAsync();
You can use a fresh DbContext to query the updated data, or you can perform the bulk update before loading the entities that need to be tracked.
Choose the approach that fits your workflow. If the application does not need the updated entity objects, avoiding entity loading in the first place is usually simpler.
ExecuteUpdate Does Not Automatically Run Entity Update Logic
When you use the traditional EF Core approach, your application can change an entity's properties, run validation logic, and save changes through the normal change-tracking process.
ExecuteUpdate works differently.
It does not load the affected entities and does not run the normal entity change-tracking workflow for each updated row. You should not assume that application logic tied to tracked entity updates will run automatically.
For example, if your application updates an audit property through custom code before calling SaveChanges, ExecuteUpdate will not automatically run that custom code for every affected record.
If you need audit information, validation, or related business operations, handle them explicitly. You may be able to include some changes directly in the update statement, but more complex business rules may still require loading entities or using a different database operation.
Does ExecuteUpdate Support Transactions?
ExecuteUpdate executes immediately. It does not wait for a later call to SaveChangesAsync.
A single database update statement is handled according to the database's transaction rules. However, if you need several operations to succeed or fail together, use an explicit transaction.
For example, suppose you need to update product prices and write a record to an audit table.
await using var transaction =
await dbContext.Database.BeginTransactionAsync();
try
{
int affectedRows = await dbContext.Products
.Where(p => p.IsActive)
.ExecuteUpdateAsync(setters =>
setters.SetProperty(
p => p.Price,
p => p.Price * 1.05m));
dbContext.AuditLogs.Add(new AuditLog
{
Action = "Active product prices increased",
CreatedAt = DateTime.UtcNow
});
await dbContext.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
This example assumes that AuditLog is an entity in your EF Core model.
The explicit transaction groups the bulk update and audit insert together. If an operation fails before the commit, the transaction can be rolled back.
Without an explicit transaction, the bulk update and the later SaveChangesAsync call are not automatically one combined transaction.
When Should You Use ExecuteUpdate?
ExecuteUpdate is a good choice when you need to change many rows and the update can be expressed directly in the database.
Consider using it for:
- Updating prices, statuses, or other simple fields across many records.
- Marking expired subscriptions as inactive.
- Applying a discount to matching products.
- Updating a timestamp or status for a group of records.
- Avoiding unnecessary entity loading for large updates.
However, the traditional approach is often better when each entity needs individual processing.
For example, if updating an order requires running complex business logic, checking several related entities, or producing a separate result for each order, loading the entities may be the clearer and safer choice.
The goal is not to replace every SaveChangesAsync call with ExecuteUpdate. It is to choose the right tool for the operation.
Common Mistakes to Avoid
Mistake | Why it causes problems | Better approach |
|---|
Expecting tracked entities to refresh automatically | In-memory values may remain outdated | Reload entities or use a fresh context |
Forgetting the Where condition | The update may affect every row | Review and test the condition |
Assuming entity update logic runs | Custom logic tied to tracked changes may be skipped | Apply required business rules explicitly |
Using it for complex per-entity processing | The operation may not fit a direct database update | Load entities when individual processing is necessary |
Assuming several calls are one transaction | Earlier operations may already be committed | Use an explicit transaction when needed |
Assuming it is always faster | Indexes and query complexity still matter | Measure performance with realistic data |
Best Practices for Using ExecuteUpdate
Keep these points in mind when using ExecuteUpdate in production applications:
- Use it for direct updates. It is especially useful when many records need the same kind of change.
- Write clear conditions. Make sure the
Where clause selects exactly the records you intend to update. - Check the affected row count. Use the returned count when your application needs to know how many rows were changed.
- Watch for stale entities. Do not assume that EF Core will refresh objects already tracked by the current context.
- Handle business rules explicitly. Bulk updates do not automatically run your usual per-entity application logic.
- Use transactions when needed. Group multiple database operations when they must succeed or fail together.
- Test with realistic data. Compare performance and verify the results before using bulk updates on large production tables.
Conclusion
ExecuteUpdate is a useful feature in EF Core when you need to update many database records without loading them into memory.
It can reduce memory use, avoid unnecessary data transfer, and let the database perform updates more directly. It works particularly well for changes such as updating prices, changing statuses, and setting timestamps across many rows.
However, it does not refresh tracked entities or automatically run the usual change-tracking workflow. If your application depends on per-entity business logic or needs several operations to succeed together, plan for those requirements explicitly.
Use ExecuteUpdate when a direct database update is the right fit, and use the traditional entity approach when your application needs to work with individual objects. Choosing between them based on the actual task will help keep your EF Core code efficient and easier to maintain.
Join the conversation! Your thoughts help the community grow.