# RetrieveMultiple with ARX.SDK on Amazon DynamoDB

## What a RetrieveMultiple does

| Rule | On DynamoDB |
|---|---|
| A query | Needs the partition: Equal conditions on the attributes of the table's partition key, or of an index's (ARX picks it, or `IndexName` names it). Conditions on the sort key's attribute narrow the range; the others filter what was read |
| Joins | ARX8001: in the single-table design, related items share a partition and `QueryPartitionAsync` reads them together |
| Paging | By the `PagingCookie` each page returns; `HasNextPage` is exact. A page number above 1 and a total count are ARX8001 |
| Paging cookies | Sealed by default (encrypted and authenticated: a client can neither read nor change one), with a random key of the process unless `PagingCookieKey` is set. Several instances, or cookies across a restart: the same `PagingCookieKey` everywhere, or a cookie is ARX1001 on the other. `PagingCookieProtection` can be `Signed` or `Plain` (Advanced 4) |
| No partition | ARX8001, unless `new ArxDynamoReadOptions { AllowScan = true }`: a scan reads, and pays for, the whole table |
| Patterns | Contains and BeginsWith only with `CaseSensitivePatterns = true` (DynamoDB compares case-sensitively); EndsWith and Like are ARX8001 |
| Order | By the sort key only, ascending or descending |
| Aggregates | A Count alone (DynamoDB counts, `Select COUNT`); other aggregates and grouping are ARX8001 |
| Indexes | Eventually consistent: a write shows in an index a moment later (ARX reads an index without ConsistentRead, which DynamoDB refuses there) |
| Sharded partitions | Every shard is queried and the results merged in sort key order, paged by one cookie |

A loop over the pages that must also run on the SQL providers ends each page with
`query.PageInfo = query.PageInfo.Next(page)` and stops on null (from 0.1.0-preview.5): the samples pass the cookie on,
which is DynamoDB's way, and on PostgreSQL, SQL Server or MySQL, which return no cookie, that loop reads the first page
again.

## The samples

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

### Retrieve many records

Basics 1 of RetrieveMultiple.

Read many items with `ArxQueryExpression`, in three scenarios: a customer's orders, newest first and filtered, a range of the sort key, and a query DynamoDB can't answer without a scan.

Good to know:

A query needs the partition: an Equal condition on the attributes of the partition key (here the customer). Conditions on the sort key's attribute narrow the range; the others filter what was read. Orders go by the sort key only. Without a partition, ARX refuses (ARX8001) unless you allow a scan (Advanced 1).

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 = Enumerable.Range(1, 6).Select(i =>
{
    var order = new ArxEntity("sales_order");
    order["customer_id"] = "C-D411";
    order["order_number"] = $"SO-D41{i}";
    order["status"] = i % 3 == 0 ? "CANCELLED" : "OPEN";
    order["ordered_on"] = $"2026-11-0{i}";
    order["total"] = 10m * i;
    return order;
}).ToList();
await service.CreateMultipleAsync(orders);
```

#### 1. A customer's orders

The customer's open orders, newest number first.

In scope: `service` (ArxDynamoDbService).

```csharp
var query = new ArxQueryExpression("sales_order");
query.ColumnSet.AddColumns("order_number", "total");
query.Criteria.AddCondition("customer_id", ArxConditionOperator.Equal, "C-D411");
query.Criteria.AddCondition("status", ArxConditionOperator.Equal, "OPEN");
query.AddOrder("order_number", ArxOrderType.Descending);

var found = await service.RetrieveMultipleAsync(query);

Console.WriteLine(string.Join(", ", found.Entities.Select(x => $"{x["order_number"]} {x["total"]}")));
```

Check it in the database:

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

Expect: 4 items, SO-D411, SO-D412, SO-D414, SO-D415.

#### 2. A range of the sort key

Order numbers from SO-D413 on: a range of the sort key, read in the key's order.

In scope: `service` (ArxDynamoDbService).

```csharp
var query = new ArxQueryExpression("sales_order");
query.ColumnSet.AddColumns("order_number");
query.Criteria.AddCondition("customer_id", ArxConditionOperator.Equal, "C-D411");
query.Criteria.AddCondition("order_number", ArxConditionOperator.GreaterEqual, "SO-D413");

