Skip to main content
SEMastery
Data Accessintermediate

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.

11 min readUpdated May 1, 2026

Imagine your school is collecting exam answer sheets. One teacher walks to each desk, picks up one sheet, walks back, files it, then returns for the next sheet. With 5 students this is fine. With 5,000 students, the teacher will be walking all day.

A smarter teacher asks everyone to stack their sheets in one big pile. Then one quick trip carries the whole stack to the office. Same work, far less walking.

This is exactly the difference between normal saving in Entity Framework Core (EF Core) and a bulk insert. In this post you will learn what bulk insert means, why plain SaveChanges gets slow, and how the Entity Framework Extensions library helps you save thousands or millions of rows quickly.

What is EF Core doing when you save?

EF Core is an ORM. That means it lets you work with C# objects (like Customer or Order) instead of writing raw SQL by hand. When you call SaveChanges(), EF Core looks at every object you changed and writes matching SQL commands.

To do this, EF Core keeps a change tracker. The change tracker is like a notebook. It remembers every object you added, edited, or deleted. This notebook is wonderful for normal apps. But when you add 100,000 rows, the notebook becomes huge and slow.

How a normal SaveChanges call works in EF Core

For a few rows, every step here is fast. The problem starts when the number of rows grows. The change tracker has to remember every single object, and EF Core has to build a command for each row. That is a lot of extra work.

Why does plain saving get slow?

Let us look at a simple insert with EF Core. This is the normal way most people start.

using var db = new AppDbContext();
 
var customers = new List<Customer>();
for (int i = 0; i < 100_000; i++)
{
    customers.Add(new Customer
    {
        Name = $"Customer {i}",
        City = "Mumbai"
    });
}
 
db.Customers.AddRange(customers);
db.SaveChanges(); // This can take several seconds for 100k rows

This code is correct. It works. But behind the scenes three slow things happen:

  1. The change tracker stores all 100,000 objects in memory.
  2. EF Core builds and batches many SQL INSERT statements.
  3. Memory usage grows a lot, because nothing is released until the save finishes.

For small lists you will never notice. For large lists, you will feel it. Your app may freeze for seconds, and memory can climb to 2,000 MB or more for very large data sets.

The slow path: one-by-one thinking

Track each row
Build many INSERTs
High memory
Slow save

Steps

1

Track each row

Notebook grows huge

2

Build many INSERTs

One command per row group

3

High memory

Nothing released early

4

Slow save

Seconds, not milliseconds

EF Core tracks and processes each row, which adds up fast at scale.

What is a bulk insert?

A bulk insert sends many rows to the database in one fast, special operation. Instead of treating each row as a separate task, the database engine copies a whole block of rows at once.

Most databases have a built-in fast lane for this:

  • SQL Server has SqlBulkCopy.
  • PostgreSQL has the COPY command.
  • MySQL and others have their own fast bulk paths.

These fast lanes skip a lot of the per-row checking. That is why bulk insert is so much quicker. The trade-off is that it also skips the EF Core change tracker, so you lose some of EF Core's automatic features during the operation. For inserting big piles of data, that trade-off is usually worth it.

Bulk insert uses the database fast lane instead of per-row work

Meet Entity Framework Extensions

Entity Framework Extensions is a popular library made by ZZZ Projects. It adds methods like BulkInsert, BulkUpdate, BulkDelete, and BulkMerge directly onto your DbContext. It works with SQL Server, PostgreSQL, MySQL, MariaDB, Oracle, and SQLite.

The library has been downloaded more than 50 million times. According to its own benchmarks, it can insert up to 14x faster and cut save time by over 90% compared to plain SaveChanges.

One honest note: Entity Framework Extensions is a commercial (paid) library. It has a free trial so you can test it during development, but production use needs a license. If you need a fully free, open-source option, EFCore.BulkExtensions by borisdj is a well-known alternative with similar ideas.

Installing the library

You add it with the .NET CLI or NuGet. The package name depends on your EF version.

// Install the NuGet package (run in your terminal):
// dotnet add package Z.EntityFramework.Extensions.EFCore
 
// Then just call BulkInsert on your DbContext:
using var db = new AppDbContext();
 
var customers = new List<Customer>();
for (int i = 0; i < 100_000; i++)
{
    customers.Add(new Customer { Name = $"Customer {i}", City = "Delhi" });
}
 
db.BulkInsert(customers); // Fast! No SaveChanges needed for this call.

Notice that with BulkInsert you do not call SaveChanges() afterward. The bulk method talks to the database directly and finishes the job itself.

A clear performance comparison

Numbers help more than words. Here is a rough comparison based on community benchmarks for inserting rows into SQL Server. Your exact times will vary by machine and database, but the shape of the results is consistent.

RowsAddRange + SaveChangesBulkInsertRoughly how much faster
1,000~30 ms~25 msAlmost the same
10,000~3,200 ms~620 msAbout 5x faster
100,000~32,000 ms~4,000 msAbout 8x faster
1,000,000very slow, high memorya few sec10x or more, less memory

