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.

Advanced120–155 minutesbulk-path decision + correctness labEF Core 10.0.11 · .NET 10.0.11SQLite provider 10.0.11 baselineSDK checkpoint: 10.0.400Last reviewed: August 2026

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.

01

Classify tracked batching, set-based DML, provider-native ingestion, and third-party bulk libraries by mechanism.

02

List correctness responsibilities that may be bypassed: generated identities, relationships, triggers, constraints, concurrency, interceptors, and tracker state.

03

Explain transaction/connection coordination when native provider APIs must participate in an EF unit of work.

04

Treat third-party licensing/support/provider matrices as deployment dependencies, not implementation details.

05

Choose a free/local learning path without requiring commercial tooling or cloud infrastructure.

06

Design a benchmark that validates result correctness before comparing throughput.

Reproducible baseline

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

csharp · optional SQL Server concept sketch
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.

Optional provider path

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

csharp · optional Npgsql binary COPY sketch
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

csharp · unsafe mixed write boundary
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.

No universal recommendation

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

  1. Generate 1,000 deterministic ServiceHub import rows in memory.
  2. Run bounded tracked AddRange + one SaveChangesAsync against SQLite; validate row count, unique work-order numbers, defaults, and relationships.
  3. For an existing-row mass change, use ExecuteUpdateAsync and validate rows affected and tracker boundary.
  4. Write a decision record describing when a native loader would be justified by insert volume and latency requirements.
  5. Optionally reproduce SQL Server SqlBulkCopy or PostgreSQL COPY locally; record provider/driver/database versions and transaction behavior.
  6. 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

  1. Is SaveChanges batching the same mechanism as SqlBulkCopy/COPY?
  2. Do native loaders automatically update EF ChangeTracker state?
  3. What must happen before throughput comparison?
  4. Why is licensing part of technical correctness?
  5. Can the mandatory lab require a commercial bulk package?
  6. 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

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.

\n