var found = await service.RetrieveMultipleAsync(query);

Console.WriteLine(string.Join(", ", found.Entities.Select(x => x["order_number"])));
```

Check it in the database:

```sql
SELECT "order_number" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D411' AND "SK" >= 'ORDER#SO-D413'
```

Expect: 4 items, SO-D413 to SO-D416.

#### 3. No partition

Every order over 30, whoever the customer: no partition, so ARX8001 rather than a hidden scan.

In scope: `service` (ArxDynamoDbService).

```csharp
var query = new ArxQueryExpression("sales_order");
query.Criteria.AddCondition("total", ArxConditionOperator.GreaterThan, 30m);

try
{
    await service.RetrieveMultipleAsync(query);
}
catch (ArxNotSupportedException exception)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

```sql
SELECT "order_number" FROM "crm" WHERE "entity_type" = 'sales_order' AND "total" > 30
```

Expect: the orders over 30 (PartiQL scans the table for this).

### Page through results

Basics 2 of RetrieveMultiple.

Page through a partition with paging cookies, in three scenarios: the first page, the next pages from the cookie, and a page number, which DynamoDB can't jump to.

Good to know:

A page continues from where the last one ended: pass back the `PagingCookie` the page returned, with the same query. `HasNextPage` is exact, and the cookie is null on the last page. DynamoDB can't count or skip, so a page number above 1 and a total count are ARX8001.

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 = Enumerable.Range(1, 12).Select(i =>
{
    var order = new ArxEntity("sales_order");
    order["customer_id"] = "C-D421";
    order["order_number"] = $"SO-D42{i:00}";
    order["status"] = "OPEN";
    order["ordered_on"] = "2026-11-08";
    return order;
}).ToList();
await service.CreateMultipleAsync(orders);
```

#### 1. The first page

Five orders, and the cookie for the rest.

In scope: `service` (ArxDynamoDbService).

```csharp
var page = await service.RetrieveMultipleAsync(Query(new ArxPagingInfo { Count = 5 }));

Console.WriteLine($"{string.Join(", ", page.Entities.Select(x => x["order_number"]))}; more: {page.HasNextPage}");

return page;
```

Defined in the sample, used above:

```csharp
private static ArxQueryExpression Query(ArxPagingInfo page)
{
    var query = new ArxQueryExpression("sales_order");
    query.ColumnSet.AddColumns("order_number");
    query.Criteria.AddCondition("customer_id", ArxConditionOperator.Equal, "C-D421");
    query.PageInfo = page;
    return query;
}
```

Check it in the database:

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

Expect: 12 items; the first page holds SO-D4201 to SO-D4205.

#### 2. The next pages

Each page from the last one's cookie, until there is none.

In scope: `service` (ArxDynamoDbService), `page` (ArxEntityCollection).

```csharp
while (page.PagingCookie != null)
{
    page = await service.RetrieveMultipleAsync(Query(new ArxPagingInfo { Count = 5, PagingCookie = page.PagingCookie }));
    Console.WriteLine($"{string.Join(", ", page.Entities.Select(x => x["order_number"]))}; more: {page.HasNextPage}");
}
```

Check it in the database:

```sql
SELECT "order_number" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D421' AND "SK" > 'ORDER#SO-D4205'
```

Expect: 7 items, SO-D4206 to SO-D4212.

#### 3. A page number

Page 3 straight away: ARX8001. Page with cookies.

In scope: `service` (ArxDynamoDbService).

```csharp
try
{
    await service.RetrieveMultipleAsync(Query(new ArxPagingInfo { Count = 5, PageNumber = 3 }));
}
catch (ArxNotSupportedException exception)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

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

Expect: 12 items (PartiQL pages by its own NextToken, never by number).

### A customer and its orders in one request

Real case 1 of RetrieveMultiple.

The single-table design's reason to exist: a customer and its orders in one request, in two scenarios: the whole item collection, and a page of it.

Good to know:

