Why Every EF Core Developer Needs to Try Entity Framework Extensions
A friendly guide to Entity Framework Extensions: bulk insert, update, delete and merge for EF Core, with simple analogies, diagrams, tables and code examples.
The grocery shopping analogy
Imagine your mother sends you to the local kirana shop with a list of 50 items. You have two ways to do this.
The first way: you walk to the shop, buy one item, walk back home, drop it, then walk back to the shop for the next item. Fifty trips for fifty items. By evening you are tired and the day is gone.
The second way: you take one big bag, buy all 50 items together, and walk home once. Same shopping, but one trip instead of fifty.
This is exactly the difference between normal EF Core saving and Entity Framework Extensions.
When you call SaveChanges on thousands of new rows, EF Core often talks to the database many, many times. Each round trip is like one walk to the shop. Entity Framework Extensions packs everything into one big bag and makes far fewer trips. The result is the same data in your database, but the work finishes much faster.
This article explains, in simple words, what this library does, when to use it, and how to write the code.
What is Entity Framework Extensions?
Entity Framework Extensions is a library made by a company called ZZZ Projects. It plugs into EF Core (and the older EF6) and adds a small set of very powerful methods:
BulkInsert— add many rows fast.BulkUpdate— change many rows fast.BulkDelete— remove many rows fast.BulkMerge— insert if new, update if it already exists (this is called an upsert).BulkSynchronize— make a table match your list exactly (insert, update, and delete in one go).
These methods sit right next to your normal DbContext. You do not throw away EF Core. You keep your entities, your DbContext, and your queries. You just reach for these methods when you have a lot of rows to save.
Why normal SaveChanges gets slow
To understand the gift, you first need to feel the pain.
When you add 10,000 new objects and call SaveChanges, EF Core does a few heavy things:
- It tracks every single object in its change tracker.
- It works out what changed for each one.
- It sends commands to the database, often in small batches or close to one row at a time.
For ten or twenty rows, you will never notice. For ten thousand or a million rows, the change tracker fills up your memory and the back-and-forth chatter with the database eats up time.
How normal SaveChanges handles many rows
Steps
Add objects
You add 10,000 entities.
Track each one
Change tracker grows large.
Send small batches
Many round trips to DB.
Wait for each
Time adds up fast.
Here is what plain EF Core code looks like. It is correct, it is simple, but it struggles at large sizes.
// Plain EF Core: fine for small lists, slow for huge ones
using var context = new AppDbContext();
var orders = new List<Order>();
for (int i = 0; i < 100_000; i++)
{
orders.Add(new Order
{
CustomerName = $"Customer {i}",
Amount = i * 10m,
CreatedOn = DateTime.UtcNow
});
}
context.Orders.AddRange(orders);
context.SaveChanges(); // change tracker holds all 100,000 entitiesThe bulk way
Now look at the same job using Entity Framework Extensions. First install the package:
// In the Package Manager Console:
// Install-Package Z.EntityFramework.Extensions.EFCore
using Z.EntityFramework.Extensions; // brings in the bulk methodsThen the saving code becomes almost the same, but with one changed line:
using var context = new AppDbContext();
var orders = new List<Order>();
for (int i = 0; i < 100_000; i++)
{
orders.Add(new Order
{
CustomerName = $"Customer {i}",
Amount = i * 10m,
CreatedOn = DateTime.UtcNow
});
}
// One method. No AddRange. No giant change tracker.
context.BulkInsert(orders);That single BulkInsert call sends the rows in efficient batches and skips the heavy change tracking. The data lands in the same table, but the work is done in far fewer trips to the shop.
How BulkInsert handles many rows
Steps
Add objects
You build the list.
Skip tracking
No heavy tracker.
Batch all rows
Rows grouped together.
Few round trips
Database finishes quickly.
How much faster, really?
The numbers below come from the library's own published benchmarks. Real results depend on your rows, your network, and your database, so treat these as a guide, not a promise. The point is the shape of the gain: bulk methods are a different league for large data.
| Operation | Compared to SaveChanges | Roughly how much faster |
|---|---|---|
| Bulk Insert | Adding many rows | Up to about 15x faster |
| Bulk Update | Changing many rows | About 4x faster |
| Bulk Delete | Removing many rows | About 3x faster |
| Memory used | Holding entities | Down to around 20% of normal |
The memory line matters as much as speed. With millions of rows, plain SaveChanges can balloon your memory because the change tracker keeps everything. Bulk methods stay lean because they do not need that big tracker.
Meet the other bulk methods
BulkInsert is the star, but its siblings are just as useful.
BulkUpdate
When you already have a list of changed objects and want them written back quickly:
foreach (var order in orders)
{
order.Amount += 50m; // give everyone a small bump
}
context.BulkUpdate(orders);BulkDelete
When you need to clear out a large set of rows in one shot:
context.BulkDelete(oldOrders);BulkMerge (the upsert)
This is the clever one. For each row, the library checks the database. If a match exists, it updates. If not, it inserts. You do not have to split your list into "new" and "existing" yourself.
// Some orders are brand new, some already exist.
// BulkMerge sorts that out for you in one call.
context.BulkMerge(orders);BulkSynchronize
Synchronize is merge plus cleanup. It makes the table match your list exactly: new rows are inserted, changed rows are updated, and rows that are not in your list are deleted. Use it with care, because it removes data that you did not include.
A real example: importing a file
Picture a nightly job that reads a big CSV of products from a supplier and loads it into your database. Some products are new. Some have new prices. This is a perfect job for BulkMerge.
public void ImportProducts(List<Product> productsFromFile)
{
using var context = new AppDbContext();
// Insert new products, update the ones we already have.
context.BulkMerge(productsFromFile, options =>
{
options.ColumnPrimaryKeyExpression = p => p.Sku; // match on SKU
options.BatchSize = 2000; // tune trip size
});
}Two small options do a lot here. ColumnPrimaryKeyExpression tells the library how to decide if two rows are "the same" (here, by the SKU code, not the database id). BatchSize controls how many rows ride in each trip to the database.
Nightly product import with BulkMerge
Steps
Read CSV
Load supplier file.
Match by SKU
Find existing rows.
Insert or update
Upsert each product.
Save in batches
Few fast round trips.
Handy options you will actually use
The bulk methods accept an options lambda. Here are the ones beginners reach for most.
| Option | What it does | When to use it |
|---|---|---|
BatchSize | Rows per database trip | Tune for very large or very small rows |
ColumnPrimaryKeyExpression | Picks the match key | Merge or update by a business key like SKU |
IncludeGraph | Saves related objects too | Order plus its OrderLines in one call |
InsertIfNotExists | Skip rows already there | Safe re-runs of an import |
ColumnInputExpression | Save only chosen columns | Update price but not the name |
A quick taste of IncludeGraph, which saves an object and its children together:
// Saves each Order and all of its OrderLines in one bulk call.
context.BulkInsert(orders, options =>
{
options.IncludeGraph = true;
});When should you use it, and when not?
Bulk methods are a tool, not a rule. Reach for them when the size of the work makes plain SaveChanges hurt.
Good times to use bulk methods:
- Importing files with thousands or millions of rows.
- Nightly sync jobs that refresh large tables.
- Seeding a database with lots of test or reference data.
- Clearing or archiving big batches of old records.
Times to stick with plain SaveChanges:
- A web request saving one order and a few lines. Here EF Core is simpler and just fine.
- Anywhere the row count is small. The bulk setup is not worth it for ten rows.
- When you depend on EF Core's automatic features for those few entities, like certain interceptors or events that the bulk path may handle differently.
A word on cost and choices
Entity Framework Extensions is a commercial library. It has a paid license, with a free trial that resets each month for evaluation. For a business saving real money on server time, the price is often easy to justify. But it is fair to know your options.
A popular free-ish alternative is EFCore.BulkExtensions by Borisdj. It also gives bulk insert, update, delete and read. Keep in mind that its commercial use is restricted based on company revenue, so read the license before you ship. It works very well for flat, large inserts, but can slow down sharply on deep object graphs.
| Library | License | Best at | Watch out for |
|---|---|---|---|
| Entity Framework Extensions | Paid (trial available) | All bulk ops, graphs, many providers | Cost for small teams |
| EFCore.BulkExtensions | Free with revenue limit | Flat large inserts and updates | Slows on big object graphs |
So the honest advice: try the free trial of Entity Framework Extensions on your own slow import. Measure it. If it saves you real time and the price fits, it is a strong pick. If your needs are simple and flat, the community option may be enough.
Common mistakes to avoid
A few gentle warnings so you do not learn these the hard way.
- Do not call
SaveChangesafter a bulk method for the same rows. Bulk methods talk to the database directly. There is nothing left forSaveChangesto save. - Watch
BulkSynchronize. It deletes rows that are not in your list. If you pass a half-filled list by mistake, it will wipe the rest. - Set a sensible
BatchSize. Too small means many trips. Too large can strain memory or the database. Start around 2000 and adjust. - Remember the change tracker is bypassed. Objects saved by bulk methods are not tracked afterwards. If you need to keep working with them in the same context, plan for that.
// WRONG: SaveChanges has nothing to do here and just adds confusion
context.BulkInsert(orders);
context.SaveChanges(); // remove this line
// RIGHT: the bulk call already saved everything
context.BulkInsert(orders);Quick recap
- EF Core's
SaveChangesis great for small saves but slows down with many rows, because of change tracking and many database trips. - Entity Framework Extensions adds
BulkInsert,BulkUpdate,BulkDelete,BulkMerge, andBulkSynchronizeto do large saves in far fewer trips. BulkMergeis an upsert: it inserts new rows and updates existing ones in one call.- Bulk methods can be many times faster and use only a fraction of the memory for large data.
- Useful options include
BatchSize,ColumnPrimaryKeyExpression, andIncludeGraph. - Use bulk methods for big imports and sync jobs; keep plain
SaveChangesfor small, everyday saves. - The library is paid with a free trial.
EFCore.BulkExtensionsis a free-ish alternative with a revenue limit. Measure first, then decide.
References and further reading
- Entity Framework Extensions — Official Site (ZZZ Projects)
- Bulk Insert in EF Core with Entity Framework Extensions
- Bulk Update in EF Core with Entity Framework Extensions
- Entity Framework Extensions — Learn EF Core
- EntityFramework-Extensions on GitHub
- EFCore.BulkExtensions on GitHub (free alternative)
Related Posts
EF Core Bulk Insert: Boost Performance with Entity Framework Extensions
Learn how EF Core bulk insert with Entity Framework Extensions saves data faster, using simple examples, diagrams, and clear performance comparisons.
EF Core DbContext Options Explained: A Beginner's Friendly Guide
Learn EF Core DbContext options in simple words: AddDbContext, the options builder, retry on failure, query splitting, logging, lifetimes and pooling, with diagrams and examples.
How I Increased a Production Payment System's Performance by 15x With EF Core Extensions
A true-to-life story of making a slow EF Core payment system 15x faster using bulk extensions, ExecuteUpdate, and ExecuteDelete with simple examples.
EF Core Query Optimization: From 30 Seconds to 30 Milliseconds
Learn EF Core query optimization step by step: fix N+1 queries, use projections, AsNoTracking, indexes, and compiled queries to turn a 30-second query into 30ms.
EF Core Query Splitting: Fix Slow Queries and Cartesian Explosion
Learn how EF Core query splitting (AsSplitQuery) fixes the cartesian explosion problem with simple examples, diagrams, and real performance numbers. Know when to split and when not to.
How I Made My EF Core Query Faster With Batching
A beginner-friendly guide to EF Core batching. Learn how SaveChanges groups SQL into fewer database trips, how to tune MaxBatchSize, and when it helps.