← Back to all posts

EF Core: Bulk Operations

EF Core: Bulk Operations

Reading Time: 3 minutes

Problem

Standard EF Core write workflows are entity-by-entity:

  • load records into memory
  • modify each entity
  • call SaveChangesAsync()

That approach is fine for small batches, but it breaks down at scale.

Typical pain points:

  • slow jobs that process tens of thousands of rows
  • high memory usage from tracked entities
  • long transaction durations
  • too many SQL statements

When teams say they need “bulk operations,” they usually mean efficient set-based updates, deletes, and inserts.

Solution

Use operation-specific strategies:

  1. Bulk update: use ExecuteUpdateAsync() (EF Core 7+).
  2. Bulk delete: use ExecuteDeleteAsync() (EF Core 7+).
  3. Bulk insert:
  • EF Core has no native ExecuteInsertAsync() equivalent today.
  • Use AddRange + batched SaveChanges for moderate volume.
  • For very large inserts, use provider-native bulk APIs or a vetted bulk library.

Also keep these guardrails:

  • filter aggressively before bulk operations
  • run inside explicit transactions when consistency requires it
  • avoid mixing tracked entities with set-based operations in the same unit of work without care

Description

1) Bulk updates with ExecuteUpdateAsync

ExecuteUpdateAsync sends a direct SQL UPDATE for the matching rows without loading entities.

var affectedRows = await db.Orders
    .Where(o => o.Status == OrderStatus.Pending && o.CreatedAt < cutoff)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(o => o.Status, OrderStatus.Expired)
        .SetProperty(o => o.UpdatedAtUtc, DateTime.UtcNow));

Why this is faster:

  • no entity materialization
  • no change tracker overhead
  • single set-based SQL statement

2) Bulk deletes with ExecuteDeleteAsync

ExecuteDeleteAsync generates a direct SQL DELETE.

var deletedRows = await db.AuditLogs
    .Where(x => x.CreatedAtUtc < retentionCutoff)
    .ExecuteDeleteAsync();

This is ideal for retention cleanup and archival workflows.

3) Bulk inserts in EF Core

There is no built-in set-based insert API equivalent to ExecuteUpdateAsync/ExecuteDeleteAsync.

For moderate batch sizes, use batched inserts:

const int batchSize = 1000;

foreach (var batch in records.Chunk(batchSize))
{
    await db.Products.AddRangeAsync(batch);
    await db.SaveChangesAsync();

    // Keeps memory stable during long-running import jobs.
    db.ChangeTracker.Clear();
}

For very large imports (hundreds of thousands to millions of rows), prefer:

  • SQL Server SqlBulkCopy
  • PostgreSQL COPY
  • or a trusted EF-oriented bulk package after validation

4) Important behavioral notes

Set-based APIs (ExecuteUpdateAsync, ExecuteDeleteAsync) do not behave like tracked updates:

  • they bypass EF change tracking
  • they do not trigger per-entity domain logic in memory
  • they can bypass concurrency workflows that depend on tracked originals

If you rely on audit fields, interceptors, domain events, or optimistic concurrency stamps, verify behavior explicitly in integration tests.

5) Example: transaction-safe maintenance job

await using var tx = await db.Database.BeginTransactionAsync();

var updated = await db.Orders
    .Where(o => o.Status == OrderStatus.Pending && o.CreatedAt < cutoff)
    .ExecuteUpdateAsync(s => s
        .SetProperty(o => o.Status, OrderStatus.Expired)
        .SetProperty(o => o.UpdatedAtUtc, DateTime.UtcNow));

var deleted = await db.OrderDrafts
    .Where(d => d.CreatedAtUtc < retentionCutoff)
    .ExecuteDeleteAsync();

await tx.CommitAsync();

This keeps multi-step maintenance work consistent and operationally simple.

Summary

EF Core supports true set-based bulk operations for updates and deletes via:

  • ExecuteUpdateAsync
  • ExecuteDeleteAsync

For inserts, there is no native set-based bulk insert API yet, so use:

  • AddRange + batched saves for medium workloads
  • provider-native bulk tools or a mature library for large ingestion pipelines

Choosing the correct path per operation gives predictable performance and avoids unnecessary tracker overhead.

References