EF Core ExecuteDelete

ExecuteDeleteAsync() deletes rows directly in the database without loading entities into memory, without tracking them, and without calling SaveChangesAsync().

It is useful when you want to delete all rows that match a query.

πŸ“‰ Reduce Memory Usage When Saving Data

Processing millions of entities? EF Extensions can reduce memory usage while improving bulk operation performance.

Explore Performance Benefits →

Delete Rows with a Filter

Most of the time, you will combine Where() with ExecuteDeleteAsync().

language-csharp
|
using var context = new AppDbContext();

await context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteDeleteAsync();

Executed SQL:

language-sql
|
DELETE FROM [a]
FROM [Blogs] AS [a]
WHERE [a].[Rating] < 3

Only rows that match the filter are deleted.

This is the safest and most common ExecuteDeleteAsync() pattern because the delete operation is limited by a query condition.

Delete All Rows

If you call ExecuteDeleteAsync() directly from a DbSet, EF Core deletes every row in that table.

language-csharp
|
using var context = new AppDbContext();

await context.Blogs
    .ExecuteDeleteAsync();

This executes a database DELETE for all Blogs.

language-sql
|
DELETE FROM [a]
FROM [Blogs] AS [a]

Use this very carefully because no filter is applied.

Delete with a Navigation Filter

You can use navigation data in the query filter before calling ExecuteDeleteAsync().

language-csharp
|
using var context = new AppDbContext();

await context.Blogs
    .Where(b => b.Posts.All(p => p.PublishedOn.Year < 2018))
    .ExecuteDeleteAsync();

This deletes all blogs that contain only posts published before 2018.

The filter is translated to SQL and executed directly in the database.

Rows Affected

ExecuteDeleteAsync() returns an int value that indicates the number of rows deleted.

language-csharp
|
using var context = new AppDbContext();

var rowsAffected = await context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteDeleteAsync();

You can use this value for logging, validation, or checking whether the delete matched the expected rows.

Important Behavior

ExecuteDeleteAsync() executes immediately.

That means:

  • it does not require SaveChangesAsync()
  • it does not use the ChangeTracker
  • it does not load entities into memory
  • it does not remove tracked entity instances already loaded in memory
  • it sends a DELETE command directly to the database

For tracked entity workflows, see SaveChanges.

For a deeper step-by-step explanation of what EF Core does internally when changes are saved through the normal tracked workflow, see How SaveChanges Works.

For normal tracked deletes, see Deleting Data.

For relationship delete behavior and cascade rules, see Cascade Delete.

For a deeper explanation of tracking behavior, see ChangeTracker.

ExecuteDelete Requirements

ExecuteDeleteAsync() is available starting with EF Core 7.

(For older versions of EF Core (EF Core 2+), or for unsupported providers, you can use DeleteFromQuery (Entity Framework Extensions).)

To use it correctly, keep these requirements in mind:

  • the delete must be based on an EF Core query
  • the query must be translatable to SQL
  • the operation runs immediately in the database
  • tracked entities already loaded in the current DbContext are not automatically removed from memory
  • SaveChangesAsync() is not required after calling ExecuteDeleteAsync()
  • database constraints, foreign keys, and cascade delete rules still apply

For example, this works because the filter can be translated to SQL:

language-csharp
|
using var context = new AppDbContext();

await context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteDeleteAsync();

This deletes matching rows directly in the database.

However, custom .NET methods that cannot be translated to SQL cannot be used in the query filter.

language-csharp
|
using var context = new AppDbContext();

await context.Blogs
    .Where(b => IsLowQuality(b))
    .ExecuteDeleteAsync();

This kind of expression cannot be translated unless the method is mapped or translated by the provider.

How ExecuteDelete Works

ExecuteDeleteAsync() sends a DELETE command directly to the database.

It does not follow the normal tracked entity workflow.

A normal tracked delete usually looks like this:

language-csharp
|
using var context = new AppDbContext();

var blog = await context.Blogs
    .FirstAsync(b => b.Name == "Old Blog");

context.Blogs.Remove(blog);

await context.SaveChangesAsync();

In that workflow, EF Core:

  1. loads the entity
  2. tracks it in the DbContext
  3. marks it as Deleted
  4. generates the delete when SaveChangesAsync() runs

ExecuteDeleteAsync() skips that workflow.

language-csharp
|
using var context = new AppDbContext();

