Last Updated on September 24, 2025 by Aram
Working with databases is at the heart of most .NET apps, and Entity Framework Core makes it easy to query and persist data. But when you start dealing with large volumes of records, the default methods can quickly become slow and inefficient.
That’s where bulk operations come in. These operations allow you to insert, update, or delete thousands of records in a fraction of the time it would normally take with EF Core’s standard methods.
In this article, we’ll be exploring the different ways to perform bulk operations in EF Core 9, starting with the traditional ways, and moving into more powerful solutions like Entity Framework Extensions from ZZZ Projects.
Note that this article is proudly sponsored by ZZZ Projects, the owner of Entity Framework Extensions and a wide array of highly valuable and useful libraries.
Also, it is worth mentioning that all the sample code throughout this article will be included in DotNetFiddle, this is one of the best online tools that enable you write, run, and benchmark your code directly from your browser, no IDEs involved. And it is part of ZZZ Projects’ library of tools.
So, let’s get started by Exploring Bulk Operations in EF Core:
The Super Slow Way
Standard EF Core loops can be painfully slow with relatively large datasets. You should avoid this at all costs.
Having SaveChanges inside a loop, even if it was for few number of records is not recommended because it will trigger a separate SQL command on the database on each iteration.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var orders = Enumerable.Range(1, 1000).Select(i => new Order { Description = $"Order {i}", CustomerName = $"Customer {i}", Quantity = i * 2, Amount = 10.5m * i, CreatedDate = DateTime.UtcNow.AddDays(-i), IsProcessed = i % 2 == 0, Address = $"Address {i}, {i} + 1", Notes = $"Notes {i}" }).ToList(); foreach (var order in orders) { context.Orders.Add(order); await context.SaveChangesAsync(); } |

You can find the full source code of this in DotNetFiddle, it is connected to SqlServer Database, so you can completely test it and see the benchmark yourself.
The EF Core Native Way – AddRange
Bulk insert using EF Core through AddRange is the default option if you need a basic and native bulk insert. Instead of calling SaveChanges
repeatedly, you can add all entities in one go.
This implies one roundtrip to the database instead of many.
The limitation of this is that EF Core still generates individual INSERT statements, which can be slow with very large datasets.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var orders = Enumerable.Range(1, 10000).Select(i => new Order { Description = $"Order {i}", CustomerName = $"Customer {i}", Quantity = i * 2, Amount = 10.5m * i, CreatedDate = DateTime.UtcNow.AddDays(-i), IsProcessed = i % 2 == 0, Address = $"Address {i}, {i} + 1", Notes = $"Notes {i}" }).ToList(); await context.Orders.AddRangeAsync(orders); await context.SaveChangesAsync(); |

And this is the DotNetFiddle to check it out.
EF Core Built-in Bulk Operations
EF Core has some powerful bulk operations that support update and delete, and these are pretty fast, since these allow set-based operations directly in SQL without loading entities.
So in SQL, UPDATE and DELETE statements are generated.
The limitation here is that EF Core only supports these 2 methods, so no similar methods for insert or merge.
These methods were introduced since EF Core 7, and kept receiving updates and EF Core 9 is the fastest of them.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Orders: 10000 | IsProcessed: 5000 // Bulk delete: // remove orders that are not processed int deletedRows = await context.Orders .Where(o => !o.IsProcessed) .ExecuteDeleteAsync(); // Bulk update: // mark all processed orders as not processed int updatedRows = await context.Orders .Where(o => o.IsProcessed) .ExecuteUpdateAsync(s => s.SetProperty(o => o.IsProcessed, false)); |


The code sample for the native EF Core Update can be found and benchmarked in this DotNetFiddle. And for the delete function, you can check it in this fiddle.
Entity Framework Extensions by ZZZ Projects
Entity Framework Extensions of ZZZ projects offer a complete suite of bulk operations.
Simple bulk methods with blazing fast speeds and low memory consumption that work with virtually any DB provider.
The main methods provided within the Entity Framework Extensions are:
- BulkInsert
- BulkUpdate
- BulkDelete
- BulkMerge
- BulkSynchronize
These methods are capable of processing thousands of records in milliseconds.
Async Versions of these methods are also supported. Just postfix each call with Async, and add await to use the async version.
Here are some highlights on key methods:
Bulk Insert
Call BulkInsert or BulkInsertAsync without having to call SaveChanges.
Bulk insert features high speed, minimal memory footprint, and flexible options.
Supports identity retrieval, triggers, computed columns, and more.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var orders = Enumerable.Range(1, 10000).Select(i => new Order { Description = $"Order {i}", CustomerName = $"Customer {i}", Quantity = i * 2, Amount = 10.5m * i, CreatedDate = DateTime.UtcNow.AddDays(-i), IsProcessed = i % 2 == 0, Address = $"Address {i}, {i} + 1", Notes = $"Notes {i}" }).ToList(); await context.BulkInsertAsync(orders); |

Bulk Insert Optimized
An optimized version of bulk insert is also available, where it skips outputting values of identity fields to maximize speed.
This means that AutoMapOutputDirection = false, which will skip the step to create a temp/staging table and reducing unnecessary round-trips.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var orders = Enumerable.Range(1, 10000).Select(i => new Order { Description = $"Order {i}", CustomerName = $"Customer {i}", Quantity = i * 2, Amount = 10.5m * i, CreatedDate = DateTime.UtcNow.AddDays(-i), IsProcessed = i % 2 == 0, Address = $"Address {i}, {i} + 1", Notes = $"Notes {i}" }).ToList(); await context.BulkInsertOptimizedAsync(orders); |

