
Top 7 Options You Should Try in EF Core Bulk Operations
If you’ve already read The Complete Guide to Bulk Operations with Entity Framework Extensions, you know that Entity Framework Extensions transforms how EF Core handles large datasets — making bulk inserts, updates, and deletes incredibly fast and efficient.
But once you start pushing millions of records, performance alone isn’t enough. You need control — over how operations are executed, logged, retried, or isolated. That’s where Entity Framework Extensions’s bulk operation options come into play.
With EF Core 10 now available, these options give developers unmatched precision for optimizing bulk inserts, updates, and deletes — especially in distributed and high-volume systems.
Let’s explore the top 7 advanced options that every serious EF Core developer should know and use.
1. IncludeGraph — Insert or Update Entire Entity Graphs Automatically
When working with parent-child relationships in EF Core, saving multiple related entities often requires separate insert calls — first for the parent, then for each child.
The IncludeGraph option in Entity Framework Extensions automates this process by detecting the entire entity graph and executing all required inserts, updates, or merges in the correct order — all in one operation.
using Z.EntityFramework.Extensions;
var customers = new List<Customer>
{
new Customer
{
Name = "Oliver Smith",
Email = "oliver.smith@email.com",
Orders = new List<Order>
{
new Order
{
OrderDate = DateTime.UtcNow,
Items =
{
new OrderItem { Product = "Laptop", Quantity = 1, Price = 1500 },
new OrderItem { Product = "Headphones", Quantity = 2, Price = 120 }
}
}
}
}
};
// Insert customer, orders, and order items together
await context.BulkInsertAsync(customers, options =>
{
options.IncludeGraph = true;
});Entity Framework Extensions automatically inserts the Customer, then the Order, and finally the OrderItems — ensuring all relationships are correctly linked without manual handling.
Why it matters:
- Saves complete entity graphs with a single bulk call.
- Automatically maintains foreign key order.
- Greatly simplifies inserting or merging related entities.
Customizing Behavior with IncludeGraphOperationBuilder
When dealing with more complex graphs, you might need to fine-tune how each entity type is processed — for instance, using a custom merge key or skipping updates for specific types. That’s where IncludeGraphOperationBuilder comes in.
using Z.EntityFramework.Extensions;
context.BulkMerge(customers, options =>
{
options.IncludeGraph = true;
options.IncludeGraphOperationBuilder = operation =>
{
if (operation is BulkOperation<Customer> customerOp)
{
// Use Email as the primary key for merging
customerOp.ColumnPrimaryKeyExpression = x => x.Email;
}
else if (operation is BulkOperation<Order> orderOp)
{
// Keep order records read-only (don’t update)
orderOp.IsReadOnly = true;
}
};
});In this example:
- The Customer entity uses Email as the merge key instead of an ID.
- The Order entity is marked as read-only to prevent updates.
2. BatchSize — Fine-Tune How Data Is Sent to the Database
When working with large datasets, performance often depends on how data is sent to the database — not just how fast your code runs.
EF Extensions gives you three batch-level options to help fine-tune bulk operations:
- BatchSize – Number of rows processed per batch.
- BatchTimeout – Maximum time (in seconds) to wait before a batch is considered failed.
- BatchDelayInterval – Delay (in milliseconds) between batches to control database load.
Together, these settings let you optimize for both speed and stability depending on your environment.
BatchSize — Control How Many Rows Go per Round Trip
The BatchSize option defines how many records are sent to the database in one go. Once that limit is reached (or all records are processed), EF Extensions sends the batch for execution.
using Z.EntityFramework.Extensions;
List<Order> orders = GenerateOrders(200_000);
// Process 10,000 records per batch
await context.BulkInsertAsync(orders, options =>
{
options.BatchSize = 10_000;
});EF Extensions automatically splits your dataset into chunks and sends them sequentially — balancing performance and memory usage.
Default behavior: The default value is auto-selected based on the database provider and operation type.
For example:
- SQL Server / PostgreSQL: 10,000
- MySQL / MariaDB: 200
- Oracle: 2,000 (insert/delete), 200 (update/merge)
- SQLite: 1 (no batching)
When to adjust it:
- Too low: More round trips, slower overall execution.
- Too high: Risk of timeouts or high memory consumption.
BatchTimeout — Prevent Long-Running Operations from Failing
Large batches can sometimes take longer than expected — especially when dealing with indexes, triggers, or slower networks. The BatchTimeout option defines how long (in seconds) EF Extensions should wait before canceling the batch.
using Z.EntityFramework.Extensions;
// Allow up to 3 minutes per batch
context.BulkInsert(customers, options =>
{
options.BatchTimeout = 180;
});Default behavior: If you don’t specify a timeout, EF Extensions uses your EF Core context’s configured value — typically 30 seconds.
When to adjust it:
- When handling millions of records.
- When network or database performance fluctuates.
- During data migration or heavy ETL jobs.
BatchDelayInterval — Add a Pause Between Batches
The BatchDelayInterval option adds a small delay (in milliseconds) between each batch. It’s rarely needed, but it can help throttle load when the database server is under stress.
using Z.EntityFramework.Extensions;
// Add a 100 ms delay between each batch
context.BulkInsert(orders, options =>
{
options.BatchDelayInterval = 100;
});Default behavior: No delay (value = 0).
Note:
Avoid using delays inside a transaction — it keeps the transaction open longer, increases lock durations, and can lead to deadlocks. Use this only when performing non-transactional, background bulk jobs.
3. Audit — Capture and Log What Changed in Your Bulk Operations
In large-scale systems, visibility matters just as much as performance. The Audit option in Entity Framework Extensions allows you to capture exactly what data was inserted, updated, or deleted during any bulk operation — giving you a complete view of how your data changed.
This feature works across all bulk operations such as: BulkInsert, BulkUpdate, BulkDelete, BulkMerge, BulkSynchronize, and even BulkSaveChanges.
When enabled, EF Extensions records each affected row and its details — including the action performed, timestamp, table name, and column-level changes (old vs. new values).
You can then store this information in a database table, write it to a log file, or use it directly inside your application.
using Z.EntityFramework.Extensions;
// Define a list to capture audit data
List<AuditEntry> auditEntries = new();
// Perform a bulk merge and capture detailed audit information
await context.BulkMergeAsync(orders, options =>
{
options.UseAudit = true;
options.AuditEntries = auditEntries;
});In this example, the AuditEntries list will be populated automatically by EF Extensions — each AuditEntry representing a row and each AuditEntryItem representing a changed column.
Why it matters:
- Keeps a complete change history for compliance or debugging.
- Tracks who, what, and when each modification occurred.
- Helps you compare old vs. new values for validation or reporting.
4. Log — Capture Executed SQL and Execution Details
When optimizing or debugging bulk operations, it’s often useful to know exactly what SQL was executed, what parameters were passed, and how long each batch took.
The Log option in Entity Framework Extensions lets you capture this information in real time, so you can analyze or redirect it wherever you need.
You can use it in two ways:
- Capture log messages instantly with the
Logdelegate. - Collect all logs at once using the
LogDumpandUseLogDumpoptions.
Log
The simplest way to view SQL statements is to handle log messages as they are generated during a bulk operation. You can write them directly to the console, a file, or even send them to a third-party logger like Serilog, NLog, or Application Insights.
using Z.EntityFramework.Extensions;
// Example 1: Write SQL logs directly to the console
context.BulkMerge(customers, options =>
{
options.Log = Console.WriteLine;
});
// Example 2: Append all log messages to a StringBuilder
var sb = new StringBuilder();
context.BulkUpdate(orders, options =>
{
options.Log = message => sb.AppendLine(message);
});
Console.WriteLine(sb.ToString());Every log entry includes the SQL command being executed and contextual details — such as parameters, execution time, and progress.
When to use:
- While debugging query behavior or validating generated SQL.
- To integrate with structured logging tools (e.g., Serilog, Seq, Application Insights).
- To capture runtime performance metrics in background jobs.
LogDump + UseLogDump
If you prefer to review all logs after the operation finishes, you can use UseLogDump together with LogDump. This approach stores all log messages inside a StringBuilder, giving you a single combined output once the operation is complete.
using Z.EntityFramework.Extensions;
// Collect logs for analysis after the operation completes
var logBuilder = new StringBuilder();
context.BulkInsert(products, options =>
{
options.UseLogDump = true; // Enable log dumping
options.LogDump = logBuilder;
});
Console.WriteLine(logBuilder.ToString());When to use:
- When you want a complete log of the entire bulk operation.
- Ideal for debugging in non-production or test environments.
- Useful for storing logs alongside operation reports or audit records.
Logging gives developers visibility into what’s happening under the hood during bulk operations. With it, you can:
- Inspect the SQL generated by EF Extensions.
- Diagnose parameter or timeout issues quickly.
- Capture executed SQL for testing, auditing, or compliance.
In large data pipelines or enterprise workloads, this transparency is invaluable for both troubleshooting and long-term performance tuning.
5. Events — Customize Bulk Operations with Lifecycle Hooks
Bulk operations are often more than just inserts or updates — sometimes you need to run business logic before or after the data hits the database. The Events feature in Entity Framework Extensions gives you that flexibility by exposing lifecycle hooks you can plug into.
You can use these hooks to:
- Automatically set audit fields like
CreatedDateorModifiedDate. - Validate data before it’s saved.
- Adjust mappings or destination tables dynamically.
- Log results or clean up after an operation completes.
Think of events as checkpoints in the bulk operation pipeline — a place where you can intercept and customize behavior without repeating logic across your codebase.
Pre and Post Events
Each bulk operation (Insert, Update, Delete, Merge, Synchronize, etc.) has a Pre event that fires before execution, and a Post event that fires afterward.
These are the perfect spots to enforce consistency rules, set metadata, or perform post-processing tasks.
using Z.EntityFramework.Extensions;
// Automatically set CreatedDate before insert
EntityFrameworkManager.PreBulkInsert = (ctx, entities) =>
{
if (entities is IEnumerable<Customer> list)
{
foreach (var customer in list)
{
customer.CreatedDate = DateTime.UtcNow;
}
}
};
// Automatically set ModifiedDate before update
EntityFrameworkManager.PreBulkUpdate = (ctx, entities) =>
{
if (entities is IEnumerable<Customer> list)
{
foreach (var customer in list)
{
customer.ModifiedDate = DateTime.UtcNow;
}
}
};Common Use Cases:
- PreBulkInsert/PostBulkInsert – Initialize audit fields or log inserted rows.
- PreBulkUpdate/PostBulkUpdate – Set ModifiedDate, refresh cache, or trigger domain events.
- PreBulkDelete/PostBulkDelete – Implement soft deletes or archive removed data.
- PreBulkMerge/PostBulkMerge – Set both CreatedDate and ModifiedDate during merges.
- PreBulkSynchronize/PostBulkSynchronize – Track sync results or update audit fields.
Note:
When using IncludeGraph, pre/post events are only triggered for root entities, not for related entities included in the graph.
PostConfiguration
The PostConfiguration event runs right before the bulk operation executes — after all your options are configured but before the SQL is generated.
It’s ideal for last-minute adjustments like enabling logging, applying filters, or modifying column mappings dynamically.
using Z.EntityFramework.Extensions;
context.BulkInsert(products, options =>
{
options.PostConfiguration = config =>
{
var column = config.ColumnMappings
.Find(x => x.SourceName == "Description");
column.FormulaInsert = "'N/A'";
};
});When to use:
- To apply a formula or computed value right before the bulk SQL runs.
- To dynamically enable or disable options at runtime.
BulkOperationExecuting and BulkOperationExecuted
These two events run for each internal operation within a bulk call — for example, when a single bulk method performs both an insert and an update phase. They’re great for adjusting in-memory data or capturing precise operation-level logs.
using Z.EntityFramework.Extensions;
context.BulkSaveChanges(options =>
{
options.BulkOperationExecuting = op =>
{
Console.WriteLine("Bulk operation starting...");
};
options.BulkOperationExecuted = op =>
{
Console.WriteLine("Bulk operation completed successfully.");
};
});When to use:
- To adjust values just before execution.
- To collect operation-level performance metrics or logs.
Why Events Are Useful
Using events in Entity Framework Extensions helps you centralize business logic that would otherwise be scattered across repositories or services.
With events, you can:
- Automatically manage audit fields (
CreatedDate,ModifiedDate,IsDeleted). - Enforce validation and consistency rules before saving.
- Add flexible post-processing without modifying core data logic.
- Keep bulk operations clean, reusable, and easier to maintain.
6. Temporary Table — Control How Staging Works
Temporary tables act as a staging area so large writes can be applied quickly and safely to your real tables. With Entity Framework Extensions, you can steer how that staging table is named, created, batched, and locked—useful for tuning performance and for debugging.
What you can control (at a glance)
- Naming & Schema
- TemporaryTableName, TemporaryTableSchemaName
- TemporaryTableUseSameName (stable, predictable names)
- Inspect with ResolvedTemporaryTableName
- Creation & Lifecycle
- TemporaryTableCreate, TemporaryTableCreateAndDrop
- TemporaryTablePersist (keep table/data for inspection within the open connection)
- TemporaryTableIsMemory (memory-optimized staging, if supported)
- UsePermanentTable (persist across sessions—remember to drop later)
- Performance & Batching
- TemporaryTableInsertBatchSize (rows per insert into the staging table)
- TemporaryTableMinRecord (skip staging for tiny sets)
- DisableTemporaryTableClusteredIndex (faster raw insert; slower merge)
- Locking & Concurrency
- TemporaryTableUseTableLock (default true; faster inserts, higher exclusivity)
7. Transaction — Handle Bulk Operations Safely
Transactions ensure that all your database changes either succeed together or fail together. When working with bulk operations, understanding how Entity Framework Extensions manages transactions is key to keeping your data consistent.
BulkSaveChanges
Just like the standard SaveChanges method, BulkSaveChanges automatically runs inside an internal transaction. That means you don’t need to manually begin or commit one — it’s handled for you.
However, if you’ve already started a transaction in your EF Core context, BulkSaveChanges will detect and use it, instead of creating its own.
using Z.EntityFramework.Extensions;
using var transaction = context.Database.BeginTransaction();
try
{
context.BulkSaveChanges(); // Uses the existing transaction
transaction.Commit();
}
catch
{
transaction.Rollback();
}Key takeaway:
- BulkSaveChanges always runs safely inside a transaction.
- If one already exists, it participates in that transaction automatically.
Bulk Operations
Bulk operations like BulkInsert, BulkUpdate, BulkDelete, and BulkMerge don’t automatically create a transaction. This gives you more control — for example, when you want multiple operations to be grouped together as a single atomic unit.
If you want all operations to either succeed or fail as one, you can wrap them inside your own EF Core transaction.
using Z.EntityFramework.Extensions;
using var transaction = context.Database.BeginTransaction();
try
{
context.BulkInsert(customers);
context.BulkUpdate(orders);
context.BulkDelete(expiredCoupons);
transaction.Commit();
}
catch
{
transaction.Rollback();
}Best practice:
- Use explicit transactions when performing multiple bulk operations together.
- Keep transactions as short as possible to minimize lock durations.
Summary
Bulk operations are one of the best ways to scale data-heavy workloads in EF Core. By using Entity Framework Extensions, you gain fine-grained control over how data is processed, logged, audited, and optimized — far beyond what EF Core provides by default.
In this article, we explored the top seven options that help you tune both performance and reliability:
- IncludeGraph – Automatically handle related entities during inserts or merges.
- Batch Options – Balance speed and stability with
BatchSize,BatchTimeout, andBatchDelayInterval. - Log – Capture executed SQL, parameters, and performance details for debugging or auditing.
- Audit – Track what changed, including old and new values for each affected record.
- Events – Hook into the bulk operation lifecycle to set audit fields, validate data, or trigger custom logic.
- Temporary Table – Control how staging tables are created, named, and persisted.
- Transaction – Ensure atomic and consistent updates across all your bulk operations.
Together, these options give you the flexibility and power to handle large-scale data scenarios efficiently — whether it’s data migration, synchronization, or background imports.