await context.Blogs
    .Where(b => b.Name == "Old Blog")
    .ExecuteDeleteAsync();

In this case, EF Core translates the query into a direct database command.

No entity instance is loaded, and the ChangeTracker is not involved.

ExecuteDelete vs SaveChanges

ExecuteDeleteAsync() and SaveChangesAsync() can both delete data, but they are designed for different workflows.

Feature ExecuteDeleteAsync() SaveChangesAsync()
Loads entities No Usually yes
Uses ChangeTracker No Yes
Requires SaveChangesAsync() No It is the save operation
Best for Set-based deletes Tracked entity workflows
Removes in-memory tracked entities No Yes
Can use per-entity business logic in memory No Yes

Use ExecuteDeleteAsync() when you want to delete rows directly in the database.

Use SaveChangesAsync() when you are working with tracked entities and want EF Core to manage the normal delete workflow during a unit of work.

For the tracked entity workflow, see SaveChanges.

For normal tracked deletes, see Deleting Data.

ExecuteDelete vs BulkDelete

Is ExecuteDelete a real β€œBulk Delete”? Not quite. Yes, it deletes rows in bulk directly in SQL. However, it does not delete entities from a list with per-row control like BulkDelete (Entity Framework Extensions).

ExecuteDeleteAsync() is a set-based delete API built into EF Core.

It is useful when every matching row can be deleted using the same SQL-translatable query.

language-csharp
|
using var context = new AppDbContext();

await context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteDeleteAsync();

This works well because the delete is expressed as one database rule.

However, ExecuteDeleteAsync() is not the same as a full bulk delete library.

Use BulkDelete from Entity Framework Extensions when:

  • the rows to delete come from a list of entities in memory
  • you need to delete many specific entities by key
  • 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 ExecuteDeleteAsync() for set-based deletes expressed as a query
  • use BulkDelete when you need to delete many specific entities from memory
Question ExecuteDeleteAsync() BulkDelete (EF Extensions)
How are rows selected? Query filter translated to SQL Entity list or key list from memory
Where does the data come from? Database query In-memory entity list
Different rows by key list? Not directly Yes
Uses ChangeTracker? No Not the normal EF Core tracking workflow
Best for Set-based deletes High-volume deletes with entity/key lists

Performance Benchmarks

Deleting 100,000 rows:

Method Time Memory
ExecuteDelete 200 ms Very low
BulkDelete (EF Extensions) 1050 ms Low
SaveChanges 2250 ms High

Benchmark Source

Important: For BulkDelete and SaveChanges, part of the time is spent materializing the list of entities from the database before the delete starts. This step is required because BulkDelete and SaveChanges work from entities, not from a LINQ rule.

Takeaway: ExecuteDelete can be much faster than the traditional tracked approach when the delete can be expressed as a set-based SQL rule. BulkDelete remains more appropriate when the rows to delete come from an entity list or key list in memory.

Bulk Delete - Concrete Example

ExecuteDeleteAsync() works well when the rows to delete can be selected by a SQL-translatable query.

A real bulk delete from EF Extensions is different: the entities or keys to delete can come from memory.

For example, imagine you imported a CSV file containing 1,000,000 customer IDs that must be deleted.

language-csharp
|
// @nuget: Z.EntityFramework.Extensions.EFCore
using Z.EntityFramework.Extensions;

var customers = GetCustomersToDeleteFromCsv();

context.BulkDelete(customers, options =>
{
    options.BatchSize = 10000;
});

Doing the same with ExecuteDeleteAsync() is not practical unless you build the staging pipeline yourself, such as inserting the IDs into a temporary table and writing a manual DELETE JOIN.

EF Extensions automates that pipeline.

ExecuteDeleteAsync() and BulkDelete complement each other rather than compete.

When to Use ExecuteDelete

Use ExecuteDeleteAsync() when:

  • you want to delete rows directly in the database
  • the delete can be expressed as a SQL-translatable query
  • you do not need to load entities first
  • you do not need the ChangeTracker
  • you do not need entity instances removed from memory
  • you want to delete many rows with one database command

Good examples include:

  • deleting old records
  • removing inactive rows
  • cleaning up temporary data
  • deleting records that match a status or date filter
  • removing related rows through a query filter
language-csharp
|
using var context = new AppDbContext();

await context.Blogs
    .Where(b => b.LastUpdated < cutoffDate)
    .ExecuteDeleteAsync();
⚑ Bulk Insert Related Entities Automatically

