# UpdateMultiple with ARX.SDK on Amazon DynamoDB

## What an UpdateMultiple does

| Rule | On DynamoDB |
|---|---|
| Many entities | UpdateMultiple is all or nothing, in one `TransactWriteItems` (up to 100) |
| By a filter | `UpdateMultipleAsync(values, filter)` and `DeleteMultipleAsync(table, filter)`: the filter gives a partition key of the table or an index (ARX8001 otherwise); one query, then one transaction of at most 100 writes, each conditioned on the filter still matching (Basic2). From 0.1.0-preview.5 |

## The samples

Every UpdateMultiple 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.

### Update many records in one call

Basics 1 of UpdateMultiple.

Many items in one call, all or nothing, in two scenarios: a different price for each product, and a list with one SKU no item has, where none of them changes.

Good to know:

The updates go as one `TransactWriteItems`, each conditional on its item existing: at most 100 items in a call (more is ARX8001 unless `AllowPartial`). Each entity writes only the attributes it sets.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var products = new[] { "SKU-D271", "SKU-D272", "SKU-D273" }.Select(sku =>
{
    var product = new ArxEntity("product");
    product["sku"] = sku;
    product["name"] = $"Trail sock {sku}";
    product["price"] = 12.00m;
    return product;
}).ToList();
var ids = await service.CreateMultipleAsync(products);
```

#### 1. A price per product

One entity per item, each with its own price, in one transaction.

In scope: `service` (ArxDynamoDbService), `ids` (IReadOnlyList<ArxRecordId>).

```csharp
var changes = new List<ArxEntity>();

for (var i = 0; i < ids.Count; i++)
{
    var change = new ArxEntity("product") { RecordId = ids[i] };
    change["price"] = 10.00m + i;
    changes.Add(change);
}

int changed = await service.UpdateMultipleAsync(changes);

Console.WriteLine($"{changed} items changed in one call");
```

Check it in the database:

```sql
SELECT "sku", "price" FROM "product" WHERE "sku" IN ['SKU-D271', 'SKU-D272', 'SKU-D273']
```

Expect: 3 items, price 10, 11, 12.

#### 2. All or nothing

SKU-D279 is no item: RecordNotFound, and SKU-D271 keeps its price too.

In scope: `service` (ArxDynamoDbService).

```csharp
var first = new ArxEntity("product");
first["sku"] = "SKU-D271";
first["price"] = 99.00m;

var missing = new ArxEntity("product");
missing["sku"] = "SKU-D279";
missing["price"] = 99.00m;

try
{
    await service.UpdateMultipleAsync(new[] { first, missing });
}
catch (ArxException exception) when (exception.Code == ArxErrorCode.RecordNotFound)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

```sql
SELECT "sku", "price" FROM "product" WHERE "sku" IN ['SKU-D271', 'SKU-D279']
```

Expect: 1 item, SKU-D271, price 10 (not 99).

### Update or delete every item a filter matches

Basics 2 of UpdateMultiple.

