# Delete with ARX.SDK on Amazon DynamoDB

## What a delete does

| Rule | On DynamoDB |
|---|---|
| Which item | By its full key: the id (an entity whose key is its id), the id with the key's other attributes, or `ArxMatch.Columns`/`ArxMatch.Key` naming the key's attributes |
| Other columns | When they give a partition key, of the table or an index: one query finds the items (at most 100), then they are deleted by their keys, all or nothing, each conditioned on still matching; none found is 0 (ARX2002 with RequireSingleRow), several with RequireSingleRow ARX2003 |
| No partition | ARX8001: a write never scans |
| Nothing found | `DeleteAsync(name, id)` returns false; `DeleteAsync(entity)` returns 0: deleting twice isn't an error |
| In a transaction | The items are read now (so the count is known), and deleted with the transaction's other writes at the commit |
| Names | Placeholders only; expression or SQL syntax in a match name is ARX1017, checked before the match itself |
| Rows by a condition | Delete finds rows by equal values: the id, a declared key, or columns you give values for (`ArxMatch.Columns`). To delete every row that meets a condition (`amount > 100`), call `DeleteMultipleAsync(table, filter)` (the filter must give a partition key; at most 100 items): one call, all or nothing, from 0.1.0-preview.5 (DeleteMultiple, Basics). Before that version: read the rows with `RetrieveMultipleAsync` and pass them to `DeleteMultipleAsync`. |

## The samples

Every Delete sample, in reading order. Each scenario shows the code (verified against DynamoDB Local) and the PartiQL statement that checks what it did. `service` is the application's `ArxDynamoDbService`; values marked "In scope" come from earlier in the sample. What the samples' code refers to beyond the packages (SampleCatalog.cs, `dbContext`) is in the sample application file, arx-dynamodb-sample-application.md.

### Delete one record

Basics 1 of Delete.

The smallest delete, in two scenarios: by the id Create returned, and an id no item has.

Good to know:

`DeleteAsync(entityName, id)` returns true when an item went, false when none had the id: deleting twice isn't an error. Several ids in one call are DeleteMultiple's.

Usings:

```csharp
using ARX.SDK.DynamoDb.Services;
using ARX.SDK.Services;
```

Set up by the sample before its scenarios:

```csharp
var product = new ArxEntity("product");
product["sku"] = "SKU-D301";
product["name"] = "Clearance item SKU-D301";
var id = await service.CreateAsync(product);
```

#### 1. By the id Create returned

The id CreateAsync returned for SKU-D301.

In scope: `service` (ArxDynamoDbService), `id` (ArxRecordId).

```csharp
bool deleted = await service.DeleteAsync("product", id);

Console.WriteLine($"deleted: {deleted}");
```

Check it in the database:

```sql
SELECT "sku" FROM "product" WHERE "sku" = 'SKU-D301'
```

Expect: no items.

#### 2. An id no item has

SKU-D301 again: nothing to delete, so false, not an error.

In scope: `service` (ArxDynamoDbService).

```csharp
bool deleted = await service.DeleteAsync("product", "SKU-D301");

Console.WriteLine($"deleted: {deleted}");
```

Check it in the database:

```sql
SELECT "sku" FROM "product" WHERE "sku" = 'SKU-D301'
```

Expect: no items.

### How a delete finds its items

Basics 2 of Delete.

How a delete finds its items in DynamoDB, in four scenarios: an order by its id and its customer, several orders by their keys, the rest of a customer's orders through a query, and a match no partition answers.

Good to know:

An item is deleted by its full key. An order's key is its customer and its number, so give both: the id and the customer, or `ArxMatch.Columns` naming the key's attributes. Columns that give a partition key, of the table or an index, are answered by a query: the items found are deleted by their keys, all or nothing (at most 100). Columns that give no partition are ARX8001: a write never scans.

Usings:

```csharp
using ARX.SDK;
using ARX.SDK.DynamoDb.Services;
using ARX.SDK.Services;
```

Set up by the sample before its scenarios:

```csharp
var customer = new ArxEntity("customer");
customer["customer_id"] = "C-D311";
customer["name"] = "Lucerne Publishing";
await service.CreateAsync(customer);
var orders = new[] { "SO-D311", "SO-D312", "SO-D313", "SO-D314" }.Select(number =>
{
    var order = new ArxEntity("sales_order");
    order["customer_id"] = "C-D311";
    order["order_number"] = number;
    order["status"] = "CANCELLED";
    order["ordered_on"] = "2026-10-30";
    return order;
}).ToList();
await service.CreateMultipleAsync(orders);
```

