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.
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.
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 rowsThis code is correct. It works. But behind the scenes three slow things happen:
- The change tracker stores all 100,000 objects in memory.
- EF Core builds and batches many SQL
INSERTstatements. - 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
Steps
Track each row
Notebook grows huge
Build many INSERTs
One command per row group
High memory
Nothing released early
Slow save
Seconds, not milliseconds
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
COPYcommand. - 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.
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.
| Rows | AddRange + SaveChanges | BulkInsert | Roughly how much faster |
|---|---|---|---|
| 1,000 | ~30 ms | ~25 ms | Almost the same |
| 10,000 | ~3,200 ms | ~620 ms | About 5x faster |
| 100,000 | ~32,000 ms | ~4,000 ms | About 8x faster |
| 1,000,000 | very slow, high memory | a few sec | 10x 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
Steps
Collect rows
Build your list
Skip tracker
No per-row notebook
Bulk copy
SqlBulkCopy or COPY
Fast finish
Milliseconds, low memory
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.
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.
| Tool | Best for | Free? | Uses change tracker? |
|---|---|---|---|
| AddRange + SaveChanges | Small inserts (under ~5k) | Yes (built-in) | Yes |
| ExecuteUpdate / ExecuteDelete | Bulk update or delete | Yes (built-in) | No |
| EFCore.BulkExtensions | Large inserts, open source | Yes (MIT) | No |
| Entity Framework Extensions | Large inserts, many DBs | No (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.
Common mistakes to avoid
A few small habits will save you trouble:
- Do not call
SaveChanges()afterBulkInsert. 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
AddRangeis 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
SaveChangesis 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
SqlBulkCopyfor SQL Server orCOPYfor PostgreSQL. - Entity Framework Extensions adds
BulkInsert,BulkUpdate,BulkDelete,BulkMerge, andBulkSaveChangesto yourDbContext. - 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 callSaveChangesagain.
References and further reading
- Entity Framework Extensions - Bulk Insert (official docs)
- Entity Framework Extensions - home page
- Entity Framework Fastest Way to Insert
- Microsoft Learn - EF Core overview
- Learn EF Core - Bulk Extensions
- EFCore.BulkExtensions (free, open source) on GitHub
- Bulk Operations in EF Core - benchmarks by codewithmukesh
Related Posts
Understanding Change Tracking for Better Performance in EF Core
Learn how EF Core change tracking works, the entity states it uses, and simple tricks like AsNoTracking to make your .NET apps faster.
Fast SQL Bulk Inserts With C# and EF Core: A Beginner Guide
Learn fast SQL bulk inserts in C# and EF Core using AddRange, batching, SqlBulkCopy, and bulk libraries, with simple diagrams and clear examples.
The Correct Way to Use Batch Update and Batch Delete in EF Core
Learn the correct, safe way to use ExecuteUpdate and ExecuteDelete batch methods in EF Core, with transactions, change tracker tips, and EF Core 10 features.
Optimizing Bulk Database Updates in .NET: A Practical Guide
Learn how to make bulk database updates fast in .NET using EF Core ExecuteUpdate, batching, and transactions, with simple examples and EF Core 10 tips.
5 EF Core Features You Need to Know (Beginner Friendly)
Learn 5 must-know EF Core features with simple examples: change tracking, AsNoTracking, eager loading, bulk ExecuteUpdate, and query filters.
Calling Views, Stored Procedures and Functions in EF Core
A friendly, beginner guide to calling database views, stored procedures, and functions in EF Core with FromSql, SqlQuery, ExecuteSql, and ToView.