Skip to content

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:

  1. DynamoSaveChangesPlanner walks 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.
  2. DynamoWriteExecutor sends the compiled statements to DynamoDB via the ExecuteStatement, ExecuteTransaction, or BatchExecuteStatement APIs 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 INTO "Orders"
VALUE {
    'pk': ?,
    'sk': ?,
    'status': ?,
    'total': ?
}

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](concurrency.md) 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:

UPDATE "Orders"
SET "status" = ?, "shippedAt" = ?, "legacyField" = ?
WHERE "pk" = ? AND "sk" = ?

Key behaviors:

  • Only modified properties appear. Unchanged properties are omitted from the statement entirely — there is no full-document replace.
  • Scalar null writes DynamoDB NULL. Setting a scalar property to null writes an explicit { NULL: true } attribute. Null complex properties and complex collections can be represented as removed nested attributes when the provider emits a REMOVE for that path.
  • Primary keys cannot be modified. Attempting to change a [Key]-annotated or key-mapped property on a tracked entity 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:

DELETE FROM "Orders"
WHERE "pk" = ? AND "sk" = ?

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.

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.

See also