#### 1. By the id and the customer

SO-D311: its id, and its customer for the partition.

In scope: `service` (ArxDynamoDbService).

```csharp
var match = new ArxEntity("sales_order") { RecordId = ArxRecordId.Of("order_number", "SO-D311") };
match["customer_id"] = "C-D311";

int deleted = await service.DeleteAsync(match);

Console.WriteLine($"{deleted} item deleted");
```

Check it in the database:

```sql
SELECT "SK" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D311' AND "SK" = 'ORDER#SO-D311'
```

Expect: no items.

#### 2. Several by their keys

SO-D312 and SO-D313 by their key attributes, in one transaction.

In scope: `service` (ArxDynamoDbService).

```csharp
var matches = new[] { "SO-D312", "SO-D313" }.Select(number =>
{
    var match = new ArxEntity("sales_order");
    match["customer_id"] = "C-D311";
    match["order_number"] = number;
    return match;
}).ToList();

int deleted = await service.DeleteMultipleAsync(matches, new ArxDeleteOptions { Match = ArxMatch.Columns("customer_id", "order_number") });

Console.WriteLine($"{deleted} items deleted");
```

Check it in the database:

```sql
SELECT "order_number" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D311' AND begins_with("SK", 'ORDER#')
```

Expect: 1 item, SO-D314.

#### 3. Through a query

The customer's remaining orders, through a query of its partition: the profile in the same partition stays.

In scope: `service` (ArxDynamoDbService).

```csharp
var match = new ArxEntity("sales_order");
match["customer_id"] = "C-D311";

int deleted = await service.DeleteAsync(match, new ArxDeleteOptions { Match = ArxMatch.Columns("customer_id") });

Console.WriteLine($"{deleted} item deleted");
```

Check it in the database:

```sql
SELECT "SK" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D311'
```

Expect: 1 item, PROFILE: the customer itself isn't a sales_order.

#### 4. No partition

Every order of a date: no partition key in the match, so ARX8001; nothing is deleted.

In scope: `service` (ArxDynamoDbService).

```csharp
var match = new ArxEntity("sales_order");
match["ordered_on"] = "2026-10-30";

try
{
    await service.DeleteAsync(match, new ArxDeleteOptions { Match = ArxMatch.Columns("ordered_on") });
}
catch (ArxNotSupportedException exception)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

```sql
SELECT "order_number" FROM "crm" WHERE "ordered_on" = '2026-10-30'
```

Expect: no items: the orders of that date went in scenarios 1 to 3.

### Catch an injection attempt

Real case 1 of Delete.

A "remove my order" endpoint attacked two ways: a match column named with expression syntax, and a value written to widen the delete to every order; then the honest request.

Good to know:

A name with expression or SQL syntax is ARX1017. Values are never part of an expression: ARX sends no PartiQL, and a value only ever names one key.

Usings:

```csharp
using ARX.SDK;
using ARX.SDK.DynamoDb.Services;
using ARX.SDK.Services;
```

Set up by the sample before its scenarios:

```csharp
foreach (var (customerId, number) in new[] { ("C-D321", "SO-D321"), ("C-D322", "SO-D322") })
{
    var order = new ArxEntity("sales_order");
    order["customer_id"] = customerId;
    order["order_number"] = number;
    order["status"] = "OPEN";
    order["ordered_on"] = "2026-11-05";
    await service.CreateAsync(order);
}
```

#### 1. Expression syntax in the match

The match names "order_number OR attribute_exists(PK)": ARX1017.

In scope: `service` (ArxDynamoDbService).

```csharp
var match = new ArxEntity("sales_order");
match["customer_id"] = "C-D321";
match["order_number OR attribute_exists(PK)"] = "x";

try
{
    await service.DeleteAsync(match, new ArxDeleteOptions { Match = ArxMatch.Columns("customer_id", "order_number OR attribute_exists(PK)") });
}
catch (ArxValidationException exception) when (exception.Code == ArxErrorCode.SuspectedInjection)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

```sql
SELECT "SK" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D321'
```

Expect: 1 item, ORDER#SO-D321.

#### 2. A value that widens the delete

A customer id of "C-D321' OR '1'='1": only a value, it names no item, so nothing goes.

In scope: `service` (ArxDynamoDbService).

