
The Complete Guide to Bulk Operations with Entity Framework Extensions
When using Entity Framework Core (EF Core), developers enjoy the simplicity of LINQ queries and the convenience of calling SaveChanges()
to persist data. This approach works perfectly for small and medium workloads.
But when the application needs to handle large datasets, inserting 10,000+ rows, updating thousands of records, or deleting large batches, EF Core starts to struggle. The issue comes from the way SaveChanges()
works:
- Executes one SQL statement per entity.
- Runs validation and change tracking on each row.
- Sends thousands of round-trips to the database.
The result? Operations that take minutes instead of seconds.
This is where Entity Framework Extensions (EF Extensions) makes the difference. It’s a library that extends EF Core with high-performance bulk operations such as Bulk Insert, Bulk Update, Bulk Delete, Bulk Merge (Upsert), and Bulk Synchronize. These methods generate optimized SQL, allowing you to process thousands of rows in milliseconds.
In this article, we’ll look at:
- What makes EF Core bulk operations slow.
- How Entity Framework Extensions transform those same operations.
- Examples of each bulk method with key benefits.
- Supported EF versions and providers.
If your application works with large datasets, this guide will show you how Entity Framework Extensions can save time, reduce load, and unlock true scalability.
Installing Entity Framework Extensions
To install Entity Framework Extensions, you can add the package from NuGet:
You can install it using the .NET CLI:
dotnet add package Z.EntityFramework.Extensions.EFCore
Or from the Package Manager Console in Visual Studio:
Install-Package Z.EntityFramework.Extensions.EFCore
After installation, bulk methods like BulkInsert
, BulkInsertOptimized
, BulkUpdate
, BulkDelete
, BulkMerge
, and BulkSynchronize
are available directly on your DbContext.
Which Versions of Entity Framework Are Supported?
Entity Framework Extensions is designed to work with both modern and legacy versions of Entity Framework:
- Entity Framework Core: all versions up to EF Core 9 (future versions will be supported when released).
- Entity Framework: EF6 and earlier
Whether you’re on the latest EF Core or maintaining an older EF6 project, Entity Framework Extensions provides full support.
Which Providers Are Supported by Entity Framework Extensions?
Entity Framework Extensions works seamlessly with all major EF Core database providers, including:
- SQL Server
- MySQL
- MariaDB
- Oracle
- PostgreSQL
- SQLite
No matter which provider your application uses, Entity Framework Extensions can handle bulk operations efficiently.
Why EF Core Bulk Operations Are Slow?
EF Core prioritizes flexibility and consistency. That comes at a cost when dealing with large datasets.
When you call SaveChanges()
:
- Each entity generates its own SQL command.
- Change tracking and validation are performed for every record.
- Each command requires a database round-trip.
For example, inserting 10,000 records will trigger 10,000 separate INSERT statements. That overhead quickly becomes a bottleneck.
Bulk Insert
The BulkInsert method adds large collections of entities to the database in one optimized command. It’s designed for scenarios where you need to insert thousands or even millions of rows quickly and with minimal memory usage.
EF Core Insert:
var products = GenerateProducts(10_000);
dbContext.Products.AddRange(products);
await dbContext.SaveChangesAsync(); // 10,000 Individual INSERTS
- Generates 10,000
INSERT
commands. - May take several seconds or minutes depending on dataset size.
Entity Framework Extensions Bulk Insert:
using Z.EntityFramework.Extensions;
var products = GenerateProducts(10_000);
await dbContext.BulkInsertAsync(products); // Single batch INSERT
- This alone can cut execution time from minutes to milliseconds.
Bulk Insert Options
- Keep Identity Values
When InsertKeepIdentity = true
, Entity Framework Extensions uses the IDs already assigned to your entities instead of letting the database generate new ones. In SQL Server, it automatically manages the SET IDENTITY_INSERT [TableName] ON/OFF commands during the bulk insert.
using Z.EntityFramework.Extensions;
await dbContext.BulkInsertAsync(products, options =>
{
options.InsertKeepIdentity = true;
});
This is handy for migrations, data restores, or syncing systems where the original IDs must be preserved.
- Insert Related Entities (IncludeGraph)
When IncludeGraph = true
, Entity Framework Extensions inserts not only the main entities but also their related child entities. For example, inserting a list of Product entities with their Reviews can be done in a single bulk insert call. This makes it easy to persist the entire object graph, both products and their associated reviews, in one operation.
public sealed class Product
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; } = 0.0m;
public bool IsActive { get; set; } = false;
public ICollection<Review> Reviews { get; set; } = [];
}
public sealed class Review
{
public int Id { get; set; }
public string Comment { get; set; } = string.Empty;
public Guid ProductId { get; set; }
public Product Product { get; set; } = default!;
}
public static List<Product> GenerateProductsWithReviews()
{
return new()
{
new()
{
Name = "Laptop",
Price = 1200.00m,
Reviews = new List<Review>
{
new() { Comment = "Excellent performance."},
new () { Comment = "Battery life could be better." }
}
},
new Product
{
Name = "Smartphone",
Price = 850.00m,
Reviews = new List<Review>
{
new () { Comment = "Amazing camera quality."}
}
},
new Product
{
Name = "Headphones",
Price = 150.00m,
Reviews = new List<Review>
{
new() { Comment = "Comfortable and great sound."},
new() { Comment = "Bass is too heavy."}
}
}
};
}
using Z.EntityFramework.Extensions;
var productsWithReviews = GenerateProductsWithReviews();
// Inserts Products AND thier Reviews in one call
await dbContext.BulkInsertAsync(productsWithReviews,
options =>
{
options.IncludeGraph = true;
});
With IncludeGraph = true
, Entity Framework Extensions will insert both the Products and their related Reviews in one optimized bulk insert operation, no need to save them separately.
Key Benefits of Bulk Insert
- High performance – handle millions of inserts in just seconds, instead of waiting minutes with standard EF Core.
- Low memory footprint – optimized to keep resource usage minimal, even when processing massive datasets.
- Flexible configuration – dozens of options let you fine-tune how inserts are handled so they align perfectly with your business needs.
Bulk Insert Optimized
While the regular BulkInsert
method is already extremely fast, Entity Framework Extensions also provides BulkInsertOptimized, designed for maximum insert performance in EF Core 9 and earlier.
The key difference: by default, it doesn’t return output values (like auto-generated identity values), which allows it to skip temporary tables and write directly into the destination table using SqlBulkCopy
. This makes it even faster than BulkInsert
in high-volume scenarios.
using Z.EntityFramework.Extensions;
var products = GenerateProducts(10_000);
await dbContext.BulkInsertOptimizedAsync(products);
BulkInsertOptimized Analysis
When you call BulkInsertOptimized
, it can also return an analysis object with performance tips:
using Z.EntityFramework.Extensions;
var products = GenerateProducts(10_000);
var analysis = await dbContext.BulkInsertOptimizedAsync(products);
if (!analysis.IsOptimized)
{
Console.WriteLine(analysis.TipsText); // View recommendations to optimize
}
The analysis tells you:
- IsOptimized – whether your insert is running in the fastest mode.
- Tips – reasons why it may not be fully optimized.
- TipsText – human-readable guidance for logging or debugging.
Bulk Update
The BulkUpdate method applies changes to existing records in bulk. Instead of updating rows one by one, all modified entities are sent to the database in a single, efficient operation.
EF Core Update:
var products = await dbContext
.Products
.Take(5000)
.ToListAsync();
products.ForEach(p => p.IsActive = false);
await dbContext.SaveChangesAsync(); // 5,000 UPDATE
Entity Framework Extensions Bulk Update:
using Z.EntityFramework.Extensions;
var products = await dbContext
.Products
.Take(5000)
.ToListAsync();
products.ForEach(p => p.IsActive = true);
await dbContext.BulkUpdateAsync(products); // One batch UPDATE
Key Benefits of Bulk Update
- Update with precision – choose which columns to update, skip nulls, or apply conditions.
- Fast at scale – update tens of thousands of rows in seconds.
- Skip entity tracking – no need to load every entity into memory.
- Highly configurable – batch size, conditions, and behavior can all be fine-tuned.
Bulk Delete
The BulkDelete method removes entities in bulk directly from the database. Rather than deleting row by row, it issues one optimized delete statement for the entire set.
EF Core Delete:
var inactiveProducts = await dbContext
.Products
.Where(p => !p.IsActive)
.ToListAsync();
dbContext.Products.RemoveRange(inactiveProducts);
await dbContext.SaveChangesAsync(); // One DELETE per entity
Entity Framework Extensions Bulk Delete:
using Z.EntityFramework.Extensions;
var inactiveProducts = await dbContext
.Products
.Where(p => !p.IsActive)
.ToListAsync();
await dbContext.BulkDeleteAsync(inactiveProducts); // One optimized DELETE
Key Benefits of Bulk Delete
- Delete on your terms – remove rows using filters, custom keys, or even full graphs of related entities.
- Performance at scale – handle thousands or millions of deletions in just a few seconds.
- Skip unnecessary tracking – delete directly in the database without loading entities into memory.
- Flexible options – fine-tune how relationships are treated, how keys are matched, and how conditions are applied.
Bulk Merge
The BulkMerge method allows you to combine inserts and updates in one operation, often called an upsert (insert or update). Matching rows are updated, while new ones are inserted — all in a single efficient call.
using Z.EntityFramework.Extensions;
var existingProducts = await dbContext
.Products
.Take(1000)
.ToListAsync();
var newProducts = GenerateProducts(1000);
existingProducts.AddRange(newProducts);
await dbContext.BulkMergeAsync(existingProducts);
Key Benefits of Bulk Merge
- Upsert with control – decide how rows are matched, which properties get updated, and what values are inserted.
- High performance – process thousands or millions of upserts in just seconds.
- No entity tracking required – work directly at the database level without loading everything into memory.
- Flexible options – fine-tune keys, conditions, and update rules to match your business logic.
Bulk Synchronize
The BulkSynchronize method is designed to keep a table in sync with a given dataset. Instead of just inserting or updating like BulkMerge
, it also removes records that no longer exist in your source list, giving you a one-step way to mirror data.
When you run a synchronize operation:
- Existing rows that match are updated.
- New rows from your source are inserted.
- Extra rows in the database that aren’t in your list are deleted.
using Z.EntityFramework.Extensions;
var existingProducts = await dbContext
.Products
.Take(1000)
.ToListAsync();
var newProducts = GenerateProducts(1000);
existingProducts.AddRange(newProducts);
await dbContext.BulkSynchronizeAsync(existingProducts);
Note: To protect against accidents, an empty list won’t trigger a synchronize call
Key Benefits of Bulk Synchronize
- Complete alignment – insert, update, and remove rows in one step to keep tables current.
- Customizable behavior – control which columns define matches and how deletes are handled (hard or soft).
- Efficient at scale – reconcile entire tables quickly without loading entities into memory.
- Flexible options – tailor the synchronization logic to your business needs.
Performance Benchmarks
Compared to SaveChanges()
, the performance gains with Entity Framework Extensions are significant.
- Insert: up to 14× faster, reducing execution time by around 93% (Online Benchmark).
- Update: up to 4× faster, cutting execution time by roughly 75% (Online Benchmark).
- Delete: up to 3× faster, lowering execution time by about 65% (Online Benchmark).
These improvements mean that operations which normally take minutes with EF Core can be completed in seconds with Entity Framework Extensions, making it a reliable solution for high-volume data processing.
Summary
Entity Framework Core does an excellent job for day-to-day operations, but once the dataset grows into thousands or millions of rows, its row-by-row approach becomes a serious performance bottleneck.
This is where Entity Framework Extensions proves its value. By providing bulk methods such as BulkInsert
, BulkInsertOptimized
, BulkUpdate
, BulkDelete
, BulkMerge
, and BulkSynchronize
, the library makes large-scale operations simple, efficient, and highly performant. Instead of waiting minutes for EF Core to process changes, developers can achieve the same results in seconds, making Entity Framework Extensions a practical choice for production environments that demand speed and scalability.
Takeaway
The main lesson is that EF Core alone is not designed for heavy bulk workloads, but with Entity Framework Extensions, those limitations disappear. BulkInsert
and BulkInsertOptimized
make it possible to load massive datasets quickly and with minimal memory usage. BulkUpdate
and BulkDelete
allow you to apply large-scale changes or remove records in a single optimized call. BulkMerge
simplifies upsert scenarios by handling inserts and updates together, while BulkSynchronize
goes even further by ensuring that your database always matches the dataset you provide.
Together, these methods transform EF Core into a framework capable of handling both everyday operations and enterprise-scale data processing with ease.