EfCore.BulkOperations
2.0.0
dotnet add package EfCore.BulkOperations --version 2.0.0
NuGet\Install-Package EfCore.BulkOperations -Version 2.0.0
<PackageReference Include="EfCore.BulkOperations" Version="2.0.0" />
<PackageVersion Include="EfCore.BulkOperations" Version="2.0.0" />
<PackageReference Include="EfCore.BulkOperations" />
paket add EfCore.BulkOperations --version 2.0.0
#r "nuget: EfCore.BulkOperations, 2.0.0"
#:package EfCore.BulkOperations@2.0.0
#addin nuget:?package=EfCore.BulkOperations&version=2.0.0
#tool nuget:?package=EfCore.BulkOperations&version=2.0.0
EfCore.BulkOperations
BulkInsertAsync, BulkUpdateAsync, BulkDeleteAsync and BulkMergeAsync on your DbContext.
Each one builds a single parameterised statement from EF Core's own model metadata and sends it
straight to the connection, instead of going through the change tracker.
MySQL and MariaDB only. Identifiers are backtick-quoted in every statement, update and delete
use MySQL's multi-table INNER JOIN form, and BulkMergeAsync is built on
ON DUPLICATE KEY UPDATE. None of the four is portable to SQL Server or PostgreSQL.
Source and full benchmark · NuGet
Example
Every call takes a list and returns the number of rows affected. The examples below share one list:
var products = new List<Product> { new("Product1", 100m) };
Bulk Insert
var rowsAffected = await dbContext.BulkInsertAsync(products);
await dbContext.BulkInsertAsync(products, option =>
{
option.BatchSize = 1000;
option.CommandTimeout = 120;
option.IgnoreOnInsert = x => new { x.CreatedAt };
});
Bulk Update
await dbContext.BulkUpdateAsync(products);
// Leave a column as the database has it
await dbContext.BulkUpdateAsync(products, option => option.IgnoreOnUpdate = x => new { x.CreatedAt });
// Match rows on something other than the unique index found in the model
await dbContext.BulkUpdateAsync(products, option => option.UniqueKeys = x => new { x.Id });
Bulk Delete
await dbContext.BulkDeleteAsync(products);
await dbContext.BulkDeleteAsync(products, option => option.UniqueKeys = x => new { x.Id });
Bulk Merge
Insert rows that are new and update the ones that are not, in one statement.
await dbContext.BulkMergeAsync(products);
The number returned follows MySQL's rule for ON DUPLICATE KEY UPDATE: 1 for a row that was
inserted and 2 for one that was updated, so it is not the number of rows in the list. A row that
matched but already had these values counts 1 with MySqlConnector's default UseAffectedRows=false
(the connection reports matched rows), and 0 if the connection string sets UseAffectedRows=true.
await dbContext.BulkMergeAsync(products, option =>
{
option.IgnoreOnInsert = x => new { x.CreatedAt };
option.IgnoreOnUpdate = x => new { x.CreatedAt };
});
Options
| Option | Default | What it does |
|---|---|---|
BatchSize |
500 |
Rows per statement. See Batch size for how the number was chosen. |
CommandTimeout |
the provider's | Seconds before the command is abandoned. |
UniqueKeys |
the unique index in the model, or else its primary key | Which columns update and delete match rows on. Only x => x.Prop and x => new { x.A, x.B } are accepted; anything else throws. |
IgnoreOnInsert |
none | Columns to leave out of an insert, so the database's own default applies. |
IgnoreOnUpdate |
none | Columns to leave untouched by an update. |
SortByKeys |
true |
Order rows by their keys before sending. Worth several times the speed on a large write — see Why the rows are sorted. Turn it off only if the rows must arrive in the order given. |
Sharing one transaction
Each call runs in its own transaction unless you hand it one. A transaction you pass in stays yours:
the library will not commit it, roll it back, or close the connection. The same applies to a
transaction the context already has open through Database.BeginTransactionAsync() - it is picked
up automatically - and to a connection you opened yourself, which is left open.
Entities with a shadow property (a foreign key EF Core added for a navigation, a TPH discriminator,
or one declared with Property<T>("Name")) are rejected with NotSupportedException: there is no
CLR property to read the value from.
var transaction = await dbContext.BeginTransactionAsync();
try
{
await dbContext.Products.AddAsync(product);
await dbContext.SaveChangesAsync();
await dbContext.BulkInsertAsync(orders, null, transaction);
await dbContext.BulkInsertAsync(logs, null, transaction);
await dbContext.CommitAsync();
}
catch
{
await dbContext.RollbackAsync();
throw;
}
Benchmark
Against plain EF Core on the same data, schema and MySQL instance, at MySQL's default settings. Ratio is this library's mean divided by EF Core's, so 0.26 means it took 26% of the time.
| Operation | 1,000 | 10,000 | 100,000 | 1,000,000 |
|---|---|---|---|---|
| Delete | 0.18 | 0.16 | 0.18 | 0.33 |
| Update | 0.26 | 0.34 | 0.29 | 0.26 |
| Insert | 0.47 | 0.59 | 0.39 | 0.34 |
| Merge | 0.70 | 0.89 | 0.86 | 0.70 |
Allocation is lower everywhere too, most of all on delete, which sends only the key columns: 86 MB against EF Core's 576 MB at a hundred thousand rows.
Merge is the narrowest margin because EF Core's baseline for it is already the fastest thing EF Core can do - look the rows up in chunks, then update or add each one.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Microsoft.EntityFrameworkCore (>= 8.0.30)
- Microsoft.EntityFrameworkCore.Relational (>= 8.0.30)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
performance improve