Chapter 12 · Saving Data: Insert, Update, Delete, Batching, ExecuteUpdate, and ExecuteDelete
Bulk-Operation Boundaries: EF APIs, Third-Party Libraries, COPY/BulkCopy, and Correctness
Choose between tracked EF writes, set-based EF DML, provider-native loaders, and third-party bulk libraries by correctness, transaction, identity, trigger, licensing, and provider evidence.
Learning outcomes
“Bulk” is overloaded. A tracked SaveChanges batch,
one set-based UPDATE, SQL Server
SqlBulkCopy, PostgreSQL COPY, and a
third-party EF bulk library solve different problems and
preserve different semantics. This lesson builds a decision
contract before choosing the fastest-looking API.
Classify tracked batching, set-based DML, provider-native ingestion, and third-party bulk libraries by mechanism.
List correctness responsibilities that may be bypassed: generated identities, relationships, triggers, constraints, concurrency, interceptors, and tracker state.
Explain transaction/connection coordination when native provider APIs must participate in an EF unit of work.
Treat third-party licensing/support/provider matrices as deployment dependencies, not implementation details.
Choose a free/local learning path without requiring commercial tooling or cloud infrastructure.
Design a benchmark that validates result correctness before comparing throughput.
Mandatory labs use .NET SDK 10.0.400, .NET runtime 10.0.11, Microsoft.EntityFrameworkCore/SQLite 10.0.11, dotnet-ef 10.0.11, the disposable servicehub-lab.db, deterministic ServiceHub seed data, and no paid tooling. SQL Server/PostgreSQL native-loader examples are optional provider comparisons. EF Core 11 previews are excluded from the mandatory path.
1. Four mechanisms called “bulk”
| Mechanism | Best fit | EF tracker | Typical correctness responsibility |
|---|---|---|---|
SaveChanges batching |
Hundreds/bounded aggregate writes with domain logic. | Yes | EF state, relationships, concurrency, interceptors participate. |
ExecuteUpdate/Delete |
Set-based modification of existing rows. | No synchronization | Application supplies predicate/concurrency and reload boundary. |
| Provider-native loader | High-volume inserts/imports. | Usually bypassed | Map columns, generated keys, constraints/triggers, transaction, error recovery. |
| Third-party bulk library | Provider-specific convenience/performance features. | Library-specific | Verify semantics, version/provider support, licensing, tracker behavior. |
2. SQL Server: SqlBulkCopy is a provider-native ingestion path
await using var connection = new SqlConnection(connectionString);await connection.OpenAsync(ct);await using var bulk = new SqlBulkCopy(connection){ DestinationTableName = "dbo.WorkOrders"};bulk.ColumnMappings.Add("WorkOrderNumber", "work_order_number");bulk.ColumnMappings.Add("Summary", "summary");await bulk.WriteToServerAsync(reader, ct);
This is not an EF insert pipeline. Unless separately coordinated, it does not create tracked entities, call ServiceHub domain methods, invoke EF SaveChanges interceptors, or synchronize generated keys into existing CLR objects. Database constraints/triggers still execute according to SQL Server configuration; options can also change trigger/check behavior and must be reviewed.
SQL Server Developer/Express/container is a free local learning option when licensing/platform requirements permit. It is not mandatory for this chapter.
3. PostgreSQL: COPY is a protocol, not an EF SaveChanges optimization
await using var conn = new NpgsqlConnection(connectionString);await conn.OpenAsync(ct);await using var writer = await conn.BeginBinaryImportAsync( "COPY work_orders (work_order_number, summary) FROM STDIN (FORMAT BINARY)", ct);foreach (var row in rows){ await writer.StartRowAsync(ct); await writer.WriteAsync(row.Number, cancellationToken: ct); await writer.WriteAsync(row.Summary, cancellationToken: ct);}await writer.CompleteAsync(ct);
PostgreSQL COPY has its own type mappings, failure/transaction behavior, and Npgsql API/version requirements. If the production system uses Npgsql, verify the current provider/driver documentation at adoption time; this chapter does not freeze a third-party provider version into the mandatory SQLite lab.
4. Correctness checklist before throughput
| Question | Why it can change the result |
|---|---|
| Who generates primary keys? | Native loaders may not return/generated identities in the same shape as EF. |
| How are parent/child FKs produced? | Loading tables in the wrong order or without key maps can break relationships. |
| Do triggers/defaults/computed columns run? | Some APIs/options can change behavior; returned values may not enter CLR instances. |
| What validates invariants? | Bypassing domain methods means the database/import validation must enforce required rules. |
| What transaction owns the operation? | A loader on another connection is not automatically atomic with EF writes. |
| What happens on retry? | Ambiguous/partial ingestion can duplicate rows without idempotent keys. |
| Does the tracker know? | Usually not; a long-lived context can remain stale. |
| What is the license/support matrix? | Commercial libraries can impose runtime/deployment terms and provider/version constraints. |
5. Deliberately wrong: native insert plus tracked SaveChanges on stale state
var tracked = await db.WorkOrders.SingleAsync(w => w.Id == id, ct);await RunNativeLoaderOnAnotherConnectionAsync(rows, ct);// The DbContext does not automatically know what the loader inserted/changed.tracked.ReviseSummary("Continue as if the external write were part of this unit of work");await db.SaveChangesAsync(ct);
The loader may have used another connection/transaction and may have changed constraints or rows the context has already observed. Repair by treating the native load as its own bounded operation, verifying it, and starting a fresh EF unit of work; or deliberately share connection/transaction only where the provider APIs support it and test the failure semantics. Chapter 14 covers cross-context/ADO.NET transaction coordination.
6. Third-party bulk libraries: evaluate, do not bless
A library may offer bulk insert/update/merge APIs, identity propagation, graph support, or provider-specific optimizations. Those are attractive, but the acceptance checklist includes the exact library version, EF Core major/patch support, provider/database support, licensing for development/CI/production, trigger/constraint behavior, transaction/retry semantics, tracker synchronization, concurrency handling, and maintenance cadence. The mandatory course path requires none of them.
Do not infer that a package is safe because it exposes an EF-like API or restores against EF Core 10. Run provider-specific integration tests and review the current license/support policy before adoption.
7. Free/local lab: decide the path before installing anything
- Generate 1,000 deterministic ServiceHub import rows in memory.
-
Run bounded tracked
AddRange+ oneSaveChangesAsyncagainst SQLite; validate row count, unique work-order numbers, defaults, and relationships. -
For an existing-row mass change, use
ExecuteUpdateAsyncand validate rows affected and tracker boundary. - Write a decision record describing when a native loader would be justified by insert volume and latency requirements.
-
Optionally reproduce SQL Server
SqlBulkCopyor PostgreSQLCOPYlocally; record provider/driver/database versions and transaction behavior. - For any third-party library considered, record license, EF10/provider matrix, generated-key handling, triggers/constraints, transaction/retry, and tracker semantics before measuring speed.
Check your understanding
- Is SaveChanges batching the same mechanism as SqlBulkCopy/COPY?
- Do native loaders automatically update EF ChangeTracker state?
- What must happen before throughput comparison?
- Why is licensing part of technical correctness?
- Can the mandatory lab require a commercial bulk package?
- When should a fresh DbContext follow native ingestion?
Review the answers
No. SaveChanges batches EF modification commands; native loaders use provider/database ingestion APIs/protocols.
No; treat tracker synchronization as an explicit boundary.
Validate row counts, keys, relationships, constraints, triggers/defaults, and transaction/failure behavior.
A library can be technically compatible yet unusable in CI/production under its license/support terms.
No. The course keeps a free/local path.
Normally whenever the loader bypassed the tracked unit of work, so subsequent EF reads/writes start from current database state.
8. Production judgment and bridge
Pick the lowest-level mechanism only when workload evidence justifies owning its additional correctness surface. Tracked SaveChanges is often fast enough and preserves rich semantics; set-based DML is ideal for many update/delete operations; native loaders excel at ingestion but move responsibility downward. Lesson 5 returns to the tracked write pipeline and asks where cross-cutting audit/domain-event/outbox behavior belongs.
Authoritative references
- Saving Data - EF Core — tracked versus direct write mechanisms.
- Efficient Updating - EF Core — batching and set-based updates.
- SqlBulkCopy API — SQL Server native bulk-copy mechanism and options.
- Npgsql COPY — PostgreSQL COPY API and binary import/export.
- Using Transactions - EF Core — transaction coordination concepts relevant to mixed EF/ADO.NET work.