Remigiusz ZalewskiRemigiusz Zalewski

Insane Performance Boost in EF Core using Entity Framework Extensions

insane-performance-boost-in-ef-core-using-entity-framework-extensions

ZZZ Projects' EF Core Extensions Struggling with slow EF Core operations? Boost performance like never before. Experience up to 14× faster Bulk Insert, Update, Delete, and Merge - and cut your save time by as much as 94%. Learn more →

Introduction

Imagine that you've built a great application using .NET and Entity Framework Core. You start to process thousands - or tens of thousands - of records at once, and you notice a huge bottleneck: your application is getting slower and slower.

Standard EF Core is like a delivery driver who takes one package at a time to the same house, driving back and forth a thousand times. Entity Framework Extensions is the freight truck that delivers all 1,000 packages in a single trip.

In this article I'll show you how to use the Entity Framework Extensions library by ZZZ Projects to significantly improve the performance of your .NET application. We'll compare standard EF Core and EF Extensions on real use-case scenarios with actual benchmark numbers.

Here's what we'll cover:

  • Installing the EF Extensions NuGet package
  • Why EF Core slows down at scale
  • BulkInsert, BulkInsertOptimized
  • BulkUpdate, BulkDelete
  • BulkMerge (upsert)
  • BulkSynchronize (upsert + delete missing)
  • BulkSaveChanges

🎬 Watch the full video here:


Why Standard EF Core Struggles at Scale

EF Core's SaveChanges() is convenient - it tracks entity state and handles inserts, updates, and deletes automatically. But it generates one SQL statement per entity. Insert 10,000 products? That's 10,000 round trips.

There are two reasons this kills performance:

1. The change tracker becomes a bottleneck. Tracking the state of 50,000 objects in memory consumes massive amounts of CPU and RAM. Entity Framework Extensions allows you to process data without the overhead of tracking every single property change - freeing your server to handle actual logic instead of managing memory.

2. Too many database round trips. You can see it directly in the console: EF Core floods it with individual INSERT statements, one per record. EF Extensions reduces all of those into a single bulk operation.


Installing Entity Framework Extensions

dotnet add package Z.EntityFramework.Extensions.EFCore

Or search for Z.EntityFramework.Extensions.EFCore directly in the Visual Studio NuGet package manager.

The package supports .NET 10 and works with:

  • SQL Server
  • PostgreSQL
  • MySQL
  • Oracle
  • SQLite

Full docs, online benchmarks, and live examples are available at entityframework-extensions.net.

Licensing: A free trial is available so you can evaluate the library end-to-end. For commercial projects, a paid license is required from ZZZ Projects. Check the pricing page once you've benchmarked it against your own workloads.


Benchmarks at a Glance

All results below are measured on 10,000 records against a PostgreSQL database.

OperationEF CoreEF ExtensionsSpeedup
Insert~2,800 ms177 ms~16x
Insert Optimized~2,800 ms72 ms~39x
Update~1,500 ms100 ms~15x
Delete~800 ms31 ms~26x
Merge (upsert)~1,800 ms~100 ms~18x
Synchronize~1,700 ms126 ms~13x
BulkSaveChanges~1,400 ms425 ms~3x

⚡ Bulk Insert

EF Core (standard):

await context.Products.AddRangeAsync(products);
await context.SaveChangesAsync();

EF Extensions:

await context.BulkInsertAsync(products);

Result: ~2,800 ms - 177 ms.

Need even more speed and don't need output values (like generated IDs) back? Use the optimized variant:

await context.BulkInsertOptimizedAsync(products);

This skips the output value pass-back entirely, dropping the operation to 72 ms - roughly 39x faster than standard EF Core.


Bulk Update

EF Core:

foreach (var p in products)
    p.Price *= 1.10m;
await context.SaveChangesAsync();

EF Extensions:

foreach (var p in products)
    p.Price *= 1.10m;
await context.BulkUpdateAsync(products);

Result: ~1,500 ms - 100 ms - 15x faster.


Bulk Delete

EF Core:

context.Products.RemoveRange(products);
await context.SaveChangesAsync();

EF Extensions:

await context.BulkDeleteAsync(products);

Result: ~800 ms - 31 ms - over 25x faster.


Bulk Merge (Upsert)

A merge inserts new records and updates existing ones in a single operation. EF Extensions automatically detects what needs to be inserted vs. updated.

EF Core (manual):

foreach (var p in existing)
    p.Price *= 1.15m;
context.Products.AddRange(incoming);
await context.SaveChangesAsync();

EF Extensions:

foreach (var p in existing)
    p.Price *= 1.15m;
var all = existing.Concat(incoming).ToList();
await context.BulkMergeAsync(all);

Result (5,000 updates + 5,000 inserts = 15,000 total): ~1,800 ms - ~100 ms.


Bulk Synchronize

Synchronize goes one step further than Merge - it also deletes records that are not in your source list:

  • Rows that match the entity key are updated
  • Rows in the source but not in the database are inserted
  • Rows in the database but not in the source are deleted

Think of it as mirroring your source to the database.

EF Core (manual - a lot of code to get right):

var existingIds = source.Where(p => p.Id > 0).Select(p => p.Id).ToHashSet();
var toDelete = await context.Products
    .Where(p => !existingIds.Contains(p.Id)).ToListAsync();
context.Products.RemoveRange(toDelete);
foreach (var p in source.Where(p => p.Id > 0))
    context.Products.Update(p);
context.Products.AddRange(source.Where(p => p.Id == 0));
await context.SaveChangesAsync();

EF Extensions:

await context.BulkSynchronizeAsync(source);

Result: ~1,700 ms - 126 ms - and a fraction of the code.


Bulk SaveChanges

Already using the EF Core change tracker with a mix of Add, Update, and Delete? Drop in BulkSaveChangesAsync() as a near-zero-friction replacement:

await context.BulkSaveChangesAsync(); // instead of SaveChangesAsync()

This is the easiest migration path if you have existing code and just want a performance lift.

Result on a mixed Add/Update/Delete workload: ~1,400 ms - 425 ms.


When Should You Reach for This?

You don't need bulk operations for typical CRUD in a web app handling a handful of records at a time. But if any of these apply, the standard EF Core approach will hurt:

  • Importing data from CSVs or external feeds
  • Syncing records between systems
  • Running nightly price or inventory updates across thousands of rows
  • Handling data migrations

A one-line swap to BulkInsertAsync, BulkMergeAsync, or BulkSynchronizeAsync makes a real difference.


Key Takeaways

  • BulkInsertAsync replaces AddRangeAsync + SaveChangesAsync, up to 16x faster.
  • BulkInsertOptimizedAsync skips output values for maximum insert speed (~40x).
  • BulkUpdateAsync replaces foreach + SaveChangesAsync, 15x faster.
  • BulkDeleteAsync replaces RemoveRange + SaveChangesAsync, 25x faster.
  • BulkMergeAsync handles upsert in one call, auto-detects insert vs. update.
  • BulkSynchronizeAsync adds deletion of records missing from the source on top of upsert.
  • BulkSaveChangesAsync is a drop-in replacement for SaveChangesAsync with the change tracker.
  • A free trial is available - a commercial license is required for production use.

What's Next?

If you found this useful, the next step is benchmarking these operations against your own database and workload size. Subscribe to get more weekly .NET content and never miss a performance tip.

Resources