Every item a filter matches, changed or deleted in one call. DynamoDB reads by partition, so the filter gives one (a customer's orders); ARX finds the items with one query, then writes each by its key in one transaction, conditioned on the filter still matching it.

Good to know:

A filter that gives no partition key, of the table or an index, is refused (ARX8001): a write never scans. At most 100 items, the most one transaction can write.

The entity carries only the values to set: an id or a key attribute is refused (ARX1001).

Needs ARX.SDK 0.1.0-preview.5 or later.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var orders = new List<ArxEntity>();
for (var i = 1; i <= 4; i++)
{
    var order = new ArxEntity("sales_order");
    order["customer_id"] = "C-DF01";
    order["order_number"] = $"SO-DF0{i}";
    order["status"] = "OPEN";
    order["ordered_on"] = "2026-11-11";
    order["total"] = i * 40m;
    orders.Add(order);
}
await service.CreateMultipleAsync(orders);
```

#### 1. Update by a filter

The customer's small orders cancelled: the GSI1 key moves with the status.

In scope: `service` (ArxDynamoDbService).

```csharp
var change = new ArxEntity("sales_order");
change["status"] = "CANCELLED";

var filter = new ArxFilterExpression();
filter.AddCondition("customer_id", ArxConditionOperator.Equal, "C-DF01");
filter.AddCondition("total", ArxConditionOperator.LessThan, 100m);

int changed = await service.UpdateMultipleAsync(change, filter);

Console.WriteLine($"{changed} orders cancelled");
```

Check it in the database:

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

Expect: 4 items; SO-DF01 and SO-DF02 (40, 80) CANCELLED with GSI1PK STATUS#CANCELLED, the other two OPEN.

#### 2. Delete by a filter

The cancelled orders deleted by the same kind of filter; the call returns how many went.

In scope: `service` (ArxDynamoDbService).

```csharp
var filter = new ArxFilterExpression();
filter.AddCondition("customer_id", ArxConditionOperator.Equal, "C-DF01");
filter.AddCondition("status", ArxConditionOperator.Equal, "CANCELLED");

int deleted = await service.DeleteMultipleAsync("sales_order", filter);

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

Check it in the database:

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

Expect: 2 items, SO-DF03 and SO-DF04, both OPEN.

### A security check around updates

Advanced case 1 of UpdateMultiple.

A security check around a batch of updates (`SecurityGuard`), in three scenarios: a batch of changes passes, an attack in one item refuses the whole batch, and repeated attacks block the user.

Good to know:

ARX checks every name of every item before it sends the transaction, so one bad item means nothing is written. The guard logs the ARX1017, shows a plain message, and blocks the user at the third strike.

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-D241", "SKU-D242" }.Select(sku =>
{
    var product = new ArxEntity("product");
    product["sku"] = sku;
    product["price"] = 10.00m;
    return product;
}).ToList();
await service.CreateMultipleAsync(products);
```

#### 1. A batch of changes passes

Two price changes go through.

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

```csharp
var result = await PatchProductsAsync(service, guard, "ana", new[]
{
    ("SKU-D241", new Dictionary<string, object> { ["price"] = 12.00m }),
    ("SKU-D242", new Dictionary<string, object> { ["price"] = 14.00m })
});

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

Defined in the sample, used above:

```csharp
private static Task<GuardResult<int>> PatchProductsAsync(ArxDynamoDbService service, SecurityGuard guard, string user, IReadOnlyList<(string Sku, Dictionary<string, object> Fields)> items)
{
    var changes = new List<ArxEntity>();

    foreach (var (sku, fields) in items)
    {
        var change = new ArxEntity("product") { RecordId = ArxRecordId.Of("sku", sku) };

        foreach (var field in fields)
        {
            change[field.Key] = field.Value;
        }

        changes.Add(change);
    }

    return guard.RunAsync(user, "patch products", NameSource.UserInput, () => service.UpdateMultipleAsync(changes));
}
```

Check it in the database:

```sql
SELECT "sku", "price" FROM "product" WHERE "sku" IN ['SKU-D241', 'SKU-D242']
```

Expect: 2 items, prices 12 and 14.

#### 2. An attack in one item refuses the batch

The second item names a placeholder: the batch is refused, the first item's change isn't written.

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

```csharp
var result = await PatchProductsAsync(service, guard, "mallory", new[]
{
    ("SKU-D241", new Dictionary<string, object> { ["price"] = 0.01m }),
    ("SKU-D242", new Dictionary<string, object> { ["#n0"] = 0.01m })
});

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

Check it in the database:

```sql
SELECT "sku", "price" FROM "product" WHERE "sku" IN ['SKU-D241', 'SKU-D242']
```

Expect: 2 items, prices still 12 and 14.

#### 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[] { "price = :v0 REMOVE name", "price; --" })
{
    await PatchProductsAsync(service, guard, "mallory", new[] { ("SKU-D241", new Dictionary<string, object> { [probe] = 0.01m }) });
}

var blocked = await PatchProductsAsync(service, guard, "mallory", new[] { ("SKU-D241", new Dictionary<string, object> { ["price"] = 0.01m }) });

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

Check it in the database:

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

Expect: 1 item, price 12.