```csharp
var match = new ArxEntity("sales_order") { RecordId = ArxRecordId.Of("order_number", "SO-D322") };
match["customer_id"] = "C-D321' OR '1'='1";

int deleted = await service.DeleteAsync(match);

Console.WriteLine($"{deleted} items deleted");
```

Check it in the database:

```sql
SELECT "SK" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D322'
```

Expect: 1 item, ORDER#SO-D322.

#### 3. The honest request

The customer removes its own order.

In scope: `service` (ArxDynamoDbService).

```csharp
var match = new ArxEntity("sales_order") { RecordId = ArxRecordId.Of("order_number", "SO-D321") };
match["customer_id"] = "C-D321";

int deleted = await service.DeleteAsync(match);

Console.WriteLine($"{deleted} item deleted");
```

Check it in the database:

```sql
SELECT "SK" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D321'
```

Expect: no items.

### Replace a record in a transaction

Advanced case 1 of Delete.

Replace an order in one ARX transaction: delete the old one, create the new one, commit, in two scenarios: the replacement, and a replacement that fails and leaves the old order in place.

Good to know:

An item's key can't change in DynamoDB, so a new order number means a new item: delete and create in one transaction, sent together at the commit, so a reader never sees neither or both.

Usings:

```csharp
using ARX.SDK;
using ARX.SDK.DynamoDb.Services;
using ARX.SDK.Services;
```

Set up by the sample before its scenarios:

```csharp
foreach (var number in new[] { "SO-D331", "SO-D339" })
{
    var order = new ArxEntity("sales_order");
    order["customer_id"] = "C-D331";
    order["order_number"] = number;
    order["status"] = "OPEN";
    order["ordered_on"] = "2026-11-06";
    await service.CreateAsync(order);
}
```

#### 1. Replace

Delete SO-D331 and create SO-D332 in its place, committed together.

In scope: `service` (ArxDynamoDbService).

```csharp
await using var connection = new ArxDynamoConnection(service);
await using var transaction = await connection.BeginTransactionAsync();

var old = new ArxEntity("sales_order") { RecordId = ArxRecordId.Of("order_number", "SO-D331") };
old["customer_id"] = "C-D331";
await service.DeleteAsync(old, transaction);

await service.CreateAsync(Order("SO-D332"), transaction);

await transaction.CommitAsync();
Console.WriteLine("SO-D331 replaced by SO-D332");
```

Defined in the sample, used above:

```csharp
private static ArxEntity Order(string number)
{
    var order = new ArxEntity("sales_order");
    order["customer_id"] = "C-D331";
    order["order_number"] = number;
    order["status"] = "OPEN";
    order["ordered_on"] = "2026-11-07";
    return order;
}
```

Check it in the database:

```sql
SELECT "order_number" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D331' AND begins_with("SK", 'ORDER#')
```

Expect: 2 items, SO-D332 and SO-D339.

#### 2. A failed replacement

Replace SO-D332 by SO-D339, which exists: the commit fails, and SO-D332 stays.

In scope: `service` (ArxDynamoDbService).

```csharp
await using var connection = new ArxDynamoConnection(service);
await using var transaction = await connection.BeginTransactionAsync();

var old = new ArxEntity("sales_order") { RecordId = ArxRecordId.Of("order_number", "SO-D332") };
old["customer_id"] = "C-D331";
await service.DeleteAsync(old, transaction);

await service.CreateAsync(Order("SO-D339"), transaction);

try
{
    await transaction.CommitAsync();
}
catch (ArxDatabaseException exception)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

```sql
SELECT "order_number" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D331' AND begins_with("SK", 'ORDER#')
```

Expect: 2 items, SO-D332 and SO-D339: the delete wasn't sent alone.

### A security check around deletes

Advanced case 2 of Delete.

A security check around deletes (`SecurityGuard`), in three scenarios: a normal delete, an attack in the match that is logged, and repeated attacks that block the user.

Good to know:

The endpoint lets the client say which attribute identifies the product, a design that puts user input into a name. ARX refuses the attacks (ARX1017); the guard logs them and blocks the user at the third strike. The fix is to take the attribute from a fixed list.

Usings:

```csharp
using ARX.SDK.Services;
```

Set up by the sample before its scenarios:

```csharp
var guard = new SecurityGuard(blockAfter: 3);
var products = new[] { "SKU-D341", "SKU-D342" }.Select(sku =>
{
    var product = new ArxEntity("product");
    product["sku"] = sku;
    return product;
}).ToList();
await service.CreateMultipleAsync(products);
```

#### 1. A normal delete

by=sku: SKU-D341 goes.

In scope: `service` (ArxDynamoDbService), `guard` (SecurityGuard).

```csharp
var result = await DeleteProductAsync(service, guard, "ana", "sku", "SKU-D341");