`QueryPartitionAsync` reads every item of the partition a key gives, whatever its entity, and returns each as the entity its `entity_type` names, in sort key order: here the orders (`ORDER#…`) and then the profile (`PROFILE`). A relational database would join two tables; DynamoDB stores them together so it needn't.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var customer = new ArxEntity("customer");
customer["customer_id"] = "C-D431";
customer["name"] = "Adventure Works Cycles";
var batch = new List<ArxEntity> { customer };
for (int i = 1; i <= 4; i++)
{
    var order = new ArxEntity("sales_order");
    order["customer_id"] = "C-D431";
    order["order_number"] = $"SO-D43{i}";
    order["status"] = "OPEN";
    order["ordered_on"] = $"2026-10-2{i}";
    order["total"] = 100m * i;
    batch.Add(order);
}
await service.CreateMultipleAsync(batch);
```

#### 1. The whole collection

The customer's partition: four orders and the profile, each its own entity.

In scope: `service` (ArxDynamoDbService).

```csharp
var key = new ArxEntity("customer");
key["customer_id"] = "C-D431";

var items = await service.QueryPartitionAsync(key);

var profile = items.Entities.Single(x => x.EntityName == "customer");
var orders = items.Entities.Where(x => x.EntityName == "sales_order").ToList();
Console.WriteLine($"{profile["name"]}: {orders.Count} orders, {orders.Sum(x => (decimal)x["total"]!)} in all");
```

Check it in the database:

```sql
SELECT "SK", "entity_type" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D431'
```

Expect: 5 items: ORDER#SO-D431 to ORDER#SO-D434 (sales_order) and PROFILE (customer).

#### 2. A page of it

Two items at a time, with the cookie.

In scope: `service` (ArxDynamoDbService).

```csharp
var key = new ArxEntity("customer");
key["customer_id"] = "C-D431";

var pages = 0;
string? cookie = null;

do
{
    var page = await service.QueryPartitionAsync(key, new ArxPagingInfo { Count = 2, PagingCookie = cookie });
    pages++;
    cookie = page.PagingCookie;
}
while (cookie != null);

Console.WriteLine($"read in {pages} pages");
```

Check it in the database:

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

Expect: 5 items.

### Orders by status, through an index

Real case 2 of RetrieveMultiple.

The operations screen: every open order, across customers, through the GSI1 index, in three scenarios: by status, by status and a date range, and the count of open orders.

Good to know:

The model gives orders a second key, `GSI1`: `STATUS#{status}` and the order date. A query whose Equal conditions give the index's partition (the status) reads the index; ARX picks it, or `IndexName` names it. An index is eventually consistent: a write shows there a moment later. A Count aggregate is allowed; totals of pages aren't.

Usings:

```csharp
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>();
foreach (var (customerId, number, status, on) in new[]
{
    ("C-D441", "SO-D441", "SHIPPED", "2026-09-01"), ("C-D441", "SO-D442", "SHIPPED", "2026-09-15"),
    ("C-D442", "SO-D443", "SHIPPED", "2026-10-01"), ("C-D443", "SO-D444", "SHIPPED", "2026-10-20"),
    ("C-D443", "SO-D445", "CANCELLED", "2026-10-21")
})
{
    var order = new ArxEntity("sales_order");
    order["customer_id"] = customerId;
    order["order_number"] = number;
    order["status"] = status;
    order["ordered_on"] = on;
    orders.Add(order);
}
await service.CreateMultipleAsync(orders);
```

#### 1. By status

