CodeToClarity Logo
Published on ·8 min read·.NET

The Complete EF Core Bulk Operations Guide: BulkInsert, BulkUpdate, BulkMerge & More

Kishan KumarKishan Kumar

Struggling with slow EF Core bulk operations? Learn how Entity Framework Extensions cuts insert/update/delete times by up to 95% with BulkInsert, BulkUpdate, and more.

You know that feeling when you're building a .NET app and everything's running smoothly? You're writing LINQ queries, calling SaveChanges(), and life is good. Then your boss walks in and says, "Hey, we need to import 50,000 product records from this CSV file."

You think, "No problem, I'll just loop through and save them." Three hours later, you're still waiting for it to finish.

Welcome to the world of bulk operations, where standard EF Core starts showing its limitations.

Here's the thing: Entity Framework Core is fantastic for everyday database work. But when you need to handle thousands (or millions) of records, the traditional approach becomes a performance nightmare. That's where Entity Framework Extensions comes in and trust me, once you see what it can do, you'll wonder how you ever lived without it.

Why Does EF Core Struggle with Large Datasets?

Before we jump into solutions, let's understand the problem.

When you call SaveChanges() in EF Core, here's what happens behind the scenes:

  • One SQL statement per entity – inserting 10,000 rows means 10,000 separate INSERT commands
  • Change tracking overhead – EF Core validates and tracks every single entity
  • Database round-trips – each command requires a separate trip to the database server

Think of it like this: imagine you're moving houses and you decide to carry one box at a time instead of renting a truck. Sure, it works for a few boxes, but if you've got 10,000 boxes? You're going to be there a while.

That's exactly what happens with SaveChanges(). What should take seconds ends up taking minutes (or even hours) because EF Core wasn't designed for bulk operations.

Enter Entity Framework Extensions

Entity Framework Extensions (or EF Extensions for short) is a library that supercharges EF Core with high-performance bulk operations. We're talking about methods like:

  • BulkInsert – add thousands of records in milliseconds
  • BulkUpdate – update massive datasets efficiently
  • BulkDelete – remove large batches quickly
  • BulkMerge – the famous "upsert" operation (insert or update)
  • BulkSynchronize – keep your database in sync with your data source

Instead of executing one command per row, these methods generate optimized SQL that processes everything in batches. It's like switching from carrying boxes one at a time to using that moving truck.

Getting Started: Installation

Let's get you set up. Installing Entity Framework Extensions is straightforward → just add it via NuGet.

Using the .NET CLI:

dotnet add package Z.EntityFramework.Extensions.EFCore

Or if you prefer the Package Manager Console in Visual Studio:

Install-Package Z.EntityFramework.Extensions.EFCore

Once installed, all the bulk methods become available directly on your DbContext. No complicated setup required.

What Versions and Databases Are Supported?

One question I get a lot: "Will this work with my setup?"

Good news → Entity Framework Extensions has you covered:

Entity Framework Versions:

  • All EF Core versions (including the latest EF Core 10)
  • EF6 and earlier legacy versions

Database Providers:

  • SQL Server
  • MySQL and MariaDB
  • PostgreSQL
  • Oracle
  • SQLite

Whether you're running the latest and greatest or maintaining a legacy application, EF Extensions will work with your stack.

BulkInsert: Inserting Data at Lightning Speed

Let's start with the most common scenario: inserting a large number of records.

Here's the traditional EF Core approach:

var products = GenerateProducts(10_000);
dbContext.Products.AddRange(products);

await dbContext.SaveChangesAsync(); // 10,000 individual INSERTs

This generates 10,000 separate INSERT statements. On my machine, this takes around 45 seconds. Not terrible for a one-time operation, but what if you're doing data imports regularly?

Now watch what happens with BulkInsert:

using Z.EntityFramework.Extensions;

var products = GenerateProducts(10_000);

await dbContext.BulkInsertAsync(products); // Single optimized operation

Same result, but it completes in about 2 seconds. That's a 95% reduction in execution time.

Keeping Your Identity Values

Sometimes you need to preserve specific ID values → maybe you're migrating data or syncing between systems. That's where InsertKeepIdentity comes in:

using Z.EntityFramework.Extensions;

await dbContext.BulkInsertAsync(products, options =>
{
    options.InsertKeepIdentity = true;
});

This tells SQL Server (or your database) to use the IDs you've already assigned instead of generating new ones. Entity Framework Extensions handles all the database-specific commands automatically.

Here's where things get really powerful. Let's say you have products with reviews:

public sealed class Product
{
    public Guid Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public ICollection<Review> Reviews { get; set; } = [];
}

public sealed class Review
{
    public int Id { get; set; }
    public string Comment { get; set; } = string.Empty;
    public Guid ProductId { get; set; }
}

Normally, you'd need to insert products first, then reviews separately. But with IncludeGraph, you can do it all in one shot:

using Z.EntityFramework.Extensions;

var productsWithReviews = new List<Product>
{
    new()
    {
        Name = "Laptop",
        Price = 1200.00m,
        Reviews = new List<Review>
        {
            new() { Comment = "Excellent performance." },
            new() { Comment = "Battery life could be better." }
        }
    },
    new()
    {
        Name = "Smartphone",
        Price = 850.00m,
        Reviews = new List<Review>
        {
            new() { Comment = "Amazing camera quality." }
        }
    }
};

await dbContext.BulkInsertAsync(productsWithReviews, options =>
{
    options.IncludeGraph = true;
});

Entity Framework Extensions will insert both the products AND their related reviews in one optimized bulk operation. No need for multiple calls or complex logic.

BulkInsertOptimized: When Speed Is Everything