The pattern is easy to read. For small data, both ways are fine. The bigger the data, the bigger the win for bulk insert. Memory tells the same story: a giant SaveChanges may use around 2,000 MB, while bulk insert may need only about 400 MB for the same data.

The fast path: stack-and-go thinking

Collect rows
Skip tracker
Bulk copy
Fast finish

Steps

1

Collect rows

Build your list

2

Skip tracker

No per-row notebook

3

Bulk copy

SqlBulkCopy or COPY

4

Fast finish

Milliseconds, low memory

Bulk insert bypasses tracking and uses the database bulk-copy lane.

How does the library decide what to do?

When you call BulkInsert, the library reads your EF Core model to learn the table name, the columns, and the keys. Then it picks the best bulk strategy for your database provider. You do not have to write any SQL.

Decision flow inside BulkInsert

Useful options you should know

BulkInsert is not just fast; it is also flexible. You can change its behavior with simple options. Here are a few that beginners find handy.

db.BulkInsert(customers, options =>
{
    // Send rows in groups of 5,000 at a time
    options.BatchSize = 5000;
 
    // After insert, fill the generated Id values back into your objects
    options.AutoMapOutputDirection = true;
 
    // Insert only if the row does not already exist (by key)
    options.InsertIfNotExists = true;
});

These options let you control batch size for memory, get database-generated keys back, and avoid duplicate rows. There is also an async version, BulkInsertAsync, for use inside async methods so your app stays responsive.

When you still want change tracking

Sometimes you load data, change it, and want EF Core to figure out everything for you. For that the library also offers BulkSaveChanges(). It works like the normal SaveChanges() but runs much faster for large numbers of tracked entities. Use plain BulkInsert when you have a fresh list to push in, and BulkSaveChanges when you have tracked changes to flush.

Comparing your main options

Here is a small table to help you pick the right tool for the job. There is no single "best" choice; it depends on how much data you have and what your project allows.

ToolBest forFree?Uses change tracker?
AddRange + SaveChangesSmall inserts (under ~5k)Yes (built-in)Yes
ExecuteUpdate / ExecuteDeleteBulk update or deleteYes (built-in)No
EFCore.BulkExtensionsLarge inserts, open sourceYes (MIT)No
Entity Framework ExtensionsLarge inserts, many DBsNo (paid)No (BulkInsert)

A good rule: start with the built-in tools. EF Core's own AddRange, ExecuteUpdate, and ExecuteDelete solve most needs with no extra packages. Reach for a bulk library only when you actually hit the wall, meaning your saves take seconds and users notice the wait.

A complete mini example

Here is a small but complete picture of using bulk insert inside a real method. It shows reading a CSV-like list of records and pushing them in fast.

public async Task ImportCustomersAsync(List<Customer> incoming)
{
    using var db = new AppDbContext();
 
    // Bulk insert with options, the async way
    await db.BulkInsertAsync(incoming, options =>
    {
        options.BatchSize = 10_000;
        options.AutoMapOutputDirection = true; // get back generated Ids
    });
 
    // 'incoming' objects now have their database Id values filled in
    Console.WriteLine($"Inserted {incoming.Count} customers.");
}

This single method can import hundreds of thousands of rows in the time it would take plain SaveChanges to do a fraction of that work.

How the whole journey looks

To tie it together, here is the full path of data from your code to the database when you choose the bulk route.

End to end bulk insert journey

Common mistakes to avoid

A few small habits will save you trouble:

  • Do not call SaveChanges() after BulkInsert. The bulk method already saved the data. Calling save again does nothing useful and can confuse you.
  • Do not use bulk insert for tiny lists. For 50 rows, plain AddRange is simpler and just as fast. Keep your code simple when you can.
  • Watch your batch size. A very large batch can use more memory. Tuning BatchSize (for example 5,000 to 20,000) often gives the best balance.
  • Remember validation and triggers. Because bulk insert skips the change tracker, your EF Core validation and some database triggers may behave differently. Test on real data before going live.

Quick recap

  • Plain SaveChanges is great for small data but slows down with many rows because the change tracker has to remember every object.
  • A bulk insert sends a whole block of rows using the database fast lane, such as SqlBulkCopy for SQL Server or COPY for PostgreSQL.
  • Entity Framework Extensions adds BulkInsert, BulkUpdate, BulkDelete, BulkMerge, and BulkSaveChanges to your DbContext.
  • It can be roughly 5x to 10x faster for large inserts and uses much less memory.
  • It is a paid library with a free trial; EFCore.BulkExtensions is a free open-source alternative.
  • Start with built-in EF Core tools and only add a bulk library when saves become slow enough to hurt.
  • After BulkInsert, do not call SaveChanges again.

References and further reading

Related Posts