You can use this fiddle to check both BulkInsert and BulkInsertOptimized Methods.
Bulk Update
With Entity Framework Extensions, you can easily update a list of entities by loading them into memory with LINQ.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
var orders = Enumerable.Range(1, 10000).Select(i => new Order { Description = $"Order {i}", CustomerName = $"Customer {i}", Quantity = i * 2, Amount = 10.5m * i, CreatedDate = DateTime.UtcNow.AddDays(-i), IsProcessed = i % 2 == 0, Address = $"Address {i}, {i} + 1", Notes = $"Notes {i}" }).ToList(); var notProcessedOrders = await context .Orders .Where(o => !o.IsProcessed) .ToListAsync(); notProcessedOrders.ForEach(o => { o.IsProcessed = true; }); await context.BulkUpdateAsync(notProcessedOrders); |

Access this fiddle to explore the bulk update further and see the benchmarks yourself.
Another variation of this method is the BatchUpdate, which serves the same purpose but it can work directly at the database level without having to load the entities into the memory.
Bulk Delete
Similar to Bulk Update, you can load entities into memory using LINQ and remove them directly using BulkDelete
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
var orders = Enumerable.Range(1, 10000).Select(i => new Order { Description = $"Order {i}", CustomerName = $"Customer {i}", Quantity = i * 2, Amount = 10.5m * i, CreatedDate = DateTime.UtcNow.AddDays(-i), IsProcessed = i % 2 == 0, Address = $"Address {i}, {i} + 1", Notes = $"Notes {i}" }).ToList(); var processedOrders = context .Orders .Where(o => o.IsProcessed) .ToList(); await context.BulkDeleteAsync(processedOrders); |

You can test the bulk delete now using this fiddle, just try it.
Also a variant for delete exists using BatchDelete, which is similar to BatchUpdate, it deletes at database level without loading entities.
Bulk Operations Beyond Basic CRUD
Entity Framework Extensions doesn’t stop at bulk insert, update, and delete.
On top of the standard bulk operations as mentioned above, Entity Framework Extensions also provides a set of advanced capabilities designed for more complex scenarios.
Whether you need to upsert data, keep large tables synchronized, speed up save pipelines, filter against massive lists, or quickly load entities, these features are built to handle the heavy lifting efficiently.
Here are some important capabilities that Entity Framework Extensions offer on top of the key CRUD operations mentioned in this article:
BulkMerge
Perform insert or update (upsert) in one call. In other words, it lets you insert new records and update existing ones in a single step, which would be perfect for keeping data consistent without writing extra logic.
1 2 3 4 5 |
await context.BulkMergeAsync(new List<Order> { new(1, "Shipped"), new(2, "Processing") }); |
BulkSynchronize
Sync your entity list with the database (insert, update, delete). Keeps a database table in sync with your in-memory data. If a record is missing, it’s added; if it’s outdated, it’s updated; if it’s no longer needed, it’s removed.
1 2 3 4 5 |
await context.BulkSynchronizeAsync(new List<Order> { new(1, "Shipped"), new(3, "Pending") }); |
BulkSaveChanges
Save thousands of tracked entities in one fast batch. This is a faster alternative to EF Core’s default SaveChanges, grouping operations into efficient bulk commands instead of processing each row one by one.
1 2 3 4 5 6 |
var newOrders = Enumerable.Range(1, 1000) .Select(i => new Order(0, $"Customer {i}", i * 10)) .ToList(); context.Orders.AddRange(newOrders); await context.BulkSaveChangesAsync(); |
WhereBulkContains
Efficiently filter queries with a large in-memory list. This method handles large lists of values in a single query, avoiding performance issues when filtering with thousands of IDs or keys.
1 2 3 |
var matchingOrders = await context.Orders .WhereBulkContains(new[] { 1, 2, 3, 4, 5 }) .ToListAsync(); |
BulkRead
Quickly load entities from the database by matching keys from an in-memory list, avoiding multiple roundtrips.
1 2 |
var ordersToFetch = new List<Order> { new(1), new(2) }; await context.BulkReadAsync(ordersToFetch); |
Final Thoughts
Working with large datasets in EF Core doesn’t have to be slow or complicated. By leveraging Entity Framework Extensions, you can handle inserts, updates, deletes, and even complex data synchronization efficiently and reliably.
These tools take care of the heavy lifting behind the scenes, letting you focus on building features instead of wrestling with performance issues.
So, do you think it is worth trying Entity Framework Extensions today?
Check the official website to learn more.
Spread the knowledge by sharing this with your social network.
And let us know your thoughts and feedback in the comments.
References
Entity Framework Extensions Overview
Great Read about Performance Optimization in EF Core
Recent Posts
- 7 Types of Authorization in ASP.NET Core Web API
- Structured Logging in ASP.NET Core
- Options Pattern in ASP.NET Core
- ASP.NET Core Data Protection API
- Unleash the Power of Bulk Extensions with Dapper Plus
Bonus
Enjoy this complete melodious masterpiece by one of the greatest composers in the late Barqoue era – J.S. Bach.
J.S. Bach: French Suites
Played by Yuan Sheng, presented by Piano Classics, a label of Brilliant Classics.