Skip to content

Upserts

Cratis.Arc.EntityFrameworkCore provides two upsert APIs on DbSet<TEntity>. Both match the complete primary key configured in the EF model.

await context.Items.Upsert(item) keeps the existing tracked behavior: it finds an existing entity, copies values into its tracked instance, or marks a new entity as added. Call SaveChangesAsync to persist it. This is a read followed by a write, not an atomic database upsert; concurrent inserts can still conflict.

await context.Items.UpsertAsync(item, cancellationToken) executes one parameterized SQL statement on supported relational mappings:

ProviderStatement
SQL ServerMERGE ... WITH (HOLDLOCK)
PostgreSQLINSERT ... ON CONFLICT ... DO UPDATE
SQLiteINSERT ... ON CONFLICT ... DO UPDATE

The primary-key match and write are atomic, including concurrent inserts of the same key. Retrying the same values updates that row rather than adding another. A key-only entity uses a no-op on conflict instead of an empty update clause. Deadlocks, unrelated unique constraints, and other database errors still propagate; this API does not introduce an automatic retry policy.

Table, schema, column, and key names come from EF metadata and are quoted by the provider. Values are parameters using EF’s relational type mappings, including configured value converters. Null values remain null. Native execution uses the context’s connection and current transaction and honors the cancellation token.

Native execution is immediate: do not call SaveChangesAsync to persist the upsert. It does not attach the supplied entity, reconcile already tracked instances, or refresh store-generated properties. Reload previously tracked instances before relying on their values. Pending tracked changes are not flushed.

Use application-assigned keys and single-table scalar mappings for native execution. Other providers, unset generated keys, SQL Server identity columns, navigations (including owned and many-to-many relationships), shadow or complex properties, inheritance, and multi-table mappings fall back to the tracked upsert and require SaveChangesAsync. Properties whose generated value is still unset also use the tracked fallback so EF can apply database defaults. The fallback does not provide the native path’s concurrency guarantee. Choose the tracked Upsert explicitly when consistent deferred persistence is required.