Saving Orders with OrderItems? Use IncludeGraph to bulk insert entire entity graphs while handling their relationships automatically.

Discover IncludeGraph →

When Not to Use ExecuteDelete

ExecuteDeleteAsync() is not the best choice when the delete depends on tracked entity behavior.

Avoid it when:

  • you need to delete already loaded entity instances and keep the context state consistent
  • you rely on ChangeTracker state
  • you need entity events or in-memory business logic before deleting
  • the rows to delete come from a local entity list or key list
  • the query filter cannot be translated to SQL
  • you need the normal SaveChangesAsync() pipeline

For example, if your application loads an entity, applies domain logic, validates whether it can be deleted, and then removes it, a tracked workflow is usually clearer.

language-csharp
|
using var context = new AppDbContext();

var blog = await context.Blogs
    .FirstAsync(b => b.Name == "Old Blog");

context.Blogs.Remove(blog);

await context.SaveChangesAsync();

For normal tracked deletes, see Deleting Data.

Common Pitfalls

Be careful with the following mistakes.

Expecting SaveChangesAsync to Be Required

ExecuteDeleteAsync() runs immediately.

This is unnecessary:

language-csharp
|
using var context = new AppDbContext();

await context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteDeleteAsync();

// DO NOT USE!
await context.SaveChangesAsync();

The SaveChangesAsync() call does not save the ExecuteDeleteAsync() operation. The delete already happened.

Deleting All Rows by Mistake

Calling ExecuteDeleteAsync() directly from a DbSet deletes every row in that table.

language-csharp
|
using var context = new AppDbContext();

await context.Blogs.ExecuteDeleteAsync();

Use a Where() filter unless you really intend to delete all rows.

Expecting Tracked Entities to Be Removed from Memory

If an entity is already tracked, ExecuteDeleteAsync() does not automatically remove that in-memory instance.

language-csharp
|
using var context = new AppDbContext();

var blog = await context.Blogs
    .FirstAsync(b => b.Name == "Old Blog");

await context.Blogs
    .Where(b => b.Name == "Old Blog")
    .ExecuteDeleteAsync();

// The tracked blog instance may still exist in the current DbContext.

If you need a clean view of the database state, use a new DbContext, detach the entity, or reload data as needed.

Ignoring Foreign Keys and Cascade Rules

ExecuteDeleteAsync() sends a direct database DELETE.

Foreign key constraints and database cascade rules still apply.

If related rows block the delete, the database can reject the command.

If cascade delete is configured, related rows may also be deleted according to the configured database rules.

For more information about related data deletion, see Deleting Data.

ExecuteDelete Release History

  • EF Core 8.0: Improved ExecuteUpdate and ExecuteDelete to support more complex queries (owned types, unions, and TPT), as long as all operations target a single database table.
  • EF Core 7.0: Introduced ExecuteUpdate and ExecuteDelete as native set-based operations in EF Core.
  • EF Core 2.0+: For older versions of EF Core, or for unsupported providers, you can use DeleteFromQuery (Entity Framework Extensions).

ExecuteDeleteAsync() was introduced in EF Core 7.

The main idea is the same across EF Core 7, 8, 9, 10 and later versions: delete 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 - ExecuteDelete

The following videos are useful if you want to see ExecuteDeleteAsync() in real EF Core examples, especially how it differs from tracked deletes with SaveChangesAsync() and how it avoids unnecessary entity loading.

Video 1 - Entity Framework 7 - Bulk Editing

by @CodingTutorialsAreGo (Jasper Kent)

Jasper Kent introduces the EF Core 7 set-based execute methods and shows how ExecuteDelete can replace traditional delete workflows that load entities, remove them, and call SaveChanges. He also compares the generated SQL and shows how to retrieve the number of affected rows.

Key timestamps:

  • 0:00 β€” Overview of ExecuteDelete and ExecuteUpdate
  • 2:09 β€” Traditional way to delete
  • 7:52 β€” Using ExecuteDeleteAsync() and noting the async/non-async overloads
  • 24:35 β€” Conclusion and performance insights

Sample repository:

Video 2 - EF Core 7 - Performance Improvements With ExecuteUpdate and ExecuteDelete

Milan Jovanović shows how ExecuteDelete removes multiple records in a single SQL DELETE without loading entities. He demonstrates a conditional delete, the generated SQL, and the efficiency compared to the classic deletion loop.

