EF Core Bulk Updates and Deletes: Performance vs Change Tracking

Leader 24 59 119
calendar_todayschedule3 min read

Entity Framework Core 7 introduced two powerful methods for bulk operations:

ExecuteUpdateAsync and ExecuteDeleteAsync.

These methods are designed for high-performance scenarios where you need to update or delete multiple records efficiently without loading them into memory. They work great, but there's one caveat: EF Core's Change Tracker is not aware of these changes.

In this article, we explore how these methods work, why they bypass the Change Tracker, and most importantly—how you can audit or track changes manually when using them.

If you're not familiar with EF Core's Change Tracker, check out this introductory guide on Change Tracking to understand how EF Core monitors entity states.

Why Use ExecuteUpdateAsync and ExecuteDeleteAsync?

These methods:

  1. Generate direct SQL UPDATE and DELETE statements.
  2. Do not load entities into memory, improving performance.
  3. Bypass change tracking, which avoids unnecessary overhead.

Example Usage:

// Bulk update
await context.Users
    .Where(u => u.LastLogin < DateTime.UtcNow.AddYears(-1))
    .ExecuteUpdateAsync(setters => setters.SetProperty(u => u.IsInactive, true));

// Bulk delete
await context.Users
    .Where(u => u.IsInactive)
    .ExecuteDeleteAsync();

The Problem: No Change Tracking

Because EF Core does not load entities into memory during these operations, the Change Tracker never sees them. This means:

  1. No automatic change detection.
  2. No SaveChangesAsync() involvement.
  3. No triggers for audit or domain events.

So, how do you track what was changed or deleted?

1.Auditing Options for Bulk Operations

  1. Manual Pre-Query Before Execution

Fetch affected entities before performing the batch operation.

var affectedUsers = await context.Users
    .Where(u => u.LastLogin < DateTime.UtcNow.AddYears(-1))
    .Select(u => new { u.Id, u.Name, OldValue = u.IsInactive })
    .ToListAsync();

await context.Users
    .Where(u => u.LastLogin < DateTime.UtcNow.AddYears(-1))
    .ExecuteUpdateAsync(setters => setters.SetProperty(u => u.IsInactive, true));

// Log changes manually
foreach (var user in affectedUsers)
{
    Console.WriteLine($"User {user.Name} updated: IsInactive {user.OldValue} → true");
}

Pros:

Precise control over what you log.

Access to old values.

Cons:

Requires an extra query.

May impact performance for large datasets.

  1. Audit Table Logging

Save the affected data into an audit table before executing the bulk operation.

Example:

await context.AuditLogs.AddRangeAsync(affectedUsers.Select(user => new AuditLog
{
    EntityName = "User",
    EntityId = user.Id,
    ChangeType = "Updated",
    ChangedAt = DateTime.UtcNow,
    Description = $"User {user.Name} was updated: IsInactive {user.OldValue} → true"
    }));
    
    await context.SaveChangesAsync();


3. Database Triggers

Define SQL triggers to capture changes directly in the database.

Example :


    CREATE TRIGGER trg_User_Delete
    ON Users
    AFTER DELETE
    AS
    BEGIN
        INSERT INTO AuditLogs (EntityName, EntityId, ChangeType, ChangedAt, Description)
        SELECT 'User', Id, 'Deleted', GETUTCDATE(), CONCAT('User ', Name, ' was deleted') FROM deleted;
    END;


Pros:

Fully automated.

Ensures auditing even if EF Core is bypassed.

Cons:

Less flexible.

Harder to manage and version control.


4. EF Core Interceptors (Advanced)

Capture executed SQL commands using DbCommandInterceptor.

Limitation:

You can log the SQL query but not the previous values of affected entities.


    public class AuditInterceptor : DbCommandInterceptor
    {
        public override InterceptionResult<DbDataReader> ReaderExecuting(
            DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
        {
            Console.WriteLine($"SQL Executed: {command.CommandText}");
        return base.ReaderExecuting(command, eventData, result);
    }
}

Registering the Interceptor :

public class ApplicationDbContext : DbContext
{
    public DbSet<User> Users { get; set; }
    public DbSet<AuditLog> AuditLogs { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("YourConnectionStringHere");
        optionsBuilder.AddInterceptors(new AuditInterceptor());
    }

Conclusion

EF Core's ExecuteUpdateAsync and ExecuteDeleteAsync offer incredible performance for bulk data operations but come at the cost of losing automatic change tracking.

If you need to audit or track changes, the best approach is to manually query the affected data before execution or use a dedicated audit system such as audit tables or SQL triggers.

Choose the method that balances performance and auditing needs for your application.

What do you think? Do you prefer performance with manual tracking, or do you rely on EF Core's change tracking? Let us know your approach!

You can find the full code example at: https://github.com/stevsharp/EfCoreExamples/tree/BulkUpdateAndDeletes

References

Microsoft Docs - EF Core 7: ExecuteUpdate and ExecuteDelete

Microsoft Docs - Change Tracking in EF Core

Microsoft Docs - Interceptors in EF Core

Community Discussion - EF Core GitHub Issue on Change Tracking and Bulk Operations

2 Comments

0 votes
0 votes
🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

5 EF Core Features Every Enterprise Developer Should Know

Spyros - Jun 5

EF Core: Lazy Loading, Eager Loading, and Loading Data on Demand

Spyros - Mar 6, 2025

Optimistic vs. Pessimistic Concurrency in EF Core (with Table Hints)

Spyros - Mar 31, 2025

EF Core Global Query Filters: A Complete Guide

Spyros - Mar 2, 2025

Supercharging EF Core Specifications with EF.CompileQuery

Spyros - Aug 5, 2025
chevron_left
7.8k Points202 Badges
Athens Greeceweb-partner.gr
43Posts
164Comments
98Connections
Passionate about building robust and scalable software solutions with a focus on .NET technologies. ... Show more

Related Jobs

View all jobs →

Commenters (This Week)

2 comments
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!