SQL Bulk Inserts in .NET: A Practical Guide to Every Option You Have

Article Sponsors

EF Core too slow? Insert data up to 14x faster and cut save time by 94%. Boost performance with extension methods fully integrated into EF Core — Bulk Insert, Update, Delete, and Merge.

Join 5,000+ developers who’ve trusted our library since 2014.

👉 Try it now — and feel the difference

If you’ve ever tried to seed a database with a few hundred thousand rows and watched the progress bar barely move, you already know the problem this article solves. A regular INSERT statement is built for correctness and simplicity, not throughput — every row you send it means another parse, another execution plan check, and another round trip between your app and the database. That’s fine for a signup form. It falls apart the moment you’re importing a CSV, migrating a legacy table, or syncing data from an external system.

This article walks through every practical way to get large amounts of data into a SQL database from .NET. The focus here is on understanding what each option actually does under the hood — how it talks to the database, and where it saves you time — so you can pick the right one for your situation rather than leaning on a number measured on someone else’s hardware.

Quick answer: For small datasets, EF Core’s AddRange + a single SaveChangesAsync() is enough. For genuine bulk loads in .NET, your main options are SqlBulkCopy (SQL Server’s native bulk protocol, fastest but SQL Server–only), Dapper (lightweight, no ORM overhead), and EF Core–integrated bulk libraries like EFCore.BulkExtensions or Entity Framework Extensions (bulk speed without leaving your DbContext).

Note: The examples below use a simple Product entity mapped to a Products table in SQL Server, but the concepts apply to PostgreSQL, MySQL, and other providers too — just swap the provider-specific pieces.

C#
public sealed class Product
{
    public int Id { get; set; }
    public string Sku { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }
}

What Actually Happens When You Run an INSERT

Before comparing options, it’s worth understanding what a single INSERT costs — this is the mental model every technique below is trying to improve on.

  • Round trip — your application sends the statement over the network to the database server, and waits for a response.
  • Parse and plan — the database parses the SQL, checks permissions, and (unless it has a cached plan) builds an execution plan for it.
  • Constraint and index maintenance — the engine validates constraints (foreign keys, uniqueness, check constraints) and updates every index on the table, not just the data itself.
  • Log write — the change is written to the transaction log before it’s considered durable.

Do that once, and it’s instant. Do it 500,000 times in a loop, and you’re paying for a full network round trip and a full planning cycle on every single row — that’s the bottleneck bulk insert techniques are built to eliminate. They either reduce the number of round trips, skip unnecessary overhead like change tracking, or bypass the standard INSERT pipeline entirely in favor of a database’s native bulk-loading protocol.

Option 1: Insert Rows One at a Time in EF Core (The Naive Loop)

This is where almost everyone starts, and it’s worth showing explicitly so it’s clear why it doesn’t scale.

C#
using var context = new AppDbContext();

foreach (var product in GetProducts())
{
    context.Products.Add(product);
    await context.SaveChangesAsync();
}

Every call to SaveChangesAsync() here triggers its own round trip to the database. On top of that, EF Core’s change tracker is doing bookkeeping on every entity you add, even though you’re only ever going to insert it once. For a handful of rows this is invisible. For a data import job, this is the pattern responsible for jobs that “used to take a few seconds” suddenly timing out.

Use this only for: one-off inserts, form submissions, or anywhere you’re inserting a single row triggered by user action — not for loading data in bulk.

Option 2: Batch Insert in EF Core with AddRange

The simplest real improvement is to stop calling SaveChangesAsync() inside the loop. Add every entity first, then save once.

C#
using var context = new AppDbContext();

context.Products.AddRange(GetProducts());

await context.SaveChangesAsync();

EF Core will batch multiple INSERT statements together into fewer round trips instead of one round trip per row. It’s still going through the change tracker, and it’s still generating standard INSERT statements — just far fewer round trips than before. This is usually the first fix worth trying, since it requires no new dependencies and works with a DbContext you already have.

Rule of thumb: for datasets under a few thousand rows, this is often good enough. You don’t need a specialized library for every bulk operation — reach for one when AddRange genuinely stops being sufficient.

Option 3: Bulk Insert with Dapper in C#

Dapper skips EF Core’s change tracker and query pipeline entirely. You write the SQL yourself, and Dapper maps a collection of objects onto the parameters.

C#
using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();

const string sql = """
    INSERT INTO Products (Sku, Name, Price, StockQuantity)
    VALUES (@Sku, @Name, @Price, @StockQuantity);
    """;

await connection.ExecuteAsync(sql, GetProducts());

Under the hood, Dapper still issues one parameterized INSERT per object — there’s no tracking overhead, but it isn’t a true bulk protocol either. It’s a solid middle ground: no ORM overhead, works with any ADO.NET provider, and is a natural fit if Dapper is already part of your stack. It just won’t match the raw throughput of a native bulk-copy approach for very large datasets.