Key timestamps:

  • 8:32 β€” Introducing the EF Core 7 ExecuteDelete method
  • 8:45 β€” Running ExecuteDelete with a conditional filter
  • 9:30 β€” Observing the generated SQL
  • 10:15 β€” SQL DELETE produced by EF Core 7
  • 10:45 β€” Final explanation of the direct range delete behavior

Video 3 - Entity Framework 7 Makes Performing Updates and Deletes Easy

Israel Quiroz demonstrates how ExecuteDeleteAsync() removes entities directly in the database using a filtered query, without retrieving them first. He compares the traditional 3-step delete process with the single-roundtrip method and highlights the returned count of affected rows.

Key timestamps:

  • 1:40 β€” Traditional delete workflow: find, remove, SaveChanges
  • 2:00 β€” Replacing the workflow with ExecuteDeleteAsync
  • 2:15 β€” Returned value: number of deleted rows
  • 2:40 β€” One database round-trip vs. multiple calls
  • 4:40 β€” Runs without loading or tracking entities

Video 4 - Spanish: Borrado y Actualizaciones Masivas - Nuevo de EF Core 7

Felipe Gavilan shows how ExecuteDelete removes thousands of rows directly in SQL Server without loading entities. He demonstrates a full-table delete, a filtered delete, and a performance comparison with RemoveRange.

Key timestamps:

  • 1:47 β€” Full-table delete using ExecuteDelete
  • 3:16 β€” Conditional delete using Where(...).ExecuteDelete()
  • 4:32 β€” Verifying filtered results in SQL Server
  • 8:31 β€” Starting stopwatch for performance comparison
  • 9:31 β€” Final numbers: 100k deletes

Summary

ExecuteDeleteAsync() lets you delete 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 deletes all rows that match the query
  • it does not use the ChangeTracker
  • it does not remove tracked entity instances already loaded in memory
  • database constraints, foreign keys, and cascade rules still apply
  • it is best for set-based deletes that can be translated to SQL

Use SaveChangesAsync() when you need the normal tracked entity delete workflow.

Use BulkDelete from Entity Framework Extensions when the rows to delete come from an entity list or key list in memory, or when you need advanced bulk options.

πŸš€ SaveChanges Is Slow? Just Add Bulk

Keep your familiar EF Core workflow and improve saving performance. BulkSaveChanges processes your pending changes using high-performance bulk operations.

Discover BulkSaveChanges →

If you want to compare ExecuteDeleteAsync() with the most closely related EF Core saving patterns, these pages are the best next step:

  • ExecuteUpdate β€” how to update rows directly in the database without loading entities
  • SaveChanges β€” how EF Core persists tracked changes
  • How SaveChanges Works β€” what EF Core does internally during the normal tracked save pipeline
  • Deleting Data β€” how to delete entities with the normal tracked workflow
  • Cascade Delete β€” how relationship delete behavior and foreign key constraints affect related data
  • ChangeTracker β€” how EF Core stores tracking information internally
  • Tracking Changes of Entities β€” how EF Core detects changes before saving
  • Bulk Delete β€” how to delete large datasets with Entity Framework Extensions

FAQ

Does ExecuteDeleteAsync require SaveChangesAsync?

No. ExecuteDeleteAsync() executes immediately in the database. You do not need to call SaveChangesAsync() afterward.

Does ExecuteDeleteAsync use the ChangeTracker?

No. ExecuteDeleteAsync() does not use the ChangeTracker and does not remove tracked entity instances already loaded in memory.

Can ExecuteDeleteAsync delete rows with a filter?

Yes. You can use Where() before ExecuteDeleteAsync() to delete only rows that match a condition.

Can ExecuteDeleteAsync delete all rows?

Yes. Calling ExecuteDeleteAsync() directly from a DbSet deletes every row in that table. Use this carefully because no filter is applied.

What happens with foreign keys and cascade delete?

ExecuteDeleteAsync() sends a direct DELETE command to the database. Foreign key constraints and database cascade rules still apply.

When should I use ExecuteDeleteAsync instead of SaveChangesAsync?

Use ExecuteDeleteAsync() when you want to delete 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 ExecuteDeleteAsync the same as BulkDelete?

No. ExecuteDeleteAsync() is a set-based delete API. A BulkDelete (from EF Extensions) is a better option when the rows to delete come from a list of entities or keys in memory.


Date Modified: 2026-07-26
Author:

Edit this page in GitHub

Got any EF Core Question?