Skip to main content
SEMastery
Data Accessbeginner

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.

12 min readUpdated March 7, 2026

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.

Entity Framework Extensions adds bulk methods on top of your normal EF Core setup.

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:

  1. It tracks every single object in its change tracker.
  2. It works out what changed for each one.
  3. 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

Add objects
Track each one
Send small batches
Wait for each

Steps

1

Add objects

You add 10,000 entities.

2

Track each one

Change tracker grows large.

3

Send small batches

Many round trips to DB.

4

Wait for each

Time adds up fast.

Each row carries tracking weight and adds database chatter.

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 entities

The 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 methods

Then 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

Add objects
Skip tracking
Batch all rows
Few round trips

Steps

1

Add objects

You build the list.

2

Skip tracking

No heavy tracker.

3

Batch all rows

Rows grouped together.

4

Few round trips

Database finishes quickly.

Skip tracking, batch everything, make few trips.

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.

OperationCompared to SaveChangesRoughly how much faster
Bulk InsertAdding many rowsUp to about 15x faster
Bulk UpdateChanging many rowsAbout 4x faster
Bulk DeleteRemoving many rowsAbout 3x faster
Memory usedHolding entitiesDown 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.

More rows means the gap between SaveChanges and bulk methods grows wider.

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);
BulkMerge decides insert or update for each incoming row.

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

Read CSV
Match by SKU
Insert or update
Save in batches

Steps

1

Read CSV

Load supplier file.

2

Match by SKU

Find existing rows.

3

Insert or update

Upsert each product.

4

Save in batches

Few fast round trips.

Read the file, match by SKU, save in batches.

Handy options you will actually use

The bulk methods accept an options lambda. Here are the ones beginners reach for most.

OptionWhat it doesWhen to use it
BatchSizeRows per database tripTune for very large or very small rows
ColumnPrimaryKeyExpressionPicks the match keyMerge or update by a business key like SKU
IncludeGraphSaves related objects tooOrder plus its OrderLines in one call
InsertIfNotExistsSkip rows already thereSafe re-runs of an import
ColumnInputExpressionSave only chosen columnsUpdate 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 simple way to decide which path to take.

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.

LibraryLicenseBest atWatch out for
Entity Framework ExtensionsPaid (trial available)All bulk ops, graphs, many providersCost for small teams
EFCore.BulkExtensionsFree with revenue limitFlat large inserts and updatesSlows 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.

  1. Do not call SaveChanges after a bulk method for the same rows. Bulk methods talk to the database directly. There is nothing left for SaveChanges to save.
  2. 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.
  3. Set a sensible BatchSize. Too small means many trips. Too large can strain memory or the database. Start around 2000 and adjust.
  4. 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 SaveChanges is 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, and BulkSynchronize to do large saves in far fewer trips.
  • BulkMerge is 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, and IncludeGraph.
  • Use bulk methods for big imports and sync jobs; keep plain SaveChanges for small, everyday saves.
  • The library is paid with a free trial. EFCore.BulkExtensions is a free-ish alternative with a revenue limit. Measure first, then decide.

References and further reading

Related Posts