Every shipped order, oldest first (the index's sort key is the date).

In scope: `service` (ArxDynamoDbService).

```csharp
var query = Shipped();
query.AddOrder("ordered_on", ArxOrderType.Ascending);

var found = await service.RetrieveMultipleAsync(query, new ArxDynamoReadOptions { IndexName = "GSI1", ConsistentRead = false });

Console.WriteLine(string.Join(", ", found.Entities.Select(x => $"{x["order_number"]} {x["ordered_on"]}")));
```

Defined in the sample, used above:

```csharp
private static ArxQueryExpression Shipped()
{
    var query = new ArxQueryExpression("sales_order");
    query.ColumnSet.AddColumns("order_number", "customer_id", "ordered_on");
    query.Criteria.AddCondition("status", ArxConditionOperator.Equal, "SHIPPED");
    return query;
}
```

Check it in the database:

```sql
SELECT "order_number", "ordered_on" FROM "crm"."GSI1" WHERE "GSI1PK" = 'STATUS#SHIPPED'
```

Expect: 4 items, SO-D441 to SO-D444 (among any other shipped orders).

#### 2. By status and date

Shipped in October: the date range is the index's sort key range.

In scope: `service` (ArxDynamoDbService).

```csharp
var query = Shipped();
query.Criteria.AddCondition("ordered_on", ArxConditionOperator.Between, "2026-10-01", "2026-10-31");

var found = await service.RetrieveMultipleAsync(query, new ArxDynamoReadOptions { ConsistentRead = false });

Console.WriteLine(string.Join(", ", found.Entities.Select(x => x["order_number"])));
```

Check it in the database:

```sql
SELECT "order_number" FROM "crm"."GSI1" WHERE "GSI1PK" = 'STATUS#SHIPPED' AND "GSI1SK" BETWEEN '2026-10-01' AND '2026-10-31'
```

Expect: SO-D443 and SO-D444.

#### 3. How many

How many orders are shipped: a Count aggregate, counted by DynamoDB (Select COUNT).

In scope: `service` (ArxDynamoDbService).

```csharp
var query = new ArxQueryExpression("sales_order");
query.AggregateInfo = new ArxAggregateInfo();
query.AggregateInfo.AddAggregateColumn("shipped", "", ArxAggregateOperator.Count);
query.Criteria.AddCondition("status", ArxConditionOperator.Equal, "SHIPPED");

var found = await service.RetrieveMultipleAsync(query, new ArxDynamoReadOptions { ConsistentRead = false });

Console.WriteLine($"shipped orders: {found.Entities[0]["shipped"]}");
```

Check it in the database:

```sql
SELECT "order_number" FROM "crm"."GSI1" WHERE "GSI1PK" = 'STATUS#SHIPPED'
```

Expect: as many items as the count.

### Catch an injection attempt

Real case 3 of RetrieveMultiple.

A search endpoint whose sort and filter fields come from the request, attacked three ways: expression syntax in a filter name, an index name that isn't in the model, and a partition value written to read every customer; then the honest search.

Good to know:

Names reach DynamoDB as placeholders, so expression syntax in one is ARX1017. An index name is checked against the model. A value is only a value: the partition it names is one key, and ARX sends no PartiQL.

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
foreach (var (customerId, number) in new[] { ("C-D451", "SO-D451"), ("C-D452", "SO-D452") })
{
    var order = new ArxEntity("sales_order");
    order["customer_id"] = customerId;
    order["order_number"] = number;
    order["status"] = "OPEN";
    order["ordered_on"] = "2026-11-01";
    await service.CreateAsync(order);
}
```

#### 1. Expression syntax in a filter

A filter on "status OR attribute_exists(PK)": ARX1017.

In scope: `service` (ArxDynamoDbService).

```csharp
var query = OrdersOf("C-D451");
query.Criteria.AddCondition("status OR attribute_exists(PK)", ArxConditionOperator.Equal, "OPEN");

try
{
    await service.RetrieveMultipleAsync(query);
}
catch (ArxValidationException exception) when (exception.Code == ArxErrorCode.SuspectedInjection)
{
    Console.WriteLine(exception.Message);
}
```

Defined in the sample, used above:

```csharp
private static ArxQueryExpression OrdersOf(string customerId)
{
    var query = new ArxQueryExpression("sales_order");
    query.Criteria.AddCondition("customer_id", ArxConditionOperator.Equal, customerId);
    return query;
}
```

Check it in the database:

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

Expect: 1 item, SO-D451.

#### 2. An unknown index

An index name from the request that the model doesn't have: refused before DynamoDB.

In scope: `service` (ArxDynamoDbService).

```csharp
try
{
    await service.RetrieveMultipleAsync(OrdersOf("C-D451"), new ArxDynamoReadOptions { IndexName = "AllCustomersIndex" });
}
catch (ArxException exception)
{
    Console.WriteLine(exception.Message);
}
```

Note: In the AWS CLI: aws dynamodb describe-table --table-name crm --endpoint-url http://localhost:18000 one global secondary index, GSI1.

#### 3. A value that widens the read

The customer id "C-D451' OR '1'='1": one partition that doesn't exist, so no orders at all.

In scope: `service` (ArxDynamoDbService).

```csharp
var found = await service.RetrieveMultipleAsync(OrdersOf("C-D451' OR '1'='1"));

Console.WriteLine($"{found.Entities.Count} orders");

var own = await service.RetrieveMultipleAsync(OrdersOf("C-D451"));
Console.WriteLine($"the honest search: {string.Join(", ", own.Entities.Select(x => x["order_number"]))}");
```

Check it in the database:

```sql
SELECT "order_number" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D451'' OR ''1''=''1'
```

Expect: no items.

### Scan when you must

Advanced case 1 of RetrieveMultiple.

Reads with no partition, in three scenarios: refused by default, a scan you allow, and a text search, which needs you to say that DynamoDB compares case-sensitively.

Good to know:

A scan reads the whole table and pays for every item it reads, filter or not, so ARX makes it explicit: `new ArxDynamoReadOptions { AllowScan = true }`. In a single table the scan keeps to the entity's items. DynamoDB's `contains` and `begins_with` are case-sensitive, unlike the SQL providers' Like, so ARX asks for `CaseSensitivePatterns = true` before using them.

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 products = new[] { ("SKU-D461", "Trail tent", 320m), ("SKU-D462", "trail lamp", 25m), ("SKU-D463", "Camp stove", 60m) }.Select(x =>
{
    var product = new ArxEntity("product");
    product["sku"] = x.Item1;
    product["name"] = x.Item2;
    product["price"] = x.Item3;
    return product;
}).ToList();
await service.CreateMultipleAsync(products);
```

#### 1. Refused by default

Products over 50: no key condition, so ARX8001.

In scope: `service` (ArxDynamoDbService).

```csharp
try
{
    await service.RetrieveMultipleAsync(OverFifty());
}
catch (ArxNotSupportedException exception)
{
    Console.WriteLine(exception.Message);
}
```

Defined in the sample, used above:

```csharp
private static ArxQueryExpression OverFifty()
{
    var query = new ArxQueryExpression("product");
    query.ColumnSet.AddColumns("sku", "price");
    query.Criteria.AddCondition("price", ArxConditionOperator.GreaterThan, 50m);
    query.Criteria.AddCondition("sku", ArxConditionOperator.BeginsWith, "SKU-D46");
    return query;
}
```

Check it in the database:

```sql
SELECT "sku", "price" FROM "product" WHERE "price" > 50 AND begins_with("sku", 'SKU-D46')
```

Expect: 2 items, SKU-D461 and SKU-D463 (PartiQL scans for this).

#### 2. A scan you allow

The same query with AllowScan.

In scope: `service` (ArxDynamoDbService).

```csharp
var found = await service.RetrieveMultipleAsync(OverFifty(), new ArxDynamoReadOptions { AllowScan = true, CaseSensitivePatterns = true });

Console.WriteLine(string.Join(", ", found.Entities.Select(x => x["sku"]).OrderBy(x => x)));
```

Check it in the database:

```sql
SELECT "sku" FROM "product" WHERE "price" > 50 AND begins_with("sku", 'SKU-D46')
```

Expect: 2 items, SKU-D461 and SKU-D463.

#### 3. A text search

Names containing "rail": "Trail tent" and "trail lamp" both match; "Trail" alone wouldn't find the lamp.

In scope: `service` (ArxDynamoDbService).

```csharp
var query = new ArxQueryExpression("product");
query.Criteria.AddCondition("name", ArxConditionOperator.Contains, "rail");
query.Criteria.AddCondition("sku", ArxConditionOperator.BeginsWith, "SKU-D46");

try
{
    await service.RetrieveMultipleAsync(query, new ArxDynamoReadOptions { AllowScan = true });
}
catch (ArxNotSupportedException exception)
{
    Console.WriteLine(exception.Message);
}

var found = await service.RetrieveMultipleAsync(query, new ArxDynamoReadOptions { AllowScan = true, CaseSensitivePatterns = true });
Console.WriteLine(string.Join(", ", found.Entities.Select(x => x["name"]).OrderBy(x => x)));
```

Check it in the database:

```sql
SELECT "name" FROM "product" WHERE contains("name", 'rail') AND begins_with("sku", 'SKU-D46')
```

Expect: Trail tent and trail lamp.

### A security check around searches

Advanced case 2 of RetrieveMultiple.

A security check around searches (`SecurityGuard`), in three scenarios: a normal search, an attack in the requested columns that is logged, and repeated attacks that block the user.

Good to know:

The endpoint lets the client choose the columns it wants back, so the names come from the user. ARX refuses one with expression syntax (ARX1017); the guard logs it and blocks the user at the third strike.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var guard = new SecurityGuard(blockAfter: 3);
var order = new ArxEntity("sales_order");
order["customer_id"] = "C-D471";
order["order_number"] = "SO-D471";
order["status"] = "OPEN";
order["ordered_on"] = "2026-11-02";
order["total"] = 75m;
await service.CreateAsync(order);
```

#### 1. A normal search

fields=order_number,total.

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

```csharp
var result = await OrdersAsync(service, guard, "ana", "C-D471", "order_number", "total");

Console.WriteLine($"ana: {result.Outcome}, {result.Value!.Entities.Count} order");
```

Defined in the sample, used above:

```csharp
private static Task<GuardResult<ArxEntityCollection>> OrdersAsync(ArxDynamoDbService service, SecurityGuard guard, string user, string customerId, params string[] fields)
{
    var query = new ArxQueryExpression("sales_order");
    query.ColumnSet.AddColumns(fields);
    query.Criteria.AddCondition("customer_id", ArxConditionOperator.Equal, customerId);

    return guard.RunAsync(user, "list orders", NameSource.UserInput, () => service.RetrieveMultipleAsync(query));
}
```

Check it in the database:

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

Expect: 1 item, SO-D471, 75.

#### 2. An attack is logged

fields="total) OR (PK": expression syntax in a column name, refused and logged.

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

```csharp
var result = await OrdersAsync(service, guard, "mallory", "C-D471", "total) OR (PK");

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

Note: Nothing to check: no request reached DynamoDB.

#### 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", "total; SELECT * FROM crm" })
{
    await OrdersAsync(service, guard, "mallory", "C-D471", probe);
}

var blocked = await OrdersAsync(service, guard, "mallory", "C-D471", "order_number");

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

Note: Nothing to check: the guard answered without calling ARX.

### Protect paging cookies

Advanced case 3 of RetrieveMultiple.

Protect the paging cookies your API hands to clients, in three scenarios: sealed by default, one key shared by every instance of the application, and plain cookies when you choose them.

Good to know:

A paging cookie carries the last item's key values: here a customer's email. A client could decode it, and change it to page from somewhere else. So ARX seals cookies by default (encrypted and authenticated, AES-256-GCM): a client can neither read nor change one. Without a key from you, ARX seals with a random key of the process, which every service in the process shares: right for one process. When several instances serve the same clients, or cookies must survive a restart, give every instance the same `PagingCookieKey` (32 bytes or more, from your configuration or secret store); otherwise a cookie that reaches another instance is ARX1001. `PagingCookieProtection` can also be `Signed` (readable, not changeable) or `Plain`.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var orders = Enumerable.Range(1, 6).Select(i =>
{
    var order = new ArxEntity("sales_order");
    order["customer_id"] = "ana@example.com";
    order["order_number"] = $"SO-D44{i}";
    order["status"] = "OPEN";
    order["ordered_on"] = "2026-11-10";
    return order;
}).ToList();
await service.CreateMultipleAsync(orders);
```

#### 1. Sealed by default

The default: the cookie hides the email it pages by, and the next page reads it.

In scope: `service` (ArxDynamoDbService).

```csharp
var first = await service.RetrieveMultipleAsync(Query());
Console.WriteLine($"protection {service.PagingCookieProtection}; the cookie shows the email: {Readable(first.PagingCookie).Contains("ana@example.com")}");

var second = await service.RetrieveMultipleAsync(Query(first.PagingCookie));
Console.WriteLine($"next page: {string.Join(", ", second.Entities.Select(x => x["order_number"]))}");
```

Defined in the sample, used above:

```csharp
private static ArxQueryExpression Query(string? cookie = null)
{
    var query = new ArxQueryExpression("sales_order");
    query.ColumnSet.AddColumns("order_number");
    query.Criteria.AddCondition("customer_id", ArxConditionOperator.Equal, "ana@example.com");
    query.PageInfo = new ArxPagingInfo { Count = 2, PagingCookie = cookie };
    return query;
}

// What a client could read from a cookie: its bytes as text.
private static string Readable(string cookie)
{
    var base64 = cookie.Replace('-', '+').Replace('_', '/');
    return Encoding.UTF8.GetString(Convert.FromBase64String(base64.PadRight(base64.Length + (4 - base64.Length % 4) % 4, '=')));
}
```

Check it in the database:

```sql
SELECT "order_number" FROM "crm" WHERE "PK" = 'CUSTOMER#ana@example.com' AND begins_with("SK", 'ORDER#')
```

Expect: 6 items, SO-D441 to SO-D446; the second page holds SO-D443 and SO-D444.

#### 2. One key for every instance

Two instances of the application: with one shared key, either reads the other's cookies; with their own, it is ARX1001.

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

```csharp
// In production the key comes from configuration or a secret store, the same on every instance.
var sharedKey = ArxPagingCookie.NewKey();
var instanceA = new ArxDynamoDbService(client, model, SampleModel.Catalog()) { PagingCookieKey = sharedKey };
var instanceB = new ArxDynamoDbService(client, model, SampleModel.Catalog()) { PagingCookieKey = sharedKey };

var first = await instanceA.RetrieveMultipleAsync(Query());
var second = await instanceB.RetrieveMultipleAsync(Query(first.PagingCookie));
Console.WriteLine($"instance B continued: {string.Join(", ", second.Entities.Select(x => x["order_number"]))}");

var withItsOwnKey = new ArxDynamoDbService(client, model, SampleModel.Catalog()) { PagingCookieKey = ArxPagingCookie.NewKey() };

try
{
    await withItsOwnKey.RetrieveMultipleAsync(Query(first.PagingCookie));
}
catch (ArxValidationException exception)
{
    Console.WriteLine(exception.Message);
}
```

Defined in the sample, used above:

```csharp
private static ArxQueryExpression Query(string? cookie = null)
{
    var query = new ArxQueryExpression("sales_order");
    query.ColumnSet.AddColumns("order_number");
    query.Criteria.AddCondition("customer_id", ArxConditionOperator.Equal, "ana@example.com");
    query.PageInfo = new ArxPagingInfo { Count = 2, PagingCookie = cookie };
    return query;
}
```

Check it in the database:

```sql
SELECT "order_number" FROM "crm" WHERE "PK" = 'CUSTOMER#ana@example.com' AND begins_with("SK", 'ORDER#')
```

Expect: 6 items (nothing is written: the check is in the cookie).

#### 3. Plain, when you choose it

Plain cookies: readable and changeable by clients, so only where their values aren't sensitive.

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

```csharp
var service = new ArxDynamoDbService(client, model, SampleModel.Catalog()) { PagingCookieProtection = ArxPagingCookieProtection.Plain };
var first = await service.RetrieveMultipleAsync(Query());
Console.WriteLine($"the cookie shows the email: {Readable(first.PagingCookie).Contains("ana@example.com")}");
```

Check it in the database:

```sql
SELECT "order_number" FROM "crm" WHERE "PK" = 'CUSTOMER#ana@example.com' AND begins_with("SK", 'ORDER#')
```

Expect: 6 items.

