HomeNikola Knezevic

In this article

Banner

Bulk Delete with EF Core and Dapper in .NET

06 Nov 2025
7 min

Sponsor Newsletter

Working with large datasets can make bulk operations challenging, especially when performance matters.

When you need to delete thousands or even millions of records, the methods or packages you choose make a big difference.

In today's blog post, we'll explore how to perform bulk deletes using EF Core and Dapper.

NOTE: This post lays the groundwork for a future article, where we’ll go beyond these examples and achieve even better performance using NuGet packages such as ZZZ Projects' Bulk Operations, which takes performance to another level.

Getting Started

To get a clear picture of performance, we’ll use BenchmarkDotNet.

BenchmarkDotNet is a popular .NET library for accurate and reliable performance measurements. To learn more, check out my detailed blog post: Benchmark Code using BecnhamarkDotNet

To get started with BenchmarkDotNet, you need to install the NuGet package. You can do this via the NuGet Package Manager or by running the following command in the Package Manager Console:

bash
Install-Package BenchmarkDotNet

For this benchmark, we will use a simple Product entity:

csharp
public sealed class Product
{
    public Guid Id { get; set; }

    public DateTime CreatedAt { get; set; }

    public DateTime? ModifiedAt { get; set; }

    public string Name { get; set; }

    public string Description { get; set; }

    public decimal Price { get; set; }
}

We also need rows to delete for the benchmark:

sql
[IterationSetup]
public void IterationSetup()
{
    var random = new Random(42);

    var products = Enumerable.Range(1, 1000)
        .Select(i => new Product(
            Guid.NewGuid(),
            DateTime.UtcNow,
            $"Product {i}",
            $"Description {i}",
            random.Next()))
        .ToList();

    _dbContext.AddRange(products);

    _dbContext.SaveChangesAsync();
}

For this example I am using IterationSetup so for each iteration we have new rows.

NOTE: For this test I am using PostgreSQL.

EF Core Remove Range

The simplest way to delete entities looks like this:

csharp
[Benchmark]
public async Task EfCoreFetchAndDelete()
{
    var productsToDelete = await _dbContext.Products
        .ToListAsync();

    _dbContext.RemoveRange(productsToDelete);

    await _dbContext.SaveChangesAsync();
}

Fetching products in memory and removing them works for a few entities but is inefficient for thousands or millions.

Using Raw SQL

One efficient way is to execute raw SQL:

csharp
await using var connection = new NpgsqlConnection(ConnectionString);
await connection.OpenAsync();

await connection.ExecuteAsync($"Delete from \"Products\"");

This is very fast however we can achieve the same result using EF Core as well:

csharp
await _dbContext.Database.ExecuteSqlInterpolatedAsync(
    $"Delete from \"Products\"");

Both approaches run the SQL directly on the database, bypassing the EF Core change tracker which makes them ideal for scenarios where speed and simplicity are more important than entity tracking.

EF Core ExecuteDelete

However, if you’re using EF Core and want to avoid raw SQL, there’s a cleaner and nearly as performant alternative:

csharp
await _dbContext.Products
    .ExecuteDeleteAsync();

ExecuteDelete runs a bulk delete directly in the database.

NOTE: ExecuteDelete runs SQL immediately, it does not wait for SaveChangesAsync().

If you want it to execute within a transaction, wrap it manually in a transaction.

csharp
await using var connection = new NpgsqlConnection(ConnectionString);
await connection.OpenAsync();

var transaction = await connection.BeginTransactionAsync();

await _dbContext.Products
    .ExecuteDeleteAsync();

await transaction.CommitAsync();

Benchmark Results

To back this up with real data, here are the benchmark results:

benchmark
| Method                  | Mean      | Error      | StdDev    |
|-------------------------|-----------|------------|-----------|
| EfCoreFetchAndDelete    | 80.719 ms | 1.6005 ms  | 4.0154 ms |
| EfCoreExecuteDelete     | 3.434 ms  | 0.0553 ms  | 0.1604 ms |
| EfCoreSql               | 3.175 ms  | 0.1301 ms  | 0.3712 ms |
| Dapper                  | 3.282 ms  | 0.1105 ms  | 0.3170 ms |

Using raw SQL is consistently a bit faster, but ExecuteDelete remains the cleanest and safest approach overall.

Next Step

If you’re looking to push performance even further, stay tuned for the future blog posts where we’ll explore NuGet packages that take EF Core bulk delete performance to an entirely new level.

Conclusion

Dapper is amazingly fast and easy to use, although EF Core can be as fast if used properly.

With EF Core’s ExecuteDelete, it can be even easier to use while being on par with Dappers performance.

If you want to check out examples I created, you can find the source code here:

Source Code

I hope you enjoyed it, subscribe and get a notification when a new blog is up!

Subscribe

Stay tuned for valuable insights every Thursday morning.