Zack.EFCore.BatchInsert: bulk inserts that skip the change tracker
Deleting or Updating multiple records from a LINQ Query in a SQL statement without loading entities
At a glance
- What is it?
- Zack.EFCore.BatchInsert is a set of Entity Framework Core provider packages that insert many rows in one go by generating bulk load SQL directly against the database, without going through SaveChanges. It supports SQL Server, MySQL, PostgreSQL, Oracle and DM, and it is explicit about the two consequences of bypassing the change tracker.
- Who is it for?
- Zack.EFCore.BatchInsert fits a team that seeds or imports large volumes of rows and has measured the change tracker to be the bottleneck, since the insert path is a single bulk load rather than one statement per row.
- Can I use it commercially?
- Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
- Is it still maintained?
- Yes. The repository last received commits 151 days ago.
- What is it written in?
- Mainly C#, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the library no longer does, and why that matters
The most important thing to understand about this repository is what it stopped doing. The README opens with a notice for users of the old Zack.EFCore.Batch packages, explaining that since .NET 7 Entity Framework Core has built-in support for batch delete and update through ExecuteDelete and ExecuteUpdate, and therefore the library no longer supports DeleteRangeAsync or BatchUpdate, and that Microsoft's official APIs should be used instead. The legacy packages remain on NuGet but are no longer maintained, and the legacy code lives on a backup branch whose name records exactly when the removal happened. The surviving feature is BulkInsert, and the package names changed to match, so the packages are now called Zack.EFCore.BatchInsert.* while the old Zack.EFCore.Batch.* packages for .NET 5 and 6 are still on NuGet but frozen. The description at the top of the repository still says deleting or updating multiple records from a LINQ query in a SQL statement without loading entities, which is a description of the library's history rather than of its current surface.
One package per database provider, one version for all .NET releases
The installation is a single package per database, and the version support is handled inside it. The README says the library supports .NET 5, 6, 7, 8, 9 and 10, and that a single NuGet package covers all supported versions, with no need to install different packages per .NET version. The provider packages are listed with their install commands, one per database, and a further package covers the DM database:
SQLServer: Install-Package Zack.EFCore.BatchInsert.MSSQL
MySQL: Install-Package Zack.EFCore.BatchInsert.MySQL.Pomelo
Postgresql: Install-Package Zack.EFCore.BatchInsert.Npgsql
Oracle: Install-Package Zack.EFCore.BatchInsert.OracleSupport of MySQL is based on Pomelo.EntityFrameworkCore.MySql, which is worth noting because it means the MySQL path is not built on the Microsoft MySQL provider and inherits that provider's conventions. There is no configuration step, which is the second step in the instructions: after installing the provider package you can call BulkInsert or BulkInsertAsync directly. The repository layout mirrors the package split, with a directory for each provider including a Dm one, a shared Zack.EFCore.BatchInsert directory, a Demo directory with one project per database, a Tests directory, and a solution file.
The API, and the MySQL local infile caveat
Using the library is a bulk insert of a list of entities. The example builds a list of a hundred books and calls the insert inside a using block over the DbContext:
List<Book> books = new List<Book>();
for (int i = 0; i < 100; i++)
{
books.Add(new Book { AuthorName = "abc" + i, Price = new Random().NextDouble(), PubTime = DateTime.Now, Title = Guid.NewGuid().ToString() });
}
using (TestDbContext ctx = new TestDbContext())
{
ctx.BulkInsert(books);
}There is one MySQL specific requirement, and it is a server side setting rather than a code change. To use BulkInsert on MySQL you have to enable local_infile on both the server and the client side, setting local_infile=ON on the MySQL server and adding AllowLoadLocalInfile=true to the connection string on the client side. That is a consequence of the mechanism, since MySQL's bulk load path is the local infile protocol, and it means the feature is off by default in a stock MySQL installation. Everything else is provider agnostic, and the README lists the databases that have been tested as MS SQLServer, MySQL through Pomelo, PostgreSQL through Npgsql and Oracle, with DM listed among the install options.
What bypassing the change tracker costs you
This section of the README is the reason to read the documentation, and it is unusually direct. BulkInsert and BulkInsertAsync bypass EF Core's change tracker, generating bulk load SQL commands and executing them directly against the database without going through the SaveChanges pipeline, and that has two consequences. First, database generated values are not written back to entities. Auto-increment primary keys, server-side defaults and computed columns remain at their original in-memory values after the call, and the example given is that a database-generated integer id will still be 0 on every entity object even after a successful insert. Second, the DbContext cache is unaware of the inserted rows. EF Core maintains an internal identity map, and because the insert does not go through SaveChanges the rows are never registered in it, so a Find or a LINQ query against the same DbContext immediately afterwards may return stale results or miss the new rows entirely.
The two ways to read back what you just wrote
The README gives a choice between two patterns for getting correct data after a bulk insert. The first is to use a new DbContext, which is described as the simplest option, creating a fresh context for the subsequent queries so that the identity cache cannot be stale. The second is to re-query with the same DbContext, using AsNoTracking so that EF Core reads fresh data from the database rather than returning cached entities:
using (var ctx = new TestDbContext())
{
ctx.BulkInsert(books);
// Re-query to get the up-to-date data including generated values
var inserted = ctx.Books.AsNoTracking().ToList();
}Neither pattern gives you the generated key values on the entities you inserted, and that is a separate limitation from the caching one. The note explains that AsNoTracking bypasses the identity cache so EF Core reads fresh data from the database instead of returning cached entities. So if your workflow needs the identity back, you need to read it, and the library will not put it there for you.
How the package is tested and released
The CI setup is more thorough than most libraries of this size manage, and it is the part of the README that tells you what is actually supported. Pull requests run real-database integration tests for SQL Server, MySQL, PostgreSQL and Oracle using GitHub Actions, in a workflow file named pr-integration.yml, with the connection strings supplied through environment variables named TEST_DB_SQLSERVER_CS, TEST_DB_MYSQL_CS, TEST_DB_PG_CS and TEST_DB_ORACLE_CS. For databases that are not supported, the fallback behaviour of AddRange plus SaveChanges is covered by tests in the BulkInsertExecutorRoutingTests file under the test project, using SQLite. DM integration tests are not part of the pull request workflow yet, which is a useful signal about where the confidence is. Publishing to NuGet is automated by a workflow named publish-nuget.yml, and the release instructions, whether triggered by a git tag or from the GitHub Actions interface, are documented in a separate release document at the root of the repository.
Where this is the wrong choice today
Three limits are worth naming. First, if you are on EF Core 7 or later and your problem was deleting or updating many rows, this library no longer helps, because the built-in ExecuteDelete and ExecuteUpdate do that work and the maintainer says so plainly. The only remaining value is the insert path, and even that only pays off when you are inserting in bulk rather than adding a handful of rows to a tracked graph. Second, the change tracker bypass is not a detail, it is the defining behaviour, and it means the library is a poor fit for any workflow that depends on the context knowing what is in the database. Third, the licence. The project is GPL-3.0, which is a strong copyleft, and for a library that is embedded in an application this is a decision your legal team will want to look at, particularly since the alternatives it points you to for delete and update are the Microsoft ones. And there is a smaller point about the DM support, which has an install package but no integration tests in the pull request workflow, so it is the provider to try last.
Editorial conclusion
Zack.EFCore.BatchInsert fits a team that seeds or imports large volumes of rows and has measured the change tracker to be the bottleneck, since the insert path is a single bulk load rather than one statement per row. It is the wrong choice now that you have EF Core 7 or later, because the built in ExecuteDelete and ExecuteUpdate cover the delete and update cases the library used to provide, and it is the wrong choice if you need the database generated values written back onto your entities, since the change tracker is bypassed by design. Before you adopt it, read the section on entity state after BulkInsert, because the stale identity map is a bug waiting to happen, and check the provider package for your database. The last push was on 2026-05-25, the repository publishes no GitHub releases, and the licence is GPL-3.0.
Frequently asked questions
How do I install Zack.EFCore.BatchInsert for my database?
Install the provider package for your database with NuGet, for example `Install-Package Zack.EFCore.BatchInsert.MSSQL` for SQL Server, `Zack.EFCore.BatchInsert.MySQL.Pomelo` for MySQL, `Zack.EFCore.BatchInsert.Npgsql` for PostgreSQL, `Zack.EFCore.BatchInsert.Oracle` for Oracle or `Zack.EFCore.BatchInsert.Dm` for DM. One package covers .NET 5 through 10, and no extra configuration is required.
Does Zack.EFCore.BatchInsert still support batch delete and update?
No. Since .NET 7 EF Core has built-in ExecuteDelete and ExecuteUpdate, and the README states the library no longer supports DeleteRangeAsync or BatchUpdate, recommending Microsoft's APIs instead. The legacy packages remain on NuGet but are no longer maintained, and the legacy code is on a backup branch.
What happens to entity state after a BulkInsert?
Database generated values are not written back, so auto-increment keys, server-side defaults and computed columns keep their original in-memory values, and a database-generated id stays 0 on every entity. The DbContext identity map also does not know about the inserted rows, so a query on the same context can return stale results unless you use a new context or re-query with AsNoTracking.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/yangzhongke-zack-efcore-batch)Was this useful?