ExecuteUpdateAsync() updates rows directly in the database without loading entities into memory, without tracking them, and without calling SaveChangesAsync().
It is useful when you want to update one or more property (column) for all rows that match a query.
Update Rows with a Filter
Most of the time, you will combine Where() with ExecuteUpdateAsync().
using var context = new AppDbContext();
await context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdateAsync(s => s
.SetProperty(b => b.IsVisible, false));
Executed SQL:
UPDATE [b]
SET [b].[IsVisible] = CAST(0 AS bit)
FROM [Blogs] AS [b]
WHERE [b].[Rating] < 3
Only rows that match the filter are updated.
Update All Rows
If you call ExecuteUpdateAsync() directly from a DbSet, EF Core updates every row in that table.
using var context = new AppDbContext();
await context.Blogs
.ExecuteUpdateAsync(s => s
.SetProperty(b => b.IsVisible, true));
This executes a database UPDATE for all Blogs.
UPDATE [b]
SET [b].[IsVisible] = CAST(0 AS bit)
FROM [Blogs] AS [b]
Use this carefully because no filter is applied.
Update One Property
Use SetProperty() to specify the property you want to update.
using var context = new AppDbContext();
await context.Blogs
.Where(b => b.Name == "ZZZ Projects")
.ExecuteUpdateAsync(s => s
.SetProperty(b => b.Rating, 5));
This updates only the Rating property for the matching blog.
Update Multiple Properties
You can chain multiple SetProperty() calls to update more than one property.
using var context = new AppDbContext();
await context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdateAsync(s => s
.SetProperty(b => b.Rating, 3)
.SetProperty(b => b.IsVisible, false));
This updates both Rating and IsVisible in the same database command.
Update Using the Current Value
The new value can also be based on the current database value.
using var context = new AppDbContext();
await context.Blogs
.Where(b => b.IsVisible)
.ExecuteUpdateAsync(s => s
.SetProperty(b => b.Rating, blog => blog.Rating + 1));
This increments the current Rating value for all visible blogs.
The expression must be translatable to SQL.
Update with a Navigation Filter
You can use navigation data in the query filter before calling ExecuteUpdateAsync().
using var context = new AppDbContext();
await context.Blogs
.Where(b => b.Posts.All(p => p.PublishedOn.Year < 2018))
.ExecuteUpdateAsync(s => s
.SetProperty(post => post.IsVisible, false));
This updates all blogs which contains only posts published before 2018.
The filter is translated to SQL and executed directly in the database.
Update using a regular lambda
Starting in EF Core 10, ExecuteUpdateAsync accepts a regular lambda, not only expression trees.
This makes dynamic/conditional updates easier to write, as long as every SetProperty remains SQL-translatable.
bool nameChanged = true; // Local variable (not a column)
await context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdateAsync(s =>
{
s.SetProperty(b => b.Views, 8);
if (nameChanged)
{
s.SetProperty(b => b.Name, "foo");
}
});
Rows Affected
ExecuteUpdateAsync() returns an int value that indicates the number of rows updated.
using var context = new AppDbContext();
var rowsAffected = await context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdateAsync(s => s
.SetProperty(b => b.IsVisible, false));
You can use this value for logging, validation, or checking whether the update matched the expected rows.
Important Behavior
ExecuteUpdateAsync() executes immediately.
That means:
- it does not require for
SaveChangesAsync() - it does not use the
ChangeTracker - it does not update tracked entity instances already loaded in memory
- it sends an
UPDATEcommand directly to the database
For tracked entity workflows, see SaveChanges.
For a deeper explanation of tracking behavior, see ChangeTracker.
ExecuteUpdate Requirements
ExecuteUpdateAsync() is available starting with EF Core 7.
(For older versions of EF Core (EF Core 2+) or if you prefer the syntax provided by EF Extensions over using SetProperty, you can use UpdateFromQuery.)
To use it correctly, keep these requirements in mind:
- the update must be based on an EF Core query
- the values passed to
SetProperty()must be translatable to SQL - the operation runs immediately in the database
- tracked entities already loaded in the current
DbContextare not automatically updated in memory SaveChangesAsync()is not required after callingExecuteUpdateAsync()
For example, this works because the expression can be translated to SQL:
using var context = new AppDbContext();
await context.Blogs
.Where(b => b.Rating < 5)
.ExecuteUpdateAsync(s => s
.SetProperty(b => b.Rating, b => b.Rating + 1));
This increments the Rating column directly in the database.
However, custom .NET methods that cannot be translated to SQL cannot be used inside SetProperty().
using var context = new AppDbContext();
await context.Blogs
.ExecuteUpdateAsync(s => s
.SetProperty(b => b.Name, b => NormalizeName(b.Name)));
This kind of expression cannot be translated unless NormalizeName() is mapped or translated by the provider.
How ExecuteUpdate Works
ExecuteUpdateAsync() sends an UPDATE command directly to the database.
It does not follow the normal tracked entity workflow.
A normal tracked update usually looks like this:
using var context = new AppDbContext();
var blog = await context.Blogs
.FirstAsync(b => b.Name == "ZZZ Projects");
blog.Rating = 5;
await context.SaveChangesAsync();
In that workflow, EF Core:
- loads the entity
- tracks it in the
DbContext - detects the property change
- generates the update when
SaveChangesAsync()runs
ExecuteUpdateAsync() skips that workflow.
using var context = new AppDbContext();
await context.Blogs
.Where(b => b.Name == "ZZZ Projects")
.ExecuteUpdateAsync(s => s
.SetProperty(b => b.Rating, 5));
In this case, EF Core translates the query and update expression into a direct database command.
No entity instance is loaded, and the ChangeTracker is not involved.
ExecuteUpdate vs SaveChanges
ExecuteUpdateAsync() and SaveChangesAsync() both update data, but they are designed for different workflows.
| Feature | ExecuteUpdateAsync() |
SaveChangesAsync() |
|---|---|---|
| Loads entities | No | Usually yes |
| Uses ChangeTracker | No | Yes |
Requires SaveChangesAsync() |
No | It is the save operation |
| Best for | Set-based updates | Tracked entity workflows |
| Updates in-memory tracked entities | No | Yes |
| Can use per-entity business logic in memory | No | Yes |
Use ExecuteUpdateAsync() when you want to update rows directly in the database.
Use SaveChangesAsync() when you are working with tracked entities and want EF Core to detect changes during a unit of work.
For the tracked entity workflow, see SaveChanges.
For tracking behavior, see ChangeTracker.
ExecuteUpdate vs BulkUpdate
Is ExecuteUpdate a real “Bulk Update”? Not quite. Yes, it update rows in bulk directly in SQL. However, it does not bulk update entities with their own per-row values like BulkUpdate (Entity Framework Extensions).
ExecuteUpdateAsync() is a set-based update API built into EF Core.
It is useful when every matching row can be updated using the same SQL-translatable rule.
using var context = new AppDbContext();
await context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdateAsync(s => s
.SetProperty(b => b.IsVisible, false));
This works well because the update is expressed as one database rule.
However, ExecuteUpdateAsync() is not the same as a full bulk update library.
Use BulkUpdate from Entity Framework Extensions when:
- each row has different values coming from memory
- you need to update many entities from a list
- you need advanced options such as batching, auditing, custom mappings, or output values
- you need more control over large data synchronization scenarios
In short:
- use
ExecuteUpdateAsync()for set-based updates expressed as a query - use
BulkUpdatewhen each row needs different values from entities in memory
| Question | ExecuteUpdateAsync() |
BulkUpdate (EF Extensions) |
|---|---|---|
| How are values applied? | One SQL rule for all matching rows | Values come from each entity |
| Where does the data come from? | Query expression translated to SQL | In-memory entity list |
| Different values per row? | No | Yes |
| Uses ChangeTracker? | No | Not the normal EF Core tracking workflow |
| Best for | Set-based updates | High-volume updates with per-row values |
Performance Benchmarks
Updating 100,000 rows (3 int columns + 3 string columns):
| Method | Time | Memory |
|---|---|---|
ExecuteUpdate |
365 ms | Very low |
BulkUpdate (EF Extensions) |
1900 ms | Low |
SaveChanges |
4800 ms | High |
Important: For
BulkUpdateandSaveChanges, part of the time is spent materializing the list of entities from the database before the update starts. This step is required becauseBulkUpdateandSaveChangeswork from entities, not from a LINQ rule.
Takeaway: ExecuteUpdate can be much faster than the traditional tracked approach when the update can be expressed as a set-based SQL rule. BulkUpdate remains more appropriate when each row needs different values coming from entities in memory.
Bulk Update - Concrete Example
ExecuteUpdateAsync() works well when the same SQL rule applies to every matching row.
A real bulk update from EF Extensions is different: each entity can carry its own value from memory.
For example, imagine you need to update 1,000,000 products, and each product already has a different NewPrice value calculated in memory.
// @nuget: Z.EntityFramework.Extensions.EFCore
using Z.EntityFramework.Extensions;
var products = GetMillionProductsWithNewPrices();
context.BulkUpdate(products, options =>
{
options.ColumnInputExpression = p => new { p.Id, p.NewPrice };
options.BatchSize = 10000;
});
Doing the same with ExecuteUpdateAsync() in bulk is impossible.
ExecuteUpdateAsync() and BulkUpdate complement each other rather than compete.
When to Use ExecuteUpdate
Use ExecuteUpdateAsync() when:
- you want to update rows directly in the database
- the update can be expressed as a SQL-translatable rule
- you do not need to load entities first
- you do not need the
ChangeTracker - you do not need entity instances updated in memory
- you want to update many rows with one database command
Good examples include:
- hiding old records
- increasing a numeric value
- setting a status for all rows that match a filter
- updating a flag based on related data
- applying a simple batch correction
using var context = new AppDbContext();
await context.Blogs
.Where(b => b.LastUpdated < cutoffDate)
.ExecuteUpdateAsync(s => s
.SetProperty(b => b.IsArchived, true));
When Not to Use ExecuteUpdate
ExecuteUpdateAsync() is not the best choice when the update depends on tracked entity behavior.
Avoid it when:
- you need to update already loaded entity instances in memory
- you rely on
ChangeTrackerstate - you need entity events or in-memory business logic before saving
- each row needs a different value from a local object list
- the update expression cannot be translated to SQL
- you need the normal
SaveChangesAsync()pipeline
For example, if your application loads an entity, applies domain logic, validates it, and then saves it, a tracked workflow is usually clearer.
using var context = new AppDbContext();
var blog = await context.Blogs
.FirstAsync(b => b.Name == "ZZZ Projects");
blog.Rating = 5;
await context.SaveChangesAsync();
For normal tracked updates, see Updating Data.
Common Pitfalls
Be careful with the following mistakes.
Expecting SaveChangesAsync to Be Required
ExecuteUpdateAsync() runs immediately.
This is unnecessary:
using var context = new AppDbContext();
await context.Blogs
.Where(b => b.Rating < 3)
.ExecuteUpdateAsync(s => s
.SetProperty(b => b.IsVisible, false));
// DO NOT USE!
await context.SaveChangesAsync();
The SaveChangesAsync() call does not save the ExecuteUpdateAsync() operation. The update already happened.
Expecting Tracked Entities to Be Updated in Memory
If an entity is already tracked, ExecuteUpdateAsync() does not automatically update that in-memory instance.
using var context = new AppDbContext();
var blog = await context.Blogs
.FirstAsync(blog => blog.Name == "ZZZ Projects");
await context.Blogs
.Where(b => b.Name == "ZZZ Projects")
.ExecuteUpdateAsync(s => s
.SetProperty(b => b.Rating, 5));
// The tracked blog instance may still have the old Rating value.
If you need fresh values, reload the entity or use a new DbContext.
Using Non-Translatable Expressions
The update expression must be translatable to SQL.
using var context = new AppDbContext();
await context.Blogs
.ExecuteUpdateAsync(s => s
.SetProperty(b => b.Name, b => NormalizeName(b.Name)));
This fails unless the method can be translated by the provider.
Keep SetProperty() expressions simple and SQL-translatable.
ExecuteUpdate Release History
- EF Core 10:
- JSON support: Added
ExecuteUpdatesupport for relational JSON columns, allowing efficient bulk updates of JSON properties when mapped as complex types. - Easier dynamic updates:
ExecuteUpdateAsyncnow accepts a regular lambda (not only expression trees), making dynamic and conditional updates much easier to write.
- JSON support: Added
- EF Core 9: Improved
ExecuteUpdateto support complex type properties. - EF Core 8: Improved
ExecuteUpdateandExecuteDeleteto support more complex queries (owned types, unions, and TPT), as long as all updates target a single database table. - EF Core 7: Introduced
ExecuteUpdateandExecuteDelete. - EF Core 2+: For older versions of EF Core or if you prefer the syntax provided by EF Extensions over using
SetProperty, you can use UpdateFromQuery.
ExecuteUpdateAsync() was introduced in EF Core 7.
The main idea is the same across EF Core 7, 8, 9, and 10: update matching rows directly in the database without loading entities.
However, newer EF Core versions may improve translation support, provider behavior, and supported query shapes.
The exact behavior can still depend on the database provider.
External Resources - ExecuteUpdate
The following videos are useful if you want to see ExecuteUpdateAsync() in real EF Core examples, especially how it differs from tracked updates with SaveChangesAsync() and how it avoids unnecessary entity loading.
Video 1 - EF Core 7 - Performance Improvements With ExecuteUpdate and ExecuteDelete
Milan Jovanović demonstrates how to replace a slow foreach-based update with ExecuteUpdate. He applies a computed value, explains how the update runs directly in the database without tracking entities, and compares performance against the traditional approach.
Key timestamps:
- 1:45 — Creating the new endpoint based on
ExecuteUpdate - 2:30 — Filtering employees by
CompanyIdusing LINQ - 3:00 — Applying a percentage salary increase with
SetProperty - 3:45 — No tracking and no
SaveChangescall - 4:45 — SQL
UPDATEgenerated by EF Core 7 - 6:00 — Performance comparison: foreach update vs.
ExecuteUpdate
Video 2 - Entity Framework 7 Makes Performing Updates and Deletes Easy
Israel Quiroz explains how ExecuteUpdateAsync() replaces the traditional workflow of retrieving an entity, modifying properties, and calling SaveChangesAsync(). He shows how a single database call can update several fields using SetProperty, without tracking entities.
Key timestamps:
- 3:28 — Traditional update workflow: retrieve, assign,
SaveChanges - 3:40 —
ExecuteUpdateAsyncwithSetProperty - 4:00 — Chained
SetPropertycalls - 4:20 — Fewer database round-trips
- 4:40 — No change tracking during the update
Video 3 - Spanish: Borrado y Actualizaciones Masivas - Nuevo de EF Core 7
Felipe Gavilan demonstrates mass updates using ExecuteUpdate with multiple SetProperty calls. He updates a timestamp and a string field, verifies the results in SQL Server, and shows how all changes are performed in one consolidated SQL UPDATE.
Key timestamps:
- 5:32 — First
SetProperty: assigning the update timestamp - 6:48 — Second
SetProperty: appending text - 7:48 — Running the mass update and reviewing results
- 8:31 — Beginning of performance comparison
- 9:31 — Final numbers: 100k updates
Summary
ExecuteUpdateAsync() lets you update rows directly in the database without loading entities, tracking them, or calling SaveChangesAsync().
Key points:
- it executes immediately
- it works from an EF Core query
- it uses
SetProperty()to define updated columns - it can update one or multiple properties
- it can use filters to control which rows are updated
- it does not update tracked entity instances already loaded in memory
- it is best for set-based updates that can be translated to SQL
Use SaveChangesAsync() when you need the normal tracked entity workflow.
Use BulkUpdate from Entity Framework Extensions when each row has different values coming from entities in memory or when you need advanced bulk options.
Related Articles
If you want to compare ExecuteUpdateAsync() with the most closely related EF Core saving patterns, these pages are the best next step:
- SaveChanges — how EF Core persists tracked changes
- Updating Data — how to update entities with the normal tracked workflow
- ChangeTracker — how EF Core stores tracking information internally
- Tracking Changes of Entities — how EF Core detects changes before saving
- ExecuteDelete — how to delete rows directly in the database without loading entities
- EF Core 10: What’s New — ExecuteUpdateAsync improvements in EF Core 10
FAQ
Does ExecuteUpdateAsync require SaveChangesAsync?
No. ExecuteUpdateAsync() executes immediately in the database. You do not need to call SaveChangesAsync() afterward.
Does ExecuteUpdateAsync use the ChangeTracker?
No. ExecuteUpdateAsync() does not use the ChangeTracker and does not update tracked entity instances already loaded in memory.
Can ExecuteUpdateAsync update multiple columns?
Yes. You can chain multiple SetProperty() calls to update several columns in the same database command.
Can ExecuteUpdateAsync use the current column value?
Yes. The new value can be based on the current database value, as long as the expression can be translated to SQL.
When should I use ExecuteUpdateAsync instead of SaveChangesAsync?
Use ExecuteUpdateAsync() when you want to update rows directly in the database using a query. Use SaveChangesAsync() when you are working with tracked entities and need the normal EF Core change-tracking workflow.
Is ExecuteUpdateAsync the same as BulkUpdate?
No. ExecuteUpdateAsync() is a set-based update API. A bulk update library is usually better when each row has different values coming from a list of entities in memory.