Option 4: SqlBulkCopy – Native Bulk Insert for SQL Server in C#

This is where you stop sending individual INSERT statements altogether. SqlBulkCopy uses SQL Server’s native bulk-loading protocol — the same mechanism behind the BULK INSERT T-SQL command — to stream rows directly into a table with minimal logging and no per-row statement overhead.

C#
using var bulkCopy = new SqlBulkCopy(connectionString)
{
    DestinationTableName = "dbo.Products",
    BatchSize = 5000
};

bulkCopy.ColumnMappings.Add(nameof(Product.Sku), "Sku");
bulkCopy.ColumnMappings.Add(nameof(Product.Name), "Name");
bulkCopy.ColumnMappings.Add(nameof(Product.Price), "Price");
bulkCopy.ColumnMappings.Add(nameof(Product.StockQuantity), "StockQuantity");

await bulkCopy.WriteToServerAsync(BuildProductsTable(GetProducts()));

SqlBulkCopy works against a DataTable (or an IDataReader), not your entity classes directly, so you need a small mapping step:

C#
DataTable BuildProductsTable(IEnumerable<Product> products)
{
    var table = new DataTable();
    table.Columns.Add(nameof(Product.Sku), typeof(string));
    table.Columns.Add(nameof(Product.Name), typeof(string));
    table.Columns.Add(nameof(Product.Price), typeof(decimal));
    table.Columns.Add(nameof(Product.StockQuantity), typeof(int));

    foreach (var product in products)
    {
        table.Rows.Add(product.Sku, product.Name, product.Price, product.StockQuantity);
    }

    return table;
}

BatchSize controls how many rows are sent per network packet — tuning it matters more as your row count grows, since too small a batch adds overhead and too large a batch increases memory pressure and the risk of a long-running transaction. This is the option to reach for when you specifically need SQL Server and want the fastest possible load, and you’re comfortable with the extra setup.

Option 5: EFCore.BulkExtensions – Bulk Insert Library for EF Core

EFCore.BulkExtensions is an open-source library that adds BulkInsert, BulkUpdate, BulkDelete, and BulkMerge methods directly onto your existing DbContext.

C#
using var context = new AppDbContext();

await context.BulkInsertAsync(GetProducts());

It bypasses EF Core’s change tracker under the hood while still using your existing entity model and DbContext, and it supports multiple database providers, not just SQL Server. This is the option worth reaching for when you want most of the speed of a native bulk protocol but don’t want to give up the convenience of working through EF Core — you don’t need to hand-build a DataTable, and your entity configuration (column mappings, conventions) is reused automatically.

EF Core performance optimization sponsor banner showing bulk insert, update, delete, and merge features with 14x faster data operations.

Option 6: Entity Framework Extensions – Commercial EF Core Bulk Insert Library

Entity Framework Extensions is a commercial library that offers a similar API to EFCore.BulkExtensions, with an optimized bulk insert method:

C#
using var context = new AppDbContext();

await context.BulkInsertOptimizedAsync(GetProducts());

It’s built and maintained specifically around performance, and it’s worth evaluating if your team is open to a commercial license and wants a supported product rather than a community-maintained open-source library. Functionally, it sits in the same category as EFCore.BulkExtensions — an EF Core–integrated wrapper around a bulk-loading strategy — but comes with a different support and licensing model.

Comparing the Options

Not every project needs the fastest possible option — sometimes the simplest one that works is the right call. Here’s how they stack up structurally:

MethodEF Core IntegrationBypasses Change TrackingDatabase SupportLicense
Naive loop (Add + SaveChanges)FullNoAll EF providersFree
AddRange + single SaveChangesFullNoAll EF providersFree
Dapper ExecuteAsyncNoneN/A (no tracking to begin with)Any ADO.NET providerFree
SqlBulkCopyNoneN/ASQL Server onlyFree
EFCore.BulkExtensionsPartialYesMultiple providersFree (community)
Entity Framework ExtensionsPartialYesMultiple providersCommercial

Summary

There’s no single “correct” way to bulk insert in .NET — there’s a spectrum, and where you land on it depends on your dataset size, your database, and how much you value staying inside EF Core’s programming model. Start with AddRange when you can, since it costs nothing and works everywhere. Reach for Dapper if it’s already part of your stack. And when you genuinely need to move from correctness-first to speed-first, SqlBulkCopy, EFCore.BulkExtensions, and Entity Framework Extensions are all solid, production-proven ways to get there — pick based on database support, licensing, and how tightly you want it wired into your existing DbContext.

This article is sponsored by ZZZ Projects.

Thousands of developers fixed EF Core performance — with one library: Entity Framework Extensions.

👉 Insert data 14x faster with Bulk Insert

Found this article useful? Share it with your network and spark a conversation.