If you thought BulkInsert was fast, meet its even faster cousin: BulkInsertOptimized.

The key difference? By default, BulkInsertOptimized doesn't return output values (like auto-generated IDs). This lets it skip temporary tables and write directly to the destination table using SqlBulkCopy, making it blazingly fast.

using Z.EntityFramework.Extensions;

var products = GenerateProducts(10_000);

await dbContext.BulkInsertOptimizedAsync(products);

Performance Analysis

Want to know if your insert is running at maximum efficiency? BulkInsertOptimized returns an analysis object:

using Z.EntityFramework.Extensions;

var products = GenerateProducts(10_000);
var analysis = await dbContext.BulkInsertOptimizedAsync(products);

if (!analysis.IsOptimized)
{
    Console.WriteLine(analysis.TipsText); // Get optimization tips
}

This tells you:

  • Whether your operation is fully optimized
  • What might be slowing it down
  • Specific recommendations to improve performance

It's like having a performance coach built into your code.

BulkUpdate: Changing Thousands of Records Instantly

Updating large datasets is another pain point. Let's say you need to deactivate 5,000 products:

The EF Core way:

var products = await dbContext
    .Products
    .Take(5000)
    .ToListAsync();

products.ForEach(p => p.IsActive = false);

await dbContext.SaveChangesAsync(); // 5,000 separate UPDATE statements

The EF Extensions way:

using Z.EntityFramework.Extensions;

var products = await dbContext
    .Products
    .Take(5000)
    .ToListAsync();

products.ForEach(p => p.IsActive = false);

await dbContext.BulkUpdateAsync(products); // Single batch UPDATE

Same logic, massively different performance. The bulk update happens in seconds instead of minutes.

Why It Matters

Here's a real-world scenario: you're running an e-commerce platform and need to update prices across thousands of products based on a new pricing strategy. With standard EF Core, this could take several minutes during which your database is under heavy load. With BulkUpdate, it's done before anyone notices.

BulkDelete: Removing Data in Bulk

Deleting large batches of data? Same story.

Standard approach:

var inactiveProducts = await dbContext
    .Products
    .Where(p => !p.IsActive)
    .ToListAsync();

dbContext.Products.RemoveRange(inactiveProducts);

await dbContext.SaveChangesAsync(); // One DELETE per entity

Optimized approach:

using Z.EntityFramework.Extensions;

var inactiveProducts = await dbContext
    .Products
    .Where(p => !p.IsActive)
    .ToListAsync();

await dbContext.BulkDeleteAsync(inactiveProducts); // One optimized DELETE

Clean, fast, and efficient. Perfect for maintenance tasks, data cleanup, or archiving operations.

BulkMerge: The Magic of Upsert

Here's one of my favorite features: BulkMerge. It handles the classic "insert if new, update if exists" scenario → also known as an upsert.

Imagine you're syncing data from an external API. Some records already exist in your database, others are new. Instead of checking each record individually, just use BulkMerge:

using Z.EntityFramework.Extensions;

var existingProducts = await dbContext
    .Products
    .Take(1000)
    .ToListAsync();

var newProducts = GenerateProducts(1000);

existingProducts.AddRange(newProducts);

await dbContext.BulkMergeAsync(existingProducts);

Entity Framework Extensions automatically figures out which records to insert and which to update based on your primary key. It's intelligent, fast, and saves you from writing tons of conditional logic.

BulkSynchronize: Keep Your Database in Perfect Sync

BulkSynchronize takes things one step further. It doesn't just insert and update → it also deletes records that don't exist in your source data anymore.

Think of it as a complete mirror operation:

  • Existing records that match → updated
  • New records in your list → inserted
  • Extra records in the database → deleted
using Z.EntityFramework.Extensions;

var currentProducts = GetProductsFromExternalAPI();

await dbContext.BulkSynchronizeAsync(currentProducts);

After this operation, your database table will be an exact match of your source data. Perfect for keeping reference data up-to-date or syncing with external systems.

Important safety feature: An empty list won't trigger the synchronize operation. This prevents accidental mass deletions.

Real-World Performance Numbers

Okay, enough talk. Let's look at actual benchmarks (tested on SQL Server with standard hardware):

  • Insert: Up to 14x faster – operations that took 100 seconds now take about 7 seconds (93% reduction)
  • Update: Up to 4x faster – 40-second operations drop to 10 seconds (75% reduction)
  • Delete: Up to 3x faster – 30-second deletions complete in 10 seconds (65% reduction)

These aren't theoretical numbers → this is what developers are seeing in production environments.

When Should You Use Bulk Operations?

Not every operation needs bulk methods. Here's my rule of thumb:

Use bulk operations when:

  • You're inserting/updating/deleting hundreds or thousands of records
  • Performance is critical (data imports, batch jobs, migrations)
  • You're working with time-sensitive operations
  • Memory usage is a concern with large datasets

Stick with SaveChanges() when:

  • You're working with fewer than 100 records
  • The operation happens infrequently
  • You need EF Core's change tracking features
  • Simplicity is more important than performance

The Bottom Line

Entity Framework Core is excellent for everyday database operations. But when you scale up to thousands or millions of records, its one-row-at-a-time approach becomes a serious bottleneck.

That's where Entity Framework Extensions transforms your application. Methods like BulkInsert, BulkUpdate, BulkDelete, BulkMerge, and BulkSynchronize turn what would be multi-minute operations into seconds-long tasks.

The library works with all EF Core versions, all major databases, and requires minimal code changes. Install it, replace your SaveChanges() calls with the appropriate bulk method, and watch your performance skyrocket.

If you're building production applications that handle large datasets, Entity Framework Extensions isn't just nice to have → it's essential.