What's New in EF Core 10: ExecuteUpdate Gets a Major Upgrade
Entity Framework Core 10 brings significant improvements to the ExecuteUpdate API, making bulk updates more intuitive and powerful. Let's explore the two major enhancements that will change how you write update operations.
1. Say Goodbye to Expression Trees: Regular Lambdas Are Here
One of the most frustrating aspects of ExecuteUpdate in previous versions was the requirement to use expression trees for dynamic updates. EF Core 10 changes this fundamentally.
Before (EF Core 9 and Earlier)
If you wanted to conditionally update properties, you had to manually construct expression trees:
Expression<Func<SetPropertyCalls<Blog>, SetPropertyCalls<Blog>>> setters = b => b.SetProperty(x => x.Views, 8);
if (nameChanged)
{
var parameter = Expression.Parameter(typeof(SetPropertyCalls<Blog>), "s");
var blogParameter = Expression.Parameter(typeof(Blog), "b");
var viewsSetter = Expression.Call(
parameter,
nameof(SetPropertyCalls<Blog>.SetProperty),
new[] { typeof(int) },
Expression.Lambda(Expression.Property(blogParameter, nameof(Blog.Views)), blogParameter),
Expression.Constant(8));
var nameSetter = Expression.Call(
viewsSetter,
nameof(SetPropertyCalls<Blog>.SetProperty),
new[] { typeof(string) },
Expression.Lambda(Expression.Property(blogParameter, nameof(Blog.Name)), blogParameter),
Expression.Constant("foo"));
setters = Expression.Lambda<Func<SetPropertyCalls<Blog>, SetPropertyCalls<Blog>>>(
nameSetter,
parameter);
}
await context.Blogs.ExecuteUpdateAsync(setters);
This was complicated, error-prone, and made a common scenario unnecessarily difficult.
After (EF Core 10)
Now you can use regular lambdas with standard control flow:
await context.Blogs.ExecuteUpdateAsync(s =>
{
s.SetProperty(b => b.Views, 8);
if (nameChanged)
{
s.SetProperty(b => b.Name, "foo");
}
});
That's it! No more manual expression tree construction. You can use if statements, loops, and any other C# constructs naturally.
Real-World Example: Conditional Updates
public async Task UpdateBlogAsync(int blogId, BlogUpdateDto updates)
{
await context.Blogs
.Where(b => b.Id == blogId)
.ExecuteUpdateAsync(s =>
{
if (updates.Title != null)
{
s.SetProperty(b => b.Title, updates.Title);
}
if (updates.Rating.HasValue)
{
s.SetProperty(b => b.Rating, updates.Rating.Value);
}
if (updates.IsPublished.HasValue)
{
s.SetProperty(b => b.IsPublished, updates.IsPublished.Value);
}
// Always update the modified date
s.SetProperty(b => b.ModifiedAt, DateTime.UtcNow);
});
}
This generates optimized SQL that only includes the properties you actually want to update.
2. ExecuteUpdate Now Supports JSON Columns
EF Core has supported JSON columns for a while, but ExecuteUpdate couldn't touch them—until now. EF Core 10 enables efficient bulk updates of JSON data.
Prerequisites
This feature requires mapping your types as complex types (not owned entities):
public class Blog
{
public int Id { get; set; }
public BlogDetails Details { get; set; }
}
public class BlogDetails
{
public string Title { get; set; }
public int Views { get; set; }
public DateTime LastUpdated { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.ComplexProperty(b => b.Details, bd => bd.ToJson());
}
Before (EF Core 9 and Earlier)
You had to load entities, modify them, and call SaveChanges:
// Inefficient: loads all matching blogs into memory
var blogs = await context.Blogs
.Where(b => b.Details.Views > 1000)
.ToListAsync();
foreach (var blog in blogs)
{
blog.Details.Views += 1;
blog.Details.LastUpdated = DateTime.UtcNow;
}
await context.SaveChangesAsync();
For bulk operations, this was slow and memory-intensive.
After (EF Core 10)
Direct bulk updates on JSON properties:
await context.Blogs
.Where(b => b.Details.Views > 1000)
.ExecuteUpdateAsync(s => s
.SetProperty(b => b.Details.Views, b => b.Details.Views + 1)
.SetProperty(b => b.Details.LastUpdated, DateTime.UtcNow));
Generated SQL (SQL Server 2025)
EF Core 10 generates highly optimized SQL using the new modify function:
UPDATE [b]
SET [Details].modify('$.Views', JSON_VALUE([b].[Details], '$.Views' RETURNING int) + 1),
[Details].modify('$.LastUpdated', @p0)
FROM [Blogs] AS [b]
WHERE JSON_VALUE([b].[Details], '$.Views' RETURNING int) > 1000
For older SQL Server versions that use nvarchar(max) for JSON, EF generates appropriate SQL as well.
Nested JSON Updates
You can update deeply nested properties too:
public class BlogDetails
{
public string Title { get; set; }
public Statistics Stats { get; set; }
}
public class Statistics
{
public int Views { get; set; }
public int Likes { get; set; }
public int Shares { get; set; }
}
// Update nested property
await context.Blogs.ExecuteUpdateAsync(s =>
s.SetProperty(b => b.Details.Stats.Views, b => b.Details.Stats.Views + 1));
Performance Benefits
Both improvements significantly boost performance:
- Regular lambdas: Simpler code means less development time and fewer bugs
- JSON updates: No entity materialization, no change tracking overhead, single database roundtrip
Performance Comparison
// ❌ Old way: ~500ms for 10,000 rows
var blogs = await context.Blogs.ToListAsync();
foreach (var blog in blogs)
{
blog.Details.Views += 1;
}
await context.SaveChangesAsync();
// ✅ New way: ~50ms for 10,000 rows
await context.Blogs.ExecuteUpdateAsync(s =>
s.SetProperty(b => b.Details.Views, b => b.Details.Views + 1));
Important Considerations
Change Tracking
ExecuteUpdate bypasses EF's change tracker completely:
var blog = await context.Blogs.FindAsync(1);
Console.WriteLine(blog.Views); // Output: 5
await context.Blogs
.Where(b => b.Id == 1)
.ExecuteUpdateAsync(s => s.SetProperty(b => b.Views, b => b.Views + 1));
// The tracked instance is NOT updated!
Console.WriteLine(blog.Views); // Still outputs: 5
// The database has: 6
Best practice: Avoid mixing ExecuteUpdate with change-tracked entities, or reload entities after bulk updates.
Concurrency Control
ExecuteUpdate doesn't automatically apply concurrency tokens, but you can implement it manually:
var rowsUpdated = await context.Blogs
.Where(b => b.Id == blogId && b.ConcurrencyToken == currentToken)
.ExecuteUpdateAsync(s => s.SetProperty(b => b.Title, newTitle));
if (rowsUpdated == 0)
{
throw new DbUpdateConcurrencyException("Blog was modified by another user");
}
Conclusion
EF Core 10's ExecuteUpdate improvements represent a significant step forward in developer experience and performance. Regular lambda support eliminates boilerplate and makes dynamic updates natural, while JSON column support unlocks efficient document-style updates in relational databases.
Whether you're building high-performance APIs or simplifying your data access code, these features are worth exploring.