Console.WriteLine($"ana: {result.Outcome}, {result.Value} deleted");
```

Defined in the sample, used above:

```csharp
private static Task<GuardResult<int>> DeleteProductAsync(ArxDynamoDbService service, SecurityGuard guard, string user, string by, string value)
{
    var match = new ArxEntity("product");
    match[by] = value;

    return guard.RunAsync(user, "delete product", NameSource.UserInput,
        () => service.DeleteAsync(match, new ArxDeleteOptions { Match = ArxMatch.Columns(by) }));
}
```

Check it in the database:

```sql
SELECT "sku" FROM "product" WHERE "sku" = 'SKU-D341'
```

Expect: no items.

#### 2. An attack is logged

by="sku OR size(sku) > :v0": refused, logged, a plain message.

In scope: `service` (ArxDynamoDbService), `guard` (SecurityGuard).

```csharp
var result = await DeleteProductAsync(service, guard, "mallory", "sku OR size(sku) > :v0", "0");

Console.WriteLine($"mallory sees: \"{result.ForTheUser}\"; log: {guard.Events[^1].Kind}");
```

Check it in the database:

```sql
SELECT "sku" FROM "product" WHERE "sku" = 'SKU-D342'
```

Expect: 1 item.

#### 3. Repeated attacks block the user

Two more attempts block mallory; the next request isn't tried.

In scope: `service` (ArxDynamoDbService), `guard` (SecurityGuard).

```csharp
foreach (var probe in new[] { "#n0", "sku; DROP TABLE product" })
{
    await DeleteProductAsync(service, guard, "mallory", probe, "SKU-D342");
}

var blocked = await DeleteProductAsync(service, guard, "mallory", "sku", "SKU-D342");

Console.WriteLine($"mallory blocked: {guard.IsBlocked("mallory")}; next request: {blocked.Outcome}");
```

Check it in the database:

```sql
SELECT "sku" FROM "product" WHERE "sku" = 'SKU-D342'
```

Expect: 1 item.

### Set up the service from DynamoDB's tables

Advanced case 3 of Delete.

The service set up from DynamoDB's description of the lead table, in two scenarios: preload at startup in the KeysOnly mode, then delete by the preloaded key.

Good to know:

A delete needs the key only, so `ArxPreLoadMode.KeysOnly` is enough: the table's key becomes the business key `ArxMatch.Key("email")` finds the item by.

Usings:

```csharp
using ARX.SDK.DynamoDb.Model;
using ARX.SDK.DynamoDb.Services;
using ARX.SDK.Services;
using Amazon.DynamoDBv2;
```

Set up by the sample before its scenarios:

```csharp
var lead = new ArxEntity("lead");
lead["email"] = "lead-d351@example.com";
lead["full_name"] = "Mia Hansen";
await service.CreateAsync(lead);
```

#### 1. Preload at startup

Preload the lead table's key once, at startup.

In scope: `client` (IAmazonDynamoDB), `model` (ArxDynamoModel).

```csharp
var preload = new ArxPreLoadTable(new ArxPreLoadTableRegister().PreLoadTable("lead"), ArxPreLoadMode.KeysOnly);
var catalog = await preload.LoadAsync(client, model);

catalog.TryGetTable("lead", out var table);
Console.WriteLine($"lead: key {string.Join(", ", table.Keys)}");

return new ArxDynamoDbService(client, model, catalog);
```

Note: In the AWS CLI: aws dynamodb describe-table --table-name lead --endpoint-url http://localhost:18000 KeySchema email (HASH).

#### 2. Delete by the preloaded key

Delete by the preloaded key.

In scope: `service` (ArxDynamoDbService).

```csharp
var match = new ArxEntity("lead");
match["email"] = "lead-d351@example.com";

int deleted = await service.DeleteAsync(match, new ArxDeleteOptions { Match = ArxMatch.Key("email") });

Console.WriteLine($"{deleted} item deleted");
```

Check it in the database:

```sql
SELECT "email" FROM "lead" WHERE "email" = 'lead-d351@example.com'
```

Expect: no items.

