Add, Update, and Delete¶
SaveChangesAsync reads the EF Core change tracker, compiles each pending entity state change into a PartiQL statement, and sends the write to DynamoDB — only modified properties are included in UPDATE statements, and UPDATE/DELETE statements target the item's primary key.
Async only
The DynamoDB SDK does not expose synchronous write APIs. SaveChanges (synchronous) always
throws NotSupportedException. Always use SaveChangesAsync.
How Writes Are Compiled¶
When you call SaveChangesAsync, the provider runs a two-stage pipeline before any network call
is made:
DynamoSaveChangesPlannerwalks the change tracker and compiles each pending entity into a PartiQL statement and a parameter list. Statements are validated at this stage — if a statement exceeds the 8,192-byte limit, an exception is thrown before any write is attempted.DynamoWriteExecutorsends the compiled statements to DynamoDB via theExecuteStatement,ExecuteTransaction, orBatchExecuteStatementAPIs depending on the number of operations and the configured transaction behavior (see Transactions).
Adding Entities¶
Add an entity to a DbSet and call SaveChangesAsync. The entity must have all primary key
properties populated; if you are using GeneratedKeyProperties, the provider assigns values
before saving.
var order = new Order
{
Pk = "CUSTOMER#42",
Sk = "ORDER#2026-001",
Status = "pending",
Total = 149.99m,
};
db.Orders.Add(order);
await db.SaveChangesAsync(cancellationToken);
The provider generates a PartiQL INSERT INTO … VALUE {…} statement. All mapped scalar
properties are included in the VALUE clause as positional parameters:
INSERT statements are unconditional — there is no existence check. If an item with the same
partition key and sort key already exists in the table, DynamoDB raises an error that the
provider maps to DbUpdateException:
try
{
db.Orders.Add(new Order { Pk = "CUSTOMER#42", Sk = "ORDER#2026-001", ... });
await db.SaveChangesAsync(cancellationToken);
}
catch (DbUpdateException ex)
{
// Item with this PK+SK already exists.
// ex.InnerException is DuplicateItemException.
}
Note
DbUpdateException (not DbUpdateConcurrencyException) is thrown for duplicate key
violations because the item already existing is a uniqueness constraint failure, not a
stale-read conflict. See Optimistic Concurrency for the distinction.
If your application cannot guarantee key uniqueness at the application layer, perform a read-before-write to check existence before adding, or use a conditional write pattern at the DynamoDB level.
Updating Entities¶
Load an entity, mutate its properties, and call SaveChangesAsync. EF Core's change tracker
records which properties changed from their snapshot values.
var order = await db.Orders
.Where(o => o.Pk == "CUSTOMER#42" && o.Sk == "ORDER#2026-001")
.AsAsyncEnumerable()
.SingleAsync(cancellationToken);
order.Status = "shipped";
order.ShippedAt = DateTimeOffset.UtcNow;
order.LegacyField = null; // scalar null writes a DynamoDB NULL attribute
await db.SaveChangesAsync(cancellationToken);
The provider generates an UPDATE … SET … WHERE pk = ? AND sk = ? statement that includes only
the properties that actually changed:
Key behaviors:
- Only modified properties appear. Unchanged properties are omitted from the statement entirely — there is no full-document replace.
- Scalar
nullwrites DynamoDBNULL. Setting a scalar property tonullwrites an explicit{ NULL: true }attribute. Null complex properties and complex collections can be represented as removed nested attributes when the provider emits aREMOVEfor that path. - Finalized table keys cannot be modified. Attempting to change the EF primary key that maps to the DynamoDB partition key or sort key throws
NotSupportedException. To change an item's key, delete the existing entity and add a new one.
Complex Properties in Updates¶
Complex properties and complex collections are stored as nested attributes (sub-documents) in the same DynamoDB item. When a complex-property path changes, the provider emits a targeted SET or REMOVE clause rather than replacing the entire item:
| Complex-property change | Generated clause |
|---|---|
| Complex property added | SET "address" = ? (full sub-document) |
| Complex property removed | REMOVE "address" |
| Property inside complex property modified | SET "address"."city" = ? (nested path) |
This is different from relational providers, where related data may live in separate rows or tables. In the DynamoDB provider, every complex-property mutation targets a path within the same item.
Primitive collections inside complex properties are serialized using the same DynamoDB wire shapes
as root properties: lists become L, dictionaries become M, string/number/binary sets become
SS/NS/BS, and byte[] remains a binary scalar (B). Null list elements or dictionary
values serialize as DynamoDB NULL; null complex collections are removed when updated. DynamoDB
sets cannot be empty, contain null elements, or mix string, number, and binary member kinds.
Deleting Entities¶
Call db.Remove(entity) (or set db.Entry(entity).State = EntityState.Deleted) and then call
SaveChangesAsync.
var order = await db.Orders
.Where(o => o.Pk == "CUSTOMER#42" && o.Sk == "ORDER#2026-001")
.AsAsyncEnumerable()
.SingleAsync(cancellationToken);
db.Orders.Remove(order);
await db.SaveChangesAsync(cancellationToken);
The provider generates a DELETE FROM … WHERE pk = ? AND sk = ? statement:
Deleting a non-existent item is a silent success. DynamoDB returns success when the item identified by the WHERE predicate is not found. This is by design: the goal of a DELETE is for the item to not exist; if it is already gone, the outcome is the same. The provider does not treat this as an error.
If you have configured concurrency tokens, the token value is appended to the WHERE predicate.
If the item exists but its token has changed since the entity was loaded, DynamoDB raises a
conflict and the provider throws DbUpdateConcurrencyException. See
Optimistic Concurrency.
ExecuteUpdateAsync¶
ExecuteUpdateAsync applies a single-item update directly against the table without loading the
entity into the change tracker. It executes immediately — there is no SaveChangesAsync step and
the change tracker is never consulted or updated.
var affected = await db.Orders
.Where(o => o.Pk == "CUSTOMER#42" && o.Sk == "ORDER#2026-001")
.ExecuteUpdateAsync(setters
=> setters
.SetProperty(o => o.Status, "shipped")
.SetProperty(o => o.ShippedAt, DateTimeOffset.UtcNow),
cancellationToken);
The provider generates one PartiQL statement:
UPDATE "Orders"
SET "status" = ?, "shippedAt" = ?
WHERE "pk" = 'CUSTOMER#42' AND "sk" = 'ORDER#2026-001'
Key behaviors:
- The WHERE clause must equality-constrain the full primary key.
ExecuteUpdateAsynctargets one item: the predicate must contain a partition-key equality, plus a sort-key equality when the entity has a sort key. Filters on other attributes are allowed as additional predicates. Queries without key equality (or withIN, range comparisons, or OR touching a key) throw at translation time. - The result is 0 or 1. A successful update returns
1. When a key-targetedUPDATEmatches no item, DynamoDB Local raises a condition failure and the provider maps that to0instead of throwing. This behavior is verified against DynamoDB Local; the real service may differ — a non-matching key-targetedUPDATEcan complete silently, in which case the affected count is1. There is no multi-row count. - Nothing touches the change tracker. Tracked entities are not read or updated. If the tracked instance is still in scope, re-query it to observe the new values.
- No implicit transaction. The update is a single statement, so no transaction wrapper is involved and none can be combined with other writes.
- Nested paths are supported.
SetProperty(o => o.Address.City, "Seattle")writes the nested attribute path (SET "address"."city" = ?). - Numeric self-reference supports
+and-only.SetProperty(o => o.Count, o => o.Count + 1)becomesSET "count" = "count" + 1, evaluated server-side against the stored value. Multiplication, division, and string concatenation are rejected: DynamoDB PartiQL SET expressions support numeric addition and subtraction only (see the AWS PartiQL UPDATE reference). nullbecomesSET attr = ?with a DynamoDBNULLparameter. Clearing an attribute withSetProperty(o => o.Note, (string?)null)writes an explicit NULL attribute, matchingSaveChangesAsyncscalar-null behavior.- Synchronous
ExecuteUpdateis not supported. DynamoDB execution is asynchronous only; useExecuteUpdateAsync. - Setter targets must be mapped scalar properties. Navigations and whole complex properties cannot be assigned; assign the leaf scalar of a complex-property path instead. Key properties cannot be mutated.
ExecuteDeleteAsync is not implemented. See
Bulk update limitations.
Statement Size Limit¶
Each PartiQL statement has an 8,192-byte size limit enforced by DynamoDB. The provider
validates the compiled statement length before executing any writes and throws
InvalidOperationException at planning time if the limit is exceeded.
Statement size limit
The limit is most likely to be hit on INSERT statements with many mapped properties or large complex-property sub-documents. The error message reports the actual character or byte count:
The generated PartiQL statement is 9,841 UTF-8 bytes, which exceeds DynamoDB's
8,192-byte statement-size limit. Consider reducing the number of mapped scalar
properties or splitting the write unit across multiple SaveChanges calls.
To fix: reduce the number of mapped properties, split large nested documents into separate items,
or batch smaller sets of entities per SaveChangesAsync call.