Table of Contents

Class DbContextIdempotencyExtensions

Namespace
Trellis.EntityFrameworkCore
Assembly
Trellis.EntityFrameworkCore.dll

Idempotency helpers for DbContext that convert provider-level unique-constraint violations into Result<TValue> failures with a structured Error.Conflict payload, so callers can express "insert this row unless it already exists" without having to write provider-specific exception handling at each call site.

public static class DbContextIdempotencyExtensions
Inheritance
DbContextIdempotencyExtensions
Inherited Members

Methods

TryInsertUniqueAsync<TEntity>(DbContext, TEntity, CancellationToken)

Adds entity to context, persists the change via SaveChangesAsync(CancellationToken), and converts a duplicate-key violation into Result.Fail(new Error.Conflict(Resource: null, Code: FaultCodes.DuplicateKey)). Use this helper to implement idempotent inserts on a unique constraint without catching DbUpdateException at the call site.

public static Task<Result<TEntity>> TryInsertUniqueAsync<TEntity>(this DbContext context, TEntity entity, CancellationToken cancellationToken = default) where TEntity : class

Parameters

context DbContext

The DbContext to add the entity to.

entity TEntity

The entity instance to insert.

cancellationToken CancellationToken

A token to observe while waiting for the save.

Returns

Task<Result<TEntity>>

On success: Ok<TValue>(TValue) wrapping the same entity instance that was passed in (EF Core populates generated values such as primary keys, row versions, and timestamps in-place on that instance).

On a unique-constraint violation: Fail<TValue>(Error) containing an Error.Conflict with Code = DuplicateKey and the provider-reported ConstraintName and ConstraintTableName populated on a best-effort basis (see ExtractConstraintIdentity(DbUpdateException)). On this path the change tracker is left exactly as the caller saw it on entry: every entry this call newly attached as Added (the root plus any owned / dependent / join entries reached through the navigation graph) is detached, and any already-tracked entry that Add(object) flipped to Added (e.g., an entity the context loaded earlier) is restored to its prior state.

Type Parameters

TEntity

The reference-type entity to insert.

Remarks

Exception propagation. Only DbUpdateException with a duplicate-key inner exception is converted. All other exceptions — DbUpdateConcurrencyException, foreign-key violations, other DbUpdateException variants, connection/timeout exceptions, and OperationCanceledException — propagate to the caller so that global handlers and retry policies see them. When OperationCanceledException is raised from SaveChangesAsync(CancellationToken) (the cancellationToken tripped mid-save) the helper still performs the same change-tracker cleanup it performs on the duplicate-key path before rethrowing, so a cancelled call leaves the change tracker exactly as the caller saw it on entry — a later SaveChangesAsync(CancellationToken) on the same DI-scoped context will not silently insert the cancelled entity, and the clean-context guard will not block a retry on the same context.

Clean-context requirement. The helper requires the DbContext to have no pending changes before the call (ChangeTracker.HasChanges() must be false). If the context already has tracked changes, the helper throws InvalidOperationException because a duplicate-key violation surfaced by SaveChangesAsync(CancellationToken) cannot be unambiguously attributed to the entity being inserted — it could belong to one of the pre-existing pending changes. Call SaveChangesAsync(CancellationToken) first, use a fresh context, or use the lower-level SaveChangesResultAsync(DbContext, CancellationToken) helper if you intentionally want to flush mixed changes.

Telemetry safety. The Error.Conflict.Detail string is a safe generic message suitable for API responses. The constraint identity fields are marked JsonIgnoreAttribute on Error.Conflict and are intended for structured logging only.

Exceptions

ArgumentNullException

Thrown when context or entity is null.

InvalidOperationException

Thrown when context already has pending changes (HasChanges() is true) before the call.