# ARX.SDK for Amazon DynamoDB: the whole knowledge base

Every file of this knowledge base in one, in this order: Guide, Types, The sample application, Supported data types, Error codes, Create, CreateMultiple, Update, UpdateMultiple, Delete, DeleteMultiple, Retrieve, RetrieveMultiple, Benchmarks.

# ARX.SDK for Amazon DynamoDB: a guide for AI assistants

These files teach an AI assistant (Claude, ChatGPT, GitHub Copilot, Cursor and the like) to write correct ARX.SDK code
for Amazon DynamoDB. They are generated from the same verified samples, READMEs and doc comments as the documentation
site: every code example in them was compiled and run against DynamoDB Local.

ARX.SDK is an entity-based data access library for .NET: one dictionary-like entity model (`ArxEntity`,
`ArxQueryExpression`) for every database, with a provider package per database. This knowledge base covers the
DynamoDB provider, `ARX.SDK.DynamoDb`, built on the AWS SDK for .NET (`AWSSDK.DynamoDBv2`).

## Instructions for the assistant

You help a developer write C# code that uses ARX.SDK with Amazon DynamoDB (package `ARX.SDK.DynamoDb`). Follow these
rules.

1. **Use ARX's types for database work.** Create, update, delete and read items, and run transactions through
   `ArxDynamoDbService`, `ArxDynamoConnection` and `ArxTransaction`. Don't write `PutItemRequest`, `QueryRequest` or
   `DynamoDBContext` code for work ARX does. The application makes the `IAmazonDynamoDB` client (endpoint, region,
   credentials from the AWS SDK's chain) and gives it to the service.
2. **Don't invent API.** Use only the types, methods, properties and parameters listed in `arx-dynamodb-types.md`. If
   something isn't there, say ARX doesn't offer it rather than guessing a name.
3. **An item is an `ArxEntity`.** `var product = new ArxEntity("product");` then values by attribute name:
   `product["name"] = "Trail running shoe";`.
4. **Lay out the entities once, at startup, in an `ArxDynamoModel`:** a table of the entity's own,
   `.Table("product", partitionKey: "sku")`, or a single table whose keys are templates,
   `.SingleTable("crm", "PK", "SK", t => t.Entity("customer", pk: "CUSTOMER#{customer_id}", sk: "PROFILE")
   .Entity("sales_order", pk: "CUSTOMER#{customer_id}", sk: "ORDER#{order_number}").Index("GSI1", "GSI1PK", "GSI1SK")
   .EntityIndex("sales_order", "GSI1", pk: "STATUS#{status}", sk: "{ordered_on}"))`. ARX writes `PK`, `SK`, the index
   keys and `entity_type`; the application never sets them. ARX never creates tables: the tables and indexes the model
   names must exist (made by the application's infrastructure code or the AWS CLI), with the same keys.
5. **Declare ids in an `ArxSchemaCatalog`:** DynamoDB generates none, so ids are supplied:
   `.Table("customer", ArxIdColumn.Supplied<string>("customer_id"))`. A generated id is ARX8001: give
   `Guid.CreateVersion7()` or the application's own. Lookups are declared: `.Lookup("sales_order", "customer_id", "customer")`.
   Then `new ArxDynamoDbService(client, model, catalog)`, one for the application.
6. **Every call is asynchronous** (`CreateAsync`, `CreateMultipleAsync`, `UpdateAsync`, `UpdateMultipleAsync`,
   `DeleteAsync`, `DeleteMultipleAsync`, `RetrieveAsync`, `RetrieveMultipleAsync`, `QueryPartitionAsync`); await it,
   and pass the caller's `CancellationToken`.
7. **A create never overwrites** (ARX3001 for an existing key). Writes of several items are all or nothing, in one
   DynamoDB transaction of at most 100 items; more is ARX8001 unless `new ArxDynamoWriteOptions { AllowPartial = true }`.
8. **Update and Delete find items by their key:** the id (with the key's other attributes, such as an order's
   `customer_id`) or `ArxMatch.Columns(...)` naming exactly the key's attributes. A match by other columns works when they
   give a partition key, of the table or an index (`ArxMatch.Columns("customer_id")`, `ArxMatch.Columns("status")` through
   GSI1): ARX queries it, then writes each item found by its key, all or nothing, at most 100. Columns that give no
   partition are ARX8001 (a write never scans). To give an item a new id, set its `RecordId` to the old one and the id
   attribute to the new, and pass `new ArxDynamoWriteOptions { AllowKeyChange = true }`. An update writes only the attributes set; null removes an attribute. When
   an attribute an index is keyed by changes, set the index key's other attributes too. A create that leaves out an index key's
   attribute writes the item outside that index (a sparse GSI): give every attribute an index is keyed by when the item
   must be found through it.
   **To change or delete every item a filter matches** (from 0.1.0-preview.5): `UpdateMultipleAsync(values, filter)`
   or `DeleteMultipleAsync("sales_order", filter)`, the entity carrying only the values (no id or key attribute:
   ARX1001). The filter must give a partition key with Equal, of the table or an index (ARX8001 otherwise) and match at
   most 100 items; each is written conditioned on still matching, and the call returns the count. An empty filter is
   ARX1001. `CreateMultipleAsync(entities, new ArxCreateMultipleOptions { ReturnIds = false })` returns
   `ArxRecordId.None` for each.
9. **Reads need the partition.** `RetrieveMultipleAsync` takes an `ArxQueryExpression` whose Equal conditions give the
   partition key's attributes, of the table or an index; without them it is ARX8001 unless
   `new ArxDynamoReadOptions { AllowScan = true }`. Order by the sort key only. Page with each page's `PagingCookie`
   (a page number above 1 and totals are ARX8001). Contains and BeginsWith need `CaseSensitivePatterns = true`. No
   joins: read a single table's item collection with `QueryPartitionAsync(new ArxEntity("customer") { ["customer_id"] = id })`.
   `RetrieveAsync` returns null when nothing matches. Execute is ARX8001.
   **Query cheat sheet** (the exact names; there are no others, such as `AddGroupBy` or `MoreRecords`):
   `var query = new ArxQueryExpression("sales_order");` · columns: `query.ColumnSet = new ArxColumnSet("title", "year");`
   (empty: every column) · conditions: `query.Criteria.AddCondition("year", ArxConditionOperator.Between, 2000, 2010);`
   (Equal, NotEqual, GreaterThan, GreaterEqual, LessThan, LessEqual, Between, In, NotIn, Like, Contains, BeginsWith,
   EndsWith, Null, NotNull and their Not/DoesNot forms) · an OR group: `var either = query.Criteria.AddFilter(ArxLogicalOperator.Or);`
   then `either.AddCondition(…)` · order: `query.AddOrder("title", ArxOrderType.Ascending);`
   · no joins and no grouping (ARX8001); a lone Count aggregate is answered · paging by cookie only:
   `query.PageInfo = new ArxPagingInfo { Count = 20 }`, then after each page
   `query.PageInfo = query.PageInfo.Next(result)` until it is null (from 0.1.0-preview.5; it moves by the cookie here and
   by the page number on the SQL providers, so the same loop runs everywhere: a loop that only passes the cookie on reads
   page 1 again on PostgreSQL, SQL Server and MySQL).
10. **Values are never part of an expression:** ARX sends names as placeholders and values as values. A name with `#`,
    `:` or SQL syntax is ARX1017, but a valid name is used as given and reaches any attribute of the item: map a name
    from a user to one the application allows (an allow list), or set `Strict = true` with a preloaded schema.
11. **Dates are sortable UTC text:** a `DateTime` is stored as `2026-11-09T10:30:00.0000000Z` (Local converted,
    Unspecified taken as UTC), a `DateTimeOffset` as its UTC instant, a `DateOnly` as `2026-11-09`. Never
    `default(DateTime)` (ARX1013: use `null`). Numbers are read back as `decimal` unless the preload declares a type.
12. **Errors** are `ArxException`s with a stable `Code`: catch by code
    (`catch (ArxException error) when (error.Code == ArxErrorCode.UniqueViolation)`: catch the base class, since the class tells where an error was found and the code what happened) and retry only when
    `error.IsTransient` is true (throttling, transaction conflicts). `arx-dynamodb-error-codes.md` lists every code.
13. **Transactions:** `await using var connection = new ArxDynamoConnection(service);`,
    `await using var transaction = await connection.BeginTransactionAsync();`, pass `transaction` to each write, then
    `await transaction.CommitAsync();`. The writes are sent together at the commit (one `TransactWriteItems`, at most
    100); a failure shows at the commit and writes nothing. Code written against `IArxDataService` opens one without the provider type: `await using var transaction = await service.BeginTransactionAsync();` (from 0.1.0-preview.5). Batch scopes have no savepoints: leave `Savepoints` unset (`true` is ARX8001), and a failed call rolls the scope back.
14. **Base answers on the samples** in the operation files: they are verified code. Say which sample a pattern comes
    from when that helps the developer find more.
15. **Reading values and namespaces.** `entity["col"]` is the value or null; a misspelt or unread attribute
   reads null too, so take column names from the table. `GetAttributeValue<T>` of NULL is `default(T)` (0 for `int`:
   ask for `int?` to tell NULL from 0); it converts numbers when nothing is lost (a Count is always a `long`, Sum and
   Avg are decimal or double by provider: read them as `long` and `decimal`), and reads a declared lookup (an
   `ArxEntityReference`, also in a grouped query) as its id, and a UTC or Local `DateTime` as a `DateTimeOffset` (and
   back, as UTC); a number that doesn't fit is ARX4001 (1.5 as `int`), any other mismatch is ARX4004, an `ArxException`.
   Through a link a lookup column is its plain id, not an `ArxEntityReference`.
   Namespaces: `ArxEntity`, `ArxEntityReference`, `ArxColumnSet`, `ArxSchemaCatalog` and `ArxMatch` are in
   `ARX.SDK.Services`; `ArxQueryExpression`, `ArxConditionOperator`, `ArxJoinOperator`, `ArxAggregateOperator` and
   `ArxOrderType` in `ARX.SDK.Query`; `ArxException` and `ArxErrorCode` in `ARX.SDK`; `ArxDynamoDbService` and `ArxDynamoConnection` in `ARX.SDK.DynamoDb.Services`. Write every `using` a file needs.
   Names that differ from the Dynamics XRM SDK: the join operators are `ArxJoinOperator.Inner` and `ArxJoinOperator.Left` (not XRM's `LeftOuter`);
   a link's columns are `link.ColumnSet`, its alias `link.EntityAlias`, its conditions `link.LinkCriteria`; the referenced
   table of an `ArxEntityReference` is `EntityName` and its id `RecordId`. `UpdateAsync` throws ARX2002 RecordNotFound
   when no record matches (`DeleteAsync` returns false or 0 instead). A page's `TotalRecordCount` is an `int`; an aggregate
   Count is a `long`. On DynamoDB a declared lookup reads back without its display name (`Name` is null: no joins).
16. **Ids and linked columns are plain values.** `entity.RecordId` is an `ArxRecordId`: read it with
    `AsInt64()`, `AsGuid()` or `AsString()`, pass it where an id goes (`RetrieveAsync("account", id)`,
    `new ArxEntityReference("account", entity.RecordId!)`), or pass the plain value instead. `ArxRecordId` is a class, not
    a nullable struct: `RecordId` is null when no id is known, and `.Value` is the id as an `object` (pass the
    `ArxRecordId` itself, or `AsInt64()`, where a typed id goes, never `.Value`). DynamoDB has no joins (ARX8001): read related items with their own query. Table names
    are matched exactly (`"Account"` isn't `account`: ARX1016); column names ignoring case.

## How to use these files

- **One file:** `arx-dynamodb-all.md` holds everything; add it to your assistant's knowledge (a project's files, a
  custom assistant's knowledge, or a file in your repository that your coding assistant reads).
- **Some files:** start with this guide and the types file, then add the operations you use.
- **As instructions:** paste "Instructions for the assistant" above into your assistant's custom instructions or your
  repository's instruction file (for example `CLAUDE.md`, `AGENTS.md` or `.github/copilot-instructions.md`).
- **As a skill:** coding agents that load skills (Claude Code and others) can use the `arx-sdk` skill, which holds
  these files with a workflow for writing and checking ARX.SDK code; ready-made instruction files for Claude Code,
  GitHub Copilot, Cursor and `AGENTS.md` are on the AI Instructions page.

## Packages and setup

- .NET: .NET SDK 10.0.100 or later 10.0.x (target `net10.0`); database: Amazon DynamoDB, or DynamoDB Local for development (tested with DynamoDB Local 2.5.2); driver: AWSSDK.DynamoDBv2 4.0.110 or later (the lowest version, which the provider is built on; Versions and compatibility lists the versions it is tested on).
- `ARX.SDK.DynamoDb` 1.0.0-rc.1: The Amazon DynamoDB provider. It brings ARX.SDK and AWSSDK.DynamoDBv2 4.0.110 with it. Install: `dotnet add package ARX.SDK.DynamoDb --version 1.0.0-rc.1`.
- The samples read the connection string from the `ARX_SAMPLES_DYNAMODB` environment variable, for example `http://localhost:8000`. Never put real credentials in code.

## Quick reference

The first scenario of each operation's first sample: verified code.

### Create: Create one record, in either design, A table of its own

In scope: `client` (IAmazonDynamoDB).

```csharp
var model = ArxDynamoModel.Create()
    .Table("product", partitionKey: "sku")
    .Build();

var service = new ArxDynamoDbService(client, model);

var product = new ArxEntity("product");
product["sku"] = "SKU-D101";
product["name"] = "Trail running shoe";
product["price"] = 129.90m;

await service.CreateAsync(product);

Console.WriteLine("created product SKU-D101");
```

### CreateMultiple: Create many records in one call, All stored together

In scope: `service` (ArxDynamoDbService).

```csharp
IReadOnlyList<ArxRecordId> ids = await service.CreateMultipleAsync(new[]
{
    Product("SKU-D201", "Rain jacket"),
    Product("SKU-D202", "Fleece"),
    Product("SKU-D203", "Beanie")
});

Console.WriteLine($"created {string.Join(", ", ids.Select(x => x.AsString()))}");
```

Defined in the sample, used above:

```csharp
private static ArxEntity Product(string sku, string name)
{
    var product = new ArxEntity("product");
    product["sku"] = sku;
    product["name"] = name;
    return product;
}
```

### Update: Update one record, By the id Create returned

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

```csharp
var change = new ArxEntity("product") { RecordId = id };
change["price"] = 79.00m;

int changed = await service.UpdateAsync(change);

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

### UpdateMultiple: Update many records in one call, A price per product

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");
```

### Delete: Delete one record, By the id Create returned

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

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

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

### DeleteMultiple: Delete many records in one call, Several ids at once

In scope: `service` (ArxDynamoDbService).

```csharp
int deleted = await service.DeleteMultipleAsync("product", new[] { "SKU-D302", "SKU-D303", "SKU-D399" });

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

### Retrieve: Retrieve one record, By id

In scope: `service` (ArxDynamoDbService).

```csharp
ArxEntity? found = await service.RetrieveAsync("product", "SKU-D401");

Console.WriteLine($"{found!["name"]}, {found["price"]}");
```

### RetrieveMultiple: Retrieve many records, A customer's orders

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"]}")));
```

## The other files

- `arx-dynamodb-types.md`: Every public type: its declaration, constructors, properties, methods and their parameters, with what each does.
- `arx-dynamodb-sample-application.md`: The files the samples build on (the schema catalog, dbContext and its Service, the output helpers): what the samples' code refers to that isn't in the packages.
- `arx-dynamodb-data-types.md`: What each .NET type is stored as in Amazon DynamoDB, the date and time rules, and what each Amazon DynamoDB type reads back as.
- `arx-dynamodb-error-codes.md`: Every ARX error code: what it means, whether it is safe to retry, and the Amazon DynamoDB errors it comes from.
- `arx-dynamodb-create.md`: What a create does, its rules, and every Create sample: each scenario's code and the PartiQL statement that checks it.
- `arx-dynamodb-create-multiple.md`: What a CreateMultiple does, its rules, and every CreateMultiple sample: each scenario's code and the PartiQL statement that checks it.
- `arx-dynamodb-update.md`: What an update does, its rules, and every Update sample: each scenario's code and the PartiQL statement that checks it.
- `arx-dynamodb-update-multiple.md`: What an UpdateMultiple does, its rules, and every UpdateMultiple sample: each scenario's code and the PartiQL statement that checks it.
- `arx-dynamodb-delete.md`: What a delete does, its rules, and every Delete sample: each scenario's code and the PartiQL statement that checks it.
- `arx-dynamodb-delete-multiple.md`: What a DeleteMultiple does, its rules, and every DeleteMultiple sample: each scenario's code and the PartiQL statement that checks it.
- `arx-dynamodb-retrieve.md`: What a retrieve does, its rules, and every Retrieve sample: each scenario's code and the PartiQL statement that checks it.
- `arx-dynamodb-retrieve-multiple.md`: What a RetrieveMultiple does, its rules, and every RetrieveMultiple sample: each scenario's code and the PartiQL statement that checks it.
- `arx-dynamodb-benchmarks.md`: How ARX compares with the low-level AWS client, DynamoDBContext and BatchWriteItem, measured: the only source for performance claims.
- `arx-dynamodb-all.md`: everything in one file.

---

# ARX.SDK types (Amazon DynamoDB)

Every public type of ARX.SDK and ARX.SDK.DynamoDb, by purpose (the less common ones last, under Other public types). Members are listed with their parameters; interop overloads that take an ADO.NET `IDbConnection` are left out: application code passes an `ArxTransaction`.

## Services and connections

The service that talks to the database, and the connection, transaction and batch scope types that group calls.

### ArxDynamoDbService

class in `ARX.SDK.DynamoDb.Services`, package `ARX.SDK.DynamoDb`.

```csharp
public class ArxDynamoDbService
```

The DynamoDB service: Create, Update, Delete, Retrieve and RetrieveMultiple against the tables an `ArxDynamoModel` describes, in either design: a table per entity, or a single table with key templates. Give it the application's `IAmazonDynamoDB` client (the AWS SDK's own configuration: credentials, region, and a service URL for DynamoDB Local), the model and, usually, the schema catalog. It holds no connection, so one instance serves the application.

Constructors:

- `new ArxDynamoDbService(IAmazonDynamoDB client, ArxDynamoModel model, ArxSchemaCatalog? schema = null)`: A service for the model's tables.
  - `client`: The DynamoDB client; ARX never disposes it.
  - `model`: How each entity is laid out in its table.
  - `schema`: The schema catalog: ids (supplied only), keys, lookups, selection sets, declared types; null for none.

Properties:

- `IAmazonDynamoDB Client { get; }`: The application's DynamoDB client.
- `ArxDynamoModel Model { get; }`: Where each entity lives and how its keys are made.
- `ArxSchemaCatalog Schema { get; }`: The ids, keys, lookups and selection sets; null when the service has none.
- `IArxSelectionSetCatalog SelectionSetCatalog { get; }`: The selection sets writes are validated against.
- `byte[]? PagingCookieKey { get; set; }`: The key of the paging cookies (`PagingCookie`): 32 bytes or more. Null (the default) uses a random key of the process's own (every service in it shares it), which is right for one process. An application that runs several instances behind one address, or keeps cookies across a restart, sets the same key on every instance (from its configuration or secret store): otherwise a cookie that reaches another instance is ARX1001. Set it before the first call.
- `ArxPagingCookieProtection PagingCookieProtection { get; set; }`: How paging cookies are protected: `Sealed` by default (a client can neither read nor change a cookie, which carries the last item's key values), `Signed` (readable, not changeable) or `Plain`. Set it before the first call; every instance needs the same.
- `IArxProviderCapabilities Capabilities { get; }`: What DynamoDB supports through ARX: paging by cookie, transactions as write batches sent at commit, the multiple-item writes (up to 100 all or nothing), Or in filters, queries of a named secondary index, optimistic concurrency through a version column. Not joins, grouping, page numbers, totals, Execute, pattern conditions (case-sensitive only with an opt-in) or upsert.
- `bool Strict { get; set; }`: True: the catalog's columns are an allow list; an entity the catalog doesn't list, or an attribute it doesn't list (the key attributes always count), is ARX1018 before anything is sent. Needs a catalog whose every table has columns.

Methods:

- `Task<ArxTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default)`: Begins a transaction on a connection of its own (a new `ArxDynamoConnection`), for code that works through `IArxDataService` and so can't name the provider's connection type. Pass it to the calls, then commit it; disposing it without committing rolls it back, and closes its connection: `await using var transaction = await service.BeginTransactionAsync();`
  - `cancellationToken`: Cancels the call.
- `Task<ArxRecordId> CreateAsync(ArxEntity entity, CancellationToken cancellationToken = default)`: Inserts one item and returns its id (the declared, supplied id; `None` when none is declared). The put carries `attribute_not_exists` on the partition key, so an existing key is ARX3001 and never overwritten.
  - `entity`: The record to create: its table and the attributes to write. Its `RecordId` is set to the new id.
  - `cancellationToken`: Cancels the call.
- `Task<ArxRecordId> CreateAsync(ArxEntity entity, ArxTransaction transaction, CancellationToken cancellationToken = default)`: As `CreateAsync`, in an ARX transaction: the put is sent with the transaction's other writes at commit.
  - `entity`: The record to create: its table and the attributes to write. Its `RecordId` is set to the new id.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `cancellationToken`: Cancels the call.
- `Task<IReadOnlyList<ArxRecordId>> CreateMultipleAsync(IReadOnlyList<ArxEntity> entities, CancellationToken cancellationToken = default)`: Inserts every entity, all or nothing, and returns their ids in list order: up to 100 in one `TransactWriteItems` (conditions kept, across tables). More than 100 is ARX8001 unless `AllowPartial`. The same key twice, or the same entity twice, is ARX1001.
  - `entities`: The records to create, all or nothing; their ids come back in the same order.
  - `cancellationToken`: Cancels the call.
- `Task<IReadOnlyList<ArxRecordId>> CreateMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxDynamoWriteOptions options, CancellationToken cancellationToken = default)`: As `CreateMultipleAsync`, with write options.
  - `entities`: The records to create, all or nothing; their ids come back in the same order.
  - `options`: Options of the write: `AllowPartial` lets more than 100 items go in several transactions, one after another (not all or nothing).
  - `cancellationToken`: Cancels the call.
- `Task<IReadOnlyList<ArxRecordId>> CreateMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxTransaction transaction, CancellationToken cancellationToken = default)`: As `CreateMultipleAsync`, in an ARX transaction.
  - `entities`: The records to create, all or nothing; their ids come back in the same order.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateMultipleAsync(ArxEntity values, ArxFilterExpression filter, ArxFilterWriteOptions? options = null, CancellationToken cancellationToken = default)`: Sets `values`' attributes on every item of its entity that `filter` matches and returns how many were changed. DynamoDB has no update by a condition, so ARX finds the items with one Query (the filter must give the partition key of the table or of an index, as for RetrieveMultiple: a write never scans, ARX8001), then updates them by their keys in one transaction, each conditioned on the whole filter still matching it: an item changed in between so that it no longer matches is left as it is, and not counted.
  - `values`: The entity (EntityName) and the attributes to set: null removes an attribute.
  - `filter`: The items to change: conditions on the entity's own attributes, giving a partition key.
  - `options`: AllRows for an empty filter; null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateMultipleAsync(ArxEntity values, ArxFilterExpression filter, ArxTransaction transaction, ArxFilterWriteOptions? options = null, CancellationToken cancellationToken = default)`: The update by a filter inside an ARX transaction: the items are found now and the count answered now; the writes go at commit, still conditioned on matching.
  - `values`: The entity (EntityName) and the attributes to set: null removes an attribute.
  - `filter`: The items to change: conditions on the entity's own attributes, giving a partition key.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `options`: AllRows for an empty filter; null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteMultipleAsync(string entityName, ArxFilterExpression filter, ArxFilterWriteOptions? options = null, CancellationToken cancellationToken = default)`: Deletes every item of the entity that `filter` matches and returns how many went: one Query, then the deletes by key in one transaction, each conditioned on the filter still matching. The rules are the update's: a partition key given, at most 100 items, an empty filter ARX1001.
  - `entityName`: The entity to delete from.
  - `filter`: The items to delete: conditions on the entity's own attributes, giving a partition key.
  - `options`: AllRows for an empty filter; null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteMultipleAsync(string entityName, ArxFilterExpression filter, ArxTransaction transaction, ArxFilterWriteOptions? options = null, CancellationToken cancellationToken = default)`: The delete by a filter inside an ARX transaction: found and counted now, deleted at commit.
  - `entityName`: The entity to delete from.
  - `filter`: The items to delete: conditions on the entity's own attributes, giving a partition key.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `options`: AllRows for an empty filter; null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<IReadOnlyList<ArxRecordId>> CreateMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxCreateMultipleOptions options, CancellationToken cancellationToken = default)`: Creates the items, all or nothing. DynamoDB's ids are always supplied, so the call is the same conditional puts as `CreateMultipleAsync`; with `ReturnIds` false every id returned is `None`.
  - `entities`: The records to create, all or nothing.
  - `options`: ReturnIds false when the ids aren't needed.
  - `cancellationToken`: Cancels the call.
- `Task<IReadOnlyList<ArxRecordId>> CreateMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxCreateMultipleOptions options, ArxTransaction transaction, CancellationToken cancellationToken = default)`: CreateMultiple with options inside an ARX transaction: the puts go at commit.
  - `entities`: The records to create, all or nothing.
  - `options`: ReturnIds false when the ids aren't needed.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `cancellationToken`: Cancels the call.
- `Task<ArxEntity?> RetrieveAsync(string entityName, ArxRecordId id, ArxColumnSet? columns = null, CancellationToken cancellationToken = default)`: The item with this id (the one CreateAsync returned), or null.
  - `entityName`: The table or view to read.
  - `id`: The record's id: the value Create returned, or a plain value of the declared id's type.
  - `columns`: The columns to read; null or an empty set reads every column. The declared id is always read.
  - `cancellationToken`: Cancels the call.
- `Task<ArxEntity?> RetrieveAsync(ArxEntity match, ArxRetrieveOptions? options = null, ArxDynamoReadOptions? read = null, CancellationToken cancellationToken = default)`: The item `match`'s key finds (every key attribute given), or null.
  - `match`: The record to find: its table, and the values of the id, the declared key or the columns that find it.
  - `options`: How the row is found and read (match, command timeout, lookup names); null for the defaults.
  - `read`: DynamoDB's own read options: consistent reads (on by default), `AllowScan`, `CaseSensitivePatterns`; null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<ArxEntityCollection> RetrieveMultipleAsync(ArxQueryExpression query, ArxRetrieveOptions? options = null, CancellationToken cancellationToken = default)`: Runs a query: a Query of the table or an index when the criteria give the partition key with Equal, a Scan with `AllowScan`. Ordered by the sort key only; paged by the cookie each page returns (`PagingCookie`), HasNextPage exact (ARX reads on until it knows). A written-sharded partition is read from every shard and merged by the sort key.
  - `query`: The query: its row source, columns, criteria, links, order, paging and aggregates.
  - `options`: How the row is found and read (match, command timeout, lookup names); null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<ArxEntityCollection> RetrieveMultipleAsync(ArxQueryExpression query, ArxDynamoReadOptions read, CancellationToken cancellationToken = default)`: As `RetrieveMultipleAsync`, with DynamoDB's read options.
  - `query`: The query: its row source, columns, criteria, links, order, paging and aggregates.
  - `read`: DynamoDB's own read options: consistent reads (on by default), `AllowScan`, `CaseSensitivePatterns`; null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<ArxEntityCollection> QueryPartitionAsync(ArxEntity keyValues, ArxPagingInfo? page = null, ArxDynamoReadOptions? read = null, CancellationToken cancellationToken = default)`: Reads an item collection of a single table: every item of the partition the entity's partition key gives (`keyValues`' attributes), whatever its entity, each read back as the entity its type attribute names, in sort key order. A customer and its orders in one request. Paged by cookie with `page`.
  - `keyValues`: An entity whose attributes give the partition key (the sort key isn't used).
  - `page`: The page size and the cookie of the page to read; null for every item.
  - `read`: DynamoDB's own read options: consistent reads (on by default), `AllowScan`, `CaseSensitivePatterns`; null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<ArxServerCheck> CheckServerAsync(CancellationToken cancellationToken = default)`: What the connected endpoint allows: it answers, and every table of the model exists (a missing one is a problem the check reports, by name). Every capability of this provider works on DynamoDB and DynamoDB Local alike, so the check finds no limits. Call it once at startup.
  - `cancellationToken`: Cancels the call.
- `Task<ArxExecuteResult> ExecuteAsync(ArxProcedureExpression procedure, ArxExecuteOptions? options = null, CancellationToken cancellationToken = default)`: Not offered: DynamoDB has no stored procedures or functions. Always ARX8001.
  - `procedure`: The procedure or function to call, with its arguments.
  - `options`: Options of the call (paging, command timeout); null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task ValidateSchemaAsync(CancellationToken cancellationToken = default)`: Compares every model table with DynamoDB (`DescribeTable`): the table exists, its key attributes and their types are the model's, and each of the model's indexes exists with its keys. Reports every difference at once (ARX5003). ARX never calls it by itself: run it at startup or in a health check.
  - `cancellationToken`: Cancels the call.
- `Task<ArxEntity> RetrieveAsync(string entityName, ArxRecordId id, ArxTransaction transaction, ArxColumnSet? columns = null, CancellationToken cancellationToken = default)`: Reads the record with this id in an ARX transaction, or null. On SQL and MongoDB the read sees the transaction's own writes; DynamoDB sends a transaction's writes at commit, so a read there doesn't see them yet.
  - `entityName`: The table or view to read.
  - `id`: The record's id: the value Create returned, or a plain value of the declared id's type.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `columns`: The columns to read; null or an empty set reads every column. The declared id is always read.
  - `cancellationToken`: Cancels the call.
- `Task<ArxEntity> RetrieveAsync(ArxEntity match, ArxTransaction transaction, ArxRetrieveOptions? options = null, CancellationToken cancellationToken = default)`: Reads the one record the entity finds (by id, a key or columns) in an ARX transaction, or null.
  - `match`: The record to find: its table, and the values of the id, the declared key or the columns that find it.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `options`: How the row is found and read (match, command timeout, lookup names); null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<ArxEntityCollection> RetrieveMultipleAsync(ArxQueryExpression query, ArxTransaction transaction, ArxRetrieveOptions? options = null, CancellationToken cancellationToken = default)`: Runs the query in an ARX transaction and returns the records it finds.
  - `query`: The query: its row source, columns, criteria, links, order, paging and aggregates.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `options`: How the row is found and read (match, command timeout, lookup names); null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<ArxExecuteResult> ExecuteAsync(ArxProcedureExpression procedure, ArxTransaction transaction, ArxExecuteOptions? options = null, CancellationToken cancellationToken = default)`: Calls a procedure or function in an ARX transaction (ARX8001 where StoredProcedures isn't declared).
  - `procedure`: The procedure or function to call, with its arguments.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `options`: Options of the call (paging, command timeout); null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<IArxBatchScope> BeginBatchAsync(ArxBatchOptions? options = null, CancellationToken cancellationToken = default)`: A batch scope: one ARX transaction whose writes are sent together at commit. DynamoDB has no savepoints, so a failed call rolls the whole scope back; asking for them (`Savepoints = true`) is ARX8001.
  - `options`: The scope's options; null for the defaults (no savepoints).
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateAsync(ArxEntity entity, ArxUpdateOptions? options = null, CancellationToken cancellationToken = default)`: Updates the items the entity finds and returns how many were written. By default the entity's key attributes find one item (its RecordId counts for the id), and the result is 1. `Match` can instead name other attributes (`ArxMatch.Columns`) when a partition key of the table or of an index can be built from them: ARX queries those items (at most 100) and updates each one, each write conditioned on it still matching, so the result can be more than 1. A match that no partition key covers is ARX8001: a write never scans. Only the attributes present are written (`SET`); an attribute set to null is removed (`REMOVE`). A missing item is ARX2002, never created. Key attributes are never written unless `AllowKeyChange` moves the item: a different key is a different item.
  - `entity`: The change: its table, the attributes to write (null clears a column; an attribute left out keeps its value), and the id, key or columns that find the row.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateAsync(ArxEntity entity, ArxTransaction transaction, ArxUpdateOptions? options = null, CancellationToken cancellationToken = default)`: As `UpdateAsync`, in an ARX transaction (sent at commit; a missing item fails the commit).
  - `entity`: The change: its table, the attributes to write (null clears a column; an attribute left out keeps its value), and the id, key or columns that find the row.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateAsync(ArxEntity entity, ArxDynamoWriteOptions writeOptions, ArxUpdateOptions? options = null, CancellationToken cancellationToken = default)`: As `UpdateAsync`, with write options: with `AllowKeyChange`, an entity whose RecordId (the item) and id attribute (its new id) differ moves the item to its new key.
  - `entity`: The change: its table, the attributes to write (null clears a column; an attribute left out keeps its value), and the id, key or columns that find the row.
  - `writeOptions`: DynamoDB's own write options, such as `AllowKeyChange` to move an item to a new key.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateAsync(ArxEntity entity, ArxTransaction transaction, ArxDynamoWriteOptions writeOptions, ArxUpdateOptions? options = null, CancellationToken cancellationToken = default)`: As `UpdateAsync`, in an ARX transaction (a move is two of its writes).
  - `entity`: The change: its table, the attributes to write (null clears a column; an attribute left out keeps its value), and the id, key or columns that find the row.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `writeOptions`: DynamoDB's own write options, such as `AllowKeyChange` to move an item to a new key.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxUpdateOptions? options = null, CancellationToken cancellationToken = default)`: Updates every entity, all or nothing, in one `TransactWriteItems` (up to 100; more with AllowPartial), and returns how many. If any item is missing, nothing changes and the call is ARX2002 naming it.
  - `entities`: The changes, all or nothing: each carries the attributes to write and what finds its row.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxDynamoWriteOptions writeOptions, ArxUpdateOptions? options = null, CancellationToken cancellationToken = default)`: As `UpdateMultipleAsync`, with write options.
  - `entities`: The changes, all or nothing: each carries the attributes to write and what finds its row.
  - `writeOptions`: DynamoDB's own write options, such as `AllowKeyChange` to move items to new keys.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxTransaction transaction, ArxUpdateOptions? options = null, CancellationToken cancellationToken = default)`: As `UpdateMultipleAsync`, in an ARX transaction.
  - `entities`: The changes, all or nothing: each carries the attributes to write and what finds its row.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<bool> DeleteAsync(string entityName, ArxRecordId id, CancellationToken cancellationToken = default)`: Deletes the item with this id; true when one went.
  - `entityName`: The table to delete from.
  - `id`: The record's id: the value Create returned, or a plain value of the declared id's type.
  - `cancellationToken`: Cancels the call.
- `Task<bool> DeleteAsync(string entityName, ArxRecordId id, ArxTransaction transaction, CancellationToken cancellationToken = default)`: Deletes by id in an ARX transaction: the item is read now, and deleted at commit (a missing item then fails the commit).
  - `entityName`: The table to delete from.
  - `id`: The record's id: the value Create returned, or a plain value of the declared id's type.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteAsync(ArxEntity entity, ArxDeleteOptions? options = null, CancellationToken cancellationToken = default)`: Deletes the items the entity finds and returns how many went: by its key (1, or 0 when there was none), or by the attributes `Match` names when a partition key of the table or of an index can be built from them (at most 100, each delete conditioned on still matching). With RequireSingleRow, none is ARX2002.
  - `entity`: The record to delete: its table and the id, key or columns that find it.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteAsync(ArxEntity entity, ArxTransaction transaction, ArxDeleteOptions? options = null, CancellationToken cancellationToken = default)`: As `DeleteAsync`, in an ARX transaction.
  - `entity`: The record to delete: its table and the id, key or columns that find it.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteMultipleAsync<TId>(string entityName, IReadOnlyCollection<TId> ids, CancellationToken cancellationToken = default)`: Deletes the records with these ids, all or nothing, and returns how many existed. Ids that match nothing aren't an error: the count shows them. Duplicates count once. An empty list returns 0 without touching the database. One method for every id type (1.0.0-rc.1): a list of `long`, `int`, `Guid`, `string` or `ArxRecordId`; another element type is ARX1007 UnsupportedValueType.
  - `entityName`: The table to delete from.
  - `ids`: The ids of the records to delete; ids that match nothing are left out of the count.
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxDeleteOptions? options = null, CancellationToken cancellationToken = default)`: Deletes the items the entities' keys find, all or nothing, and returns how many went: one consistent read of the keys, then the deletes of the items found in one transaction (each conditioned on still existing). With RequireSingleRow every entity must find its item (ARX2002, nothing deleted).
  - `entities`: The records to delete, all or nothing, each found by its id or by the match's columns.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxTransaction transaction, ArxDeleteOptions? options = null, CancellationToken cancellationToken = default)`: As `DeleteMultipleAsync`, in an ARX transaction.
  - `entities`: The records to delete, all or nothing, each found by its id or by the match's columns.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.

### ArxDynamoConnection

class in `ARX.SDK.DynamoDb.Services`, package `ARX.SDK.DynamoDb`.

```csharp
public sealed class ArxDynamoConnection : ArxConnection
```

ARX transactions on DynamoDB. DynamoDB has no interactive transaction: an `ArxDynamoTransaction` collects the writes of the calls made with it and sends them as one `TransactWriteItems` at `CommitAsync`, all or nothing, across tables, with every condition kept.

What follows from that: a transaction holds at most 100 writes (a call that would pass the limit is ARX8001 and the transaction stays usable); reads made with the transaction see committed data only, not its own pending writes; a write's failure (a duplicate, a missing item) shows at commit, which then throws and writes nothing; there are no savepoints. No driver types in your code.

Constructors:

- `new ArxDynamoConnection(ArxDynamoDbService service)`: A connection for this service's transactions. It holds no network connection: DynamoDB has none to hold.
  - `service`: The service whose client and model the transaction uses.

### ArxDynamoTransaction

class in `ARX.SDK.DynamoDb.Services`, package `ARX.SDK.DynamoDb`.

```csharp
public sealed class ArxDynamoTransaction : ArxTransaction
```

The writes of an ARX transaction on DynamoDB, sent together at commit.

Properties:

- `int PendingWrites { get; }`: The writes waiting for the commit.

### ArxTransaction

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public abstract class ArxTransaction : IAsyncDisposable
```

A database transaction that ARX calls run in. Pass it to a service call, then `CommitAsync` or `RollbackAsync`. Disposing it without committing rolls it back.

ARX tracks what happened to it. A call that fails at the database aborts the transaction (`IsAborted`): later calls fail with ARX1015 before reaching the database, and `CommitAsync` rolls back and throws ARX1015 instead of appearing to commit. Some databases discard the transaction's work on any error (PostgreSQL); others undo only the failed statement (SQL Server, MySQL), so committing the rest would write only part of what the code meant: ARX rolls back on every provider, so a failed call never leaves a half-done unit of work. A call that fails validation (before the database) leaves the transaction usable. These rules live here, so every provider behaves the same; a provider supplies only the database work.

Properties:

- `ArxTransactionState State { get; }`: The transaction's state: Active until it is committed or rolled back. An aborted transaction (`IsAborted`) is still Active, because it is open and must still be rolled back; check IsAborted to know whether it can commit.
- `bool IsAborted { get; }`: True once a call in it failed at the database: it can only be rolled back.

Methods:

- `Task CommitAsync(CancellationToken cancellationToken = default)`: Commits everything written in the transaction. On an aborted transaction it rolls back and throws ARX1015. If the database refuses the commit (for example a deferred constraint or a serialization failure), the mapped error is thrown and the state is `RolledBack`. If the connection fails or times out before the database answers (ARX7005, ARX7003, or a network error), the outcome is unknown: the state is `OutcomeUnknown` and the error thrown says so, with `IsTransient` false, so retry code doesn't run the work a second time unchecked. A token cancelled before the commit starts rolls back; the commit itself isn't cancelled, because stopping it half way would leave its outcome unknown.
  - `cancellationToken`: Cancels the call.
- `Task RollbackAsync(CancellationToken cancellationToken = default)`: Rolls back everything written in the transaction. Does nothing if it is already rolled back; fails with ARX1010 if it was committed. The rollback itself isn't cancelled.
  - `cancellationToken`: Cancels the call.
- `ValueTask DisposeAsync()`: Rolls back an active transaction, then releases it. Never throws for a failed rollback.
- `static Task<ArxTransaction> BeginOnOwnConnectionAsync(ArxConnection connection, CancellationToken cancellationToken = default)`: For the providers' `IArxDataService.BeginTransactionAsync`: begins a transaction on `connection`, which the transaction then owns and closes when it is disposed. The connection is closed at once if the transaction can't begin.
  - `connection`: A new connection of the provider's own type, for this transaction only.
  - `cancellationToken`: Cancels the call.

### IArxBatchDataService

interface in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public interface IArxBatchDataService
```

A data service that can run write calls in batch scopes.

Methods:

- `Task<IArxBatchScope> BeginBatchAsync(ArxBatchOptions? options = null, CancellationToken cancellationToken = default)`: Opens a connection and a transaction, and returns a scope with manual commit: call `CommitAsync` to keep what was written. Disposing the scope without it rolls back. For automatic commit, use `RunBatchAsync`.
  - `options`: The scope's options; null for the defaults (no savepoints).
  - `cancellationToken`: Cancels the call.

### IArxBatchScope

interface in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public interface IArxBatchScope : IAsyncDisposable
```

One transaction across many write calls. Nothing written through the scope is visible to other connections until `CommitAsync`. Disposing a scope that wasn't committed rolls it back, also when an exception is passing through. Not thread-safe: make one call at a time (a concurrent call fails with ARX1014).

Properties:

- `ArxBatchState State { get; }`: The scope's state.
- `int BatchCount { get; }`: The number of calls made through the scope, successful or not. Reads count too. Each call's number (1-based) is its batch number.
- `long RowsWritten { get; }`: Rows written by successful calls that are still in the transaction; 0 once the scope is rolled back.
- `IReadOnlyList<int> FailedBatches { get; }`: The batch numbers of the calls that failed, in order.

Methods:

- `Task<ArxRecordId> CreateAsync(ArxEntity entity, CancellationToken cancellationToken = default)`: Inserts one entity inside the scope's transaction and returns its id, which is also set on `RecordId`.
  - `entity`: The record to create: its table and the attributes to write. Its `RecordId` is set to the new id.
  - `cancellationToken`: Cancels the call.
- `Task<IReadOnlyList<ArxRecordId>> CreateMultipleAsync(IReadOnlyList<ArxEntity> entities, CancellationToken cancellationToken = default)`: Inserts the entities inside the scope's transaction, all or nothing, and returns their ids: one per entity, in list order, the same instances that are set on each entity's `RecordId`. An empty list returns an empty list.
  - `entities`: The records to create, all or nothing; their ids come back in the same order.
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateAsync(ArxEntity entity, ArxUpdateOptions? options = null, CancellationToken cancellationToken = default)`: Updates the rows `entity` matches inside the scope's transaction and returns how many changed. Nothing matched is ARX2002.
  - `entity`: The change: its table, the attributes to write (null clears a column; an attribute left out keeps its value), and the id, key or columns that find the row.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxUpdateOptions? options = null, CancellationToken cancellationToken = default)`: Updates the entities inside the scope's transaction, all or nothing, and returns the rows changed. An entity that matched nothing refuses the whole call (ARX2002). An empty list returns 0.
  - `entities`: The changes, all or nothing: each carries the attributes to write and what finds its row.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateMultipleAsync(ArxEntity values, ArxFilterExpression filter, ArxFilterWriteOptions? options = null, CancellationToken cancellationToken = default)`: The update by a filter inside the scope's transaction: returns how many rows changed.
  - `values`: The table (EntityName) and the attributes to set: null clears a column.
  - `filter`: The rows to change: conditions on the table's own columns.
  - `options`: AllRows for an empty filter; null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteMultipleAsync(string entityName, ArxFilterExpression filter, ArxFilterWriteOptions? options = null, CancellationToken cancellationToken = default)`: The delete by a filter inside the scope's transaction: returns how many rows went.
  - `entityName`: The table to delete from.
  - `filter`: The rows to delete: conditions on the table's own columns.
  - `options`: AllRows for an empty filter; null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<IReadOnlyList<ArxRecordId>> CreateMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxCreateMultipleOptions options, CancellationToken cancellationToken = default)`: CreateMultiple with options inside the scope's transaction.
  - `entities`: The records to create, all or nothing.
  - `options`: ReturnIds false to skip reading the ids back.
  - `cancellationToken`: Cancels the call.
- `Task<bool> DeleteAsync(string entityName, ArxRecordId id, CancellationToken cancellationToken = default)`: Deletes the record with this id inside the scope's transaction; false when there was none. The id converts from a `long`, `int`, `Guid` or `string`, so `DeleteAsync("account", 42L)` works as it is.
  - `entityName`: The table to delete from.
  - `id`: The record's id: the value Create returned, or a plain value of the declared id's type.
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteAsync(ArxEntity entity, ArxDeleteOptions? options = null, CancellationToken cancellationToken = default)`: Deletes the rows `entity` matches inside the scope's transaction and returns how many went.
  - `entity`: The record to delete: its table and the id, key or columns that find it.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteMultipleAsync<TId>(string entityName, IReadOnlyCollection<TId> ids, CancellationToken cancellationToken = default)`: Deletes the records with these ids inside the scope's transaction, all or nothing, and returns how many existed. Duplicates count once. One method for every id type: a list of `long`, `int`, `Guid`, `string` or `ArxRecordId`; another element type is ARX1007 UnsupportedValueType.
  - `entityName`: The table to delete from.
  - `ids`: The ids of the records to delete; ids that match nothing are left out of the count.
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxDeleteOptions? options = null, CancellationToken cancellationToken = default)`: Deletes the rows the entities match inside the scope's transaction, all or nothing, and returns how many went.
  - `entities`: The records to delete, all or nothing, each found by its id or by the match's columns.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<ArxEntity?> RetrieveAsync(string entityName, ArxRecordId id, ArxColumnSet? columns = null, CancellationToken cancellationToken = default)`: Reads one record by id (the value Create returned, or a `long`, `int`, `Guid` or `string`) inside the scope's transaction; null when not found. On the SQL providers and MongoDB it sees the scope's own writes; DynamoDB sends writes at commit, so a read before it sees only what is committed.
  - `entityName`: The table or view to read.
  - `id`: The record's id: the value Create returned, or a plain value of the declared id's type.
  - `columns`: The columns to read; null or an empty set reads every column. The declared id is always read.
  - `cancellationToken`: Cancels the call.
- `Task<ArxEntity?> RetrieveAsync(ArxEntity match, ArxRetrieveOptions? options = null, CancellationToken cancellationToken = default)`: Reads the one record the entity matches (an ArxMatch in `Match`) inside the scope's transaction.
  - `match`: The record to find: its table, and the values of the id, the declared key or the columns that find it.
  - `options`: How the row is found and read (match, command timeout, lookup names); null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<ArxEntityCollection> RetrieveMultipleAsync(ArxQueryExpression query, ArxRetrieveOptions? options = null, CancellationToken cancellationToken = default)`: Runs a query inside the scope's transaction. On the SQL providers and MongoDB it sees the scope's own writes; on DynamoDB, only what is committed.
  - `query`: The query: its row source, columns, criteria, links, order, paging and aggregates.
  - `options`: How the row is found and read (match, command timeout, lookup names); null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<ArxExecuteResult> ExecuteAsync(ArxProcedureExpression procedure, ArxExecuteOptions? options = null, CancellationToken cancellationToken = default)`: Calls a procedure or function inside the scope's transaction: it sees the scope's writes, and a failed call is undone alone. A procedure that commits or rolls back by itself can't run in a scope (ARX8001).
  - `procedure`: The procedure or function to call, with its arguments.
  - `options`: Options of the call (paging, command timeout); null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task CommitAsync(CancellationToken cancellationToken = default)`: Commits everything written through the scope. If the database refuses the commit (for example a deferred constraint), the error is thrown and the scope is `RolledBack`. If the connection fails or times out before the database answers, the scope is `OutcomeUnknown` and the error, not transient, says the work may or may not be saved. A token cancelled before the commit starts rolls the scope back; the commit itself isn't cancelled, because stopping it half way would leave its outcome unknown.
  - `cancellationToken`: Cancels the call.
- `Task RollbackAsync(CancellationToken cancellationToken = default)`: Rolls back everything written through the scope. Does nothing if the scope is already rolled back; fails with ARX1010 if it was committed. The rollback itself isn't cancelled.
  - `cancellationToken`: Cancels the call.

### ArxBatchOptions

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxBatchOptions
```

Options for a batch scope.

Properties:

- `bool? Savepoints { get; init; }`: When true, each call made through the scope runs inside its own savepoint. A failed call is then undone alone: the calls before it stay in the transaction, the scope stays `Active`, and the developer decides whether to continue, retry or roll back. When false or null (the default), any failed call rolls back the whole scope, on every provider, so the same code ends the same way on every database: a body that catches the error has nothing saved, and `RunBatchAsync` throws ARX1015 when it returns. Ask for savepoints where a failed call should be undone alone; only PostgreSQL, SQL Server and MySQL have them (`Savepoints`), and true on MongoDB or DynamoDB is refused with `ArxNotSupportedException`.

## Entities and values

A record, its id, and the values with a meaning of their own: a reference to another record, a selection set code, JSON, bytes, an attribute left out.

### ArxEntity

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public class ArxEntity
```

A record of one table: its name (`EntityName`), its attributes (column name to value) and, once written or read, its id (`RecordId`). Set a value with `entity["name"] = "Contoso"`.

Reading values: `entity["name"]` is null both for a NULL column and for an attribute the record doesn't have (a column not read, or a mistyped name), so use `Contains` to tell them apart. Names are matched ignoring case, on read as on write (`entity["Name"]` finds `name`). `GetAttributeValue{T}` converts numbers when nothing is lost (ARX4001 when the number doesn't fit), reads a lookup or a record id as its id, and a UTC or Local `DateTime` as a `DateTimeOffset` (and back); any other mismatch is ARX4004. A declared lookup reads back as an `ArxEntityReference`. `entity.Attributes["x"]` is the dictionary underneath: it throws `KeyNotFoundException` for a missing attribute, where the indexer returns null.

Constructors:

- `new ArxEntity()`: An entity with no table yet: set EntityName before using it.
- `new ArxEntity(string entityName)`: An empty record of this table.
  - `entityName`: The table, or `schema.table`.
- `new ArxEntity(string entityName, ArxAttributeCollection attributes)`: An entity with the given attribute collection: a provider reading rows passes one it built.
  - `entityName`: The table, or `schema.table`.
  - `attributes`: The attributes to use as they are (not copied).
- `new ArxEntity(string entityName, int attributeCapacity)`: An entity with room for `attributeCapacity` attributes, so a provider that knows how many it reads (a result row) fills it without the dictionary growing.
  - `entityName`: The table, or `schema.table`.
  - `attributeCapacity`: How many attributes the entity will hold.

Properties:

- `ArxRecordId? RecordId { get; set; }`: The record's id, of any declared id type: read it with `AsInt64()`, `AsString()`, `AsGuid()` or `Value`. Set by Create and by every read that knows the table's id. Create reads the id to write from the attributes, never from this property, so an entity ARX created or read can be created again as a copy. An id your code sets here before a Create is refused (ARX1001) unless the id attribute holds the same id: to give a record its id, set the id attribute (`entity["id"] = …`).
- `ArxIdColumn? IdColumn { get; set; }`: The id column for this entity, when no schema declares its table: a service with no schema catalog, and no schema passed to the call. With a schema, setting it is refused (ARX1001), because a table is declared in one place; to declare a table differently for one call, pass a schema to that call instead. Null (the default) means the schema's declaration is used. `NoIdColumn` for a table with no id.
- `IReadOnlyList<ArxKey>? Keys { get; set; }`: The table's business keys for this entity, when no schema declares its table; with a schema, setting them is refused (ARX1001). Optional: null (the default) means the schema's keys, or none. Keys alone don't declare an id. Create doesn't use them; Update and Delete can find rows by them.
- `string EntityName { get; set; }`: The table (or `schema.table`) the record belongs to.
- `ArxAttributeCollection Attributes { get; set; }`: The record's values by column name. Create and Update write exactly these; a read fills them.
- `object? this[string attributeName] { get; set; }`: A column's value: null when the record has no such attribute. Setting it adds or replaces the attribute.

Methods:

- `bool Contains(string attributeName)`: True when the entity has the attribute (names are matched ignoring case).
  - `attributeName`: The attribute (column) name, matched ignoring case.
- `T? GetAttributeValue<T>(string attributeName)`: The attribute's value as `T`, or its default when the attribute is missing or null (so `GetAttributeValue<int>` of a NULL column is 0, and `GetAttributeValue<int?>` is null: ask for a nullable type to tell NULL from 0. `Contains` is true for a column that was read as NULL).
  - `attributeName`: The attribute (column) name, matched ignoring case.
- `ArxEntityReference ToEntityReference(string? primaryColumnName = null)`: A reference to this entity's record, for a lookup on another entity. Its `Name` is the value of `primaryColumnName` (a lookup's own name when the column is one), or null when no column is named or the entity doesn't have it.
  - `primaryColumnName`: The column whose value names the record (a display name), or null.

### ArxEntityCollection

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxEntityCollection
```

The rows a RetrieveMultiple returns (`Entities`), whether another page follows (`HasNextPage`), and the total (`TotalRecordCount`) when the query asked for it.

Constructors:

- `new ArxEntityCollection()`: An empty collection.
- `new ArxEntityCollection(List<ArxEntity> list)`: A collection of these rows (the list itself, not a copy).
  - `list`: The rows. The collection uses this list itself, not a copy.

Properties:

- `List<ArxEntity> Entities { get; }`: The rows, in the query's order.
- `string? EntityName { get; set; }`: The row source the rows were read from.
- `int TotalRecordCount { get; set; }`: Every row that matched, across all pages. Only set when the query asked for it (ReturnTotalRecordCount); without paging, the number of rows read.
- `int CurrentPage { get; set; }`: The page these rows are (1 when the query isn't paged).
- `int TotalPageCount { get; set; }`: How many pages there are; only set when the total was asked for.
- `bool HasNextPage { get; set; }`: Whether another page follows. Always exact: ARX reads one row more than the page to know.
- `string? PagingCookie { get; set; }`: Token paging: pass this to the next query's `ArxPagingInfo.PagingCookie` to read the next page. Null when no page follows, and always null from a provider without `TokenPaging`. On such a provider (PostgreSQL, SQL Server, MySQL) page by `ArxPagingInfo.PageNumber` instead: a loop that only passes this cookie on reads the first page again, because a null cookie means "the first page".
- `ArxEntity this[int index] { get; set; }`: The row at this position.

### ArxRecordId

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxRecordId : IEquatable<ArxRecordId>
```

The id of one record: the value of its table's id column, of any declared id type. Returned by Create and set on `RecordId`. `None` for a table declared `NoIdColumn`. Immutable; equal when the column and value are equal.

Created with:

- `implicit operator ArxRecordId(long value)`: An id given as its value alone, so `service.RetrieveAsync("account", 42L)` works through `IArxDataService` too: the service takes the column from the table's declaration. Its `Column` is empty; use `Of` to name it.
  - `value`: The id's value.
- `implicit operator ArxRecordId(int value)`: An int id given as its value alone (see the `long` conversion).
  - `value`: The id's value.
- `implicit operator ArxRecordId(Guid value)`: A Guid id given as its value alone (see the `long` conversion).
  - `value`: The id's value.
- `implicit operator ArxRecordId?(string? value)`: A text id given as its value alone (see the `long` conversion); null stays null.
  - `value`: The id's value.
- `ArxRecordId.Of(string column, object value)`: An id with its column and value, for example to update or delete by id.
  - `column`: The id column's name.
  - `value`: The id's value.

Properties:

- `static ArxRecordId None { get; }`: No id: what Create returns for a table declared `NoIdColumn`.
- `string Column { get; }`: The id column's name. Empty for `None`, and for an id given as its value alone.
- `bool HasValue { get; }`: False for `None`.
- `object Value { get; }`: The id's value, in the declared type.

Methods:

- `long AsInt64()`: The value of an integer id (short, int or long).
- `int AsInt32()`: The value of an integer id as an int.
- `Guid AsGuid()`: The value of a Guid id.
- `string AsString()`: The value of a string id.
- `bool Equals(ArxRecordId? other)`: True when `other` has an equal value (integers compare by value, whatever their width) and the same id column when both name one, so an id given as a plain value equals the id Create returned. Two ids without a value are equal.
  - `other`: The id to compare with, or null.
- `static bool operator ==(ArxRecordId? left, ArxRecordId? right)`: True when the ids are equal (see `Equals`); two nulls are equal.
  - `left`: An id, or null.
  - `right`: An id, or null.
- `static bool operator !=(ArxRecordId? left, ArxRecordId? right)`: True when the ids differ.
  - `left`: An id, or null.
  - `right`: An id, or null.

### ArxEntityReference

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public class ArxEntityReference
```

A lookup: a reference to a record of another table, by that table's name and the record's id. Written to a lookup column as the id; the table name tells ARX which table the id belongs to, so a schema can check it.

The id can be of any supported id type: short, int, long, decimal, string, Guid, DateOnly or DateTime, or the `ArxRecordId` that Create returned. Every constructor takes the table name first, then the id, then an optional display name.

Constructors:

- `new ArxEntityReference()`: An empty reference; set its table and id.
- `new ArxEntityReference(string entityName, long id, string? name = null)`: A reference by an integer id (short, int and long all fit).
  - `entityName`: The referenced table: the table the id belongs to.
  - `id`: The referenced record's integer id.
  - `name`: Its display name, if you have it; optional, ARX doesn't write it.
- `new ArxEntityReference(string entityName, Guid id, string? name = null)`: A reference by a uuid id.
  - `entityName`: The referenced table: the table the id belongs to.
  - `id`: The referenced record's uuid id.
  - `name`: Its display name, if you have it; optional, ARX doesn't write it.
- `new ArxEntityReference(string entityName, string id, string? name = null)`: A reference by a text id (a country code, a SKU).
  - `entityName`: The referenced table: the table the id belongs to.
  - `id`: The referenced record's text id.
  - `name`: Its display name, if you have it; optional, ARX doesn't write it.
- `new ArxEntityReference(string entityName, decimal id, string? name = null)`: A reference by a decimal id.
  - `entityName`: The referenced table: the table the id belongs to.
  - `id`: The referenced record's decimal id.
  - `name`: Its display name, if you have it; optional, ARX doesn't write it.
- `new ArxEntityReference(string entityName, DateOnly id, string? name = null)`: A reference by a date id.
  - `entityName`: The referenced table: the table the id belongs to.
  - `id`: The referenced record's date id.
  - `name`: Its display name, if you have it; optional, ARX doesn't write it.
- `new ArxEntityReference(string entityName, DateTime id, string? name = null)`: A reference by a timestamp id.
  - `entityName`: The referenced table: the table the id belongs to.
  - `id`: The referenced record's timestamp id.
  - `name`: Its display name, if you have it; optional, ARX doesn't write it.
- `new ArxEntityReference(string entityName, ArxRecordId id, string? name = null)`: A reference by the id Create returned (`await service.CreateAsync(account)`), of any type.
  - `entityName`: The referenced table: the table the id belongs to.
  - `id`: The id CreateAsync returned for the referenced record, of any id type.
  - `name`: Its display name, if you have it; optional, ARX doesn't write it.

Properties:

- `ArxRecordId? RecordId { get; }`: The referenced record's id, of any supported type; null when none was given. This is what Create writes to the lookup column. `None` (a record of a table with no id column) can't be referenced.
- `long Id { get; set; }`: **Obsolete.** Id is only an integer id: 0 for a Guid or text id. Use RecordId (RecordId.Value, AsInt64(), AsGuid(), AsString()). The referenced record's id when it's an integer; 0 otherwise (a Guid or text id reads as 0). Setting it sets `RecordId`. Use `RecordId`, which holds an id of any type.
- `Guid Guid { get; set; }`: **Obsolete.** Guid is only a Guid id: Guid.Empty for an integer or text id. Use RecordId (RecordId.Value, AsGuid()). The referenced record's id when it's a Guid; `Guid.Empty` otherwise. Setting it sets `RecordId`. Use `RecordId`, which holds an id of any type.
- `string EntityName { get; set; }`: The referenced table: checked against the schema when there is one (it must be declared, with an id of the reference's type).
- `string? Name { get; set; }`: The referenced record's display name, if known. Not written by Create.

### ArxSelectionSetValue

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxSelectionSetValue
```

A selection set (option set, picklist) value: the option's code, checked against the set named `SetName` in the developer's `IArxSelectionSetCatalog` and written in that set's storage form. A plain `int` or `string` attribute is written as is, without catalog checks.

Create a single-choice value with the constructor and a multiple-choice value with `Multiple`. Values read back through `ArxSelectionSetExtensions.GetSelectionSet` also carry the matched `Options`, so `DisplayText` and `IsDefined` are available.

Constructors:

- `new ArxSelectionSetValue(string setName, object value)`: A single-choice value: `value` is the option's code (an integer, string, char, bool, decimal or Guid).
  - `setName`: The catalog set to check against. Required when writing; a missing name is reported as ARX1006 by the provider.
  - `value`: The option's code. Not null, and not a collection (use `Multiple`).

Created with:

- `ArxSelectionSetValue.Multiple(string setName, params object[] values)`: A multiple-choice value. An empty list means no option is chosen.
  - `setName`: The selection set's name in the catalog.
  - `values`: The chosen codes.
- `ArxSelectionSetValue.Multiple(string setName, IEnumerable values)`: A multiple-choice value from any sequence of codes (`int[]`, `List<string>`, …). An empty sequence means no option is chosen. A single `string` is one code, not a sequence of characters.
  - `setName`: The selection set's name in the catalog.
  - `values`: The chosen codes.

Properties:

- `string SetName { get; }`: The catalog set this value belongs to.
- `object? Value { get; }`: The single-choice code; null for a multiple-choice value.
- `IReadOnlyList<object> Values { get; }`: Every chosen code: one for a single-choice value, zero or more for a multiple-choice value.
- `bool IsMultiple { get; }`: True when created with `Multiple` or decoded from a multiple-choice set.
- `IReadOnlyList<ArxSelectionSetOption?>? Options { get; }`: The catalog options matching `Values`, in the same order, when the value was decoded from storage; an entry is null when the stored code isn't in the set. Null for values created by the caller.
- `bool IsDefined { get; }`: True when the value was decoded and every stored code is an option of the set.
- `string? DisplayText { get; }`: The display text of the matched option (single choice), or of every matched option joined with ", " in stored order (multiple choice). Null when not decoded, or when a single-choice code isn't in the set.

### ArxJsonB

class in `ARX.SDK.Models`, package `ARX.SDK`.

```csharp
public class ArxJsonB
```

A JSON value: written to a `jsonb` or `json` column as its `SerializedValue` text, and what those columns read back as.

Properties:

- `string? SerializedValue { get; set; }`: The JSON text, as written to the column or read back from it.

### ArxBytea

class in `ARX.SDK.Models`, package `ARX.SDK`.

```csharp
public class ArxBytea
```

Binary data for a binary column (`Data`): PostgreSQL `bytea`, SQL Server `varbinary`, MySQL `BLOB` or `VARBINARY`, a MongoDB binary, a DynamoDB B attribute. A plain `byte[]` works too.

Properties:

- `byte[]? Data { get; set; }`: The bytes to write.

### ArxIgnore

class in `ARX.SDK.Models`, package `ARX.SDK`.

```csharp
public class ArxIgnore
```

An attribute value meaning "leave this column out": Create and Update don't write an attribute set to `new ArxIgnore()`.

## The schema catalog

What the developer declares once: each table's id column, its business keys, lookups, display names, and the selection sets.

### ArxSchemaCatalog

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxSchemaCatalog : IArxSelectionSetCatalog
```

What the developer declares about the database: every table's id column and keys, and the selection sets. Build it once at startup and give it to the service: `new ArxDynamoDbService(client, model, schema)`. ARX never reads ids, keys or option tables from the database by itself. Immutable and thread-safe.

Properties:

- `IReadOnlyCollection<ArxTableDefinition> Tables { get; }`: Every declared table, in the order added.
- `ArxSelectionSetCatalog SelectionSets { get; }`: The selection sets.

Methods:

- `string ToJson()`: The catalog's tables as JSON: ids, keys, lookups, display names, columns with their types, and for a preload the reason each table was added and its notes. Reload it with `ArxSchemaCatalog.Create().AddJson(json)`. Selection sets aren't written: they are code. ARX doesn't write the file: the application decides.
- `static ArxSchemaCatalogBuilder Create()`: Starts a catalog: declare each table's id, keys, lookups and selection sets, then call Build.
- `bool TryGetTable(string name, out ArxTableDefinition table)`: Finds a table by its exact name (table names are case-sensitive).
  - `name`: The table, as the database names it.
  - `table`: The table's declaration when found; otherwise null.
- `string? ClosestTable(string name)`: The declared table whose name is closest to `name`, for an error that names an undeclared table: the same letters in another case first (table names are case-sensitive), then a name within two edits (`acount` for `account`). Null when no declared name is that close.
  - `name`: A table name the catalog doesn't declare.
- `bool TryGetSet(string name, out ArxSelectionSetDefinition set)`: Gets a selection set by name; false when the catalog has none of that name.
  - `name`: The selection set's name.
  - `set`: The set when found; otherwise null.

### ArxSchemaCatalogBuilder

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxSchemaCatalogBuilder
```

Builds an `ArxSchemaCatalog`. Not thread-safe; build once, at startup.

Methods:

- `ArxSchemaCatalogBuilder Column(string table, ArxColumnDefinition column)`: Declares a column of a table with its type. Done by a preload; by hand for a table the preload can't read.
  - `table`: The table, as the database names it.
  - `column`: The column: its name, .NET type, database type and whether it takes null.
- `ArxSchemaCatalogBuilder AllColumns(string table)`: Marks a table's declared columns as all of its columns (a preload of every column).
  - `table`: The table, as the database names it.
- `ArxSchemaCatalogBuilder Reason(string table, string reason)`: Why a table is in the catalog: `"requested"`, `"added: referenced by …"`.
  - `table`: The table, as the database names it.
  - `reason`: Why: `"requested"`, or `"added: referenced by …"`.
- `ArxSchemaCatalogBuilder Note(string table, string note)`: Something a preload left out of a table, and why.
  - `table`: The table, as the database names it.
  - `note`: What was left out, and why.
- `ArxSchemaCatalogBuilder AddJson(string json)`: Adds the tables of a catalog written by `ToJson` (a saved preload): ids, keys, lookups, display names, columns, reasons and notes. Selection sets aren't in the JSON: add them in code.
  - `json`: A catalog saved by `ToJson`.
- `ArxSchemaCatalogBuilder Table(string name, ArxIdColumn idColumn)`: Declares a table and its id column, or `NoIdColumn`.
  - `name`: The table, as the database names it.
  - `idColumn`: Its id column (`Generated{T}`, `Supplied{T}`), or `NoIdColumn`.
- `bool DeclaresTable(string name)`: True when this builder already declares the table (with `Table`).
  - `name`: The table, as the database names it.
- `bool DeclaresKey(string table, params string[] columns)`: True when this builder already declares this key of the table, its columns in any order (with `Key`).
  - `table`: The table, as the database names it.
  - `columns`: The key's columns.
- `bool DeclaresColumn(string table, string column)`: True when this builder already declares the column of the table (with `Column`), its name matched ignoring case.
  - `table`: The table, as the database names it.
  - `column`: The column's name.
- `ArxSchemaCatalogBuilder Key(string table, params string[] columns)`: Declares a business key of a table: one column, or several for a compound key. A table can have several keys.
  - `table`: The table, as the database names it.
  - `columns`: The key's columns: one, or several for a compound key.
- `ArxSchemaCatalogBuilder Lookup(string table, string column, string referencedTable)`: Declares a lookup column: `column` of `table` holds the id of a record of `referencedTable`, which must be declared too. Retrieve reads it back as an `ArxEntityReference`.
  - `table`: The table, as the database names it.
  - `column`: The column that holds the referenced record's id.
  - `referencedTable`: The table it refers to; it must be declared with an id.
- `ArxSchemaCatalogBuilder Version(string table, string column, bool required = false)`: Declares the column that holds a record's version, for optimistic concurrency: an integer column, not the id or a key. See `VersionColumn`. With `required`, an Update must carry the version it read (`VersionRequired`).
  - `table`: The table, as the database names it.
  - `column`: The integer column ARX sets to 1 on create and adds one to on every update.
  - `required`: True to refuse an update or delete whose entity doesn't carry the version (ARX1001); false to let the last writer win.
- `ArxSchemaCatalogBuilder Alias(string table, string attribute, string databaseName)`: Declares a name alias: code uses `attribute`, a valid ARX name (ASCII letters, digits and underscores, starting with a letter or underscore), for the column the database names `databaseName`, such as `.Alias("customer", "first_name", "first-name")`. The provider quotes and escapes the database name by its own rules, in writes, reads, filters, order, matches and keys, and reads the column back as the attribute. The table's id, keys, lookups and version are declared by their attribute names. A link entity's columns keep the identifier rule in this version.
  - `table`: The table, as the database names it.
  - `attribute`: The attribute (column) name.
  - `databaseName`: The column's name in the database, quoted by the provider's rules.
- `ArxSchemaCatalogBuilder DisplayName(string table, string column)`: Declares the column that names a record of `table` to a person.
  - `table`: The table, as the database names it.
  - `column`: The column whose value is a lookup's `Name`.
- `ArxSchemaCatalogBuilder DisplayName(string table, string[] columns, string delimiter = " ")`: Declares the columns that name a record of `table`, joined by `delimiter` (`.DisplayName("contact", new[] { "first_name", "last_name" }, " ")`). NULL parts are skipped; the delimiter may be empty.
  - `table`: The table, as the database names it.
  - `columns`: The columns joined to make the name.
  - `delimiter`: The text between them.
- `ArxSchemaCatalogBuilder Add(ArxSelectionSetDefinition set)`: Adds a selection set (see `Add`).
  - `set`: A selection set built elsewhere.
- `ArxSchemaCatalogBuilder Add(string name, ArxSelectionSetKind kind, params (object Value, string DisplayText)[] options)`: Adds a single-choice selection set.
  - `name`: The set's name, used when writing and reading its values.
  - `kind`: What its codes are: integers, text or single characters.
  - `options`: The options: each code and its display text.
- `ArxSchemaCatalogBuilder Add(string name, ArxSelectionSetKind kind, ArxSelectionSetMultiplicity multiplicity, params (object Value, string DisplayText)[] options)`: Adds a selection set with the given storage form.
  - `name`: The set's name, used when writing and reading its values.
  - `kind`: What its codes are: integers, text or single characters.
  - `multiplicity`: One option per column, or several stored as an array.
  - `options`: The options: each code and its display text.
- `ArxSchemaCatalogBuilder AddEnum<TEnum>(string? name = null)`: Adds a single-choice Integer selection set from a .NET enum.
  - `name`: The set's name; the enum's name when null.
- `ArxSchemaCatalogBuilder AddFlagsEnum<TEnum>(string? name = null)`: Adds a Flags selection set from a `[Flags]` enum.
  - `name`: The set's name; the enum's name when null.
- `ArxSchemaCatalog Build()`: Builds the catalog. Throws for a table declared twice, a key of an undeclared table, or the same key twice.

### ArxIdColumn

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxIdColumn : IEquatable<ArxIdColumn>
```

A table's id column, declared by the developer: the one column that identifies a record. Every table declares exactly one, or `NoIdColumn` when it has no id column.

**Generated**: the database produces the value when the entity doesn't give one (an identity column, a sequence, a default). A value the entity does give is written. **Supplied**: the caller always gives the value (a natural id such as a country code or a SKU, a client-generated Guid).

Created with:

- `ArxIdColumn.Generated<T>(string name)`: An id column the database fills in when the entity has no value for it (identity, sequence, default).
  - `name`: The id column's name.
- `ArxIdColumn.Supplied<T>(string name)`: An id column the caller always gives a value for.
  - `name`: The id column's name.

Properties:

- `static ArxIdColumn NoIdColumn { get; }`: The table has no id column: a log or a history table, or a table identified only by a combination of columns, such as a compound primary key (declare those columns as a key). Create stores its rows and returns `None`.
- `static ArxIdColumn NoPrimaryKey { get; }`: **Obsolete.** Use ArxIdColumn.NoIdColumn: the same declaration under a name that also fits a compound primary key. The former name of `NoIdColumn`: it read as wrong for a table with a compound primary key.
- `string Name { get; }`: The column name (compared with attribute names case-insensitively, like every column). Empty for `NoIdColumn`.
- `Type Type { get; }`: The .NET type the id is read and written as.
- `bool IsGenerated { get; }`: True when the database can produce the value.
- `bool IsNoIdColumn { get; }`: True for `NoIdColumn`.
- `bool IsNoPrimaryKey { get; }`: **Obsolete.** Use IsNoIdColumn. The former name of `IsNoIdColumn`.
- `bool IsInteger { get; }`: True when the id is a short, int or long.

Methods:

- `bool Equals(ArxIdColumn? other)`: Equal when both are `NoIdColumn`, or have the same name (ignoring case), type and generated flag.
  - `other`: The id column to compare with.

### ArxKey

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxKey : IEquatable<ArxKey>
```

A business key of a table: one or more columns whose values identify a record, such as `account_number`, or `region` and `sku` together. Optional: a table whose rows may repeat declares none. Create doesn't use keys (the database's unique constraints enforce them); Update and Delete can find rows by them.

Constructors:

- `new ArxKey(params string[] columns)`: A key of one column, or of several for a compound key.
  - `columns`: The key's columns: one, or several that identify a row together.

Properties:

- `IReadOnlyList<string> Columns { get; }`: The key's columns, as declared.
- `bool IsCompound { get; }`: True when the key has more than one column.

Methods:

- `bool Equals(ArxKey? other)`: Equal when both keys have the same columns, in any order, ignoring case.
  - `other`: The key to compare with.

### ArxTableDefinition

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxTableDefinition
```

A table the developer declared: its id column and its keys. Immutable and thread-safe.

Created with:

- `ArxTableDefinition.Define(string table, ArxIdColumn idColumn, IReadOnlyList<ArxKey>? keys = null)`: A table definition declared on an entity (`IdColumn`, `Keys`) rather than in a schema catalog. The same rules apply: an id column (or `NoIdColumn`), and no key twice.
  - `table`: The table.
  - `idColumn`: Its id column, or ArxIdColumn.NoIdColumn.
  - `keys`: Its business keys; none by default.

Properties:

- `IReadOnlyDictionary<string, ArxColumnDefinition> Columns { get; }`: The table's columns and their types, by name (lower case), when the schema was preloaded with data types; empty for a table declared by hand or preloaded keys only. With `Strict`, the only columns a call may use.
- `bool HasColumns { get; }`: True when the columns are known (preloaded with data types).
- `bool HasAllColumns { get; init; }`: True when the preload loaded every column of the table (an empty column list), not a chosen few.
- `string? Reason { get; }`: Why a preload added this table: `"requested"`, or `"added: referenced by account.primary_contact_id"`; null when declared by hand.
- `IReadOnlyList<string> Notes { get; }`: What a preload left out and why (a column ARX can't name, a key not fully loaded, a reference beyond the depth).
- `string? VersionColumn { get; init; }`: The column that holds the record's version (optimistic concurrency), or null. A provider that offers it (capability OptimisticConcurrency) writes 1 on Create when the entity gives none, adds one on every Update, and checks an Update or Delete against the version the entity carries: changed since it was read is ARX2004. A provider that doesn't refuses writes to the table with ARX8001.
- `bool VersionRequired { get; init; }`: True when an Update of the table must carry the version it read (`.Version(table, column, required: true)`): an update without it is refused with ARX1001 instead of writing unconditioned. False (the default) lets an update without a version through, still adding one to the version so other writers see the change.
- `IReadOnlyDictionary<string, string> Aliases { get; init; }`: The table's name aliases: attribute name (case-insensitive) to the column's name in the database, for a column whose name isn't a valid ARX name (`first-name`, `order.total`). Code uses the attribute; the provider sends the database name, quoted by its own rules, and reads the column back as the attribute. Only declared names pass: a database name never comes from a call. Empty when none are declared.
- `bool HasAliases { get; }`: True when the table declares aliases.
- `IReadOnlyDictionary<string, string> Lookups { get; }`: The lookup columns: column name (lower case) to the table it refers to. A lookup column reads back as an `ArxEntityReference` whose id has the referenced table's declared type. Empty when none are declared.
- `ArxDisplayName? DisplayName { get; }`: How a record of this table is named to a person; null when not declared.
- `string Name { get; }`: The table (entity) name, exactly as declared (table names are case-sensitive).
- `ArxIdColumn IdColumn { get; }`: The id column, or `NoIdColumn`.
- `IReadOnlyList<ArxKey> Keys { get; }`: The business keys, as declared. Empty when the table declares none.
- `bool HasId { get; }`: True unless the table is declared `NoIdColumn`.

Methods:

- `bool TryGetDatabaseName(string attribute, out string databaseName)`: The database name of `attribute` when it is an alias; false for an ordinary name.
  - `attribute`: The attribute name code uses.
  - `databaseName`: The column's name in the database when the attribute has an alias; otherwise null.
- `bool TryGetAttribute(string databaseName, out string attribute)`: The attribute a database column is read back as, when the column has an alias; false for an ordinary column.
  - `databaseName`: The column's name in the database.
  - `attribute`: The attribute name its alias gives it, when it has one; otherwise null.
- `ArxRecordId IdOf(long value)`: The id of a record of this table from an integer value, with no boxing. For providers reading ids back.
  - `value`: The id's value.
- `ArxRecordId IdOf(object value)`: The id of a record of this table from its value, which must have the declared type.
  - `value`: The id's value.

### ArxDisplayName

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxDisplayName
```

How a record is named to a person: one column, or several joined by a delimiter, NULL parts skipped. Declared in the schema with `.DisplayName(table, column)` or `.DisplayName(table, columns, delimiter)`.

Properties:

- `IReadOnlyList<string> Columns { get; }`: The columns, in the order they are joined.
- `string Delimiter { get; }`: What goes between the parts; may be empty.

### ArxSelectionSetDefinition

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxSelectionSetDefinition
```

One selection set: its kind, storage form and options. Immutable and thread-safe. Validates selection set values and converts them to and from the stored form, so every provider applies the same rules.

Constructors:

- `new ArxSelectionSetDefinition(string name, ArxSelectionSetKind kind, IEnumerable<ArxSelectionSetOption> options, ArxSelectionSetMultiplicity multiplicity = ArxSelectionSetMultiplicity.Single, string? databaseTypeName = null, string delimiter = ",")`: A selection set and its options, checked for consistency.
  - `name`: The name selection set values refer to.
  - `kind`: What the codes are, matching the column.
  - `options`: Each code with its display text, order and whether it is still active.
  - `multiplicity`: One code (Single), or several and how they are stored.
  - `databaseTypeName`: The PostgreSQL enum type, for a DatabaseEnum set.
  - `delimiter`: The separator of a Delimited set.

Properties:

- `string Name { get; }`: The name selection set values refer to.
- `ArxSelectionSetKind Kind { get; }`: What the codes are, matching the column that stores them.
- `ArxSelectionSetMultiplicity Multiplicity { get; }`: One code, or several and in which storage form.
- `string? DatabaseTypeName { get; }`: The database enum type a DatabaseEnum set casts to (for example "mood"). The provider validates and quotes it.
- `string Delimiter { get; }`: The separator of a Delimited set.
- `IReadOnlyList<ArxSelectionSetOption> Options { get; }`: The options, by `Order` (stable for equal orders).
- `Type CodeType { get; }`: The CLR type of every code, taken from the options. Single values and array elements are written as this type.

Methods:

- `bool TryGetOption(object code, out ArxSelectionSetOption option)`: Finds the option with this code. A code of another kind (a string for an Integer set) isn't found.
  - `code`: The option's code.
  - `option`: The option when found; otherwise null.
- `object ToStoredValue(ArxSelectionSetValue value, string? entityName = null, string? attributeName = null, string? provider = null)`: Validates `value` against this set and returns what the provider writes: the option's own code (Single), a typed array (Array), the OR of the codes (Flags), the joined codes (Delimited) or an `ArxJsonB` array (Json). Multiple-choice forms list the options in set order.
  - `value`: The value to write.
  - `entityName`: The table, for the error message.
  - `attributeName`: The column, for the error message.
  - `provider`: The provider's name, for the error.
- `ArxSelectionSetValue? FromStoredValue(object? stored)`: Decodes a stored column value back into an `ArxSelectionSetValue` with its matched options. A stored code that isn't in the set is kept, with a null option (`IsDefined` false); reading never fails because of data that predates the catalog. Null gives null.
  - `stored`: The value read from the column.

### ArxSelectionSetOption

record in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed record ArxSelectionSetOption
```

One option of a selection set. Retired options (`IsActive` false) stay readable but can't be written.

### ArxSelectionSetKind

enum in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public enum ArxSelectionSetKind
```

What a selection set's codes are, matching the column that stores them.

Values:

- `Integer`: Integer codes (1, 2, 3; enum values): `byte` to `long`.
- `String`: String codes ("ACTIVE", "red", "Colors~2").
- `Character`: Single-character codes ('A', 'I').
- `Boolean`: Two options stored as a boolean.
- `Decimal`: Decimal codes.
- `Guid`: Guid codes.
- `DatabaseEnum`: A native database enum, written as its label with a cast to `DatabaseTypeName` where the database needs one.

### ArxSelectionSetMultiplicity

enum in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public enum ArxSelectionSetMultiplicity
```

How many options a record holds, and in which storage form when it's more than one.

Values:

- `Single`: One option, stored as its code.
- `Array`: Several options, stored as a typed array (PostgreSQL arrays, MongoDB arrays, DynamoDB sets).
- `Flags`: Several options, stored as the bitwise OR of their codes. Integer sets only; every code is a power of two.
- `Delimited`: Several options, stored as their codes joined with `Delimiter`.
- `Json`: Several options, stored as a JSON array of their codes.

## Preloading the schema

Reading the tables' ids, keys, lookups and column types from the database once, when the service is set up.

### ArxDynamoPreLoad

static class in `ARX.SDK.DynamoDb.Services`, package `ARX.SDK.DynamoDb`.

```csharp
public static class ArxDynamoPreLoad
```

Loads an `ArxPreLoadTable` from DynamoDB.

Methods:

- `static Task<ArxSchemaCatalog> LoadAsync(this ArxPreLoadTable preload, IAmazonDynamoDB client, ArxDynamoModel model, Action<ArxSchemaCatalogBuilder>? configure = null, CancellationToken cancellationToken = default)`: Reads, once at startup, the key schema of each requested entity's table (`DescribeTable`) and returns a schema catalog: the entity's key as a business key and the key attributes' types as columns. DynamoDB stores no other types, so the other attributes are the ones `configure` declares (`.Column`); with `Strict` they are the allow list.
  - `preload`: The entities to read.
  - `client`: The DynamoDB client.
  - `model`: The tables' layout.
  - `configure`: Declarations of your own: each entity's id, the columns and their types (DynamoDB keeps none), lookups, versions; null for none. They are kept, and the preload adds only what they leave out.
  - `cancellationToken`: Cancels the call.

### ArxPreLoadTable

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxPreLoadTable
```

A preload into a schema catalog at service setup: the registered tables, and what to read of them. Tables their loaded columns reference are added, with the reason. The provider loads it (for example `await preload.LoadAsync(connectionString)`).

Constructors:

- `new ArxPreLoadTable(ArxPreLoadTableRegister tables, ArxPreLoadMode mode = ArxPreLoadMode.DataTypes)`: A preload of the registered tables. Nothing is read until the provider's LoadAsync runs.
  - `tables`: The tables to read, each with its columns (none for every column). The preload keeps a copy.
  - `mode`: DataTypes (the default) reads the keys and every loaded column's type; KeysOnly reads the id, business keys and lookups only.

Properties:

- `IReadOnlyDictionary<string, IReadOnlyList<string>> Tables { get; }`: Each requested table and its columns (empty: all of them).
- `ArxPreLoadMode Mode { get; }`: What the preload reads: DataTypes or KeysOnly.
- `int ReferenceDepth { get; init; }`: How many references deep referenced tables are added (3 by default; 0 adds none).

### ArxPreLoadTableRegister

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxPreLoadTableRegister
```

The tables a preload reads: one `PreLoadTable` call per table, with the columns to load, or none for all of them. A mistake is refused on the call that makes it.

Properties:

- `IReadOnlyDictionary<string, IReadOnlyList<string>> Tables { get; }`: Each registered table and its columns (empty: all of them), in the order registered.

Methods:

- `ArxPreLoadTableRegister PreLoadTable(string table, string[]? columns = null)`: Registers `table` (`"table"` or `"schema.table"`) with the columns to load: `null` or `[]` for every column, a list for only those (the id column is always loaded).
  - `table`: The table to read, as the database names it.
  - `columns`: The columns to read (the id is always read); null for every column.

### ArxPreLoadMode

enum in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public enum ArxPreLoadMode
```

What a preload reads.

Values:

- `DataTypes`: "Fast": the id, business keys, lookups, and every loaded column's type. Needed for `Strict`.
- `KeysOnly`: "Convenient": the table keys only (the id, business keys, lookups), no column types.

### ArxColumnDefinition

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxColumnDefinition
```

A column a preload found: its name, the .NET type ARX reads it as, the database's type and traits.

Constructors:

- `new ArxColumnDefinition(string name, Type type, string databaseType, bool isNullable, ArxColumnTraits traits = ArxColumnTraits.None)`: A column definition, as a preload creates it (or by hand, for a table a preload can't read).
  - `name`: The column name.
  - `type`: The .NET type ARX reads the column as.
  - `databaseType`: The database's type name (for an array, its element type).
  - `isNullable`: Whether the column accepts NULL.
  - `traits`: What the database type is beyond its .NET type: an enum, a composite, an array, JSON, a default.

Properties:

- `string Name { get; }`: The column name.
- `Type Type { get; }`: The .NET type values of this column read back as (Retrieve's value mapping), and are expected as.
- `string DatabaseType { get; }`: The database's type name, as it names it (for example `int8` or `varchar` in PostgreSQL, `nvarchar` in SQL Server; for an array, its element's).
- `bool IsNullable { get; }`: Whether the column accepts NULL.
- `ArxColumnTraits Traits { get; }`: What the database type is beyond its .NET type: an enum, a composite, an array, JSON, a column with a default.

### ArxColumnTraits

enum in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public enum ArxColumnTraits
```

What a column's database type is, beyond its .NET type.

Values:

- `None` = 0: No trait.
- `Enum` = 1: A database enum type: a string is cast to it; read as text.
- `Composite` = 2: A composite (row) type: read as text.
- `Array` = 4: An array of the type.
- `Json` = 8: JSON (`json` or `jsonb`): a string is cast to it.
- `HasDefault` = 16: The database fills it in (identity, default, generated).

## Finding the rows

How Update, Delete and Retrieve find their rows: by id, by a declared key, or by any columns.

### ArxMatch

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxMatch
```

How an update or delete finds its rows: `Id` (the default), `Key` or `Columns`. The values to match come from the entity: its `RecordId` or id attribute for `Id`, the attributes with the key's or columns' names otherwise. Match columns are never written. Immutable.

Created with:

- `ArxMatch.Key(params string[] columns)`: By the declared business key with these columns (in any order). A key the table doesn't declare is refused (ARX1001). On DynamoDB the model declares each entity's key (its partition and sort key attributes), so that key needs no `.Key(...)` of its own there; on the other providers declare it in the schema catalog, so code that moves from DynamoDB adds the `.Key(...)`.
  - `columns`: The columns of a key the table declares, in any order.
- `ArxMatch.Columns(params string[] columns)`: By these columns, declared or not. May match many rows: the call returns how many it changed or deleted.
  - `columns`: Any columns of the table; their values come from the entity's attributes.

Properties:

- `static ArxMatch Id { get; }`: By the table's declared id column (the default). A table declared NoIdColumn can't be matched by id.
- `ArxMatchKind Kind { get; }`: How the rows are found.
- `IReadOnlyList<string> ColumnNames { get; }`: The key's or columns' names, as given; empty for `Id`.

### ArxUpdateOptions

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxUpdateOptions
```

Options for Update and UpdateMultiple.

Properties:

- `ArxMatch Match { get; init; }`: How the rows are found; `Id` by default.
- `bool RequireSingleRow { get; init; }`: When true, every entity must change exactly one row: none is ARX2002 RecordNotFound, more than one is ARX2003 MultipleRecordsMatched, and either way nothing is changed. Off by default: an update by custom columns may change many rows, and the call returns how many.

### ArxDeleteOptions

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxDeleteOptions
```

Options for Delete and DeleteMultiple.

Properties:

- `ArxMatch Match { get; init; }`: How the rows are found; `Id` by default.
- `bool RequireSingleRow { get; init; }`: When true, every entity must delete exactly one row: none is ARX2002 RecordNotFound, more than one is ARX2003 MultipleRecordsMatched, and either way nothing is deleted. Off by default: a delete that finds nothing is a normal outcome, and a delete by custom columns may remove many rows.

### ArxRetrieveOptions

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxRetrieveOptions
```

Options for Retrieve and RetrieveMultiple.

Properties:

- `ArxMatch Match { get; init; }`: How `RetrieveAsync(entity, options)` finds its record: `Id` by default, a declared key, or any columns. More than one matching row is ARX2003 (a Retrieve answers one record). Not used by RetrieveMultiple.
- `ArxColumnSet? Columns { get; init; }`: The columns to read for `RetrieveAsync(entity, options)`; null reads every column.
- `bool LookupNames { get; init; }`: True (the default): a requested lookup column whose referenced table declares a display name reads back with `Name` filled, through one LEFT JOIN per lookup column. False skips the joins.
- `int? CommandTimeout { get; init; }`: Seconds before the query is cancelled (ARX7003). Null keeps the driver's default (30 s in the PostgreSQL, SQL Server and MySQL drivers).

## Queries

RetrieveMultiple's query: what it reads from, which columns, the filters, the joins, the order, paging and aggregates.

### ArxQueryExpression

class in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public class ArxQueryExpression
```

A query for RetrieveMultiple: what to read from (`Source`: a table, view or function), the columns, the criteria (nested AND/OR), joins (`LinkEntities`), order, paging, distinct and aggregates. Every name in it is validated and quoted by the provider, every value sent as a parameter.

Constructors:

- `new ArxQueryExpression()`: A query with no row source yet: set EntityName or Source.
- `new ArxQueryExpression(string entityName)`: A query on a table or view (optionally schema-qualified: `reporting.v_sales`).
  - `entityName`: The table or view to read (on PostgreSQL also a materialized view), or `schema.name`.
- `new ArxQueryExpression(ArxRowSource source)`: A query on any row source: a table, a view, or a function returning rows (`Function`).
  - `source`: What to read from: a table by name, or a function with its arguments.

Properties:

- `ArxRowSource Source { get; set; }`: What the query reads from.
- `string EntityName { get; set; }`: The row source's name. Setting it makes the source that table or view.
- `bool Distinct { get; set; }`: `SELECT DISTINCT`: rows that differ in no selected column come back once. A distinct (or aggregate) query doesn't add the id to the columns, since it would make every row distinct, so its rows have no `ArxEntity.RecordId`: select the id column if you need it.
- `int? Top { get; set; }`: At most this many rows (`LIMIT`), without paging. Can't be combined with `PageInfo` (ARX1001).
- `ArxColumnSet ColumnSet { get; set; }`: The columns to read. Empty (the default) or `AllColumns` reads every column. A column can be written `column._lk.table.column1,column2` to join a referenced table directly.
- `ArxFilterExpression Criteria { get; set; }`: The conditions rows have to meet (nested And/Or).
- `List<ArxOrderExpression> Orders { get; set; }`: The sort keys, in order; AddOrder adds one.
- `List<ArxLinkEntity> LinkEntities { get; set; }`: The joins; AddLink adds one.
- `ArxPagingInfo? PageInfo { get; set; }`: Offset paging: the page size and number, and whether to return the total (in the same round trip).
- `ArxAggregateInfo? AggregateInfo { get; set; }`: Aggregates and group by; null for a plain read.

Methods:

- `void AddOrder(string attributeName, ArxOrderType orderType)`: Orders the results by an attribute; call again for the next key.
  - `attributeName`: The column to order by.
  - `orderType`: Ascending or Descending.
- `void AddOrder(string entityAlias, string attributeName, ArxOrderType orderType)`: Orders by a column of a joined entity, named by its alias.
  - `entityAlias`: The link alias the column belongs to.
  - `attributeName`: The column to order by.
  - `orderType`: Ascending or Descending.
- `ArxLinkEntity AddLink(string linkToEntityName, string linkFromAttributeName, string linkToAttributeName, ArxJoinOperator joinOperator)`: Joins a table (see `ArxLinkEntity`) and returns the join, to add its columns, alias and criteria.
  - `linkToEntityName`: The table to join.
  - `linkFromAttributeName`: The column of the query's own table that the join matches.
  - `linkToAttributeName`: The column of the joined table that it matches.
  - `joinOperator`: INNER (rows with a match only) or LEFT (every row, with nulls where nothing matches).

### ArxRowSource

class in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public sealed class ArxRowSource
```

What a query reads from: a table or view by name, or a function that returns rows, with its arguments. Immutable: `Returns` and `AllowWrites` return a new source.

Created with:

- `ArxRowSource.Table(string name)`: A table or view (on PostgreSQL also a materialized view or foreign table).
  - `name`: The table or view (on PostgreSQL also a materialized view or foreign table), or `schema.name`.
- `ArxRowSource.Function(string name)`: A function with no arguments.
  - `name`: The function, or `schema.function`.
- `ArxRowSource.Function(string name, params (string Name, object? Value)[] arguments)`: A function with named arguments: `Function("fn", ("p_region", "NL"), ("p_active", true))`.
  - `name`: The function, or `schema.function`.
  - `arguments`: Each argument by its parameter's name, with its value.
- `ArxRowSource.Function(string name, params object?[] arguments)`: A function with positional arguments: `Function("fn", "NL", true)`.
  - `name`: The function, or `schema.function`.
  - `arguments`: The arguments, in the function's parameter order.

Properties:

- `ArxRowSourceKind Kind { get; }`: A table (anything read by name) or a function called with arguments.
- `string Name { get; }`: The table, view or function name, optionally schema-qualified (`reporting.v_sales`). Validated by the provider.
- `IReadOnlyList<ArxFunctionArgument> Arguments { get; }`: The function's arguments, in order. Empty for a table.
- `IReadOnlyList<ArxResultColumn> ResultColumns { get; }`: The declared result columns of a function returning untyped rows; empty otherwise.
- `bool AllowsWrites { get; }`: False (the default): a function is read in a read-only transaction when ARX owns the connection, so a function that writes is refused. Inside an `ArxTransaction`, a batch scope or a connection the caller passes in, the read runs in that transaction as it is: a function that writes does write there, since ARX can't make the caller's transaction read-only. True after `AllowWrites`.
- `bool IsFunction { get; }`: True for a function row source.

Methods:

- `ArxRowSource Returns(params (string Name, Type Type)[] columns)`: Declares the result columns of a function that returns untyped rows (`RETURNS SETOF record`).
  - `columns`: The function's result columns and their .NET types, in order.
- `ArxRowSource AllowWrites()`: Lets a function read change data: no read-only transaction around it. Use only for functions you trust.

### ArxColumnSet

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public class ArxColumnSet
```

The columns a read returns: `new ArxColumnSet("name", "email")`, or `All` for all of them. The declared id is always read, so the record's RecordId is set.

Constructors:

- `new ArxColumnSet()`: No columns yet: add them with AddColumn or AddColumns. A read with no columns returns every column (unlike Dynamics XRM, where an empty ColumnSet reads only the id).
- `new ArxColumnSet(params string[] columns)`: These columns: `new ArxColumnSet("name", "email")`.
  - `columns`: The column names, in the order to read them.

Properties:

- `static ArxColumnSet All { get; }`: Every column of the row source: a new set each time, so changing one never changes another. It replaces the constructor that took a bool, removed in 1.0.0-rc.1: its `false` read every column too, unlike XRM's `ColumnSet(false)`. An empty `new ArxColumnSet()` also reads every column; to read only the id, name it: `new ArxColumnSet("id")`.
- `bool AllColumns { get; set; }`: True to read every column of the row source.
- `List<string> Columns { get; }`: The columns to read, by name.

Methods:

- `void AddColumn(string column)`: Adds an attribute to read.
  - `column`: The column to read.
- `void AddColumns(params string[] columns)`: Adds attributes to read.
  - `columns`: The columns to read.

### ArxFilterExpression

class in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public sealed class ArxFilterExpression
```

A group of conditions joined by And or Or (`FilterOperator`), with nested groups (`Filters`) for any mix of the two: a query's `Criteria` and a link entity's `LinkCriteria`.

Constructors:

- `new ArxFilterExpression()`: An empty filter whose conditions all have to hold (And).
- `new ArxFilterExpression(ArxLogicalOperator filterOperator)`: An empty filter joining its conditions with And or Or.
  - `filterOperator`: And: every condition has to hold. Or: any of them.

Properties:

- `List<ArxConditionExpression> Conditions { get; set; }`: The filter's conditions.
- `ArxLogicalOperator FilterOperator { get; set; }`: How the conditions and nested filters are joined: And or Or.
- `List<ArxFilterExpression> Filters { get; set; }`: Nested filters, each joined with the conditions by FilterOperator: `a AND (b OR c)`.

Methods:

- `void AddCondition(ArxConditionExpression condition)`: Adds a condition to this filter.
  - `condition`: The condition to add.
- `void AddCondition(string attributeName, ArxConditionOperator conditionOperator, params object[] values)`: Adds a condition on an attribute of the query's entity: the attribute, the operator and its values.
  - `attributeName`: The column to compare (`alias.column` for a link's).
  - `conditionOperator`: How to compare: Equal, In, Between, Contains, Null and the others.
  - `values`: The values to compare with, sent as parameters: one for most operators, several for In, two for Between, none for Null and NotNull.
- `void AddCondition(string entityName, string attributeName, ArxConditionOperator conditionOperator, params object[] values)`: Adds a condition on an attribute of a linked entity, named by its alias.
  - `entityName`: The link alias (or table) the column belongs to; the query's own table when empty.
  - `attributeName`: The column to compare (`alias.column` for a link's).
  - `conditionOperator`: How to compare: Equal, In, Between, Contains, Null and the others.
  - `values`: The values to compare with, sent as parameters: one for most operators, several for In, two for Between, none for Null and NotNull.
- `void AddFilter(ArxFilterExpression childFilter)`: Adds a nested filter, combined with this one's conditions by this filter's operator.
  - `childFilter`: The nested filter to add.
- `ArxFilterExpression AddFilter(ArxLogicalOperator logicalOperator)`: Adds an empty nested filter with this operator and returns it, to add conditions to.
  - `logicalOperator`: How the nested filter joins its conditions: And or Or.

### ArxConditionExpression

class in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public sealed class ArxConditionExpression
```

One condition: an attribute, an operator (`ArxConditionOperator`) and its values. Every value is sent as a parameter, never written into the SQL.

Constructors:

- `new ArxConditionExpression()`: An empty condition; set its attribute, operator and values.
- `new ArxConditionExpression(string attributeName, ArxConditionOperator conditionOperator)`: A condition with no value: Null and NotNull.
  - `attributeName`: The column compared.
  - `conditionOperator`: How the column is compared with the values.
- `new ArxConditionExpression(string attributeName, ArxConditionOperator conditionOperator, List<object> values)`: A condition with a list of values: In, NotIn, Between.
  - `attributeName`: The column compared.
  - `conditionOperator`: How the column is compared with the values.
  - `values`: The values compared with, each sent as a parameter.
- `new ArxConditionExpression(string attributeName, ArxConditionOperator conditionOperator, object value)`: A condition with one value.
  - `attributeName`: The column compared.
  - `conditionOperator`: How the column is compared with the values.
  - `value`: The value compared with, sent as a parameter.
- `new ArxConditionExpression(string attributeName, ArxConditionOperator conditionOperator, params object[] values)`: A condition with its values.
  - `attributeName`: The column compared.
  - `conditionOperator`: How the column is compared with the values.
  - `values`: The values compared with, each sent as a parameter.
- `new ArxConditionExpression(string entityName, string attributeName, ArxConditionOperator conditionOperator)`: A condition with no value on a joined entity's column.
  - `entityName`: The alias of the link entity whose column is compared.
  - `attributeName`: The column compared.
  - `conditionOperator`: How the column is compared with the values.
- `new ArxConditionExpression(string entityName, string attributeName, ArxConditionOperator conditionOperator, object value)`: A condition with one value on a joined entity's column.
  - `entityName`: The alias of the link entity whose column is compared.
  - `attributeName`: The column compared.
  - `conditionOperator`: How the column is compared with the values.
  - `value`: The value compared with, sent as a parameter.
- `new ArxConditionExpression(string entityName, string attributeName, ArxConditionOperator conditionOperator, params object[] values)`: A condition with its values on a joined entity's column.
  - `entityName`: The alias of the link entity whose column is compared.
  - `attributeName`: The column compared.
  - `conditionOperator`: How the column is compared with the values.
  - `values`: The values compared with, each sent as a parameter.

Properties:

- `string EntityName { get; set; }`: The alias of the link entity whose column is compared; null for the filter's own row source.
- `string AttributeName { get; set; }`: The column compared.
- `ArxConditionOperator Operator { get; set; }`: How the column is compared with the values.
- `List<object> Values { get; set; }`: The values compared with; each is sent as a parameter.

### ArxConditionOperator

enum in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public enum ArxConditionOperator
```

How a condition compares its attribute with its values. Contains, BeginsWith and EndsWith (and their negations) treat `%` and `_` as plain characters. Whether they ignore case depends on the database: PostgreSQL and MongoDB ignore it; on SQL Server and MySQL the column's collation decides (the default ignores it; a binary or case-sensitive collation such as `utf8mb4_bin` doesn't); DynamoDB compares case-sensitively and only when asked (`CaseSensitivePatterns`).

Values:

- `Equal` = 0: The attribute equals the value.
- `NotEqual` = 1: The attribute differs from the value; never matches a null or a missing attribute.
- `GreaterThan` = 2: The attribute is greater than the value.
- `LessThan` = 3: The attribute is less than the value.
- `GreaterEqual` = 4: The attribute is greater than or equal to the value.
- `LessEqual` = 5: The attribute is less than or equal to the value.
- `Like` = 6: The attribute matches a pattern (`%` any text, `_` one character); where a provider can't, ARX8001.
- `NotLike` = 7: The attribute doesn't match the pattern.
- `In` = 8: The attribute equals one of the values.
- `NotIn` = 9: The attribute equals none of the values.
- `Between` = 10: The attribute is between the two values, both included.
- `NotBetween` = 11: The attribute is outside the two values.
- `Null` = 12: The attribute is null (or, where documents may omit it, missing).
- `NotNull` = 13: The attribute has a value.
- `Contains` = 49: The attribute contains the text (the value is text, never a pattern).
- `DoesNotContain` = 50: The attribute doesn't contain the text.
- `BeginsWith` = 54: The attribute begins with the text.
- `DoesNotBeginWith` = 55: The attribute doesn't begin with the text.
- `EndsWith` = 56: The attribute ends with the text.
- `DoesNotEndWith` = 57: The attribute doesn't end with the text.

### ArxLogicalOperator

enum in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public enum ArxLogicalOperator
```

How a filter joins its conditions and nested filters: all of them (And) or any of them (Or).

Values:

- `And` = 0: Every condition must hold.
- `Or` = 1: Any condition may hold.

### ArxDateTimeConditionOption

class in `ARX.SDK.Models`, package `ARX.SDK`.

```csharp
public class ArxDateTimeConditionOption
```

A date and time condition value that says how to compare: the date only (the default) or the date and the time.

Constructors:

- `new ArxDateTimeConditionOption()`: A value compared by its date only, until ComparingType says otherwise.

Properties:

- `DateTime Value { get; set; }`: The date and time to compare with.
- `ArxDateTimeConditionOptionType ComparingType { get; set; }`: DateOnly (the default) compares the column's date with this date; DateAndTime compares the full value.

### ArxDateTimeConditionOptionType

enum in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public enum ArxDateTimeConditionOptionType
```

Whether a date and time condition compares the date only or the date and the time.

Values:

- `DateAndTime`: 
- `DateOnly`: 

### ArxLinkEntity

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public class ArxLinkEntity
```

A join from a query's row source (or from another link) to another table: the column on each side, the join (`JoinOperator`), an alias, the columns it returns (read back as `alias.column`) and its own criteria.

Constructors:

- `new ArxLinkEntity()`: An empty link; set its tables, columns and join.
- `new ArxLinkEntity(string linkToEntityName, string linkFromAttributeName, string linkToAttributeName, ArxJoinOperator joinOperator)`: A join to another table: `JOIN linkTo ON linkTo.linkToAttribute = from.linkFromAttribute`.
  - `linkToEntityName`: The table joined.
  - `linkFromAttributeName`: The column on the query's side (or the parent link's).
  - `linkToAttributeName`: The column on the joined table.
  - `joinOperator`: INNER, LEFT or RIGHT; DEFAULT joins as INNER.

Properties:

- `string LinkFromAttributeName { get; set; }`: The column on the query's side (or the parent link's) that the join matches.
- `string? LinkFromEntityName { get; set; }`: The table on the query's side; informational, the join starts from the query or the parent link.
- `string LinkToEntityName { get; set; }`: The table joined.
- `string LinkToAttributeName { get; set; }`: The column on the joined table that the join matches.
- `ArxColumnSet ColumnSet { get; set; }`: The joined table's columns to read, returned as `alias.column`.
- `string? EntityAlias { get; set; }`: The join's alias: the prefix of its columns in the result, and the name conditions and orders use for it.
- `ArxFilterExpression LinkCriteria { get; set; }`: Conditions on the joined table, applied in the join itself (a LEFT join keeps rows with no match).
- `ArxJoinOperator JoinOperator { get; set; }`: INNER, LEFT or RIGHT; DEFAULT joins as INNER.
- `List<ArxLinkEntity> LinkEntities { get; set; }`: Further joins from this joined table.

### ArxJoinOperator

enum in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public enum ArxJoinOperator
```

The join a link entity makes: `Inner`, `Left` or `Right`. `Default` joins as `Inner`. (Named INNER, LEFT, RIGHT and DEFAULT before 1.0.0-rc.1.)

Values:

- `Default`: No join kind given: an inner join.
- `Inner`: Only rows that have a match in the linked table.
- `Left`: Every row of the query's table, with the linked table's columns null where nothing matches.
- `Right`: Every row of the linked table, with the query's columns null where nothing matches (not on MongoDB).

### ArxOrderExpression

class in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public class ArxOrderExpression
```

One sort key of a query: an attribute, ascending or descending, of the main row source or of a joined entity (`FromEntityAlias`).

Constructors:

- `new ArxOrderExpression()`: An ascending order with no attribute yet.
- `new ArxOrderExpression(string attributeName, ArxOrderType orderType)`: An order by this attribute, ascending or descending.
  - `attributeName`: The column, or an aggregate column's alias, to sort by.
  - `orderType`: Ascending or descending.

Properties:

- `string? FromEntityAlias { get; set; }`: The alias of the joined entity the attribute belongs to; null (the default) for the main row source.
- `string AttributeName { get; set; }`: The column, or an aggregate column's alias, to sort by.
- `ArxOrderType OrderType { get; set; }`: Ascending or descending.

### ArxOrderType

enum in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public enum ArxOrderType
```

Ascending or descending.

Values:

- `Ascending` = 0: Smallest first.
- `Descending` = 1: Largest first.

### ArxPagingInfo

class in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public class ArxPagingInfo
```

Paging for RetrieveMultiple: the page size (`Count`, 5,000 by default) and the 1-based page number. The total is opt-in (`ReturnTotalRecordCount`): it counts every matching row, a second statement in the same round trip.

Constructors:

- `new ArxPagingInfo()`: Page 1 of 5,000 rows, without the total.

Properties:

- `int Count { get; set; }`: The page size: at most this many rows per page.
- `int PageNumber { get; set; }`: The page to read, from 1.
- `bool ReturnTotalRecordCount { get; set; }`: True to count every matching row as well (TotalRecordCount, TotalPageCount): a second statement in the same round trip. Leave it off when the screen doesn't show the total.
- `string? PagingCookie { get; set; }`: Token paging: the `ArxEntityCollection.PagingCookie` of the previous page, to read the page after it; null (the default) for the first page. The cookie is opaque, written by the provider that read the previous page, for the same query: never build or change one. With a cookie, leave `PageNumber` at 1. Only providers that declare `ArxCapability.TokenPaging` accept it; the others refuse it (ARX8001 NotSupported) rather than ignore it.

Methods:

- `ArxPagingInfo? Next(Services.ArxEntityCollection page)`: The paging that reads the page after `page`, on any provider: by its cookie where the provider returned one (MongoDB, DynamoDB), by the next page number where it didn't (PostgreSQL, SQL Server, MySQL); null when no page follows. Same page size; the total isn't asked for again. A loop that only passes the cookie on reads the first page again on a provider without cookies, so a loop meant to run everywhere ends each page with `query.PageInfo = query.PageInfo.Next(page)` and stops on null.
  - `page`: The page the query with this paging returned.

### ArxAggregateInfo

class in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public class ArxAggregateInfo
```

Aggregates and group by for RetrieveMultiple: each aggregate column has an alias, an attribute and an operator (`ArxAggregateOperator`); the group-by columns are listed apart.

Constructors:

- `new ArxAggregateInfo()`: An empty aggregate: add columns with AddAggregateColumn and group-by columns with AddGroupByColumn.

Properties:

- `List<ArxAggregateColumn> AggregateColumns { get; set; }`: The aggregate columns: each has an alias (its name in the result), an attribute and an operator.
- `List<string> GroupByColumns { get; set; }`: The columns the rows are grouped by. Each one is also returned in the result.

Methods:

- `void AddAggregateColumn(ArxAggregateColumn column)`: Adds an aggregate column (an alias, the attribute and the operator).
  - `column`: The aggregate: its alias, column and operator.
- `void AddAggregateColumn(string aliasName, string attributeName, ArxAggregateOperator aggregateOperator)`: Adds an aggregate column: `aliasName` is the name the result holds the value under.
  - `aliasName`: The attribute the result is read into, on each group's record.
  - `attributeName`: The column to aggregate (`alias.column` for a link's); empty for a Count of rows.
  - `aggregateOperator`: The aggregate: Count, CountDistinct, Sum, Avg, Min, Max and the others.
- `void AddGroupByColumn(string column)`: Groups the aggregates by this attribute.
  - `column`: The column to group by (`alias.column` for a link's); each group's record carries it.
- `void AddGroupByColumns(params string[] columns)`: Groups the aggregates by these attributes, in order.
  - `columns`: The columns to group by, in order (`alias.column` for a link's).

### ArxAggregateOperator

enum in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public enum ArxAggregateOperator
```

The value an aggregate column computes: a count, the minimum, maximum, sum or average, a standard deviation or a variance.

Values:

- `None` = 0: No aggregate.
- `Count` = 1: The number of rows (or of non-null values of the attribute).
- `CountDistinct` = 2: The number of distinct non-null values.
- `CountBig` = 3: As Count, as a 64-bit number where the database distinguishes it.
- `CountBigDistinct` = 4: As CountDistinct, as a 64-bit number where the database distinguishes it.
- `Min` = 5: The smallest value.
- `Max` = 6: The largest value.
- `Sum` = 7: The sum of the values.
- `Avg` = 8: The average of the values.
- `StDev` = 9: The sample standard deviation.
- `StDevP` = 10: The population standard deviation.
- `Var` = 11: The sample variance.
- `VarP` = 12: The population variance.

## Procedures and functions

Execute: a call to a stored procedure or function, its parameters and paging, and what comes back.

### ArxProcedureExpression

class in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public sealed class ArxProcedureExpression
```

A call to a routine the database owns: a stored procedure or a function, with its parameters, in the pattern of `ArxQueryExpression`. Never SQL text.

Constructors:

- `new ArxProcedureExpression()`: A call with no routine yet: set ProcedureName.
- `new ArxProcedureExpression(string procedureName)`: A call to this procedure; set ProcedureType to call a function.
  - `procedureName`: The routine, or `schema.routine`.

Properties:

- `string ProcedureName { get; set; }`: The routine's name, optionally schema-qualified (`reporting.sp_close_month`). Validated by the provider.
- `ArxProcedureType ProcedureType { get; set; }`: Procedure (the default) or function.
- `List<ArxProcedureParameter> Parameters { get; set; }`: The parameters, in the order they are passed.
- `bool ReturnsCursors { get; set; }`: A function that returns `refcursor` or `SETOF refcursor`: each cursor is read as a result set. A procedure declares its cursors as parameters instead.
- `List<ArxOrderExpression> Orders { get; set; }`: The order of a function's rows when ARX pages them (way 2). Not for procedures.
- `ArxPagingInfo? PageInfo { get; set; }`: The page to return: of a function's rows, of a cursor, or through the routine's own paging arguments (`PagingParameters`).
- `ArxPagingParameters? PagingParameters { get; set; }`: The routine pages itself: ARX fills these arguments from `PageInfo` (way 1).
- `string? PagedResultSet { get; set; }`: Which result set `PageInfo` pages when there are several cursors: a cursor parameter's name. The first by default.

Methods:

- `void AddParameter(string name, object? value)`: A named IN argument.
  - `name`: The routine's parameter name, as the database declares it.
  - `value`: The value, sent as a parameter; null for SQL NULL.
- `void AddParameter(object? value)`: A positional IN argument (positional arguments come before named ones).
  - `value`: The value, sent as a parameter; null for SQL NULL.
- `void AddParameter(string name, Type type, object? value)`: A named IN argument with its type, for a null the database can't type by itself (an overloaded routine).
  - `name`: The routine's parameter name, as the database declares it.
  - `type`: The parameter's .NET type, so a null is sent with the right database type.
  - `value`: The value, sent as a parameter; null for SQL NULL.
- `void AddOutParameter<T>(string name)`: An OUT parameter of a procedure, read into `Outputs[name]` as `T`.
  - `name`: The routine's OUT parameter name; its value is read back into the result's `Outputs`.
- `void AddOutParameter(string name, Type type)`: An OUT parameter of a procedure, read into `Outputs[name]` as `type`.
  - `name`: The routine's OUT parameter name; its value is read back into the result's `Outputs`.
  - `type`: The .NET type the value is read as.
- `void AddInOutParameter<T>(string name, T value)`: An INOUT parameter: `value` goes in, the procedure's value is read into `Outputs[name]`.
  - `name`: The routine's INOUT parameter name; the value it ends with is read back.
  - `value`: The value sent in.
- `void AddCursorParameter(string name)`: A table the procedure returns, through an `INOUT refcursor` parameter: read into `ResultSets[name]`.
  - `name`: The parameter through which the procedure returns a table (a refcursor on PostgreSQL), read as a result set.
- `void AddVariadicParameter(string name, Array values)`: An array passed to a named `VARIADIC` parameter.
  - `name`: The routine's VARIADIC parameter name.
  - `values`: The values, sent as one array.
- `void AddVariadicParameter(Array values)`: An array passed to a positional `VARIADIC` parameter.
  - `values`: The values, sent as one array.
- `void AddOrder(string attributeName, ArxOrderType orderType)`: Orders a function's rows (way 2).
  - `attributeName`: The result column to order by.
  - `orderType`: Ascending or Descending.

### ArxProcedureType

enum in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public enum ArxProcedureType
```

What a routine is: PostgreSQL calls them differently (`CALL` against `SELECT`), so the caller says which.

Values:

- `Procedure`: A stored procedure: `CALL name(...)`. Returns OUT values and, through cursor parameters, tables.
- `Function`: A function: `SELECT * FROM name(...)`. Returns rows (a value, a row of OUT values, a table, a set).

### ArxProcedureParameter

class in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public sealed class ArxProcedureParameter
```

One parameter of a routine: by name or by position (`Name` null), with its value and, when needed, its type.

Constructors:

- `new ArxProcedureParameter(string? name, object? value, Type? type, ArxProcedureParameterDirection direction)`: A parameter; the AddParameter methods of ArxProcedureExpression create them.
  - `name`: The parameter's name in the routine; null to pass it by position.
  - `value`: The value passed in (null for an OUT parameter).
  - `type`: Its .NET type, when the value alone doesn't say (a null, an OUT or INOUT parameter).
  - `direction`: In, Out, InOut, a cursor or a variadic list.

Properties:

- `string? Name { get; }`: The parameter's name in the routine; null for a positional argument.
- `object? Value { get; }`: The value passed in (null for OUT and cursor parameters). Sent as a typed parameter, like a write's value.
- `Type? Type { get; }`: The declared .NET type: required for OUT and INOUT, and for a null the database must type; null otherwise.
- `ArxProcedureParameterDirection Direction { get; }`: In, Out, InOut, a cursor or a variadic list.

### ArxProcedureParameterDirection

enum in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public enum ArxProcedureParameterDirection
```

How a parameter of a routine is passed.

Values:

- `In`: A value passed in.
- `Out`: A value the procedure sets: declared with its type, read into `Outputs`.
- `InOut`: A value passed in and read back, possibly changed.
- `Cursor`: A table the procedure returns: an `INOUT refcursor`, read as a result set named after the parameter.
- `Variadic`: An array passed to a `VARIADIC` parameter.

### ArxPagingParameters

class in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public sealed class ArxPagingParameters
```

The arguments of a routine that pages itself (way 1): ARX fills them from `PageInfo`, and reads the total from `TotalRecordCount` (an OUT parameter of a procedure, a result column of a function).

Created with:

- `ArxPagingParameters.PageNumber(string pageNumber, string pageSize, string? totalRecordCount = null)`: The routine takes a page number (from 1) and a page size: `PageNumber("p_page", "p_page_size", totalRecordCount: "p_total")`.
  - `pageNumber`: The routine's page number parameter.
  - `pageSize`: The routine's page size parameter.
  - `totalRecordCount`: Where the routine returns the total: an OUT parameter of a procedure, or a result column of a function. Null when it doesn't.
- `ArxPagingParameters.OffsetLimit(string offset, string limit, string? totalRecordCount = null)`: The routine takes an offset and a limit: `OffsetLimit("p_offset", "p_limit", totalRecordCount: "p_total")`.
  - `offset`: The routine's offset parameter (rows to skip).
  - `limit`: The routine's limit parameter (rows to return).
  - `totalRecordCount`: Where the routine returns the total: an OUT parameter of a procedure, or a result column of a function. Null when it doesn't.

Properties:

- `ArxPagingParametersStyle Style { get; }`: Whether the routine takes a page number and size, or an offset and a limit.
- `string First { get; }`: The page number's parameter (PageNumber style) or the offset's (OffsetLimit style).
- `string Second { get; }`: The page size's parameter (PageNumber style) or the limit's (OffsetLimit style).
- `string? TotalRecordCount { get; }`: Where the routine returns the total: a procedure's OUT parameter (ARX declares it as `bigint` if it isn't), or a function's result column. Null when the routine returns none.

### ArxExecuteOptions

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxExecuteOptions
```

Options for one Execute call.

Properties:

- `int? CommandTimeout { get; init; }`: The command timeout in seconds for this call; null keeps the driver's default (30 s in the PostgreSQL, SQL Server and MySQL drivers). Long jobs set it.
- `bool CollectMessages { get; init; }`: True (the default): the messages the routine sends while it runs (notices, warnings) are collected into `Messages`. Never logged.

### ArxExecuteResult

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxExecuteResult
```

What a routine returned: its OUT and INOUT values, its result sets (a procedure's tables, a function's rows) and the messages it sent. No rows-affected count: not every database reports one for a call.

Constructors:

- `new ArxExecuteResult(string routineName)`: An empty result for a call to this routine; the provider fills it.
  - `routineName`: The routine's name, which is also the name of the Outputs entity.

Properties:

- `const string MessagesKey`: The key under which a failed call's messages are put in `Data`.
- `ArxEntity Outputs { get; }`: The OUT and INOUT values of a procedure, one attribute per parameter name.
- `ArxResultSetCollection ResultSets { get; }`: The tables a procedure returned (by its cursor parameters' names) or a function's rows (`ResultSets[0]`).
- `IReadOnlyList<ArxDatabaseMessage> Messages { get; }`: The messages the routine sent while it ran, in order (when `CollectMessages`).

Methods:

- `void AddMessage(ArxDatabaseMessage message)`: Adds a message (providers).
  - `message`: A message the routine raised (a notice or warning).
- `T? Value<T>()`: A function's single value: the one column of its one row, converted to `T`. Default when there is no row or the value is NULL; ARX2003 when there are several rows; ARX1001 when the row has several columns or the value isn't a `T`.

### ArxResultSetCollection

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxResultSetCollection : IReadOnlyList<ArxEntityCollection>
```

The result sets of a call, in order, also by name (a cursor parameter's name, or the cursor's).

Properties:

- `int Count { get; }`: How many result sets the call returned.
- `ArxEntityCollection this[int index] { get; }`: The result set at this position, in the order the call returned them.
- `ArxEntityCollection this[string name] { get; }`: The result set with this name (case-insensitive). ARX1001 if there is none.

Methods:

- `bool TryGet(string name, out ArxEntityCollection set)`: Gets a result set by name; false when there is none.
  - `name`: The result set's name: its cursor parameter's name.
  - `set`: The result set when found; otherwise null.
- `void Add(ArxEntityCollection set)`: Adds a result set (providers).
  - `set`: The result set to add.
- `IEnumerator<ArxEntityCollection> GetEnumerator()`: The result sets, in the order the routine returned them.

### ArxDatabaseMessage

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxDatabaseMessage
```

A message a routine sent while it ran (PostgreSQL `RAISE NOTICE`, SQL Server `PRINT`, MySQL warnings), in the order sent.

Constructors:

- `new ArxDatabaseMessage(string severity, string sqlState, string text, string? detail, string? hint)`: A message, as the provider reads it from the database.
  - `severity`: NOTICE, WARNING, INFO, LOG or DEBUG.
  - `sqlState`: The message's SQLSTATE code.
  - `text`: The message text.
  - `detail`: Its detail, or null.
  - `hint`: Its hint, or null.

Properties:

- `string Severity { get; }`: NOTICE, WARNING, INFO, LOG or DEBUG.
- `string SqlState { get; }`: The message's SQLSTATE code (00000 for a plain notice).
- `string Text { get; }`: The message text.
- `string? Detail { get; }`: The message's detail, when the routine gave one.
- `string? Hint { get; }`: The message's hint, when the routine gave one.

## Errors

What ARX throws: refused before the database, refused by it, or not supported; each with a stable code.

### ArxException

class in `ARX.SDK`, package `ARX.SDK`.

```csharp
public class ArxException : Exception
```

Base type of every error ARX.SDK reports, apart from `ArgumentNullException` for null arguments. Handle errors by `Code`, `Category` or `IsTransient`, which are the same in every provider; the native details (`NativeCode`, `SqlState`, `InnerException`) are kept for diagnostics. Messages contain codes and names, never attribute values.

Constructors:

- `new ArxException(ArxErrorCode code, string message, Exception? innerException = null)`: An ARX error with its code and a message for logs and developers.
  - `code`: The ARX error code: its thousands digit is the category.
  - `message`: A message for logs and developers. It names tables, columns and constraints, never values.
  - `innerException`: The error that caused it, if any.

Properties:

- `ArxErrorCode Code { get; }`: The stable ARX error code.
- `ArxErrorCategory Category { get; }`: The category of `Code`.
- `bool IsTransient { get; init; }`: True when repeating the whole operation can succeed (deadlocks, timeouts, connection drops).
- `string? Provider { get; init; }`: The provider that reported the error, for example "PostgreSql". Null for errors raised before any provider work.
- `string? EntityName { get; init; }`: The entity (table or collection) involved, when known.
- `string? AttributeName { get; init; }`: The attribute (column) involved, when known.
- `string? ConstraintName { get; init; }`: The constraint the database reported, when known.
- `string? NativeCode { get; init; }`: The provider's own error code, for example "23505", "2627", "E11000" or an AWS exception name.
- `string? SqlState { get; init; }`: The ISO SQLSTATE, when the database supplies one.
- `string? Detail { get; init; }`: The database's detail for the error, when it gives one (a routine's `RAISE ... USING DETAIL`).
- `string? Hint { get; init; }`: The database's hint for the error, when it gives one (a routine's `RAISE ... USING HINT`).
- `int? BatchNumber { get; }`: The 1-based number of the failed call within its batch scope; null for errors raised outside a scope. Set by the scope when the error passes through it.

Methods:

- `static string FormatCode(ArxErrorCode code)`: The message prefix for a code, for example "ARX3001 UniqueViolation".
  - `code`: The error code.

### ArxValidationException

class in `ARX.SDK`, package `ARX.SDK`.

```csharp
public sealed class ArxValidationException : ArxException
```

The caller's input is invalid. Always raised before any database access.

Constructors:

- `new ArxValidationException(ArxErrorCode code, string message, Exception? innerException = null)`: A request refused before anything was sent to the database.
  - `code`: The ARX error code: its thousands digit is the category.
  - `message`: A message for logs and developers. It names tables, columns and constraints, never values.
  - `innerException`: The error that caused it, if any.

### ArxDatabaseException

class in `ARX.SDK`, package `ARX.SDK`.

```csharp
public sealed class ArxDatabaseException : ArxException
```

The database reported an error. The native exception is the `InnerException`.

Constructors:

- `new ArxDatabaseException(ArxErrorCode code, string message, Exception innerException)`: A database error mapped to its ARX code; the driver's exception is the inner exception.
  - `code`: The ARX error code: its thousands digit is the category.
  - `message`: A message for logs and developers. It names tables, columns and constraints, never values.
  - `innerException`: The driver's exception (PostgresException, SqlException, MySqlException, a MongoException or an AmazonDynamoDBException).

Methods:

- `static ArxException InLinkWhenItIs(ArxException mapped, ARX.SDK.Query.ArxQueryExpression query)`: For the providers' use: an ARX5002 UndefinedAttribute from a query whose missing column only its link entities name, said of the link's table (`EntityName`) and alias instead of the query's own table. Any other error is returned as it is.
  - `mapped`: The error the provider's mapper made, with the missing column as its `AttributeName`.
  - `query`: The query that was sent.

### ArxNotSupportedException

class in `ARX.SDK`, package `ARX.SDK`.

```csharp
public sealed class ArxNotSupportedException : ArxException
```

The provider doesn't support the requested operation or feature.

Constructors:

- `new ArxNotSupportedException(string message, Exception? innerException = null)`: Something the provider can't do (ARX8001), refused rather than ignored.
  - `message`: A message for logs and developers. It names tables, columns and constraints, never values.
  - `innerException`: The error that caused it, if any.

### ArxErrorCode

enum in `ARX.SDK`, package `ARX.SDK`.

```csharp
public enum ArxErrorCode
```

Stable ARX error codes, identical in every provider and language. The thousands digit is the category. Numbers are never reused or renumbered; names are the cross-language identifiers.

Values:

- `InvalidArgument` = 1001: Invalid input not covered by a more specific code.
- `MissingEntityName` = 1002: The entity name is null or blank.
- `InvalidIdentifier` = 1003: An entity or attribute name fails the identifier rules.
- `DuplicateAttribute` = 1004: The same attribute appears more than once (names are case-insensitive).
- `InvalidLookup` = 1005: A lookup (`ArxEntityReference`) has no usable Id.
- `InvalidSelectionSet` = 1006: A selection set value has no option set name.
- `UnsupportedValueType` = 1007: An attribute value has a type the provider can't write.
- `InvalidConnection` = 1008: The connection or transaction is of the wrong driver type, or the service has no connection string.
- `TransactionMismatch` = 1009: The transaction belongs to a different connection.
- `TransactionCompleted` = 1010: The transaction has already been committed, rolled back or disposed.
- `SelectionSetValueNotAllowed` = 1011: A selection set value isn't allowed by its set in the catalog: not one of its options, of the wrong kind, a retired option, several values for a single-choice set, a duplicate, or a value the set's storage can't hold.
- `UnknownSelectionSet` = 1012: A selection set value names a set that isn't in the catalog, or no catalog was supplied.
- `InvalidDateTimeValue` = 1013: A date or time value that would be stored as something else than the caller meant: default(DateTime), default(DateTimeOffset) or default(DateOnly) (no real date: a database would store a sentinel or refuse it), or an array mixing UTC and Unspecified DateTime values.
- `BatchScopeBusy` = 1014: A batch scope was called while another call on it was still running. A scope holds one connection, which runs one command at a time: await each call before the next.
- `TransactionAborted` = 1015: A call or a commit on a transaction that an earlier failed call has aborted. The database has already discarded the transaction's work; it can only be rolled back.
- `IdNotDeclared` = 1016: The entity's table isn't declared in the service's schema catalog, and the entity doesn't declare its own id column. A service with a catalog uses only what is declared: ARX doesn't guess an id column. A service created without a catalog reads the table's primary key instead (where the provider can), and raises `TableNotResolvable` when it can't.
- `SuspectedInjection` = 1017: A name (entity, attribute, match column or type) contains SQL syntax: a quote, a statement separator, a comment marker, a bracket, an operator or an SQL keyword after a space. That is the shape of an SQL injection attempt, not of a typo, so it gets its own code to alert on. Refused before the database: nothing was sent. Values are never checked for this: they are always parameters, and any text is stored as typed.
- `NotPreloaded` = 1018: The service is `Strict` and the call names a table or column that wasn't preloaded: refused before the database. Preload it, or turn Strict off.
- `TableNotResolvable` = 1019: A service without a schema catalog resolves each table from the database, and this table has no id the call can use: no primary key, a primary key of several columns, or one of a type ARX can't use as an id. Refused before anything is sent, only for a call that needs the id (Create, Retrieve, Update or Delete by id). Declare the table in a schema catalog (`ArxIdColumn.NoIdColumn`), or match by a key or columns.
- `RecordNotFound` = 2002: The record addressed by key doesn't exist.
- `MultipleRecordsMatched` = 2003: An update or delete with `RequireSingleRow` found more than one row for an entity. Nothing was changed.
- `RecordChanged` = 2004: The record was changed since it was read: an Update or Delete carried a version (optimistic concurrency) that isn't the record's any more. Nothing was changed: read it again, and apply the change to what is there now.
- `UniqueViolation` = 3001: A unique constraint or key was violated.
- `ForeignKeyViolation` = 3002: A referenced record is missing, or a record is still referenced.
- `NotNullViolation` = 3003: A required attribute has no value.
- `CheckViolation` = 3004: A check constraint or validation rule failed.
- `RaisedByDatabase` = 3005: A routine the database runs (a procedure or function) refused the call on purpose (PostgreSQL `RAISE EXCEPTION`, SQL Server `THROW`, MySQL `SIGNAL`). On PostgreSQL: `RAISE EXCEPTION` (SQLSTATE P0001), an `ASSERT` or a state of the routine's own (`USING ERRCODE = 'AX001'`). The message is the routine's; `SqlState`, `Detail` and `Hint` are its own.
- `ValueOutOfRange` = 4001: A number is outside the column's range.
- `ValueTooLong` = 4002: Text or binary data is longer than the column allows.
- `InvalidValueFormat` = 4003: A value can't be parsed for its column (for example invalid JSON, dates or encoding).
- `DataTypeMismatch` = 4004: A value's type doesn't fit the column.
- `UndefinedEntity` = 5001: The table or collection doesn't exist.
- `UndefinedAttribute` = 5002: The column doesn't exist or can't be written.
- `SchemaMismatch` = 5003: A declaration in the schema catalog doesn't match the table: the database returned an id of another type, or `ValidateSchemaAsync` found a difference. The exception says whether rows were stored.
- `PermissionDenied` = 6001: The database user lacks the required permission.
- `Deadlock` = 7001: The operation was chosen as a deadlock victim. Safe to retry.
- `SerializationFailure` = 7002: The operation conflicted with a concurrent transaction. Safe to retry.
- `Timeout` = 7003: The command or a lock wait timed out. Safe to retry.
- `Throttled` = 7004: The server is rate-limiting or out of connections. Safe to retry later.
- `ConnectionFailure` = 7005: The database couldn't be reached, or the connection dropped. Safe to retry.
- `NotSupported` = 8001: The provider doesn't support the requested operation or feature.
- `ProviderError` = 9001: A native error the provider doesn't map; see NativeCode and SqlState.

### ArxErrorCategory

enum in `ARX.SDK`, package `ARX.SDK`.

```csharp
public enum ArxErrorCategory
```

The category of an `ArxErrorCode`, given by its thousands digit.

Values:

- `Validation` = 1: ARX refused the call before sending it: invalid input, a misused connection, transaction or batch scope, or a name that looks like SQL injection.
- `NotFound` = 2: The rows the call addressed weren't found as asked: none, several where one was required, or one at another version than the call carried (ARX2004). Nothing was changed.
- `Constraint` = 3: The database refused the change because of a rule: a unique key, a foreign key, NOT NULL, CHECK, or a routine's own error.
- `Data` = 4: A value doesn't fit its column: out of range, too long, in the wrong format or of the wrong type.
- `Schema` = 5: A table or column doesn't exist, or a declaration in the schema catalog doesn't match the table.
- `Permission` = 6: The database user isn't allowed to do it.
- `Transient` = 7: A temporary failure: a deadlock, a conflict, a timeout, throttling or a lost connection. Safe to retry the whole operation (`IsTransient` is true).
- `NotSupported` = 8: The provider can't do what was asked, and refuses it rather than ignoring part of it.
- `Provider` = 9: A database error ARX doesn't map; the exception keeps its native code and SQLSTATE.

## Routing, paging cookies and capabilities

Sending each tenant or key to its own service, the paging cookie format, and what a provider declares it can do.

### ArxRouter

static class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxRouter<TService> : IArxRouter<TService>, IDisposable, IAsyncDisposable
```

The router `Create{TService}` makes. Disposing it disposes the services it made, not the ones it was given.

Properties:

- `IReadOnlyCollection<string> RouteKeys { get; }`: Every declared route key.

Methods:

- `static ArxRouter<TService> Create<TService>(Action<ArxRouterBuilder<TService>> configure, IServiceProvider? services = null)`: A router with the routes `configure` declares. Register it as a singleton, for example `services.AddSingleton<IArxRouter<ArxDynamoDbService>>(sp => ArxRouter.Create<ArxDynamoDbService>(r => r.Route("acme", …), sp))`.
  - `configure`: Declares the routes; at least one.
  - `services`: Optional: passed to the route factories.
- `TService For(string routeKey)`: The service for this key. An unknown key is ARX1001 InvalidArgument (the message doesn't echo the key).
  - `routeKey`: The key that picks the service: a tenant, a region, a shard.
- `bool TryFor(string routeKey, out TService service)`: The service for this key, or false when no route is declared for it.
  - `routeKey`: The key that picks the service: a tenant, a region, a shard.
  - `service`: The service when the key has a route; otherwise the default.
- `void Dispose()`: Disposes the services the router made.
- `ValueTask DisposeAsync()`: Disposes the services the router made.

### ArxRouterBuilder

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxRouterBuilder<TService>
```

Declares a router's routes, for `Create{TService}`.

Methods:

- `ArxRouterBuilder<TService> Route(string routeKey, Func<IServiceProvider, TService> factory)`: A route whose service the router makes on first use, and disposes with itself.
  - `routeKey`: The key callers pass to `For`.
  - `factory`: Makes the service; it gets the service provider given to `Create{TService}` (null without one).
- `ArxRouterBuilder<TService> Route(string routeKey, Func<TService> factory)`: A route whose service the router makes on first use, and disposes with itself.
  - `routeKey`: The key that picks this service.
  - `factory`: Makes the service for this key.
- `ArxRouterBuilder<TService> Route(string routeKey, TService service)`: A route to a service the application made and owns: the router never disposes it.
  - `routeKey`: The key that picks this service.
  - `service`: The service for this key.
- `ArxRouterBuilder<TService> KeyComparer(StringComparer comparer)`: How route keys compare: ordinal and case-sensitive by default.
  - `comparer`: How keys are compared (for example `OrdinalIgnoreCase`); ordinal by default.

### IArxRouter

interface in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public interface IArxRouter<TService>
```

Sends a call to the right service for a route key: a tenant, a region, a database. One service per key, made on first use and kept, so the router is a singleton like the services it holds. There is no mutable "current tenant": each call asks the router for its own key, so concurrent requests never see each other's.

Properties:

- `IReadOnlyCollection<string> RouteKeys { get; }`: Every declared route key.

Methods:

- `TService For(string routeKey)`: The service for this key. An unknown key is ARX1001 InvalidArgument (the message doesn't echo the key).
  - `routeKey`: The key that picks the service: a tenant, a region, a shard.
- `bool TryFor(string routeKey, out TService service)`: The service for this key, or false when no route is declared for it.
  - `routeKey`: The key that picks the service: a tenant, a region, a shard.
  - `service`: The service when the key has a route; otherwise the default.

### ArxPagingCookie

static class in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public static class ArxPagingCookie
```

Writes and reads the opaque cookies of token paging (`PagingCookie`), for providers that declare `ArxCapability.TokenPaging`. A cookie carries the provider's own state (for example DynamoDB's last evaluated key) with a version, the provider's name and a fingerprint of the query it belongs to; with a signing key it is also signed (HMAC-SHA256), so a client can't forge or edit one. A cookie from another provider, another query, another key, or one that was changed is ARX1001 InvalidArgument, and the message never echoes it.

Properties:

- `const int MaxLength`: The longest cookie ARX reads, in characters: anything longer is refused before decoding.
- `static byte[] ProcessKey { get; }`: This process's own random key: the key of every service that is given none, so services made per request still read each other's cookies. It changes with every process: another instance, or a restart, needs a shared key.

Methods:

- `static string Encode(string providerName, ReadOnlySpan<byte> state, string? query = null, byte[]? signingKey = null)`: A cookie for the next page.
  - `providerName`: The provider writing it (checked when it is read back).
  - `state`: The provider's position, in its own encoding.
  - `query`: A description of the query the cookie belongs to (its source, filter and order, never the page): a cookie used with another query is refused. Null for none.
  - `signingKey`: Optional: a key of at least 32 bytes to sign the cookie with. The same key reads it back.
- `static byte[] Decode(string cookie, string providerName, string? query = null, byte[]? signingKey = null)`: The provider's state from a cookie it wrote; ARX1001 when the cookie isn't one.
  - `cookie`: The cookie from `PagingCookie`.
  - `providerName`: The provider reading it: a cookie written by another is refused.
  - `query`: The same description of the query that `Encode` was given.
  - `signingKey`: The key it was signed with; null when it wasn't signed. A signed cookie read without the key, or the reverse, is refused.
- `static string Encode(string providerName, ReadOnlySpan<byte> state, string query, byte[] key, ArxPagingCookieProtection protection)`: A cookie for the next page, protected as `protection` says: `Sealed` (version 2: encrypted and authenticated, AES-256-GCM, so a client can neither read nor change it), `Signed` (version 1, HMAC-SHA256: readable, not changeable) or `Plain` (version 1, unsigned).
  - `providerName`: The provider writing it (checked when it is read back).
  - `state`: The provider's position, in its own encoding.
  - `query`: A description of the query the cookie belongs to; a cookie used with another query is refused. Null for none.
  - `key`: The key (32 bytes or more) for Sealed and Signed; ignored for Plain.
  - `protection`: How the cookie is protected.
- `static byte[] Decode(string cookie, string providerName, string query, byte[] key, ArxPagingCookieProtection protection)`: The provider's state from a cookie protected as `protection` says; ARX1001 when it isn't one: a cookie protected otherwise, changed, written by another provider or for another query, or sealed with another key (another instance of the application, or before a restart, without a shared key).
  - `cookie`: The cookie from `PagingCookie`.
  - `providerName`: The provider reading it.
  - `query`: The same description of the query that Encode was given.
  - `key`: The key it was sealed or signed with; ignored for Plain.
  - `protection`: The protection the service requires.
- `static byte[] NewKey()`: A new random key for paging cookies: 32 bytes.

### ArxCapability

enum in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public enum ArxCapability
```

The features that differ between providers. A provider declares the ones it supports, so code can choose a path before it calls. The flags never replace the rule: a provider refuses an unsupported part of a call with `ArxNotSupportedException` (ARX8001) naming it, and never ignores it.

Values:

- `None` = 0: No feature.
- `Joins` = 1 << 0: Linked entities in a query (`ArxLinkEntity`): SQL joins, MongoDB `$lookup`.
- `Aggregates` = 1 << 1: Aggregates and grouping in a query (`AggregateInfo`).
- `OffsetPaging` = 1 << 2: Paging by page number (`PageInfo.PageNumber` beyond 1).
- `TokenPaging` = 1 << 3: Paging by an opaque cookie from the previous page (`PageInfo.PagingCookie`).
- `TotalCount` = 1 << 4: The total of matching rows (`PageInfo.ReturnTotalRecordCount`).
- `Transactions` = 1 << 5: ARX transactions and batch scopes across several calls.
- `BulkWrite` = 1 << 6: CreateMultiple, UpdateMultiple and DeleteMultiple.
- `Upsert` = 1 << 7: Reserved for an insert-or-update operation. ARX has none yet, so no provider declares it.
- `StoredProcedures` = 1 << 8: Execute: stored procedures and functions (`ArxProcedureExpression`).
- `FullTextSearch` = 1 << 9: Full-text search conditions (`ArxFullTextSearch`).
- `LikeOperators` = 1 << 10: Pattern conditions: Like, BeginsWith, EndsWith, Contains (and their negations).
- `OrFilters` = 1 << 11: Filters joined by Or (`ArxFilterExpression` with the Or operator).
- `SecondaryIndexQuery` = 1 << 12: A read that names a secondary index (DynamoDB GSIs). Relational databases choose indexes themselves and don't declare it.
- `OptimisticConcurrency` = 1 << 13: Optimistic concurrency: a table's version column (`ArxSchemaCatalogBuilder.Version`) kept and checked on writes (ARX2004).
- `SchemaResolution` = 1 << 14: Without a schema catalog, the service reads each table's id column, keys and column types from the database when a call needs them. Without this capability, a service with no catalog refuses an undeclared table (ARX1016).
- `Savepoints` = 1 << 15: A batch scope can undo one failed call alone, through a savepoint (`Savepoints`). Without it the default batch scope has no savepoints, and asking for them is ARX8001.
- `WriteByFilter` = 1 << 16: Update and delete by a filter: `UpdateMultipleAsync(values, filter)` and `DeleteMultipleAsync(table, filter)` change the rows a filter of the table's own columns matches. DynamoDB declares it with a rule: the filter must give the partition key (else ARX8001), and at most 100 items change.

### IArxProviderCapabilities

interface in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public interface IArxProviderCapabilities
```

What a provider supports, as `ArxCapability` flags.

Properties:

- `string ProviderName { get; }`: The provider's name, as messages show it (for example "PostgreSQL").
- `ArxCapability Supported { get; }`: Every supported feature.

Methods:

- `bool Supports(ArxCapability capability)`: True when every flag in `capability` is supported. `None` is always true.
  - `capability`: One capability, or several combined with `|`: all must be supported.

### ArxProviderCapabilities

class in `ARX.SDK.Providers`, package `ARX.SDK`.

```csharp
public sealed class ArxProviderCapabilities : IArxProviderCapabilities
```

The capabilities a provider declares: a fixed set of flags with its name.

Constructors:

- `new ArxProviderCapabilities(string providerName, ArxCapability supported)`: A provider's declared features.
  - `providerName`: The provider's name, as messages show it.
  - `supported`: Every feature it supports.

Properties:

- `string ProviderName { get; }`: The provider's name, as messages show it (for example "PostgreSQL").
- `ArxCapability Supported { get; }`: Every supported feature.

Methods:

- `bool Supports(ArxCapability capability)`: True when every flag in `capability` is supported. `None` is always true.
  - `capability`: One capability, or several combined with `|`: all must be supported.
- `void Require(ArxCapability capability, string feature, string? advice = null)`: Throws `ArxNotSupportedException` (ARX8001) naming `feature` and the provider when any flag in `capability` isn't supported.
  - `capability`: The features the call needs.
  - `feature`: What the call asked for, as the message names it (for example "PageInfo.PagingCookie").
  - `advice`: Optional: what to do instead.
- `void RequireSchemaFeatures(ArxSchemaCatalog schema)`: Throws `ArxNotSupportedException` (ARX8001) when `schema` declares something this provider can't keep: a version column (`OptimisticConcurrency`) without the capability. A provider calls it wherever it takes a schema (its constructor, a call's own schema), so a declaration is never silently ignored.
  - `schema`: The schema given to the provider; null declares nothing.
- `IReadOnlyList<ArxCapability> ToList()`: The supported features, by name.

## Amazon DynamoDB's own types

What only Amazon DynamoDB has: its options and, where it has one, the model of its tables.

### ArxDynamoModel

class in `ARX.SDK.DynamoDb.Model`, package `ARX.SDK.DynamoDb`.

```csharp
public sealed class ArxDynamoModel
```

How ARX's entities are laid out in DynamoDB tables: one table per entity (`Table`), or several entities in one table with key templates (`SingleTable`), with their secondary indexes and write sharding. Build it once at startup. Immutable and thread-safe.

Properties:

- `IReadOnlyCollection<ArxDynamoEntityMap> Entities { get; }`: Every entity, by name.
- `IReadOnlyCollection<ArxDynamoTableMap> Tables { get; }`: Every physical table.

Methods:

- `static ArxDynamoModelBuilder Create()`: Starts a model: declare each table and entity, then call Build.
- `bool TryGetEntity(string name, out ArxDynamoEntityMap entity)`: Gets an entity's layout by name; false when the model doesn't have it.
  - `name`: The entity's name.
  - `entity`: Its layout when found; otherwise null.
- `IReadOnlyList<CreateTableRequest> ToCreateTableRequests()`: The create-table requests the model needs: keys, attribute definitions and the global secondary indexes, on-demand billing. For tests, samples and development: production tables belong to the infrastructure.

### ArxDynamoModelBuilder

class in `ARX.SDK.DynamoDb.Model`, package `ARX.SDK.DynamoDb`.

```csharp
public sealed class ArxDynamoModelBuilder
```

Declares an `ArxDynamoModel`. Not thread-safe; build once, at startup.

Methods:

- `ArxDynamoModelBuilder Table(string entity, string partitionKey, string? sortKey = null, ArxDynamoKeyType partitionKeyType = ArxDynamoKeyType.String, ArxDynamoKeyType sortKeyType = ArxDynamoKeyType.String, string? tableName = null)`: A table for one entity (multi-table design): the entity's own attributes are the keys. The table is named after the entity unless `tableName` says otherwise.
  - `entity`: The entity's name.
  - `partitionKey`: The attribute that is the table's partition key.
  - `sortKey`: The attribute that is its sort key; null for none.
  - `partitionKeyType`: The partition key's DynamoDB type: String, Number or Binary.
  - `sortKeyType`: The sort key's DynamoDB type: String, Number or Binary.
  - `tableName`: The table's name in DynamoDB; the entity's name when null.
- `ArxDynamoModelBuilder Index(string entity, string indexName, string partitionKey, string? sortKey = null, ArxDynamoKeyType partitionKeyType = ArxDynamoKeyType.String, ArxDynamoKeyType sortKeyType = ArxDynamoKeyType.String)`: A global secondary index on a table-per-entity table, keyed by the entity's attributes.
  - `entity`: The entity whose table has the index.
  - `indexName`: The index's name in DynamoDB.
  - `partitionKey`: The attribute that is the index's partition key.
  - `sortKey`: The attribute that is its sort key; null for none.
  - `partitionKeyType`: The partition key's DynamoDB type: String, Number or Binary.
  - `sortKeyType`: The sort key's DynamoDB type: String, Number or Binary.
- `ArxDynamoModelBuilder SingleTable(string tableName, string partitionKey, string sortKey, Action<ArxDynamoSingleTableBuilder> configure, string typeAttribute = "entity_type")`: A table that holds several entities (single-table design): its key attributes (often PK and SK), the attribute that names each item's entity, and the entities, indexes and entity index keys declared in `configure`.
  - `tableName`: The table's name in DynamoDB.
  - `partitionKey`: The table's partition key attribute (often PK).
  - `sortKey`: The table's sort key attribute (often SK).
  - `configure`: Declares the table's entities, indexes and entity index keys.
  - `typeAttribute`: The attribute that names each item's entity.
- `ArxDynamoModelBuilder WriteSharding(string entity, int shards, string by)`: Write sharding for a single-table entity: its partition key gets a `#SHARD#n` suffix, n from a stable hash of `by`, so writes spread over `shards` partitions. A read of one item still goes to one key; a read of the whole partition asks every shard and merges by the sort key.
  - `entity`: The single-table entity whose writes are spread.
  - `shards`: How many partitions to spread over.
  - `by`: The attribute whose value picks the shard.
- `ArxDynamoModel Build()`: The model, checked and read-only from now on.

### ArxDynamoSingleTableBuilder

class in `ARX.SDK.DynamoDb.Model`, package `ARX.SDK.DynamoDb`.

```csharp
public sealed class ArxDynamoSingleTableBuilder
```

Declares the entities and indexes of one single table.

Methods:

- `ArxDynamoSingleTableBuilder Entity(string name, string pk, string? sk = null)`: An entity of the table, its keys made from templates: `pk: "CUSTOMER#{customer_id}", sk: "ORDER#{order_id}"`.
  - `name`: The entity's name.
  - `pk`: The partition key's template, such as `CUSTOMER#{customer_id}`.
  - `sk`: The sort key's template, such as `ORDER#{order_id}`; null when the table has no sort key.
- `ArxDynamoSingleTableBuilder Index(string name, string partitionKey, string? sortKey = null)`: A global secondary index of the table, keyed by its own attributes (often GSI1PK, GSI1SK).
  - `name`: The index's name in DynamoDB.
  - `partitionKey`: The index's partition key attribute (often GSI1PK).
  - `sortKey`: The index's sort key attribute (often GSI1SK); null for none.
- `ArxDynamoSingleTableBuilder EntityIndex(string entity, string index, string pk, string? sk = null)`: The index keys an entity writes, from templates: `.EntityIndex("order", "GSI1", pk: "STATUS#{status}", sk: "{ordered_on}")`.
  - `entity`: The entity that writes the index keys.
  - `index`: The index, declared with `Index`.
  - `pk`: The template of the index's partition key, such as `STATUS#{status}`.
  - `sk`: The template of its sort key; null for none.

### ArxDynamoKeyType

enum in `ARX.SDK.DynamoDb.Model`, package `ARX.SDK.DynamoDb`.

```csharp
public enum ArxDynamoKeyType
```

The type of a DynamoDB key attribute: string (S), number (N) or binary (B).

Values:

- `String`: A string key (S).
- `Number`: A number key (N).
- `Binary`: A binary key (B).

### ArxDynamoReadOptions

class in `ARX.SDK.DynamoDb.Services`, package `ARX.SDK.DynamoDb`.

```csharp
public sealed class ArxDynamoReadOptions
```

Options for DynamoDB reads.

Properties:

- `bool ConsistentRead { get; init; }`: True (the default): reads of the table see every write before them. False: the cheaper eventually consistent read. Index reads are always eventually consistent.
- `string? IndexName { get; init; }`: The global secondary index to query; null lets ARX pick the one whose keys the criteria give.
- `bool AllowScan { get; init; }`: False (the default): a query without its partition key is refused (ARX8001), because DynamoDB can only answer it by reading the whole table. True: such a query is a Scan, which reads and bills every item.
- `bool CaseSensitivePatterns { get; init; }`: False (the default): Contains and BeginsWith are refused (ARX8001), because ARX's are case-insensitive and DynamoDB's `contains` and `begins_with` aren't. True: they run case-sensitively.

### ArxDynamoWriteOptions

class in `ARX.SDK.DynamoDb.Services`, package `ARX.SDK.DynamoDb`.

```csharp
public sealed class ArxDynamoWriteOptions
```

Options for DynamoDB writes of several items.

Properties:

- `bool AllowPartial { get; init; }`: False (the default): a write of more than 100 items is refused (ARX8001): DynamoDB's transactions hold at most 100. True: the items go in transactions of 100, one after another; a failure leaves the earlier transactions written, and the error says how many items were.
- `bool AllowKeyChange { get; init; }`: False (the default): an update whose RecordId and id attribute differ (a new key) is ARX1001. True: the item is moved: read, changed, and written under its new key while the old one is deleted, in one transaction (the new key taken is ARX3001; the keys, the type attribute and the index keys are rendered again). A change made by someone else between the read and the move is lost with the old item.

## Other public types

The public types no group above lists: less common needs, extension points and interop.

### ArxAggregateColumn

class in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public class ArxAggregateColumn
```

One aggregate of a query: an operator over an attribute, read back under an alias.

Constructors:

- `new ArxAggregateColumn()`: An empty aggregate column; set its properties.
- `new ArxAggregateColumn(string aliasName, string attributeName, ArxAggregateOperator aggregateOperator)`: An aggregate column: `aliasName`, the attribute it reads and the operator.
  - `aliasName`: The attribute the result is read into, on each group's record.
  - `attributeName`: The column to aggregate (`alias.column` for a link's); empty for a Count of rows.
  - `aggregateOperator`: The aggregate: Count, CountDistinct, Sum, Avg, Min, Max and the others.

Properties:

- `string AliasName { get; set; }`: The name the result holds the value under.
- `string AttributeName { get; set; }`: The attribute aggregated; empty for a row count.
- `ArxAggregateOperator Operator { get; set; }`: What is computed.

### ArxAttributeCollection

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public class ArxAttributeCollection : IEnumerable<KeyValuePair<string, object?>>
```

An entity's attributes: names and their values. Names are kept as given and matched ignoring case, on read and on write: `entity["Name"]` finds `name`, and setting `entity["Name"]` replaces it. A result with two columns whose names differ only in case is refused (ARX1004).

Unlike `ArxEntity`'s indexer, which gives null for a name the entity doesn't have, this collection's indexer throws `KeyNotFoundException`: use `TryGetValue` or `ContainsKey`.

Constructors:

- `new ArxAttributeCollection()`: An empty collection; names are matched ignoring case.
- `new ArxAttributeCollection(IDictionary<string, object?> dictionary)`: A collection over this dictionary, used as it is (not copied): its comparer decides how names match, so pass one with `OrdinalIgnoreCase` to match the rest of ARX.
  - `dictionary`: The attributes to start with; names are matched ignoring case.

Properties:

- `object? this[string key] { get; set; }`: The value of an attribute; `KeyNotFoundException` when the collection doesn't have it.
- `int Count { get; }`: The number of attributes.
- `ICollection<string> Keys { get; }`: The attribute names.
- `ICollection<object?> Values { get; }`: The attribute values.

Methods:

- `void Add(string key, object? value)`: Adds an attribute; `ArgumentException` when one with that name (ignoring case) is there.
  - `key`: The attribute name.
  - `value`: Its value; null writes NULL.
- `void Add(KeyValuePair<string, object?> item)`: Adds an attribute.
  - `item`: The name and value.
- `void AddRange(IEnumerable<KeyValuePair<string, object?>>? items)`: Adds the attributes.
  - `items`: The names and values; null adds nothing.
- `void AddRange(params KeyValuePair<string, object?>[] items)`: Adds the attributes.
  - `items`: The names and values.
- `void Clear()`: Removes every attribute.
- `bool Contains(string key)`: True when an attribute has this name (ignoring case).
  - `key`: The attribute name.
- `bool Contains(KeyValuePair<string, object?> item)`: True when this name and value are present.
  - `item`: The name and value to look for.
- `bool ContainsKey(string key)`: True when an attribute has this name (ignoring case).
  - `key`: The attribute name.
- `void CopyTo(KeyValuePair<string, object?>[] array, int arrayIndex)`: Copies the attributes into an array.
  - `array`: The array to copy into.
  - `arrayIndex`: Where in the array the first attribute goes.
- `bool Remove(string key)`: Removes the attribute with this name; true when it was there.
  - `key`: The attribute name.
- `bool Remove(KeyValuePair<string, object?> item)`: Removes this name and value; true when it was there.
  - `item`: The name and value to remove.
- `bool TryGetValue(string key, out object? value)`: Gets an attribute's value; false when the collection doesn't have it.
  - `key`: The attribute name, matched ignoring case.
  - `value`: The value when found; otherwise null.
- `IEnumerator<KeyValuePair<string, object?>> GetEnumerator()`: The attributes, as name and value pairs.

### ArxBatchDataServiceExtensions

static class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public static class ArxBatchDataServiceExtensions
```

Batch-scope helpers shared by every provider.

Methods:

- `static Task RunBatchAsync(this IArxBatchDataService service, Func<IArxBatchScope, Task> body, ArxBatchOptions? options = null, CancellationToken cancellationToken = default)`: Runs `body` in a new batch scope with automatic commit: the scope is committed when the body returns, and rolled back if it throws, whether an ARX call or the developer's own code failed. The exception is rethrown unchanged. A failure the body catches itself doesn't stop the commit when savepoints undid that call alone (the rest is kept). When the failed call rolled the whole scope back instead (no savepoints: MongoDB, DynamoDB, or `Savepoints = false`), nothing was kept, so RunBatchAsync throws ARX1015 TransactionAborted when the body returns rather than return as if the work were saved. A body that commits or rolls back itself ends the scope, and nothing more is done.
  - `service`: The service that opens the scope.
  - `body`: The scope's work: every call it makes on the scope runs in one transaction, committed when it returns.
  - `options`: The scope's options; null for the defaults (no savepoints).
  - `cancellationToken`: Cancels the call.
- `static Task<T> RunBatchAsync<T>(this IArxBatchDataService service, Func<IArxBatchScope, Task<T>> body, ArxBatchOptions? options = null, CancellationToken cancellationToken = default)`: As `RunBatchAsync`, for a body that returns a value (the ids it created, a count): the value is returned once the scope is committed.
  - `service`: The service that opens the scope.
  - `body`: The scope's work: every call it makes on the scope runs in one transaction, committed when it returns.
  - `options`: The scope's options; null for the defaults (no savepoints).
  - `cancellationToken`: Cancels the call.

### ArxBatchScope

class in `ARX.SDK.Providers`, package `ARX.SDK`.

```csharp
public abstract class ArxBatchScope : IArxBatchScope
```

The behaviour every provider's batch scope shares: call numbering, failure handling, state, the one-call-at-a-time rule and rollback on dispose. A provider supplies only the database work.

When a call fails with an `ArxException`, its `BatchNumber` is set. With savepoints, `UndoFailedCallAsync` undoes that call alone; if it can't (for example the connection dropped), the whole scope is rolled back. Without savepoints, and for any other exception (cancellation included), the whole scope is rolled back. The original exception is always the one thrown.

Properties:

- `ArxBatchState State { get; }`: The scope's state.
- `int BatchCount { get; }`: The number of calls made through the scope, successful or not. Reads count too. Each call's number (1-based) is its batch number.
- `long RowsWritten { get; }`: Rows written by successful calls that are still in the transaction; 0 once the scope is rolled back.
- `IReadOnlyList<int> FailedBatches { get; }`: The batch numbers of the calls that failed, in order.

Methods:

- `Task<ArxRecordId> CreateAsync(ArxEntity entity, CancellationToken cancellationToken = default)`: Inserts one entity inside the scope's transaction and returns its id, which is also set on `RecordId`.
  - `entity`: The record to create: its table and the attributes to write. Its `RecordId` is set to the new id.
  - `cancellationToken`: Cancels the call.
- `Task<IReadOnlyList<ArxRecordId>> CreateMultipleAsync(IReadOnlyList<ArxEntity> entities, CancellationToken cancellationToken = default)`: Inserts the entities inside the scope's transaction, all or nothing, and returns their ids: one per entity, in list order, the same instances that are set on each entity's `RecordId`. An empty list returns an empty list.
  - `entities`: The records to create, all or nothing; their ids come back in the same order.
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateAsync(ArxEntity entity, ArxUpdateOptions? options = null, CancellationToken cancellationToken = default)`: Updates the rows `entity` matches inside the scope's transaction and returns how many changed. Nothing matched is ARX2002.
  - `entity`: The change: its table, the attributes to write (null clears a column; an attribute left out keeps its value), and the id, key or columns that find the row.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxUpdateOptions? options = null, CancellationToken cancellationToken = default)`: Updates the entities inside the scope's transaction, all or nothing, and returns the rows changed. An entity that matched nothing refuses the whole call (ARX2002). An empty list returns 0.
  - `entities`: The changes, all or nothing: each carries the attributes to write and what finds its row.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateMultipleAsync(ArxEntity values, ArxFilterExpression filter, ArxFilterWriteOptions? options = null, CancellationToken cancellationToken = default)`: The update by a filter inside the scope's transaction: returns how many rows changed.
  - `values`: The table (EntityName) and the attributes to set: null clears a column.
  - `filter`: The rows to change: conditions on the table's own columns.
  - `options`: AllRows for an empty filter; null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteMultipleAsync(string entityName, ArxFilterExpression filter, ArxFilterWriteOptions? options = null, CancellationToken cancellationToken = default)`: The delete by a filter inside the scope's transaction: returns how many rows went.
  - `entityName`: The table to delete from.
  - `filter`: The rows to delete: conditions on the table's own columns.
  - `options`: AllRows for an empty filter; null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<IReadOnlyList<ArxRecordId>> CreateMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxCreateMultipleOptions options, CancellationToken cancellationToken = default)`: CreateMultiple with options inside the scope's transaction.
  - `entities`: The records to create, all or nothing.
  - `options`: ReturnIds false to skip reading the ids back.
  - `cancellationToken`: Cancels the call.
- `Task<bool> DeleteAsync(string entityName, ArxRecordId id, CancellationToken cancellationToken = default)`: Deletes the record with this id inside the scope's transaction; false when there was none. The id converts from a `long`, `int`, `Guid` or `string`, so `DeleteAsync("account", 42L)` works as it is.
  - `entityName`: The table to delete from.
  - `id`: The record's id: the value Create returned, or a plain value of the declared id's type.
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteAsync(ArxEntity entity, ArxDeleteOptions? options = null, CancellationToken cancellationToken = default)`: Deletes the rows `entity` matches inside the scope's transaction and returns how many went.
  - `entity`: The record to delete: its table and the id, key or columns that find it.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteMultipleAsync<TId>(string entityName, IReadOnlyCollection<TId> ids, CancellationToken cancellationToken = default)`: Deletes the records with these ids inside the scope's transaction, all or nothing, and returns how many existed. Duplicates count once. One method for every id type: a list of `long`, `int`, `Guid`, `string` or `ArxRecordId`; another element type is ARX1007 UnsupportedValueType.
  - `entityName`: The table to delete from.
  - `ids`: The ids of the records to delete; ids that match nothing are left out of the count.
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxDeleteOptions? options = null, CancellationToken cancellationToken = default)`: Deletes the rows the entities match inside the scope's transaction, all or nothing, and returns how many went.
  - `entities`: The records to delete, all or nothing, each found by its id or by the match's columns.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<ArxEntity?> RetrieveAsync(string entityName, ArxRecordId id, ArxColumnSet? columns = null, CancellationToken cancellationToken = default)`: Reads one record by id (the value Create returned, or a `long`, `int`, `Guid` or `string`) inside the scope's transaction; null when not found. On the SQL providers and MongoDB it sees the scope's own writes; DynamoDB sends writes at commit, so a read before it sees only what is committed.
  - `entityName`: The table or view to read.
  - `id`: The record's id: the value Create returned, or a plain value of the declared id's type.
  - `columns`: The columns to read; null or an empty set reads every column. The declared id is always read.
  - `cancellationToken`: Cancels the call.
- `Task<ArxEntity?> RetrieveAsync(ArxEntity match, ArxRetrieveOptions? options = null, CancellationToken cancellationToken = default)`: Reads the one record the entity matches (an ArxMatch in `Match`) inside the scope's transaction.
  - `match`: The record to find: its table, and the values of the id, the declared key or the columns that find it.
  - `options`: How the row is found and read (match, command timeout, lookup names); null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<ArxEntityCollection> RetrieveMultipleAsync(ArxQueryExpression query, ArxRetrieveOptions? options = null, CancellationToken cancellationToken = default)`: Runs a query inside the scope's transaction. On the SQL providers and MongoDB it sees the scope's own writes; on DynamoDB, only what is committed.
  - `query`: The query: its row source, columns, criteria, links, order, paging and aggregates.
  - `options`: How the row is found and read (match, command timeout, lookup names); null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<ArxExecuteResult> ExecuteAsync(ArxProcedureExpression procedure, ArxExecuteOptions? options = null, CancellationToken cancellationToken = default)`: Calls a procedure or function inside the scope's transaction: it sees the scope's writes, and a failed call is undone alone. A procedure that commits or rolls back by itself can't run in a scope (ARX8001).
  - `procedure`: The procedure or function to call, with its arguments.
  - `options`: Options of the call (paging, command timeout); null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task CommitAsync(CancellationToken cancellationToken = default)`: Commits everything written through the scope. If the database refuses the commit (for example a deferred constraint), the error is thrown and the scope is `RolledBack`. If the connection fails or times out before the database answers, the scope is `OutcomeUnknown` and the error, not transient, says the work may or may not be saved. A token cancelled before the commit starts rolls the scope back; the commit itself isn't cancelled, because stopping it half way would leave its outcome unknown.
  - `cancellationToken`: Cancels the call.
- `Task RollbackAsync(CancellationToken cancellationToken = default)`: Rolls back everything written through the scope. Does nothing if the scope is already rolled back; fails with ARX1010 if it was committed. The rollback itself isn't cancelled.
  - `cancellationToken`: Cancels the call.
- `ValueTask DisposeAsync()`: Rolls back a scope that wasn't committed, then releases its connection. Never throws for a failed rollback.

### ArxBatchState

enum in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public enum ArxBatchState
```

The state of a batch scope.

Values:

- `Active`: The transaction is open and calls can be made.
- `Committed`: The transaction was committed. Any further call fails with ARX1010.
- `RolledBack`: The transaction was rolled back (explicitly, by a failure, or on dispose). Any further call fails with ARX1010.
- `OutcomeUnknown`: The commit got no answer (the connection failed or timed out while committing): the work may have been saved or not. Check what is in the database before doing it again. Any further call fails with ARX1010.

### ArxConnection

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public abstract class ArxConnection : IAsyncDisposable
```

A connection to one database, opened and closed by ARX. Each provider has its own; this provider's is `new ArxDynamoConnection(service)`. Begin a transaction on it and pass the transaction to service calls; application code never needs the database driver's types.

One transaction at a time per connection, and one call at a time. Disposing it closes the connection.

Methods:

- `Task<ArxTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default)`: Opens the connection if needed and begins a transaction.
  - `cancellationToken`: Cancels the call.
- `ValueTask DisposeAsync()`: Closes the connection. An active transaction on it is rolled back.

### ArxCreateMultipleOptions

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxCreateMultipleOptions
```

Options for `CreateMultipleAsync(entities, options)`.

Properties:

- `bool ReturnIds { get; init; }`: True, the default: the new ids are read back and returned, as `CreateMultipleAsync(entities)` does. False: no id is read back, so the provider can take its bulk path (PostgreSQL's binary `COPY`, SQL Server's `SqlBulkCopy`), and the call returns `None` for every entity. Still all or nothing.

### ArxDiagnostics

static class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public static class ArxDiagnostics
```

ARX's tracing and metrics, on .NET's own instruments, so OpenTelemetry (`AddSource("ARX.SDK")`, `AddMeter("ARX.SDK")`) or any `ActivityListener` and `MeterListener` read them:

- an activity per operation (`Create`, `RetrieveMultiple`, …), kind Client, named "operation table", tagged `db.system.name`, `db.operation.name`, `db.collection.name` and `arx.provider`; on failure the status Error, `error.type` and `arx.error.code`;
- `arx.operation.duration` (a histogram, in seconds) and `arx.operation.errors` (a counter), with the same tags.
Values are never recorded, only names. Without a listener, each operation costs a check.

Properties:

- `const string SourceName`: The name of ARX's `ActivitySource` and `Meter`: `ARX.SDK`.
- `static readonly ActivitySource Source`: The source of ARX's activities.
- `static readonly Meter Meter`: The meter of ARX's metrics.

Methods:

- `static Task<T> TraceAsync<T>(string provider, string dbSystem, string operation, string table, Func<Task<T>> work)`: Traces one operation of a provider. For the providers' use: applications read the activities and metrics instead.
  - `provider`: The provider's name ("PostgreSQL").
  - `dbSystem`: The database, as OpenTelemetry names it ("postgresql", "microsoft.sql_server", "mysql", "mongodb", "aws.dynamodb").
  - `operation`: The operation ("Create", "RetrieveMultiple", …).
  - `table`: The table or entity, when known.
  - `work`: The operation itself.
- `static Task TraceAsync(string provider, string dbSystem, string operation, string table, Func<Task> work)`: As `TraceAsync{T}`, for an operation without a result.
  - `provider`: The provider's name ("PostgreSQL").
  - `dbSystem`: The database, as OpenTelemetry names it ("postgresql", "microsoft.sql_server", "mysql", "mongodb", "aws.dynamodb").
  - `operation`: The operation ("Create", "RetrieveMultiple", …).
  - `table`: The table or entity, when known.
  - `work`: The operation itself.

### ArxDynamoEntityMap

class in `ARX.SDK.DynamoDb.Model`, package `ARX.SDK.DynamoDb`.

```csharp
public sealed class ArxDynamoEntityMap
```

One entity's layout: its table, how its keys are made, its index keys, its type name and its sharding.

Properties:

- `string Name { get; }`: The entity's name.
- `ArxDynamoTableMap Table { get; }`: The table the entity's items are in.
- `ArxDynamoKeyTemplate PartitionKey { get; }`: How the entity's partition key is made: one attribute, or a template.
- `ArxDynamoKeyTemplate SortKey { get; }`: The sort key; null when the table has none.
- `IReadOnlyDictionary<string, (ArxDynamoKeyTemplate PartitionKey, ArxDynamoKeyTemplate SortKey)> IndexKeys { get; }`: The index keys this entity writes: index name → (partition key template, sort key template or null).
- `int Shards { get; }`: How many shards the partition key is spread over (1: no sharding).
- `string? ShardBy { get; }`: The attribute the shard is computed from; null without sharding.
- `IReadOnlyList<string> KeyAttributes { get; }`: The attributes that make the item's primary key, in order and without repeats.

### ArxDynamoIndexMap

class in `ARX.SDK.DynamoDb.Model`, package `ARX.SDK.DynamoDb`.

```csharp
public sealed class ArxDynamoIndexMap
```

A global secondary index: its name and its key attributes.

Properties:

- `string Name { get; }`: The index's name.
- `string PartitionKey { get; }`: The index's partition key attribute.
- `ArxDynamoKeyType PartitionKeyType { get; }`: The index partition key's type.
- `string SortKey { get; }`: The index's sort key attribute; null when it has none.
- `ArxDynamoKeyType SortKeyType { get; }`: The index sort key's type.

### ArxDynamoKeyTemplate

class in `ARX.SDK.DynamoDb.Model`, package `ARX.SDK.DynamoDb`.

```csharp
public sealed class ArxDynamoKeyTemplate
```

A key's value as an entity makes it: a plain attribute (a table per entity), or a template of literal text and `{attribute}` placeholders (a single table), whose values are escaped: `%` and `#` are percent-encoded, so `"A#1"` can't collide with another key.

Properties:

- `string Text { get; }`: The template as declared, or the plain attribute's name.
- `string PlainAttribute { get; }`: The attribute whose own value is the key (a table per entity); null for a template.
- `IReadOnlyList<string> Attributes { get; }`: The attributes the key is made of, in order.
- `string LiteralPrefix { get; }`: The literal text before the first placeholder (`ORDER#`): what every key of the entity starts with.
- `bool EndsWithPlaceholder { get; }`: True when the template ends with a placeholder, so a condition on that attribute can narrow the key's range.

### ArxDynamoTableMap

class in `ARX.SDK.DynamoDb.Model`, package `ARX.SDK.DynamoDb`.

```csharp
public sealed class ArxDynamoTableMap
```

A physical DynamoDB table: its keys and its global secondary indexes.

Properties:

- `string Name { get; }`: The DynamoDB table's name.
- `string PartitionKey { get; }`: The table's partition key attribute.
- `ArxDynamoKeyType PartitionKeyType { get; }`: The partition key's type.
- `string SortKey { get; }`: The sort key; null for a table with a partition key only.
- `ArxDynamoKeyType SortKeyType { get; }`: The sort key's type.
- `string TypeAttribute { get; }`: The attribute that names each item's entity in a single table; null for a table per entity.
- `bool IsSingleTable { get; }`: True for a single table shared by several entities through key templates.
- `IReadOnlyList<ArxDynamoIndexMap> Indexes { get; }`: The table's global secondary indexes.

### ArxFilterWrite

static class in `ARX.SDK.Providers`, package `ARX.SDK`.

```csharp
public static class ArxFilterWrite
```

For the providers' writes by a filter: the rules every provider applies the same way, before anything is sent.

Methods:

- `static void CheckFilter(string entityName, ArxFilterExpression filter, ArxFilterWriteOptions? options, string operation, string? provider)`: Checks the filter: ARX1001 when it has no condition and `AllRows` isn't set, or when it names a link (a condition with an entity name other than the table, or an `alias.column` name).
  - `entityName`: The table written.
  - `filter`: The filter that chooses the rows.
  - `options`: The call's options, or null.
  - `operation`: "UpdateMultiple" or "DeleteMultiple", for the message.
  - `provider`: The provider's name, for the error.
- `static void CheckValues(ArxEntity values, ArxTableDefinition? table, string? provider)`: Checks the values an update by a filter writes: at least one; not the id (the attribute, or a `RecordId` with a value), since a filter and an id can't both say which rows; not the version column, which ARX moves on every changed row.
  - `values`: The entity whose attributes are the values to set.
  - `table`: The table's declaration, when the call has one (a catalog, or what was resolved).
  - `provider`: The provider's name, for the error.
- `static bool IsEmpty(ArxFilterExpression filter)`: True when the filter has no condition at any depth.
  - `filter`: The filter.

### ArxFilterWriteOptions

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxFilterWriteOptions
```

Options for an update or delete by a filter (`UpdateMultipleAsync(values, filter)`, `DeleteMultipleAsync(table, filter)`).

Properties:

- `bool AllRows { get; init; }`: True to let an empty filter (no conditions) change or delete every row of the table. False, the default: an empty filter is ARX1001, so a filter that was never filled in can't touch every row by mistake.

### ArxFullTextSearch

class in `ARX.SDK.Models`, package `ARX.SDK`.

```csharp
public class ArxFullTextSearch
```

A full-text search, given as a condition's value: `query.Criteria.AddCondition("body", ArxConditionOperator.Equal, new ArxFullTextSearch("quick fox"))`. Each database runs its own full-text search: PostgreSQL `to_tsvector('english', column) @@ websearch_to_tsquery('english', text)` (English stemming and stop words); SQL Server `FREETEXT` and MySQL `MATCH … AGAINST` in natural language mode, which need a full-text index on the column (ARX8001 without one); MongoDB `$text`, which searches the collection's text index whatever field the condition names. DynamoDB has none (ARX8001); `ArxCapability.FullTextSearch` says which providers do.

The operator is `Equal` or `Contains` (and `Like` on the SQL providers): all mean "matches the search". The text is a parameter, never part of the statement. The matching rules (stemming, stop words, ranking, the minimum word length) are the database's, so the same search can find different rows on each provider.

Constructors:

- `new ArxFullTextSearch(string searchText)`: A full-text search for `searchText`.
  - `searchText`: The words to search for, as the database's full-text search reads them.

Properties:

- `string SearchText { get; set; }`: The words to search for. Empty or whitespace is ARX1001.

### ArxFunctionArgument

class in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public sealed class ArxFunctionArgument
```

One argument of a function row source: by name (`p_region => $1`) or by position (`Name` null).

Constructors:

- `new ArxFunctionArgument(string? name, object? value)`: An argument whose value says its type.
  - `name`: The function's parameter name; null to pass it by position.
  - `value`: The value, sent as a parameter.
- `new ArxFunctionArgument(string? name, object? value, Type? type, bool variadic)`: An argument with its type declared, or passed to a `VARIADIC` parameter.
  - `name`: The function's parameter name; null to pass it by position.
  - `value`: The value, sent as a parameter.
  - `type`: The argument's .NET type, sent as a cast, for a null the database can't type by itself.
  - `variadic`: The value is an array passed to a `VARIADIC` parameter.

Properties:

- `Type? DeclaredType { get; }`: The declared .NET type, sent as a cast; null to let the value (or the database) decide.
- `bool IsVariadic { get; }`: The value is an array passed to a `VARIADIC` parameter (`VARIADIC $1`).
- `string? Name { get; }`: The parameter's name, or null for a positional argument.
- `object? Value { get; }`: Sent as a typed parameter, like a write's value. Null is SQL NULL.

### ArxInjectionCheck

static class in `ARX.SDK.Providers`, package `ARX.SDK`.

```csharp
public static class ArxInjectionCheck
```

The rule every provider uses to tell a name that looks like an SQL injection attempt (ARX1017 SuspectedInjection) from a plain invalid name (ARX1003 InvalidIdentifier).

A provider calls `Inspect` only for a name that has already **failed** its identifier rules, so this check never refuses a name the provider accepts: it only decides which code the refusal carries.

Values are never inspected. They are always sent as parameters, so text such as `Robert'); DROP TABLE x;--` is stored exactly as typed and can't change the SQL; refusing it would refuse real data (O'Brien, "a -- b").

Properties:

- `const int MAX_ECHO_LENGTH`: The longest part of a suspicious name an error message repeats.

Methods:

- `static string? Inspect(string? name)`: What makes `name` look like an injection attempt (for example `contains ';'` or `contains the SQL word 'UNION'`), or null when it doesn't (a space alone, a leading digit or a long name is a plain invalid name).
  - `name`: The name that failed the identifier rules, as the caller gave it.
- `static string Echo(string? name)`: `name` made safe to put in a message or a log line: control characters and line breaks escaped (so the name can't forge log lines), and cut to `MAX_ECHO_LENGTH` characters.
  - `name`: The name to quote in a message.
- `static ArxValidationException Refuse(string kind, string? name, string evidence, string? provider = null)`: The ARX1017 error for a `kind` name ("Entity", "Attribute", "Type", …) that `Inspect` found suspicious, with `evidence` as returned by it. Providers raise this, so the message is the same everywhere.
  - `kind`: What the name is, for the message: "Entity", "Attribute", "Type" and so on.
  - `name`: The suspicious name, quoted safely in the message.
  - `evidence`: What `Inspect` found in it.
  - `provider`: The provider's name for `Provider`, or null.

### ArxMatchKind

enum in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public enum ArxMatchKind
```

How an update or delete finds its rows.

Values:

- `Id`: By the table's declared id column.
- `Key`: By one of the table's declared business keys.
- `Columns`: By any columns the developer names, declared or not.

### ArxPagingCookieProtection

enum in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public enum ArxPagingCookieProtection
```

How a provider protects its paging cookies. A cookie carries the last item's key values, which can be an email or a tenant id, so the default seals it.

Values:

- `Sealed` = 0: Encrypted and authenticated (AES-256-GCM): a client can neither read a cookie nor change it. The default.
- `Signed` = 1: Signed (HMAC-SHA256): a client can read a cookie's key values, but not change them.
- `Plain` = 2: Neither: a client can read and change a cookie. ARX still checks the provider and the query it belongs to, and the query's filter applies to every page, so an edited cookie can't reach other records; it can only move within them.

### ArxPagingParametersStyle

enum in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public enum ArxPagingParametersStyle
```

How a routine that pages itself names its page arguments and its total (way 1).

Values:

- `PageNumber`: A page number (from 1) and a page size.
- `OffsetLimit`: An offset (rows to skip) and a limit (rows to return). ARX asks for one row more, so HasNextPage is exact.

### ArxPreLoadTypes

static class in `ARX.SDK.Providers`, package `ARX.SDK`.

```csharp
public static class ArxPreLoadTypes
```

The type names a catalog's JSON uses.

Methods:

- `static string NameOf(Type type)`: A type's JSON name: `long`, `string`, `DateOnly`, `string[]`.
  - `type`: A .NET type a column can hold.
- `static Type TypeOf(string name)`: The type of a JSON name; ARX1001 for one it doesn't know.
  - `name`: A type's JSON name, as `NameOf` writes it.

### ArxResultColumn

class in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public sealed class ArxResultColumn
```

A result column of a function that returns untyped rows (`SETOF record`): its name and .NET type.

Constructors:

- `new ArxResultColumn(string name, Type type)`: A column a row source returns: its name and .NET type.
  - `name`: The column's name in the function's result.
  - `type`: The .NET type it is read as.

Properties:

- `string Name { get; }`: The column's name.
- `Type Type { get; }`: The column's .NET type.

### ArxRowSourceKind

enum in `ARX.SDK.Query`, package `ARX.SDK`.

```csharp
public enum ArxRowSourceKind
```

What a row source is.

Values:

- `Table`: A table or view (on PostgreSQL also a materialized view or foreign table): anything read by name.
- `Function`: A function that returns rows (a table, a set, one row or one value), called with arguments.

### ArxSelectionSetCatalog

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxSelectionSetCatalog : IArxSelectionSetCatalog
```

A ready-made, immutable, thread-safe catalog. Build one with `Create`.

Properties:

- `IReadOnlyCollection<ArxSelectionSetDefinition> Sets { get; }`: Every set, in the order added.

Methods:

- `static ArxSelectionSetCatalogBuilder Create()`: Starts a selection set catalog: add each set, then call Build.
- `bool TryGetSet(string name, out ArxSelectionSetDefinition set)`: Gets a selection set by name; false when the catalog has none of that name.
  - `name`: The selection set's name.
  - `set`: The set when found; otherwise null.

### ArxSelectionSetCatalogBuilder

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxSelectionSetCatalogBuilder
```

Builds an `ArxSelectionSetCatalog`. Not thread-safe; build once, at startup.

Methods:

- `ArxSelectionSetCatalogBuilder Add(ArxSelectionSetDefinition set)`: Adds a set the developer defined (for example loaded from their own option tables).
  - `set`: A selection set built elsewhere.
- `ArxSelectionSetCatalogBuilder Add(string name, ArxSelectionSetKind kind, params (object Value, string DisplayText)[] options)`: Adds a single-choice set; options get their order from their position.
  - `name`: The set's name, used when writing and reading its values.
  - `kind`: What its codes are: integers, text or single characters.
  - `options`: The options: each code and its display text.
- `ArxSelectionSetCatalogBuilder Add(string name, ArxSelectionSetKind kind, ArxSelectionSetMultiplicity multiplicity, params (object Value, string DisplayText)[] options)`: Adds a set with the given storage form; options get their order from their position.
  - `name`: The set's name, used when writing and reading its values.
  - `kind`: What its codes are: integers, text or single characters.
  - `multiplicity`: One option per column, or several stored as an array.
  - `options`: The options: each code and its display text.
- `ArxSelectionSetCatalogBuilder AddEnum<TEnum>(string? name = null)`: Adds a single-choice Integer set from a .NET enum: codes are the enum's values (in its underlying type), display text its names.
  - `name`: The set's name; the enum's name when null.
- `ArxSelectionSetCatalogBuilder AddFlagsEnum<TEnum>(string? name = null)`: Adds a Flags set from a `[Flags]` enum. The zero member ("None") isn't an option; every other member must be a single bit.
  - `name`: The set's name; the enum's name when null.
- `ArxSelectionSetCatalog Build()`: The catalog, read-only from now on.

### ArxSelectionSetExtensions

static class in `ARX.SDK.Extensions`, package `ARX.SDK`.

```csharp
public static class ArxSelectionSetExtensions
```

Reading selection sets. Retrieve returns the raw stored value; these turn it into the code, or into an `ArxSelectionSetValue` with display text from the developer's catalog.

Methods:

- `static T? GetSelectionSetValue<T>(this ArxEntity entity, string attribute)`: The raw code as `T`, whether the attribute holds a plain value or an `ArxSelectionSetValue`. Integer codes convert between integer types and to enums; string codes convert to enums by name. Missing or null gives `default`.
  - `entity`: The record that was read.
  - `attribute`: The column that holds the selection set's value.
- `static ArxSelectionSetValue? GetSelectionSet(this ArxEntity entity, string attribute, string setName, IArxSelectionSetCatalog catalog)`: The attribute decoded through set `setName` of `catalog`: multiple-choice storage (array, flags, delimited, JSON) turned back into `Values`, with the matched `Options` and `DisplayText`. A stored code missing from the set doesn't throw: its option is null and `IsDefined` is false. Missing or null gives null.
  - `entity`: The record that was read.
  - `attribute`: The column that holds the selection set's value.
  - `setName`: The selection set's name, as the catalog declares it.
  - `catalog`: The catalog that declares the selection set.
- `static string? GetSelectionSetDisplayText(this ArxEntity entity, string attribute, string setName, IArxSelectionSetCatalog catalog)`: The display text of the attribute's option (or options, joined with ", "), from the catalog; null when missing, null or not in the set.
  - `entity`: The record that was read.
  - `attribute`: The column that holds the selection set's value.
  - `setName`: The selection set's name, as the catalog declares it.
  - `catalog`: The catalog that declares the selection set.

### ArxSelectionSets

static class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public static class ArxSelectionSets
```

The write-side checks every provider runs for an `ArxSelectionSetValue`, before any database access.

Methods:

- `static ArxSelectionSetWrite PrepareWrite(IArxSelectionSetCatalog? catalog, ArxSelectionSetValue value, string? entityName, string? attributeName, string? provider = null)`: Checks the value against `catalog` and returns what to write.
  - `catalog`: The catalog that declares the selection set.
  - `value`: The value to write.
  - `entityName`: The table, for the error message.
  - `attributeName`: The column, for the error message.
  - `provider`: The provider's name, for the error.

### ArxSelectionSetWrite

record in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed record ArxSelectionSetWrite
```

What a provider writes for one `ArxSelectionSetValue`.

### ArxServerCheck

class in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed class ArxServerCheck
```

What the connected server allows, from `CheckServerAsync()`: the server's version, the provider's capabilities on this server, and for each one the server removes, why. `service.Capabilities` says what the provider can do anywhere; this says what it can do here. Call it once at startup, beside `ValidateSchemaAsync`.

Constructors:

- `new ArxServerCheck(IArxProviderCapabilities provider, string serverVersion, IEnumerable<ArxServerLimit>? limits = null, IEnumerable<string>? problems = null)`: A check: the provider's capabilities, less those the server removes.
  - `provider`: The provider's capabilities.
  - `serverVersion`: The server's version, as it reports it.
  - `limits`: The capabilities the server removes, with the reason.
  - `problems`: Anything else the check found wrong (a table of the model that doesn't exist); empty when none.

Properties:

- `string ProviderName { get; }`: The provider's name, as messages show it.
- `string ServerVersion { get; }`: The server's version, as it reports it ("16.4", "8.4.3", "7.0.12", …).
- `ArxCapability Supported { get; }`: The provider's capabilities on this server.
- `IReadOnlyList<ArxServerLimit> Limits { get; }`: The capabilities the provider has that this server doesn't allow, each with the reason.
- `IReadOnlyList<string> Problems { get; }`: Anything else the check found wrong; empty when the server is ready.
- `bool IsReady { get; }`: True when the check found nothing wrong (limits are allowed: they only remove capabilities).

Methods:

- `bool Supports(ArxCapability capability)`: True when every flag in `capability` works on this server.
  - `capability`: One capability, or several combined with `|`: all must be supported.

### ArxServerLimit

record in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public sealed record ArxServerLimit
```

A capability the provider has but the connected server doesn't allow, and why.

### ArxTransactionState

enum in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public enum ArxTransactionState
```

The state of an `ArxTransaction`.

Values:

- `Active`: Open: calls can run in it.
- `Committed`: Committed. Further calls fail with ARX1010.
- `RolledBack`: Rolled back (explicitly, on dispose, or because the commit was refused). Further calls fail with ARX1010.
- `OutcomeUnknown`: The commit got no answer (the connection failed or timed out while committing): the work may have been saved or not. Check what is in the database before doing it again; running it again blindly could save it twice. Further calls fail with ARX1010.

### IArxDataService

interface in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public interface IArxDataService : IArxBatchDataService
```

What every ARX service does, whatever its database: the operations, the capabilities, the startup checks, and batch scopes for work that must succeed or fail together. Application code can depend on it, register the provider behind it, and mock it in tests: services.AddSingleton<IArxDataService>(new ArxDynamoDbService(client, model, schema)); The provider types add what is theirs (connections, transactions, options); a call the provider can't do is refused with ARX8001 NotSupported, as `Capabilities` says (`ExecuteAsync` on MongoDB and DynamoDB).

Properties:

- `IArxProviderCapabilities Capabilities { get; }`: What the provider supports: a call outside it is ARX8001.

Methods:

- `Task<ArxServerCheck> CheckServerAsync(CancellationToken cancellationToken = default)`: What the connected server allows: its version, the capabilities it removes and why.
  - `cancellationToken`: Cancels the call.
- `Task ValidateSchemaAsync(CancellationToken cancellationToken = default)`: Compares every table of the schema catalog with the database, and throws with the differences.
  - `cancellationToken`: Cancels the call.
- `Task<ArxTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default)`: Begins a transaction on a connection of its own, the same way on every provider: pass the transaction to the calls, then commit it. Disposing it without committing rolls it back, and closes its connection (`await using var transaction = await service.BeginTransactionAsync();`).
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateMultipleAsync(ArxEntity values, ArxFilterExpression filter, ArxFilterWriteOptions? options = null, CancellationToken cancellationToken = default)`: Sets `values`' attributes on every row of its table that `filter` matches, in one call, and returns how many rows changed. `UPDATE … SET … WHERE …` on the SQL databases.
  - `values`: The table (EntityName) and the attributes to set: null clears a column.
  - `filter`: The rows to change: conditions on the table's own columns.
  - `options`: AllRows for an empty filter; null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateMultipleAsync(ArxEntity values, ArxFilterExpression filter, ArxTransaction transaction, ArxFilterWriteOptions? options = null, CancellationToken cancellationToken = default)`: The update by a filter inside an ARX transaction (see `UpdateMultipleAsync`).
  - `values`: The table (EntityName) and the attributes to set: null clears a column.
  - `filter`: The rows to change: conditions on the table's own columns.
  - `transaction`: The ARX transaction to run in. ARX doesn't commit or roll it back.
  - `options`: AllRows for an empty filter; null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteMultipleAsync(string entityName, ArxFilterExpression filter, ArxFilterWriteOptions? options = null, CancellationToken cancellationToken = default)`: Deletes every row of the table that `filter` matches, in one call, and returns how many went. The filter's rules are UpdateMultiple's: the table's own columns, and an empty filter is ARX1001 unless `AllRows` is set.
  - `entityName`: The table to delete from.
  - `filter`: The rows to delete: conditions on the table's own columns.
  - `options`: AllRows for an empty filter; null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteMultipleAsync(string entityName, ArxFilterExpression filter, ArxTransaction transaction, ArxFilterWriteOptions? options = null, CancellationToken cancellationToken = default)`: The delete by a filter inside an ARX transaction (see `DeleteMultipleAsync`).
  - `entityName`: The table to delete from.
  - `filter`: The rows to delete: conditions on the table's own columns.
  - `transaction`: The ARX transaction to run in. ARX doesn't commit or roll it back.
  - `options`: AllRows for an empty filter; null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<IReadOnlyList<ArxRecordId>> CreateMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxCreateMultipleOptions options, CancellationToken cancellationToken = default)`: Creates the records, all or nothing, as `CreateMultipleAsync`, with options. With `ReturnIds` false no id is read back: the provider can take its bulk path, a table without a usable id is created without a catalog, and every id returned is `None`.
  - `entities`: The records to create, all or nothing.
  - `options`: ReturnIds false to skip reading the ids back.
  - `cancellationToken`: Cancels the call.
- `Task<IReadOnlyList<ArxRecordId>> CreateMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxCreateMultipleOptions options, ArxTransaction transaction, CancellationToken cancellationToken = default)`: CreateMultiple with options inside an ARX transaction (see `CreateMultipleAsync`).
  - `entities`: The records to create, all or nothing.
  - `options`: ReturnIds false to skip reading the ids back.
  - `transaction`: The ARX transaction to run in. ARX doesn't commit or roll it back.
  - `cancellationToken`: Cancels the call.
- `Task<ArxRecordId> CreateAsync(ArxEntity entity, CancellationToken cancellationToken = default)`: Creates one record and returns its id (also set on `RecordId`).
  - `entity`: The record to create: its table and the attributes to write. Its `RecordId` is set to the new id.
  - `cancellationToken`: Cancels the call.
- `Task<IReadOnlyList<ArxRecordId>> CreateMultipleAsync(IReadOnlyList<ArxEntity> entities, CancellationToken cancellationToken = default)`: Creates the records, all or nothing, and returns their ids in the order of the list.
  - `entities`: The records to create, all or nothing; their ids come back in the same order.
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateAsync(ArxEntity entity, ArxUpdateOptions? options = null, CancellationToken cancellationToken = default)`: Updates the records the entity finds (by its id, or the match of the options) and returns how many: 1 by id or a key; by columns every row that matches unless RequireSingleRow. None is ARX2002.
  - `entity`: The change: its table, the attributes to write (null clears a column; an attribute left out keeps its value), and the id, key or columns that find the row.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxUpdateOptions? options = null, CancellationToken cancellationToken = default)`: Updates every entity's record, all or nothing, and returns how many; one that finds none is ARX2002.
  - `entities`: The changes, all or nothing: each carries the attributes to write and what finds its row.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<bool> DeleteAsync(string entityName, ArxRecordId id, CancellationToken cancellationToken = default)`: Deletes the record with this id: true when it was there.
  - `entityName`: The table to delete from.
  - `id`: The record's id: the value Create returned, or a plain value of the declared id's type.
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteAsync(ArxEntity entity, ArxDeleteOptions? options = null, CancellationToken cancellationToken = default)`: Deletes the records the entity finds and returns how many (0 when none; ARX2002 with RequireSingleRow).
  - `entity`: The record to delete: its table and the id, key or columns that find it.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxDeleteOptions? options = null, CancellationToken cancellationToken = default)`: Deletes the records the entities find, all or nothing, and returns how many existed.
  - `entities`: The records to delete, all or nothing, each found by its id or by the match's columns.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<ArxEntity?> RetrieveAsync(string entityName, ArxRecordId id, ArxColumnSet? columns = null, CancellationToken cancellationToken = default)`: Reads one record by id, the columns given (all when null); null when it isn't there.
  - `entityName`: The table or view to read.
  - `id`: The record's id: the value Create returned, or a plain value of the declared id's type.
  - `columns`: The columns to read; null or an empty set reads every column. The declared id is always read.
  - `cancellationToken`: Cancels the call.
- `Task<ArxEntityCollection> RetrieveMultipleAsync(ArxQueryExpression query, ArxRetrieveOptions? options = null, CancellationToken cancellationToken = default)`: Runs a query: its records, page and, when asked, total.
  - `query`: The query: its row source, columns, criteria, links, order, paging and aggregates.
  - `options`: How the row is found and read (match, command timeout, lookup names); null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<ArxExecuteResult> ExecuteAsync(ArxProcedureExpression procedure, ArxExecuteOptions? options = null, CancellationToken cancellationToken = default)`: Calls a procedure or function (ARX8001 where StoredProcedures isn't supported).
  - `procedure`: The procedure or function to call, with its arguments.
  - `options`: Options of the call (paging, command timeout); null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<ArxEntity?> RetrieveAsync(ArxEntity match, ArxRetrieveOptions? options = null, CancellationToken cancellationToken = default)`: Reads the one record the entity finds: by its id, or by the match of the options (a declared key, columns); the columns of the options (all when null); null when none is there. On DynamoDB this is how an item of a single table (or one with a sort key) is read by its whole key.
  - `match`: The record to find: its table, and the values of the id, the declared key or the columns that find it.
  - `options`: How the row is found and read (match, command timeout, lookup names); null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteMultipleAsync<TId>(string entityName, IReadOnlyCollection<TId> ids, CancellationToken cancellationToken = default)`: Deletes the records with these ids, all or nothing, and returns how many existed. Duplicates count once. One method for every id type: a list of `long`, `int`, `Guid`, `string` or `ArxRecordId`; another element type is ARX1007 UnsupportedValueType.
  - `entityName`: The table to delete from.
  - `ids`: The ids of the records to delete; ids that match nothing are left out of the count.
  - `cancellationToken`: Cancels the call.
- `Task<ArxRecordId> CreateAsync(ArxEntity entity, ArxTransaction transaction, CancellationToken cancellationToken = default)`: Creates one record in an ARX transaction (from the provider's connection) and returns its id.
  - `entity`: The record to create: its table and the attributes to write. Its `RecordId` is set to the new id.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `cancellationToken`: Cancels the call.
- `Task<IReadOnlyList<ArxRecordId>> CreateMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxTransaction transaction, CancellationToken cancellationToken = default)`: Creates the records in an ARX transaction and returns their ids in the order of the list.
  - `entities`: The records to create, all or nothing; their ids come back in the same order.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateAsync(ArxEntity entity, ArxTransaction transaction, ArxUpdateOptions? options = null, CancellationToken cancellationToken = default)`: Updates the records the entity finds, in an ARX transaction, and returns how many (as the overload without a transaction); none is ARX2002.
  - `entity`: The change: its table, the attributes to write (null clears a column; an attribute left out keeps its value), and the id, key or columns that find the row.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxTransaction transaction, ArxUpdateOptions? options = null, CancellationToken cancellationToken = default)`: Updates every entity's record in an ARX transaction and returns how many; one that finds none is ARX2002.
  - `entities`: The changes, all or nothing: each carries the attributes to write and what finds its row.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<bool> DeleteAsync(string entityName, ArxRecordId id, ArxTransaction transaction, CancellationToken cancellationToken = default)`: Deletes the record with this id in an ARX transaction: true when it was there.
  - `entityName`: The table to delete from.
  - `id`: The record's id: the value Create returned, or a plain value of the declared id's type.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteAsync(ArxEntity entity, ArxTransaction transaction, ArxDeleteOptions? options = null, CancellationToken cancellationToken = default)`: Deletes the records the entity finds in an ARX transaction and returns how many.
  - `entity`: The record to delete: its table and the id, key or columns that find it.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxTransaction transaction, ArxDeleteOptions? options = null, CancellationToken cancellationToken = default)`: Deletes the records the entities find in an ARX transaction and returns how many existed.
  - `entities`: The records to delete, all or nothing, each found by its id or by the match's columns.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<ArxEntity?> RetrieveAsync(string entityName, ArxRecordId id, ArxTransaction transaction, ArxColumnSet? columns = null, CancellationToken cancellationToken = default)`: Reads the record with this id in an ARX transaction, or null. On SQL and MongoDB the read sees the transaction's own writes; DynamoDB sends a transaction's writes at commit, so a read there doesn't see them yet.
  - `entityName`: The table or view to read.
  - `id`: The record's id: the value Create returned, or a plain value of the declared id's type.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `columns`: The columns to read; null or an empty set reads every column. The declared id is always read.
  - `cancellationToken`: Cancels the call.
- `Task<ArxEntity?> RetrieveAsync(ArxEntity match, ArxTransaction transaction, ArxRetrieveOptions? options = null, CancellationToken cancellationToken = default)`: Reads the one record the entity finds (by id, a key or columns) in an ARX transaction, or null.
  - `match`: The record to find: its table, and the values of the id, the declared key or the columns that find it.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `options`: How the row is found and read (match, command timeout, lookup names); null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<ArxEntityCollection> RetrieveMultipleAsync(ArxQueryExpression query, ArxTransaction transaction, ArxRetrieveOptions? options = null, CancellationToken cancellationToken = default)`: Runs the query in an ARX transaction and returns the records it finds.
  - `query`: The query: its row source, columns, criteria, links, order, paging and aggregates.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `options`: How the row is found and read (match, command timeout, lookup names); null for the defaults.
  - `cancellationToken`: Cancels the call.
- `Task<ArxExecuteResult> ExecuteAsync(ArxProcedureExpression procedure, ArxTransaction transaction, ArxExecuteOptions? options = null, CancellationToken cancellationToken = default)`: Calls a procedure or function in an ARX transaction (ARX8001 where StoredProcedures isn't declared).
  - `procedure`: The procedure or function to call, with its arguments.
  - `transaction`: The ARX transaction to run in (from the provider's connection type). ARX doesn't commit or roll it back.
  - `options`: Options of the call (paging, command timeout); null for the defaults.
  - `cancellationToken`: Cancels the call.

### IArxSelectionSetCatalog

interface in `ARX.SDK.Services`, package `ARX.SDK`.

```csharp
public interface IArxSelectionSetCatalog
```

The selection sets an application uses, defined by the developer. ARX only reads it, in memory.

Methods:

- `bool TryGetSet(string name, out ArxSelectionSetDefinition set)`: Finds a set by its exact name.
  - `name`: The selection set's name.
  - `set`: The set when found; otherwise null.

---

# The ARX.SDK sample application for Amazon DynamoDB

The samples run inside a small application of their own. Their code refers to these files, which aren't part of the ARX packages: `dbContext` (SampleContext.cs) is what each sample is given, `dbContext.Service` the application's ARX service, and the tables' ids, keys, lookups and display names are declared in the schema catalog below. In your application, declare your own tables the same way and create the service once.

`dbContext.Direct` (SampleContext.Direct.cs and the provider's DirectHelper) is the samples' checking code on the plain driver: it resets the sample database and reads rows back to show them. It isn't part of ARX and isn't how an application uses ARX: leave those lines out of code you copy.

## SampleContext.cs

What every sample is given: the connection string (and the ISample interface the samples implement).

```csharp
namespace ArxSamples.DynamoDb;

/// <summary>One documentation sample: a scenario a real application has, shown end to end.</summary>
public interface ISample
{
    /// <summary>The name to run it alone: <c>dotnet run -- &lt;name&gt;</c>.</summary>
    string Name { get; }

    /// <summary>One line on what it shows.</summary>
    string Title { get; }

    Task RunAsync(SampleContext dbContext);
}

/// <summary>
/// What a sample is given. Every sample gets the connection string; the application's service and catalog
/// (SampleContext.Application.cs) and the plain AWS SDK read-back helper (SampleContext.Direct.cs) live in their own
/// files, so a sample needs only the files it uses.
/// </summary>
public sealed partial class SampleContext
{
    public SampleContext(string connectionString)
    {
        ConnectionString = connectionString;
    }

    public string ConnectionString { get; }
}
```

## SampleOutput.cs

How the samples print: Step for each scenario, Show for what it did, ShowError for an error caught on purpose.

```csharp
using ARX.SDK;

namespace ArxSamples.DynamoDb;

/// <summary>How the samples print: a numbered step for each scenario, then what it did.</summary>
public static class SampleOutput
{
    /// <summary>The start of a scenario: <c>Step("1. By the id Create returned")</c>.</summary>
    public static void Step(string text) => Console.WriteLine($"  > {text}");

    /// <summary>What the scenario did.</summary>
    public static void Show(string text) => Console.WriteLine($"      {text}");

    /// <summary>An ARX error the scenario caught on purpose.</summary>
    public static void ShowError(ArxException exception)
        => Console.WriteLine($"      caught {exception.GetType().Name}: {exception.Message}");
}
```

## SampleContext.Application.cs

The application's DynamoDB client, model, schema catalog and ARX service, created once and shared by the samples.

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

namespace ArxSamples.DynamoDb;

/// <summary>The application's DynamoDB client, model, catalog and ARX service, shared by the samples.</summary>
public sealed partial class SampleContext
{
    private IAmazonDynamoDB? _client;
    private ArxDynamoModel? _model;
    private ArxSchemaCatalog? _catalog;
    private ArxDynamoDbService? _service;

    /// <summary>
    /// The DynamoDB client for the endpoint the connection string names. Credentials come from the AWS SDK's own chain
    /// (environment, profile, role), never from code; DynamoDB Local on this machine accepts any, so a local endpoint
    /// without credentials in the environment gets placeholder ones.
    /// </summary>
    public IAmazonDynamoDB Client => _client ??= CreateClient(ConnectionString);

    /// <summary>How the entities are laid out (SampleModel.cs).</summary>
    public ArxDynamoModel Model => _model ??= SampleModel.Build();

    /// <summary>The ids, lookups and selection sets (SampleModel.cs).</summary>
    public ArxSchemaCatalog Catalog => _catalog ??= SampleModel.Catalog();

    /// <summary>One service for the whole application: it holds no connection.</summary>
    public ArxDynamoDbService Service => _service ??= new ArxDynamoDbService(Client, Model, Catalog);

    private static IAmazonDynamoDB CreateClient(string endpoint)
    {
        var config = new AmazonDynamoDBConfig { ServiceURL = endpoint, AuthenticationRegion = Environment.GetEnvironmentVariable("AWS_REGION") ?? "us-east-1" };
        var local = Uri.TryCreate(endpoint, UriKind.Absolute, out var uri) && (uri.IsLoopback || uri.Host == "localhost");

        return local && Environment.GetEnvironmentVariable("AWS_ACCESS_KEY_ID") == null
            ? new AmazonDynamoDBClient(new BasicAWSCredentials("local", "local"), config)
            : new AmazonDynamoDBClient(config);
    }
}
```

## SampleModel.cs

How the application lays out its entities in Amazon DynamoDB, and its schema catalog.

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

namespace ArxSamples.DynamoDb;

/// <summary>
/// How the application lays out its entities in DynamoDB, in both designs, and its schema catalog.
/// </summary>
/// <remarks>
/// <para>
/// <b>A table per entity:</b> <c>product</c> has a table of its own, keyed by its <c>sku</c>; <c>lead</c> by its
/// <c>email</c>. The entity's attributes are the item's, keys included.
/// </para>
/// <para>
/// <b>A single table:</b> <c>crm</c> holds customers and their orders together. Each entity's keys are templates:
/// a customer is <c>PK = CUSTOMER#{customer_id}</c>, <c>SK = PROFILE</c>; an order shares its customer's partition,
/// <c>SK = ORDER#{order_number}</c>, so one query reads a customer with its orders. ARX writes the keys, the
/// <c>entity_type</c> attribute that tells the items apart, and the index keys (<c>GSI1</c>: orders by status and date).
/// </para>
/// <para>
/// <b>Write sharding:</b> <c>events</c> spreads the page views of one page over four partitions (a suffix from a hash of
/// the time), so a popular page doesn't overload one partition; ARX reads them back merged.
/// </para>
/// <para>
/// <b>The catalog</b> declares each entity's id (supplied: DynamoDB generates none, so ids come from the application),
/// the lookup from an order to its customer, the customer's display name and the order statuses.
/// </para>
/// </remarks>
public static class SampleModel
{
    public static ArxDynamoModel Build() => ArxDynamoModel.Create()
        .Table("product", partitionKey: "sku")
        .Table("lead", partitionKey: "email")
        .SingleTable("crm", "PK", "SK", crm => crm
            .Entity("customer", pk: "CUSTOMER#{customer_id}", sk: "PROFILE")
            .Entity("sales_order", pk: "CUSTOMER#{customer_id}", sk: "ORDER#{order_number}")
            .Index("GSI1", "GSI1PK", "GSI1SK")
            .EntityIndex("sales_order", "GSI1", pk: "STATUS#{status}", sk: "{ordered_on}"))
        .SingleTable("events", "PK", "SK", events => events
            .Entity("page_view", pk: "PAGE#{page}", sk: "VIEW#{viewed_at}"))
        .WriteSharding("page_view", shards: 4, by: "viewed_at")
        .Build();

    public static ArxSchemaCatalog Catalog() => ArxSchemaCatalog.Create()
        .Table("product", ArxIdColumn.Supplied<string>("sku"))
        .DisplayName("product", "name")
        .Table("lead", ArxIdColumn.Supplied<string>("email"))
        .Table("customer", ArxIdColumn.Supplied<string>("customer_id"))
        .DisplayName("customer", "name")
        .Table("sales_order", ArxIdColumn.Supplied<string>("order_number"))
        .Lookup("sales_order", "customer_id", "customer")
        .Table("page_view", ArxIdColumn.Supplied<string>("viewed_at"))
        .Add("OrderStatus", ArxSelectionSetKind.String, ("OPEN", "Open"), ("SHIPPED", "Shipped"), ("CANCELLED", "Cancelled"))
        .Build();
}
```

## SecurityGuard.cs

A security check around ARX calls: suspected injection logged as a security event, strikes, blocking.

```csharp
using ARX.SDK;

namespace ArxSamples.DynamoDb;

/// <summary>Where the names (tables, attributes, match columns) in a call come from. It decides what an ARX1017 means.</summary>
public enum NameSource
{
    /// <summary>A name built from what a user sent (a form field, a file header, a query string): ARX1017 is a suspected attack.</summary>
    UserInput,

    /// <summary>A name from your code or configuration: ARX1017 is a bug in your code, not an attack. Nobody is blocked.</summary>
    Code,
}

/// <summary>What the guard did with a call.</summary>
public enum GuardOutcome
{
    /// <summary>The call ran.</summary>
    Done,

    /// <summary>ARX refused the call with ARX1017; the event is logged and the user got a plain message.</summary>
    Refused,

    /// <summary>The user was already blocked: the call wasn't attempted.</summary>
    Blocked,
}

/// <summary>One entry of the security log: a suspected attack, or a bug when the names came from code.</summary>
public sealed record SecurityEvent(DateTimeOffset At, string User, string Action, string Kind, string Detail);

/// <summary>The outcome, the call's result when it ran, and the message to show the user (never the detection details).</summary>
public sealed record GuardResult<T>(GuardOutcome Outcome, T? Value, string ForTheUser);

/// <summary>
/// A security check around ARX calls: the pattern to copy into an application, kept small so it can be read in one
/// go. It lets every ARX error through except <c>ARX1017 SuspectedInjection</c>, which it turns into:
/// <list type="bullet">
/// <item>an entry in the security log (<see cref="Events"/>), with the user, the action and ARX's message (the name is
/// escaped and cut to 64 characters by ARX, so it is safe to log);</item>
/// <item>a plain message for the user that doesn't say what was detected;</item>
/// <item>a strike against the user. After <c>blockAfter</c> strikes the user is blocked and their calls aren't attempted.
/// One strike never blocks anyone, so a single false positive only costs one refused request;</item>
/// <item>no strike when the names came from your own code (<see cref="NameSource.Code"/>): that's a bug to fix, logged
/// as one.</item>
/// </list>
/// A false positive (a harmless name such as a file header <c>E-mail (work)</c> flagged as SQL) is handled by triage:
/// read the event, fix the mapping that let the name through, and <see cref="Unblock"/> the user if needed.
/// In a real application the log goes to your SIEM or security monitoring and the strikes live in a shared store
/// (Redis, the database), not in memory.
/// </summary>
public sealed class SecurityGuard
{
    private const string ATTACK = "suspected attack";
    private const string BUG = "bug";

    private readonly object _gate = new();
    private readonly int _blockAfter;
    private readonly Dictionary<string, int> _strikes = new(StringComparer.Ordinal);
    private readonly List<SecurityEvent> _events = new();

    public SecurityGuard(int blockAfter = 3)
    {
        _blockAfter = blockAfter;
    }

    /// <summary>The security log, oldest first.</summary>
    public IReadOnlyList<SecurityEvent> Events
    {
        get
        {
            lock (_gate)
            {
                return _events.ToArray();
            }
        }
    }

    public bool IsBlocked(string user)
    {
        lock (_gate)
        {
            return _strikes.TryGetValue(user, out var strikes) && strikes >= _blockAfter;
        }
    }

    /// <summary>After triage found the events were a false positive: clears the user's strikes (the log is kept).</summary>
    public void Unblock(string user)
    {
        lock (_gate)
        {
            _strikes.Remove(user);
        }
    }

    /// <summary>Runs <paramref name="call"/> for <paramref name="user"/> unless they are blocked, and handles ARX1017 as described above.</summary>
    public async Task<GuardResult<T>> RunAsync<T>(string user, string action, NameSource names, Func<Task<T>> call)
    {
        if (IsBlocked(user))
        {
            return new GuardResult<T>(GuardOutcome.Blocked, default, "Your access is suspended. Contact support.");
        }

        try
        {
            return new GuardResult<T>(GuardOutcome.Done, await call(), "Done.");
        }
        catch (ArxValidationException error) when (error.Code == ArxErrorCode.SuspectedInjection)
        {
            lock (_gate)
            {
                if (names == NameSource.Code)
                {
                    _events.Add(new SecurityEvent(DateTimeOffset.UtcNow, user, action, BUG, error.Message));
                    return new GuardResult<T>(GuardOutcome.Refused, default, "Something went wrong on our side. It has been reported.");
                }

                _strikes[user] = _strikes.GetValueOrDefault(user) + 1;
                _events.Add(new SecurityEvent(DateTimeOffset.UtcNow, user, action, ATTACK, error.Message));
            }

            return new GuardResult<T>(GuardOutcome.Refused, default, "The request couldn't be processed.");
        }
    }
}
```

---

# Supported data types (Amazon DynamoDB)

## Writing: .NET to Amazon DynamoDB

| .NET value | Stored as |
|---|---|
| `string` | S (string); an empty key value is ARX1001 |
| `char` | S |
| a plain `enum` | refused (ARX1007): write `(int)value` or `value.ToString()`, or use a selection set |
| `int`, `long`, `short`, `byte` and the unsigned types | N (number) |
| `decimal` | N, exact up to 38 digits (more is ARX4001) |
| `double`, `float` | N; NaN and infinity are ARX4001 |
| `bool` | BOOL |
| `Guid` | S, `D` format (`5f0c…-…`) |
| `byte[]`, `ArxBytea` | B (binary) |
| `ArxJsonB` | M (map) or L (list), its JSON parsed |
| an array or a list | L, a list of the values above |
| `ArxSelectionSetValue` | its code, checked against the catalog |
| `ArxEntityReference` | the referenced id, checked against the catalog (ARX1005) |
| `null` | NULL |

## Dates and times

DynamoDB has no date type: ARX stores dates as sortable text, so a sort key or a condition on a date compares in time
order.

| .NET value | Stored as |
|---|---|
| `DateTime` (UTC) | S, `2026-11-09T10:30:00.0000000Z` (every tick kept) |
| `DateTime` (Local) | converted to UTC |
| `DateTime` (Unspecified) | taken as UTC |
| `DateTimeOffset` | its UTC instant (the offset isn't kept) |
| `DateOnly` | S, `2026-11-09` |
| `TimeOnly`, `TimeSpan` | S (`09:30:00.0000000`, `1.02:03:04`) |
| `default(DateTime)`, `default(DateTimeOffset)`, `default(DateOnly)` | refused (ARX1013): they mean "not set" |

## Reading: Amazon DynamoDB to .NET

| Stored as | Read back as |
|---|---|
| S (string) | `string`, or the preloaded type (`Guid`, `DateTime`, `DateOnly`, …) |
| N (number) | `decimal` (a `double` past decimal's range), or the preloaded type |
| BOOL | `bool` |
| B (binary) | `byte[]` |
| M (map) | `ArxJsonB` |
| L (list) | `string[]` or `decimal[]` when every element is that kind; otherwise `ArxJsonB` |
| SS, NS, BS (sets) | `string[]`, a number array, `byte[][]` |
| NULL, or no attribute | null |
| a declared lookup | `ArxEntityReference` |

---

# ARX error codes (Amazon DynamoDB)

Every error an ARX call throws is an `ArxException` with a stable `Code` (`ArxErrorCode`), the same in every provider (a misuse found while the schema catalog is built at startup, such as a table declared twice, is an `ArgumentException`). The message starts with it (`ARX3001 UniqueViolation: …`) and the thousands digit is its `Category`. Catch by code, on the base class: `catch (ArxException e) when (e.Code == ArxErrorCode.RecordNotFound)`. Some codes come as a subclass (`ArxValidationException` before the database, `ArxDatabaseException` from it, `ArxNotSupportedException` for ARX8001), but not every one: a record not found (ARX2002) can be the base class, so a catch of a subclass can miss it. Retry only when `IsTransient` is true. The database's own error stays on the exception: `NativeCode`, and the driver's exception as `InnerException`.

## ARX1xxx Validation

ARX refused the call before sending it: invalid input, a misused connection, transaction or batch scope, or a name that looks like SQL injection.

| Code | Name | Meaning | Safe to retry | From Amazon DynamoDB |
|---|---|---|---|---|
| ARX1001 | `InvalidArgument` | Invalid input not covered by a more specific code. | no | no database error maps to it (ARX itself can still raise it) |
| ARX1002 | `MissingEntityName` | The entity name is null or blank. | no | no database error maps to it (ARX itself can still raise it) |
| ARX1003 | `InvalidIdentifier` | An entity or attribute name fails the identifier rules. | no | no database error maps to it (ARX itself can still raise it) |
| ARX1004 | `DuplicateAttribute` | The same attribute appears more than once (names are case-insensitive). | no | no database error maps to it (ARX itself can still raise it) |
| ARX1005 | `InvalidLookup` | A lookup (`ArxEntityReference`) has no usable Id. | no | no database error maps to it (ARX itself can still raise it) |
| ARX1006 | `InvalidSelectionSet` | A selection set value has no option set name. | no | no database error maps to it (ARX itself can still raise it) |
| ARX1007 | `UnsupportedValueType` | An attribute value has a type the provider can't write. | no | no database error maps to it (ARX itself can still raise it) |
| ARX1008 | `InvalidConnection` | The connection or transaction is of the wrong driver type, or the service has no connection string. | no | no database error maps to it (ARX itself can still raise it) |
| ARX1009 | `TransactionMismatch` | The transaction belongs to a different connection. | no | no database error maps to it (ARX itself can still raise it) |
| ARX1010 | `TransactionCompleted` | The transaction has already been committed, rolled back or disposed. | no | no database error maps to it (ARX itself can still raise it) |
| ARX1011 | `SelectionSetValueNotAllowed` | A selection set value isn't allowed by its set in the catalog: not one of its options, of the wrong kind, a retired option, several values for a single-choice set, a duplicate, or a value the set's storage can't hold. | no | no database error maps to it (ARX itself can still raise it) |
| ARX1012 | `UnknownSelectionSet` | A selection set value names a set that isn't in the catalog, or no catalog was supplied. | no | no database error maps to it (ARX itself can still raise it) |
| ARX1013 | `InvalidDateTimeValue` | A date or time value that would be stored as something else than the caller meant: default(DateTime), default(DateTimeOffset) or default(DateOnly) (no real date: a database would store a sentinel or refuse it), or an array mixing UTC and Unspecified DateTime values. | no | no database error maps to it (ARX itself can still raise it) |
| ARX1014 | `BatchScopeBusy` | A batch scope was called while another call on it was still running. A scope holds one connection, which runs one command at a time: await each call before the next. | no | no database error maps to it (ARX itself can still raise it) |
| ARX1015 | `TransactionAborted` | A call or a commit on a transaction that an earlier failed call has aborted. The database has already discarded the transaction's work; it can only be rolled back. | no | no database error maps to it (ARX itself can still raise it) |
| ARX1016 | `IdNotDeclared` | The entity's table isn't declared in the service's schema catalog, and the entity doesn't declare its own id column. A service with a catalog uses only what is declared: ARX doesn't guess an id column. A service created without a catalog reads the table's primary key instead (where the provider can), and raises `TableNotResolvable` when it can't. | no | no database error maps to it (ARX itself can still raise it) |
| ARX1017 | `SuspectedInjection` | A name (entity, attribute, match column or type) contains SQL syntax: a quote, a statement separator, a comment marker, a bracket, an operator or an SQL keyword after a space. That is the shape of an SQL injection attempt, not of a typo, so it gets its own code to alert on. Refused before the database: nothing was sent. Values are never checked for this: they are always parameters, and any text is stored as typed. | no | no database error maps to it (ARX itself can still raise it) |
| ARX1018 | `NotPreloaded` | The service is `Strict` and the call names a table or column that wasn't preloaded: refused before the database. Preload it, or turn Strict off. | no | no database error maps to it (ARX itself can still raise it) |
| ARX1019 | `TableNotResolvable` | A service without a schema catalog resolves each table from the database, and this table has no id the call can use: no primary key, a primary key of several columns, or one of a type ARX can't use as an id. Refused before anything is sent, only for a call that needs the id (Create, Retrieve, Update or Delete by id). Declare the table in a schema catalog (`ArxIdColumn.NoIdColumn`), or match by a key or columns. | no | no database error maps to it (ARX itself can still raise it) |

## ARX2xxx NotFound

The rows the call addressed weren't found as asked: none, several where one was required, or one at another version than the call carried (ARX2004). Nothing was changed.

| Code | Name | Meaning | Safe to retry | From Amazon DynamoDB |
|---|---|---|---|---|
| ARX2002 | `RecordNotFound` | The record addressed by key doesn't exist. | no | `ConditionalCheckFailed (ARX's condition)` an update whose item doesn't exist: ARX writes the update on condition that it does |
| ARX2003 | `MultipleRecordsMatched` | An update or delete with `RequireSingleRow` found more than one row for an entity. Nothing was changed. | no | no database error maps to it (ARX itself can still raise it) |
| ARX2004 | `RecordChanged` | The record was changed since it was read: an Update or Delete carried a version (optimistic concurrency) that isn't the record's any more. Nothing was changed: read it again, and apply the change to what is there now. | no | no database error maps to it (ARX itself can still raise it) |

## ARX3xxx Constraint

The database refused the change because of a rule: a unique key, a foreign key, NOT NULL, CHECK, or a routine's own error.

| Code | Name | Meaning | Safe to retry | From Amazon DynamoDB |
|---|---|---|---|---|
| ARX3001 | `UniqueViolation` | A unique constraint or key was violated. | no | `ConditionalCheckFailedException` a create's "doesn't exist yet" condition failed: an item with the key exists (ARX2002 for an update's "exists" condition); `TransactionCanceledException` a transaction was cancelled: mapped by its first reason (a failed condition, a conflict, throttling) |
| ARX3002 | `ForeignKeyViolation` | A referenced record is missing, or a record is still referenced. | no | no database error maps to it (ARX itself can still raise it) |
| ARX3003 | `NotNullViolation` | A required attribute has no value. | no | no database error maps to it (ARX itself can still raise it) |
| ARX3004 | `CheckViolation` | A check constraint or validation rule failed. | no | no database error maps to it (ARX itself can still raise it) |
| ARX3005 | `RaisedByDatabase` | A routine the database runs (a procedure or function) refused the call on purpose (PostgreSQL `RAISE EXCEPTION`, SQL Server `THROW`, MySQL `SIGNAL`). On PostgreSQL: `RAISE EXCEPTION` (SQLSTATE P0001), an `ASSERT` or a state of the routine's own (`USING ERRCODE = 'AX001'`). The message is the routine's; `SqlState`, `Detail` and `Hint` are its own. | no | no database error maps to it (ARX itself can still raise it) |

## ARX4xxx Data

A value doesn't fit its column: out of range, too long, in the wrong format or of the wrong type.

| Code | Name | Meaning | Safe to retry | From Amazon DynamoDB |
|---|---|---|---|---|
| ARX4001 | `ValueOutOfRange` | A number is outside the column's range. | no | no database error maps to it (ARX itself can still raise it) |
| ARX4002 | `ValueTooLong` | Text or binary data is longer than the column allows. | no | `ItemCollectionSizeLimitExceededException` an item collection is larger than 10 GB (local secondary index) |
| ARX4003 | `InvalidValueFormat` | A value can't be parsed for its column (for example invalid JSON, dates or encoding). | no | `ValidationException` a request DynamoDB refuses: an item over 400 KB is ARX4002, a number out of range ARX4001 |
| ARX4004 | `DataTypeMismatch` | A value's type doesn't fit the column. | no | no database error maps to it (ARX itself can still raise it) |

## ARX5xxx Schema

A table or column doesn't exist, or a declaration in the schema catalog doesn't match the table.

| Code | Name | Meaning | Safe to retry | From Amazon DynamoDB |
|---|---|---|---|---|
| ARX5001 | `UndefinedEntity` | The table or collection doesn't exist. | no | `ResourceNotFoundException` the table or index doesn't exist |
| ARX5002 | `UndefinedAttribute` | The column doesn't exist or can't be written. | no | no database error maps to it (ARX itself can still raise it) |
| ARX5003 | `SchemaMismatch` | A declaration in the schema catalog doesn't match the table: the database returned an id of another type, or `ValidateSchemaAsync` found a difference. The exception says whether rows were stored. | no | no database error maps to it (ARX itself can still raise it) |

## ARX6xxx Permission

The database user isn't allowed to do it.

| Code | Name | Meaning | Safe to retry | From Amazon DynamoDB |
|---|---|---|---|---|
| ARX6001 | `PermissionDenied` | The database user lacks the required permission. | no | `AccessDeniedException` the credentials lack a permission; `MissingAuthenticationTokenException` the request isn't signed; `UnrecognizedClientException` the credentials aren't valid |

## ARX7xxx Transient

A temporary failure: a deadlock, a conflict, a timeout, throttling or a lost connection. Safe to retry the whole operation (`IsTransient` is true).

| Code | Name | Meaning | Safe to retry | From Amazon DynamoDB |
|---|---|---|---|---|
| ARX7001 | `Deadlock` | The operation was chosen as a deadlock victim. Safe to retry. | yes | no database error maps to it (ARX itself can still raise it) |
| ARX7002 | `SerializationFailure` | The operation conflicted with a concurrent transaction. Safe to retry. | yes | `TransactionConflictException` another transaction is writing the same item; `TransactionInProgressException` a transaction with the same client token is still running |
| ARX7003 | `Timeout` | The command or a lock wait timed out. Safe to retry. | yes | no database error maps to it (ARX itself can still raise it) |
| ARX7004 | `Throttled` | The server is rate-limiting or out of connections. Safe to retry later. | yes | `LimitExceededException` too many control-plane operations at once; `ProvisionedThroughputExceededException` the table's provisioned capacity is exceeded; `RequestLimitExceeded` the account's request limit is exceeded; `ThrottlingException` requests are being throttled |
| ARX7005 | `ConnectionFailure` | The database couldn't be reached, or the connection dropped. Safe to retry. | yes | `InternalServerError` DynamoDB failed internally (retry); `SDK` the SDK couldn't reach the endpoint, or timed out; `ServiceUnavailable` DynamoDB is unavailable (retry) |

## ARX8xxx NotSupported

The provider can't do what was asked, and refuses it rather than ignoring part of it.

| Code | Name | Meaning | Safe to retry | From Amazon DynamoDB |
|---|---|---|---|---|
| ARX8001 | `NotSupported` | The provider doesn't support the requested operation or feature. | no | `ARX` what DynamoDB can't do: a query without its partition key (unless a scan is allowed), a page number, a total count, more than 100 writes in one transaction, a match that isn't the key, Execute |

## ARX9xxx Provider

A database error ARX doesn't map; the exception keeps its native code and SQLSTATE.

| Code | Name | Meaning | Safe to retry | From Amazon DynamoDB |
|---|---|---|---|---|
| ARX9001 | `ProviderError` | A native error the provider doesn't map; see NativeCode and SqlState. | no | any other error number |

---

# Create with ARX.SDK on Amazon DynamoDB

## What a create does

| Rule | On DynamoDB |
|---|---|
| Where it goes | The model says: a table of the entity's own (`.Table("product", partitionKey: "sku")`), or a single table shared by several entities, whose keys are templates (`PK = CUSTOMER#{customer_id}`) |
| The keys | ARX writes the table's key attributes, the index keys and `entity_type` from the model; the entity gives the values the templates name |
| An index's attributes | A create may leave out an attribute an index key is made of (a GSI is sparse): the item is written without that index key and isn't in the index, so a query through the index doesn't find it. An update that writes some of an index key's attributes must give the rest (ARX1001), because ARX writes the whole index key again; give none to leave the index key as it is |
| The id | Supplied by the application: DynamoDB generates no ids, so a generated id is ARX8001 (give a `Guid.CreateVersion7()` or your own) |
| A duplicate | ARX3001 UniqueViolation: every create is a conditional put (`attribute_not_exists`) and never overwrites |
| Names | ASCII letters, digits and `_`; `#`, `:` (expression syntax) or SQL syntax is ARX1017; names reach DynamoDB only as placeholders |
| Key values | Escaped in templates (`#` is stored as `%23`, `%` as `%25`), so a value can't make another entity's key |
| Write sharding | `.WriteSharding(entity, shards, by)` adds `#SHARD#n` to the partition key, n from a stable hash of the `by` attribute |
| Size | An item over 400 KB is ARX4002 |
| Without a catalog | Nothing to resolve: the `ArxDynamoModel` declares every table's keys and ids, and DynamoDB declares no column types. The SQL providers' `SchemaResolution` doesn't apply |

## Reference

### Value types

| .NET value | Stored as |
|---|---|
| `string` | S (string); an empty key value is ARX1001 |
| `char` | S |
| a plain `enum` | refused (ARX1007): write `(int)value` or `value.ToString()`, or use a selection set |
| `int`, `long`, `short`, `byte` and the unsigned types | N (number) |
| `decimal` | N, exact up to 38 digits (more is ARX4001) |
| `double`, `float` | N; NaN and infinity are ARX4001 |
| `bool` | BOOL |
| `Guid` | S, `D` format (`5f0c…-…`) |
| `byte[]`, `ArxBytea` | B (binary) |
| `ArxJsonB` | M (map) or L (list), its JSON parsed |
| an array or a list | L, a list of the values above |
| `ArxSelectionSetValue` | its code, checked against the catalog |
| `ArxEntityReference` | the referenced id, checked against the catalog (ARX1005) |
| `null` | NULL |

### Dates and times

DynamoDB has no date type: ARX stores dates as sortable text, so a sort key or a condition on a date compares in time
order.

| .NET value | Stored as |
|---|---|
| `DateTime` (UTC) | S, `2026-11-09T10:30:00.0000000Z` (every tick kept) |
| `DateTime` (Local) | converted to UTC |
| `DateTime` (Unspecified) | taken as UTC |
| `DateTimeOffset` | its UTC instant (the offset isn't kept) |
| `DateOnly` | S, `2026-11-09` |
| `TimeOnly`, `TimeSpan` | S (`09:30:00.0000000`, `1.02:03:04`) |
| `default(DateTime)`, `default(DateTimeOffset)`, `default(DateOnly)` | refused (ARX1013): they mean "not set" |

## The samples

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

### Create one record, in either design

Basics 1 of Create.

The smallest create, in the two designs DynamoDB tables are made in: an entity with a table of its own, then a customer and an order sharing one table through key templates.

Good to know:

The model says where each entity goes; the code that creates it is the same in both designs. DynamoDB generates no ids: the id is one of the values you give. A create never overwrites: an item with the same key is ARX3001.

Usings:

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

#### 1. A table of its own

A product in its own table, keyed by its SKU.

In scope: `client` (IAmazonDynamoDB).

```csharp
var model = ArxDynamoModel.Create()
    .Table("product", partitionKey: "sku")
    .Build();

var service = new ArxDynamoDbService(client, model);

var product = new ArxEntity("product");
product["sku"] = "SKU-D101";
product["name"] = "Trail running shoe";
product["price"] = 129.90m;

await service.CreateAsync(product);

Console.WriteLine("created product SKU-D101");
```

Check it in the database:

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

Expect: 1 item, name "Trail running shoe", price 129.9.

#### 2. One table for customers and orders

A customer and its first order in one table: ARX writes PK, SK and entity_type from the model's templates.

In scope: `service` (ArxDynamoDbService).

```csharp
var customer = new ArxEntity("customer");
customer["customer_id"] = "C-D101";
customer["name"] = "Northwind Traders";
ArxRecordId id = await service.CreateAsync(customer);

var order = new ArxEntity("sales_order");
order["customer_id"] = "C-D101";
order["order_number"] = "SO-D101";
order["status"] = "OPEN";
order["ordered_on"] = "2026-11-09";
order["total"] = 250m;
await service.CreateAsync(order);

Console.WriteLine($"created customer {id.AsString()} and order SO-D101");
```

Check it in the database:

```sql
SELECT * FROM "crm" WHERE "PK" = 'CUSTOMER#C-D101'
```

Expect: 2 items: SK "ORDER#SO-D101" (entity_type sales_order, GSI1PK "STATUS#OPEN") and SK "PROFILE" (entity_type customer).

### Catch the errors a create meets

Basics 2 of Create.

The errors a create meets most, in three scenarios: a key that exists, a key attribute left out, and an id declared as generated, which DynamoDB can't do.

Usings:

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

#### 1. A key that exists

The same customer twice: ARX3001; the first stays as it was.

In scope: `service` (ArxDynamoDbService).

```csharp
var customer = new ArxEntity("customer");
customer["customer_id"] = "C-D401";
customer["name"] = "Fabrikam";
await service.CreateAsync(customer);

customer["name"] = "Fabrikam (copy)";

try
{
    await service.CreateAsync(customer);
}
catch (ArxDatabaseException exception) when (exception.Code == ArxErrorCode.UniqueViolation)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

```sql
SELECT "name" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D401' AND "SK" = 'PROFILE'
```

Expect: 1 item, name "Fabrikam".

#### 2. A key attribute left out

An order without its customer_id: its key can't be made, so nothing is sent.

In scope: `service` (ArxDynamoDbService).

```csharp
var order = new ArxEntity("sales_order");
order["order_number"] = "SO-D402";

try
{
    await service.CreateAsync(order);
}
catch (ArxValidationException exception)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

```sql
SELECT "SK" FROM "crm" WHERE "SK" = 'ORDER#SO-D402'
```

Expect: no items.

#### 3. A generated id

An id declared as generated: DynamoDB generates nothing, so ARX8001 with the advice to supply one.

In scope: `client` (IAmazonDynamoDB).

```csharp
var catalog = ArxSchemaCatalog.Create().Table("product", ArxIdColumn.Generated<string>("sku")).Build();
var service = new ArxDynamoDbService(client, ArxDynamoModel.Create().Table("product", partitionKey: "sku").Build(), catalog);

var product = new ArxEntity("product");
product["sku"] = "SKU-D403";

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

Check it in the database:

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

Expect: no items.

### Store every kind of value

Real case 1 of Create.

What each .NET value becomes in DynamoDB, in two scenarios: numbers, text and flags; then dates, documents, lists and picklists.

Good to know:

- Numbers are N (up to 38 digits); NaN and infinity are refused. Text is S, booleans BOOL, bytes B, a Guid its text.
- Dates are sortable ISO 8601 text in UTC with seven fraction digits (an Unspecified DateTime is taken as UTC); a DateOnly is `yyyy-MM-dd`.
- `ArxJsonB` is a map; a list or an array is a list.

Usings:

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

#### 1. Numbers, text and flags

A product with numbers of every kind, a flag, bytes and a Guid.

In scope: `service` (ArxDynamoDbService).

```csharp
var product = new ArxEntity("product");
product["sku"] = "SKU-D501";
product["name"] = "Camp stove";
product["stock"] = 42;
product["weight_kg"] = 1.25;
product["price"] = 89.99m;
product["is_active"] = true;
product["barcode"] = new byte[] { 0x01, 0x02, 0x03 };
product["supplier_ref"] = Guid.Parse("3f2504e0-4f89-41d3-9a0c-0305e82c3301");

await service.CreateAsync(product);

Console.WriteLine("created product SKU-D501");
```

Check it in the database:

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

Expect: stock 42, weight_kg 1.25, price 89.99, is_active true, supplier_ref "3f2504e0-4f89-41d3-9a0c-0305e82c3301".

#### 2. Dates, documents, lists and picklists

An order with dates, an embedded address, a list of tags and a picklist value.

In scope: `service` (ArxDynamoDbService).

```csharp
var order = new ArxEntity("sales_order");
order["customer_id"] = "C-D501";
order["order_number"] = "SO-D501";
order["ordered_on"] = "2026-11-09";
order["status"] = new ArxSelectionSetValue("OrderStatus", "OPEN");
order["placed_at"] = new DateTime(2026, 11, 9, 14, 30, 0, DateTimeKind.Utc);
order["ship_by"] = new DateOnly(2026, 11, 12);
order["ship_to"] = new ArxJsonB { SerializedValue = "{\"street\":\"1 Harbour Road\",\"city\":\"Porto\"}" };
order["tags"] = new[] { "gift", "express" };

await service.CreateAsync(order);

Console.WriteLine("created order SO-D501");
```

Check it in the database:

```sql
SELECT "placed_at", "ship_by", "ship_to", "tags", "status" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D501' AND "SK" = 'ORDER#SO-D501'
```

Expect: placed_at "2026-11-09T14:30:00.0000000Z", ship_by "2026-11-12", ship_to a map with city "Porto", tags ["gift", "express"], status "OPEN".

### Catch an injection attempt

Real case 2 of Create.

A form whose fields become attributes, attacked three ways: an expression placeholder as a field name, SQL in a field name, and key separators in a value meant to land on another customer's key.

Good to know:

Names with `#`, `:` or SQL syntax are ARX1017 before anything is sent: names reach DynamoDB only as placeholders. Values in key templates are escaped, so `#` in a value can't make another entity's key.

Usings:

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

#### 1. A placeholder as a field name

A field named "#pk": ARX1017.

In scope: `service` (ArxDynamoDbService).

```csharp
var customer = new ArxEntity("customer");
customer["customer_id"] = "C-D601";
customer["#pk"] = "CUSTOMER#someone-else";

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

Check it in the database:

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

Expect: no items.

#### 2. SQL in a field name

SQL in a field name: ARX1017 too.

In scope: `service` (ArxDynamoDbService).

```csharp
var customer = new ArxEntity("customer");
customer["customer_id"] = "C-D602";
customer["name'; DROP TABLE crm; --"] = "x";

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

Check it in the database:

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

Expect: no items.

#### 3. A separator in a key value

An order number "x#ORDER#y": escaped in the key, so it stays this customer's order.

In scope: `service` (ArxDynamoDbService).

```csharp
var order = new ArxEntity("sales_order");
order["customer_id"] = "C-D603";
order["order_number"] = "x#ORDER#y";
order["status"] = "OPEN";
order["ordered_on"] = "2026-11-09";

await service.CreateAsync(order);

Console.WriteLine("stored, its key escaped");
```

Check it in the database:

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

Expect: 1 item, SK "ORDER#x%23ORDER%23y", order_number "x#ORDER#y".

### Create in an ARX transaction

Advanced case 1 of Create.

Writes across tables in one ARX transaction, in two scenarios: committed together, and a commit that fails and writes nothing.

Good to know:

DynamoDB has no open transaction: ARX collects the writes and sends them as one `TransactWriteItems` at the commit, all or nothing, across tables, up to 100. A failing write (a key that exists) shows at the commit.

Usings:

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

#### 1. Committed together

A customer (single table) and a product (its own table) committed together.

In scope: `service` (ArxDynamoDbService).

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

var customer = new ArxEntity("customer");
customer["customer_id"] = "C-D701";
customer["name"] = "Wide World Importers";
await service.CreateAsync(customer, transaction);

var product = new ArxEntity("product");
product["sku"] = "SKU-D701";
product["name"] = "Import crate";
await service.CreateAsync(product, transaction);

await transaction.CommitAsync();
Console.WriteLine("committed the customer and the product");
```

Check it in the database:

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

Expect: 1 item, name "Import crate".

#### 2. A commit that fails

The second customer repeats C-D701: the commit fails and the new product isn't written either.

In scope: `service` (ArxDynamoDbService).

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

var product = new ArxEntity("product");
product["sku"] = "SKU-D702";
await service.CreateAsync(product, transaction);

var duplicate = new ArxEntity("customer");
duplicate["customer_id"] = "C-D701";
await service.CreateAsync(duplicate, transaction);

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

Check it in the database:

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

Expect: no items.

### A security check around creates

Advanced case 2 of Create.

A security check around creates (`SecurityGuard`, in the project root), in three scenarios: a normal request, an attack that is logged, and repeated attacks that block the user.

Good to know:

The guard lets every ARX error through except ARX1017 SuspectedInjection, which it logs (user, action, ARX's evidence), answers with a plain message, and counts: three strikes block the user. The real fix is to map posted fields to names from a fixed list, so user input never becomes a name.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var guard = new SecurityGuard(blockAfter: 3);
```

#### 1. A normal request

A normal sign-up goes through untouched.

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

```csharp
var result = await SignUpAsync(service, guard, "ana", new() { ["customer_id"] = "C-D901", ["name"] = "Ana" });

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

Defined in the sample, used above:

```csharp
private static Task<GuardResult<ArxRecordId>> SignUpAsync(ArxDynamoDbService service, SecurityGuard guard, string user, Dictionary<string, object> posted)
{
    var customer = new ArxEntity("customer");

    foreach (var field in posted)
    {
        customer[field.Key] = field.Value;
    }

    return guard.RunAsync(user, "sign up", NameSource.UserInput, () => service.CreateAsync(customer));
}
```

Check it in the database:

```sql
SELECT "name" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D901' AND "SK" = 'PROFILE'
```

Expect: 1 item, name "Ana".

#### 2. An attack is logged

A field named like an expression placeholder: refused, logged, a plain message for the user.

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

```csharp
var result = await SignUpAsync(service, guard, "mallory", new() { ["customer_id"] = "C-D902", [":v0"] = "x" });

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

Check it in the database:

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

Expect: no items.

#### 3. Repeated attacks block the user

Two more attempts: the third strike blocks mallory, and the next request isn't even tried.

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

```csharp
foreach (var probe in new[] { "name; DROP TABLE crm", "#PK" })
{
    await SignUpAsync(service, guard, "mallory", new() { ["customer_id"] = "C-D903", [probe] = "x" });
}

var blocked = await SignUpAsync(service, guard, "mallory", new() { ["customer_id"] = "C-D904", ["name"] = "Innocent looking" });

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

Check it in the database:

```sql
SELECT "PK" FROM "crm" WHERE "PK" IN ['CUSTOMER#C-D903', 'CUSTOMER#C-D904']
```

Expect: no items.

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

Advanced case 3 of Create.

The service set up from DynamoDB's own description of its tables, in two scenarios: preload the lead table's key at startup, then create with Strict on, where only the declared attributes may be written.

Good to know:

DynamoDB stores the types of key attributes only, so the preload reads the key (as a business key, with its type) and you declare the other attributes you allow with `.Column`. With `Strict`, any other attribute is ARX1018.

Usings:

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

#### 1. Preload at startup

Preload the lead table once, at startup, allowing two more attributes.

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

```csharp
var catalog = await new ArxPreLoadTable(new ArxPreLoadTableRegister().PreLoadTable("lead")).LoadAsync(client, model, schema => schema
    .Column("lead", new ArxColumnDefinition("full_name", typeof(string), "S", true))
    .Column("lead", new ArxColumnDefinition("score", typeof(int), "N", true)));

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

return new ArxDynamoDbService(client, model, catalog) { Strict = true };
```

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

#### 2. Create with Strict on

A lead with the declared attributes is created; a misspelt attribute is refused before DynamoDB.

In scope: `service` (ArxDynamoDbService).

```csharp
var lead = new ArxEntity("lead");
lead["email"] = "lead-d951@example.com";
lead["full_name"] = "Kim Abercrombie";
lead["score"] = 72;
await service.CreateAsync(lead);
Console.WriteLine("created lead-d951@example.com");

var typo = new ArxEntity("lead");
typo["email"] = "lead-d952@example.com";
typo["scroe"] = 10;

try
{
    await service.CreateAsync(typo);
}
catch (ArxValidationException exception) when (exception.Code == ArxErrorCode.NotPreloaded)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

```sql
SELECT "email", "score" FROM "lead" WHERE "email" IN ['lead-d951@example.com', 'lead-d952@example.com']
```

Expect: 1 item, lead-d951@example.com with score 72.

### Set the service up for production

Advanced case 4 of Create.

Set the service up for production, in three scenarios: depend on IArxDataService, check the server at startup, and trace and measure every call.

Good to know:

`IArxDataService` is what every ARX service does, whatever its database: application code can depend on it, register the provider behind it, and mock it in tests (the provider type still has what is its own, such as its connections). `CheckServerAsync` asks the server, once, what it allows: its version, and the capabilities it removes, with the reason (a standalone MongoDB runs no transactions, for one). Every operation is an activity of the `ARX.SDK` ActivitySource and a measurement of the `ARX.SDK` Meter: OpenTelemetry reads them with `AddSource("ARX.SDK")` and `AddMeter("ARX.SDK")`, and so does a plain `ActivityListener`, as here. They carry the operation, the table and, on failure, the ARX code: never the values.

Usings:

```csharp
using ARX.SDK;
using ARX.SDK.Services;
using System.Diagnostics;
using System.Diagnostics.Metrics;
```

Set up by the sample before its scenarios:

```csharp
IArxDataService data = service;
```

#### 1. Depend on IArxDataService

An application service that depends on IArxDataService, not on the provider's type.

In scope: `data` (IArxDataService).

```csharp
var onboarding = new Onboarding(data);
await onboarding.AddAsync("Trail running shoe", "SKU-D601");

Console.WriteLine($"added Trail running shoe through IArxDataService ({data.Capabilities.ProviderName})");
```

Defined in the sample, used above:

```csharp
private sealed class Onboarding(IArxDataService data)
{
    public Task<ArxRecordId> AddAsync(string name, string key)
    {
        var record = new ArxEntity("product");
        record["name"] = name;
        record["sku"] = key;
        return data.CreateAsync(record);
    }
}
```

Check it in the database:

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

Expect: 1 item, Trail running shoe.

#### 2. Check the server at startup

Ask the server once, at startup, what it allows.

In scope: `data` (IArxDataService).

```csharp
ArxServerCheck check = await data.CheckServerAsync();

Console.WriteLine($"ready: {check.IsReady}; transactions here: {check.Supports(ArxCapability.Transactions)}; limits: {check.Limits.Count}");
```

Note: No query: the check describes every table of the model. the server's version, the same as check.ServerVersion.

#### 3. Trace and measure every call

Listen to ARX's activities and metrics: one per call, with its table, and its code when it fails.

In scope: `data` (IArxDataService).

```csharp
var calls = new List<string>();
using var tracing = new ActivityListener
{
    ShouldListenTo = source => source.Name == ArxDiagnostics.SourceName,
    Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllDataAndRecorded,
    ActivityStopped = activity => calls.Add(activity.DisplayName + (activity.GetTagItem("arx.error.code") is { } code ? $" ({code})" : string.Empty))
};
ActivitySource.AddActivityListener(tracing);

var durations = 0;
using var metrics = new MeterListener
{
    InstrumentPublished = (instrument, listener) =>
    {
        if (instrument.Meter.Name == ArxDiagnostics.SourceName && instrument.Name == "arx.operation.duration")
        {
            listener.EnableMeasurementEvents(instrument);
        }
    }
};
metrics.SetMeasurementEventCallback<double>((_, _, _, _) => Interlocked.Increment(ref durations));
metrics.Start();

await new Onboarding(data).AddAsync("Hiking boot", "SKU-D602");

try
{
    var nobody = new ArxEntity("product") { RecordId = ArxRecordId.Of("sku", "SKU-D699") };
    nobody["name"] = "Nobody";
    await data.UpdateAsync(nobody);
}
catch (ArxException exception) when (exception.Code == ArxErrorCode.RecordNotFound)
{
    Console.WriteLine(exception.Message);
}

Console.WriteLine($"traced: {string.Join("; ", calls)}; durations recorded: {durations}");
```

Check it in the database:

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

Expect: 1 item, Hiking boot (the update found nothing to change).

### Use attributes named outside the name rule, through aliases

Advanced case 5 of Create.

Use items whose attributes are named outside ARX's name rule, in three scenarios: declare an alias for each such attribute and create items through them, read and query by the aliases, and see what is still refused: the attribute's own name in a call, and an alias on an attribute the model names.

Good to know:

ARX accepts only names of ASCII letters, digits and underscores. Items written by other code may have attributes such as `list-price`, `Colour Name` or `size.label`: declare an alias for each in the schema catalog, `.Alias(table, attribute, databaseName)`. Code uses the attribute; ARX writes and filters on the item's attribute (a `.` in it is part of the name, not a path) and reads it back as the attribute.

Aliases are for ordinary attributes: the model names the keys, index keys and key templates' attributes as the table stores them, so an alias on one of those is 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 schema = ArxSchemaCatalog.Create()
    .Table("product", ArxIdColumn.Supplied<string>("sku"))
    .Alias("product", "list_price", "list-price")
    .Alias("product", "colour", "Colour Name")
    .Alias("product", "size_label", "size.label")
    .Build();
var service = new ArxDynamoDbService(dbContext.Client, dbContext.Model, schema);
```

#### 1. Create through the aliases

Create two products: each attribute is written to the item attribute its alias names.

In scope: `service` (ArxDynamoDbService).

```csharp
var shoe = new ArxEntity("product");
shoe["sku"] = "SKU-A601";
shoe["name"] = "Trail running shoe";
shoe["list_price"] = 89.90m;
shoe["colour"] = "Forest green";
shoe["size_label"] = "EU 42";

await service.CreateAsync(shoe);

var boot = new ArxEntity("product");
boot["sku"] = "SKU-A602";
boot["name"] = "Hiking boot";
boot["list_price"] = 129.00m;

await service.CreateAsync(boot);

Console.WriteLine("created SKU-A601 and SKU-A602");
```

Check it in the database:

```sql
SELECT "list-price", "Colour Name", "size.label" FROM "product" WHERE "sku" IN ['SKU-A601', 'SKU-A602']
```

Expect: 2 items: 89.9 with Forest green and EU 42, and 129.

#### 2. Read and query by the aliases

Read an item by id and query by the aliases: every attribute comes back under its alias.

In scope: `service` (ArxDynamoDbService).

```csharp
var shoe = await service.RetrieveAsync("product", ArxRecordId.Of("sku", "SKU-A601")) ?? throw new InvalidOperationException("The product record isn't there.");

Console.WriteLine($"{shoe["sku"]}: {shoe["colour"]}, {shoe["size_label"]}, {shoe["list_price"]}");

var query = new ArxQueryExpression("product");
query.ColumnSet.AddColumns("name", "list_price");
query.Criteria.AddCondition("sku", ArxConditionOperator.Equal, "SKU-A602");
query.Criteria.AddCondition("list_price", ArxConditionOperator.GreaterThan, 100m);

var products = await service.RetrieveMultipleAsync(query);

foreach (var product in products.Entities)
{
    Console.WriteLine($"{product["name"]}: {product["list_price"]}");
}
```

Check it in the database:

```sql
SELECT "name", "list-price" FROM "product" WHERE "sku" = 'SKU-A602' AND "list-price" > 100
```

Expect: 1 item, Hiking boot, 129.

#### 3. What is still refused

The attribute's own name in a call, and an alias on the table's key, are refused.

In scope: `service` (ArxDynamoDbService).

```csharp
var change = new ArxEntity("product");
change["sku"] = "SKU-A602";
change["list-price"] = 119.00m;

try
{
    await service.UpdateAsync(change);
}
catch (ArxException exception) when (exception.Code == ArxErrorCode.InvalidIdentifier)
{
    Console.WriteLine(exception.Message);
}

var keyed = new ArxDynamoDbService(dbContext.Client, dbContext.Model, ArxSchemaCatalog.Create()
    .Table("product", ArxIdColumn.Supplied<string>("sku"))
    .Alias("product", "sku", "SKU Code")
    .Build());

var sock = new ArxEntity("product");
sock["sku"] = "SKU-A603";

try
{
    await keyed.CreateAsync(sock);
}
catch (ArxNotSupportedException exception)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

```sql
SELECT "sku", "list-price" FROM "product" WHERE "sku" IN ['SKU-A602', 'SKU-A603']
```

Expect: 1 item, SKU-A602 at 129 (the update and the create were refused).

### Log every call through ILogger

Advanced case 6 of Create.

Log every ARX call through ILogger, in three scenarios: one line per call with its table and how long it took, a failed call with its ARX code (and the exception logged where it is caught), and only the calls worth keeping in production.

Good to know:

ARX has no logger of its own and takes no logging package: every call is an activity of the `ARX.SDK` ActivitySource. `ArxCallLog`, below, listens to those activities and writes one log line per call: copy it into your application and give it the `ILogger` you already have (`ILogger<T>` from dependency injection), once, at startup. Here a small logger prints to the console, so the sample's output stays in order.

A line names the operation and the table, never the values sent: what ARX logs is safe to keep. An exception's message is written the same way, so it can be logged where it is caught.

Usings:

```csharp
using ARX.SDK;
using ARX.SDK.Query;
using ARX.SDK.Services;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
```

Set up by the sample before its scenarios:

```csharp
ILogger logger = new ConsoleLineLogger();
```

#### 1. One line per call

Create, read and query a product: one Information line each, with the table and the time it took.

In scope: `data` (IArxDataService), `logger` (ILogger).

```csharp
using var callLog = new ArxCallLog(logger);

var product = new ArxEntity("product");
product["sku"] = "SKU-D611";
product["name"] = "Trail running shoe";

ArxRecordId id = await data.CreateAsync(product);
await data.RetrieveAsync("product", id);

var query = new ArxQueryExpression("product");
query.Criteria.AddCondition("sku", ArxConditionOperator.Equal, "SKU-D611");
await data.RetrieveMultipleAsync(query);
```

Defined in the sample, used above:

```csharp
private sealed class ArxCallLog : IDisposable
{
    private readonly ActivityListener _listener;

    public ArxCallLog(ILogger logger, bool logSuccessfulCalls = true)
    {
        _listener = new ActivityListener
        {
            ShouldListenTo = source => source.Name == ArxDiagnostics.SourceName,
            Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllData,
            ActivityStopped = activity => Write(logger, activity, logSuccessfulCalls)
        };

        ActivitySource.AddActivityListener(_listener);
    }

    public void Dispose() => _listener.Dispose();

    private static void Write(ILogger logger, Activity activity, bool logSuccessfulCalls)
    {
        var milliseconds = Math.Round(activity.Duration.TotalMilliseconds, 1);

        if (activity.Status == ActivityStatusCode.Error)
        {
            logger.LogWarning("ARX {Call} failed with {Code} after {Milliseconds} ms", activity.DisplayName, activity.GetTagItem("arx.error.code"), milliseconds);
        }
        else if (logSuccessfulCalls)
        {
            logger.LogInformation("ARX {Call} took {Milliseconds} ms", activity.DisplayName, milliseconds);
        }
    }
}
```

Check it in the database:

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

Expect: 1 item, Trail running shoe.

#### 2. A failed call, with its code

A call that fails: the listener logs a Warning with its ARX code; the code that caught it logs the exception.

In scope: `data` (IArxDataService), `logger` (ILogger).

```csharp
using var callLog = new ArxCallLog(logger);

var nobody = new ArxEntity("product") { RecordId = ArxRecordId.Of("sku", "SKU-D699") };
nobody["name"] = "Nobody";

try
{
    await data.UpdateAsync(nobody);
}
catch (ArxException exception) when (exception.Code == ArxErrorCode.RecordNotFound)
{
    logger.LogError(exception, "Renaming product {Sku} failed: {Code}, transient: {IsTransient}",
        "SKU-D699", ArxException.FormatCode(exception.Code), exception.IsTransient);
}
```

Check it in the database:

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

Expect: no items (the update found no product to change).

#### 3. Only what production needs

In production, log failures only: a create that works writes nothing; an update that fails writes its line.

In scope: `data` (IArxDataService), `logger` (ILogger).

```csharp
using var callLog = new ArxCallLog(logger, logSuccessfulCalls: false);

var product = new ArxEntity("product");
product["sku"] = "SKU-D612";
product["name"] = "Hiking boot";
await data.CreateAsync(product);

Console.WriteLine("created Hiking boot: nothing logged");

var gone = new ArxEntity("product") { RecordId = ArxRecordId.Of("sku", "SKU-D698") };
gone["name"] = "Gone";

try
{
    await data.UpdateAsync(gone);
}
catch (ArxException exception) when (exception.Code == ArxErrorCode.RecordNotFound)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

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

Expect: 1 item, Hiking boot.

---

# CreateMultiple with ARX.SDK on Amazon DynamoDB

## What a CreateMultiple does

| Rule | On DynamoDB |
|---|---|
| Many items | CreateMultiple is all or nothing, in one `TransactWriteItems` of up to 100 items; more is ARX8001 unless `new ArxDynamoWriteOptions { AllowPartial = true }` (transactions of 100, one after another) |

## The samples

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

### Create many records in one call

Basics 1 of CreateMultiple.

Many items in one call, in three scenarios: all stored together, none stored when one fails, and more than 100.

Good to know:

Up to 100 items go in one DynamoDB transaction: all or nothing. A transaction can't hold more, so more is refused, unless you allow them to go in transactions of 100, one after another.

Usings:

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

#### 1. All stored together

Three products in one transaction; the ids come back in the list's order.

In scope: `service` (ArxDynamoDbService).

```csharp
IReadOnlyList<ArxRecordId> ids = await service.CreateMultipleAsync(new[]
{
    Product("SKU-D201", "Rain jacket"),
    Product("SKU-D202", "Fleece"),
    Product("SKU-D203", "Beanie")
});

Console.WriteLine($"created {string.Join(", ", ids.Select(x => x.AsString()))}");
```

Defined in the sample, used above:

```csharp
private static ArxEntity Product(string sku, string name)
{
    var product = new ArxEntity("product");
    product["sku"] = sku;
    product["name"] = name;
    return product;
}
```

Check it in the database:

```sql
SELECT "sku", "name" FROM "product" WHERE "sku" IN ['SKU-D201', 'SKU-D202', 'SKU-D203']
```

Expect: 3 items.

#### 2. One fails, none is stored

SKU-D201 exists already: the transaction is cancelled and SKU-D204 isn't stored either.

In scope: `service` (ArxDynamoDbService).

```csharp
try
{
    await service.CreateMultipleAsync(new[] { Product("SKU-D204", "Gloves"), Product("SKU-D201", "Rain jacket again") });
}
catch (ArxDatabaseException exception) when (exception.Code == ArxErrorCode.UniqueViolation)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

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

Expect: no items.

#### 3. More than 100

150 items: refused as one call, stored in transactions of 100 when you allow it.

In scope: `service` (ArxDynamoDbService).

```csharp
var products = Enumerable.Range(1, 150).Select(i => Product($"SKU-D3{i:000}", $"Bulk item {i}")).ToList();

try
{
    await service.CreateMultipleAsync(products);
}
catch (ArxNotSupportedException exception)
{
    Console.WriteLine(exception.Message);
}

var ids = await service.CreateMultipleAsync(products, new ArxDynamoWriteOptions { AllowPartial = true });
Console.WriteLine($"with AllowPartial: {ids.Count} created, in two transactions");
```

Check it in the database:

```sql
SELECT "sku" FROM "product" WHERE "sku" IN ['SKU-D3001', 'SKU-D3150']
```

Expect: 2 items.

### Spread a hot partition with write sharding

Advanced case 1 of CreateMultiple.

Write sharding for a hot partition, in two scenarios: page views of one page spread over four partitions, and read back as one partition, in order.

Good to know:

The model declares it once: `.WriteSharding("page_view", shards: 4, by: "viewed_at")`. ARX adds `#SHARD#n` to the partition key, n from a stable hash of `viewed_at`, so one view is still found by its key; a read of the whole page asks the four shards and merges them by the sort key.

Usings:

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

#### 1. Spread the writes

Twenty views of the home page, written over four shards.

In scope: `service` (ArxDynamoDbService).

```csharp
var views = Enumerable.Range(0, 20).Select(i =>
{
    var view = new ArxEntity("page_view");
    view["page"] = "home-d801";
    view["viewed_at"] = $"2026-11-09T10:00:{i:00}Z";
    view["visitor"] = $"v{i % 7}";
    return view;
}).ToList();

await service.CreateMultipleAsync(views);

Console.WriteLine("20 views written");
```

Check it in the database:

```sql
SELECT "PK" FROM "events" WHERE "PK" IN ['PAGE#home-d801#SHARD#0', 'PAGE#home-d801#SHARD#1', 'PAGE#home-d801#SHARD#2', 'PAGE#home-d801#SHARD#3']
```

Expect: 20 items, under more than one of the four shard keys.

#### 2. Read them back as one

The page's views in time order, a page of 8 at a time: every view once.

In scope: `service` (ArxDynamoDbService).

```csharp
var seen = new List<string>();
string? cookie = null;

do
{
    var query = new ArxQueryExpression("page_view");
    query.Criteria.AddCondition("page", ArxConditionOperator.Equal, "home-d801");
    query.AddOrder("viewed_at", ArxOrderType.Ascending);
    query.PageInfo = new ArxPagingInfo { Count = 8, PagingCookie = cookie };
    var page = await service.RetrieveMultipleAsync(query);
    seen.AddRange(page.Entities.Select(x => (string)x["viewed_at"]!));
    cookie = page.PagingCookie;
}
while (cookie != null);

Console.WriteLine($"read {seen.Count} views, first {seen[0]}, last {seen[^1]}");
```

Check it in the database:

```sql
SELECT "viewed_at" FROM "events" WHERE "page" = 'home-d801'
```

Expect: 20 items.

---

# Update with ARX.SDK on Amazon DynamoDB

## What an update does

| Rule | On DynamoDB |
|---|---|
| What is written | Only the attributes present (`SET`); an attribute set to null is removed (`REMOVE`): DynamoDB keeps no empty attributes |
| Which item | By its full key: the id (with the key's other attributes, such as an order's customer), or `ArxMatch.Columns`/`ArxMatch.Key` naming exactly 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 each is updated by its key, all or nothing, conditioned on still matching (an index can be behind the table). The match columns are never written; RequireSingleRow refuses several (ARX2003) |
| No partition | ARX8001: a write never scans |
| Nothing matched | ARX2002 RecordNotFound: the update is conditional on the item existing, so it never creates one |
| Index keys | When an attribute an index is keyed by changes, give the index key's other attributes too: ARX rewrites the index keys |
| The table's keys | `PK`, `SK`, the index key attributes and `entity_type` are ARX's to write, and can't be matched on (ARX1001) |
| A new key | An entity whose `RecordId` and id attribute differ is ARX1001, unless `new ArxDynamoWriteOptions { AllowKeyChange = true }`: the item is read, changed, put under its new key (ARX3001 if taken) and the old one deleted, in one transaction |
| Concurrency | A version attribute declared in the schema catalog (`.Version("product", "row_version")`): a create writes 1; an update whose entity carries the version it read is written only if the item still has it (ARX2004 RecordChanged otherwise, nothing written) and adds one. Without the version the update isn't checked: the last writer wins, and the version still goes up |
| Names | Placeholders only (`#n0`); expression or SQL syntax in a name is ARX1017 |
| Rows by a condition | Update finds rows by equal values: the id, a declared key, or columns you give values for (`ArxMatch.Columns`). To change every row that meets a condition (`amount > 100`), pass the values and an `ArxFilterExpression` to `UpdateMultipleAsync(values, filter)` (the filter must give a partition key; at most 100 items): one call, all or nothing, from 0.1.0-preview.5 (UpdateMultiple, Basics). Before that version: read the rows with `RetrieveMultipleAsync` and pass them to `UpdateMultipleAsync`. |

## The samples

Every Update 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 one record

Basics 1 of Update.

The smallest update, in three scenarios: by the id Create returned, clearing a value, and an item that isn't there.

Good to know:

Only the attributes you set are written (`SET`); an attribute set to null is removed from the item (`REMOVE`: DynamoDB keeps no empty attributes). The update is conditional on the item existing, so nothing matched is ARX2002 and never creates an item.

Usings:

```csharp
using ARX.SDK;
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-D261";
product["name"] = "Rain jacket";
product["price"] = 89.00m;
product["colour"] = "Navy";
var id = await service.CreateAsync(product);
```

#### 1. By the id Create returned

Give the entity the id CreateAsync returned, and only the attributes to change.

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

```csharp
var change = new ArxEntity("product") { RecordId = id };
change["price"] = 79.00m;

int changed = await service.UpdateAsync(change);

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

Check it in the database:

```sql
SELECT "name", "price" FROM "product" WHERE "sku" = 'SKU-D261'
```

Expect: 1 item, Rain jacket, price 79.

#### 2. Clear a value

Null removes the attribute.

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

```csharp
var change = new ArxEntity("product") { RecordId = id };
change["colour"] = null;

await service.UpdateAsync(change);

Console.WriteLine("colour removed");
```

Check it in the database:

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

Expect: 1 item, with no colour attribute.

#### 3. An item that isn't there

A SKU no item has: ARX2002, and no item is created.

In scope: `service` (ArxDynamoDbService).

```csharp
var change = new ArxEntity("product") { RecordId = ArxRecordId.Of("sku", "SKU-D269") };
change["price"] = 1.00m;

try
{
    await service.UpdateAsync(change);
}
catch (ArxException exception) when (exception.Code == ArxErrorCode.RecordNotFound)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

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

Expect: no items.

### How an update finds its item

Basics 2 of Update.

How an update finds its item in DynamoDB, in four scenarios: an order by its id and its customer, by its key's attributes, every order a query of the customer's partition finds, and a match no partition answers.

Good to know:

DynamoDB writes an item by its full key. An order's key is its customer and its number, so its id alone isn't enough: give the customer too. A match by other columns works when they give a partition key, of the table or an index: ARX queries it, then updates each item found by its key, all or nothing (at most 100), each conditioned on still matching. Without a partition it is ARX8001: a write never scans. When an attribute an index is keyed by changes (the status), give the index key's other attributes too (the date): ARX rewrites the index keys from them.

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-D211";
customer["name"] = "Tailspin Toys";
await service.CreateAsync(customer);
var orders = new[] { "SO-D211", "SO-D212" }.Select(number =>
{
    var order = new ArxEntity("sales_order");
    order["customer_id"] = "C-D211";
    order["order_number"] = number;
    order["status"] = "OPEN";
    order["ordered_on"] = "2026-11-02";
    return order;
}).ToList();
await service.CreateMultipleAsync(orders);
```

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

The id Create returned plus the customer: ship SO-D211 (with its date, so ARX rewrites the GSI1 keys).

In scope: `service` (ArxDynamoDbService).

```csharp
var change = new ArxEntity("sales_order") { RecordId = ArxRecordId.Of("order_number", "SO-D211") };
change["customer_id"] = "C-D211";
change["status"] = "SHIPPED";
change["ordered_on"] = "2026-11-02";

await service.UpdateAsync(change);

Console.WriteLine("SO-D211 shipped");
```

Check it in the database:

```sql
SELECT "status", "GSI1PK" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D211' AND "SK" = 'ORDER#SO-D211'
```

Expect: 1 item, status "SHIPPED", GSI1PK "STATUS#SHIPPED".

#### 2. By the key's attributes

ArxMatch.Columns naming exactly the key's attributes: the same thing, said explicitly.

In scope: `service` (ArxDynamoDbService).

```csharp
var change = new ArxEntity("sales_order");
change["customer_id"] = "C-D211";
change["order_number"] = "SO-D212";
change["status"] = "CANCELLED";
change["ordered_on"] = "2026-11-02";

await service.UpdateAsync(change, new ArxUpdateOptions { Match = ArxMatch.Columns("customer_id", "order_number") });

Console.WriteLine("SO-D212 cancelled");
```

Check it in the database:

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

Expect: 1 item, status "CANCELLED".

#### 3. Through a query

Every order of the customer, through a query of its partition: each found order updated by its key.

In scope: `service` (ArxDynamoDbService).

```csharp
var change = new ArxEntity("sales_order");
change["customer_id"] = "C-D211";
change["priority"] = "high";

int changed = await service.UpdateAsync(change, new ArxUpdateOptions { Match = ArxMatch.Columns("customer_id") });

Console.WriteLine($"{changed} orders marked high priority");
```

Check it in the database:

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

Expect: 2 items, SO-D211 and SO-D212, both priority "high".

#### 4. No partition

Every order of a date: no partition key in the match, so ARX8001 rather than a scan.

In scope: `service` (ArxDynamoDbService).

```csharp
var change = new ArxEntity("sales_order");
change["ordered_on"] = "2026-11-02";
change["priority"] = "low";

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

Check it in the database:

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

Expect: 2 items, both still priority "high".

### Catch an injection attempt

Real case 1 of Update.

A "profile" endpoint whose posted fields become the attributes to change, attacked two ways: an update expression in a field name, and a field that tries to rewrite the item's key; then the same form used honestly.

Good to know:

Names reach DynamoDB only as placeholders (`#n0`), so a name can't add clauses to the update expression: one with expression syntax is ARX1017 before anything is sent. The table's own key attributes (`PK`, `SK`) are ARX's to write, never the caller's.

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-D221";
customer["name"] = "Coho Winery";
customer["tier"] = "standard";
await service.CreateAsync(customer);
```

#### 1. An expression in a field name

"name = :v0, tier" tries to set the tier too: ARX1017.

In scope: `service` (ArxDynamoDbService).

```csharp
try
{
    await UpdateProfileAsync(service, new() { ["name = :v0, tier"] = "gold" });
}
catch (ArxValidationException exception) when (exception.Code == ArxErrorCode.SuspectedInjection)
{
    Console.WriteLine(exception.Message);
}
```

Defined in the sample, used above:

```csharp
private static Task<int> UpdateProfileAsync(ArxDynamoDbService service, Dictionary<string, object> posted)
{
    var change = new ArxEntity("customer") { RecordId = ArxRecordId.Of("customer_id", "C-D221") };

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

    return service.UpdateAsync(change);
}
```

Check it in the database:

```sql
SELECT "tier" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D221' AND "SK" = 'PROFILE'
```

Expect: 1 item, tier "standard".

#### 2. Rewrite the key

A posted "PK" field: the table's key belongs to ARX, so it's refused.

In scope: `service` (ArxDynamoDbService).

```csharp
try
{
    await UpdateProfileAsync(service, new() { ["PK"] = "CUSTOMER#someone-else" });
}
catch (ArxException exception)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

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

Expect: 1 item, still under CUSTOMER#C-D221.

#### 3. The honest request

The same endpoint, used as meant.

In scope: `service` (ArxDynamoDbService).

```csharp
await UpdateProfileAsync(service, new() { ["name"] = "Coho Winery & Vineyard" });

Console.WriteLine("name changed");
```

Check it in the database:

```sql
SELECT "name" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D221' AND "SK" = 'PROFILE'
```

Expect: 1 item, name "Coho Winery & Vineyard".

### Detect a change made since the read

Real case 2 of Update.

Two people edit the same product: optimistic concurrency with a version attribute, in four scenarios: a create starts the version at 1, the first save wins, the second is told the item changed, and an update without the version.

Good to know:

Declare the version attribute in the schema catalog: `.Version("product", "row_version")`. A create writes 1 when the entity gives none. An update or a delete whose entity carries the version it read is written only if the item still has it, and an update adds one; otherwise it is ARX2004 RecordChanged and nothing is written (one round trip: the failed write returns the item, so a missing item is still ARX2002). An update without the version isn't checked: the last writer wins, though the version still goes up. So read the version with the item, and send it back with the change. A move to a new key (AllowKeyChange) checks the version too.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var schema = ArxSchemaCatalog.Create()
    .Table("product", ArxIdColumn.Supplied<string>("sku"))
    .Version("product", "row_version")
    .Build();
var service = new ArxDynamoDbService(dbContext.Client, dbContext.Model, schema);
```

#### 1. A create starts the version at 1

Create a product: ARX writes its version, 1.

In scope: `service` (ArxDynamoDbService).

```csharp
var product = new ArxEntity("product");
product["sku"] = "SKU-D271";
product["name"] = "Trail running shoe";
product["price"] = 129.90m;
await service.CreateAsync(product);

var read = await service.RetrieveAsync("product", "SKU-D271") ?? throw new InvalidOperationException("The product record isn't there.");
Console.WriteLine($"{read["name"]}, version {read["row_version"]}");
```

Check it in the database:

```sql
SELECT "sku", "price", "row_version" FROM "product" WHERE "sku" = 'SKU-D271'
```

Expect: 1 item, price 129.90, row_version 1.

#### 2. The first save wins

Ana and Ben both read version 1; Ana saves first, with the version she read: written, now version 2.

In scope: `service` (ArxDynamoDbService).

```csharp
var ana = await service.RetrieveAsync("product", "SKU-D271") ?? throw new InvalidOperationException("The product record isn't there.");

var change = new ArxEntity("product");
change["sku"] = "SKU-D271";
change["price"] = 119.90m;
change["row_version"] = ana["row_version"];
await service.UpdateAsync(change);

var read = await service.RetrieveAsync("product", "SKU-D271") ?? throw new InvalidOperationException("The product record isn't there.");
Console.WriteLine($"Ana's price {read["price"]}, version {read["row_version"]}");
```

Check it in the database:

```sql
SELECT "price", "row_version" FROM "product" WHERE "sku" = 'SKU-D271'
```

Expect: price 119.90, row_version 2.

#### 3. The second save is told the item changed

Ben saves with the version 1 he read: ARX2004, nothing written; he reads again and saves with version 2.

In scope: `service` (ArxDynamoDbService).

```csharp
var stale = new ArxEntity("product");
stale["sku"] = "SKU-D271";
stale["name"] = "Trail running shoe, wide";
stale["row_version"] = 1;

try
{
    await service.UpdateAsync(stale);
}
catch (ArxException exception) when (exception.Code == ArxErrorCode.RecordChanged)
{
    Console.WriteLine(exception.Message);
}

// Ben reads what is there now, and applies his change to it.
var now = await service.RetrieveAsync("product", "SKU-D271") ?? throw new InvalidOperationException("The product record isn't there.");
var retry = new ArxEntity("product");
retry["sku"] = "SKU-D271";
retry["name"] = "Trail running shoe, wide";
retry["row_version"] = now["row_version"];
await service.UpdateAsync(retry);

var read = await service.RetrieveAsync("product", "SKU-D271") ?? throw new InvalidOperationException("The product record isn't there.");
Console.WriteLine($"{read["name"]}, price {read["price"]}, version {read["row_version"]}");
```

Check it in the database:

```sql
SELECT "name", "price", "row_version" FROM "product" WHERE "sku" = 'SKU-D271'
```

Expect: Trail running shoe, wide; price 119.90 (Ana's, kept); row_version 3.

#### 4. An update without the version: the last writer wins

An update without the version isn't checked: it is written whatever the item's version, which still goes up.

In scope: `service` (ArxDynamoDbService).

```csharp
var blind = new ArxEntity("product");
blind["sku"] = "SKU-D271";
blind["price"] = 99.90m;
await service.UpdateAsync(blind);

var read = await service.RetrieveAsync("product", "SKU-D271") ?? throw new InvalidOperationException("The product record isn't there.");
Console.WriteLine($"price {read["price"]}, version {read["row_version"]}");
```

Check it in the database:

```sql
SELECT "price", "row_version" FROM "product" WHERE "sku" = 'SKU-D271'
```

Expect: price 99.90, row_version 4.

### Update in a batch scope

Advanced case 1 of Update.

Creates and updates in one batch scope, in two scenarios: committed when the body returns, and nothing written when the body throws.

Good to know:

A batch scope collects its writes and sends them as one `TransactWriteItems` when the body returns: all or nothing, up to 100 writes, and one item may be written once per scope. DynamoDB has no savepoints, so a scope opened with the default options has none (asking for them, with `new ArxBatchOptions { Savepoints = true }`, is refused). A read inside the scope doesn't see its own pending writes.

Usings:

```csharp
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-D231";
customer["name"] = "Fabrikam Residences";
customer["open_orders"] = 0;
await service.CreateAsync(customer);
```

#### 1. Committed when the body returns

A new order and the customer's open-order count: both or neither.

In scope: `service` (ArxDynamoDbService).

```csharp
await service.RunBatchAsync(async scope =>
{
    var order = new ArxEntity("sales_order");
    order["customer_id"] = "C-D231";
    order["order_number"] = "SO-D231";
    order["status"] = "OPEN";
    order["ordered_on"] = "2026-11-03";
    await scope.CreateAsync(order);

    var count = new ArxEntity("customer") { RecordId = ArxRecordId.Of("customer_id", "C-D231") };
    count["open_orders"] = 1;
    await scope.UpdateAsync(count);
});

Console.WriteLine("order created and the count updated");
```

Check it in the database:

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

Expect: 2 items: ORDER#SO-D231, and PROFILE with open_orders 1.

#### 2. Nothing written when the body throws

Your own code fails after the writes: none of them is sent.

In scope: `service` (ArxDynamoDbService).

```csharp
try
{
    await service.RunBatchAsync(async scope =>
    {
        var order = new ArxEntity("sales_order");
        order["customer_id"] = "C-D231";
        order["order_number"] = "SO-D232";
        order["status"] = "OPEN";
        order["ordered_on"] = "2026-11-04";
        await scope.CreateAsync(order);

        throw new InvalidOperationException("the stock check failed");
    });
}
catch (InvalidOperationException exception)
{
    Console.WriteLine($"caught: {exception.Message}");
}
```

Check it in the database:

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

Expect: no items.

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

Advanced case 2 of Update.

The service set up from DynamoDB's description of the lead table, in two scenarios: preload at startup, then update by the preloaded key with Strict on.

Good to know:

The preload makes the table's key (`email`) a business key, so `ArxMatch.Key("email")` finds the item; with `Strict`, an attribute the configuration doesn't declare is ARX1018 before DynamoDB.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var lead = new ArxEntity("lead");
lead["email"] = "lead-d251@example.com";
lead["full_name"] = "Nuno Teixeira";
lead["score"] = 40;
await service.CreateAsync(lead);
```

#### 1. Preload at startup

Preload the lead table once, at startup, and make the service Strict.

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

```csharp
var catalog = await new ArxPreLoadTable(new ArxPreLoadTableRegister().PreLoadTable("lead")).LoadAsync(client, model, schema => schema
    .Column("lead", new ArxColumnDefinition("full_name", typeof(string), "S", true))
    .Column("lead", new ArxColumnDefinition("score", typeof(int), "N", true)));

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

return new ArxDynamoDbService(client, model, catalog) { Strict = true };
```

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

#### 2. Update by the preloaded key

Update by the preloaded key; a misspelt attribute is refused (ARX1018), the right one is written.

In scope: `service` (ArxDynamoDbService).

```csharp
var typo = new ArxEntity("lead");
typo["email"] = "lead-d251@example.com";
typo["scroe"] = 90;

try
{
    await service.UpdateAsync(typo, new ArxUpdateOptions { Match = ArxMatch.Key("email") });
}
catch (ArxValidationException exception) when (exception.Code == ArxErrorCode.NotPreloaded)
{
    Console.WriteLine(exception.Message);
}

var change = new ArxEntity("lead");
change["email"] = "lead-d251@example.com";
change["score"] = 90;
await service.UpdateAsync(change, new ArxUpdateOptions { Match = ArxMatch.Key("email") });
Console.WriteLine("score set to 90");
```

Check it in the database:

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

Expect: 1 item, score 90 and no scroe attribute.

### Move an item to a new key

Advanced case 3 of Update.

Renumber an order: a new key for an item, in three scenarios: refused unless asked for, the move, and a move onto a number another order has.

Good to know:

DynamoDB can't change an item's key. An entity whose `RecordId` (the item it finds) and id attribute (the new id) differ is ARX1001, unless `new ArxDynamoWriteOptions { AllowKeyChange = true }`: then ARX reads the item, applies the change, puts it under its new key (keys, type and index keys written again) and deletes the old one, in one transaction. A key that is taken is ARX3001, and nothing moves. A change someone else makes between the read and the move is lost, unless the table has a version attribute (see "Detect a change made since the read"): then it is ARX2004.

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, total) in new[] { ("SO-D261", 120m), ("SO-D269", 45m) })
{
    var order = new ArxEntity("sales_order");
    order["customer_id"] = "C-D261";
    order["order_number"] = number;
    order["status"] = "OPEN";
    order["ordered_on"] = "2026-11-08";
    order["total"] = total;
    await service.CreateAsync(order);
}
```

#### 1. Refused unless asked for

SO-D261 to SO-D262 without the option: ARX1001, nothing changes.

In scope: `service` (ArxDynamoDbService).

```csharp
try
{
    await service.UpdateAsync(Renumber("SO-D261", "SO-D262"));
}
catch (ArxValidationException exception)
{
    Console.WriteLine(exception.Message);
}
```

Defined in the sample, used above:

```csharp
private static ArxEntity Renumber(string from, string to)
{
    var move = new ArxEntity("sales_order") { RecordId = ArxRecordId.Of("order_number", from) };
    move["customer_id"] = "C-D261";
    move["order_number"] = to;
    return move;
}
```

Check it in the database:

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

Expect: 1 item.

#### 2. The move

The move, with a change on the way: SO-D261 becomes SO-D262, its total and index keys kept.

In scope: `service` (ArxDynamoDbService).

```csharp
var move = Renumber("SO-D261", "SO-D262");
move["note"] = "renumbered";

await service.UpdateAsync(move, AllowMove);

Console.WriteLine("SO-D261 is now SO-D262");
```

Defined in the sample, used above:

```csharp
private static readonly ArxDynamoWriteOptions AllowMove = new() { AllowKeyChange = true };

private static ArxEntity Renumber(string from, string to)
{
    var move = new ArxEntity("sales_order") { RecordId = ArxRecordId.Of("order_number", from) };
    move["customer_id"] = "C-D261";
    move["order_number"] = to;
    return move;
}
```

Check it in the database:

```sql
SELECT "SK", "total", "note", "GSI1PK" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D261' AND begins_with("SK", 'ORDER#SO-D26')
```

Expect: 2 items: ORDER#SO-D262 (total 120, note "renumbered", GSI1PK "STATUS#OPEN") and ORDER#SO-D269.

#### 3. Onto a taken number

SO-D262 onto SO-D269, which exists: ARX3001, and both orders stay as they were.

In scope: `service` (ArxDynamoDbService).

```csharp
try
{
    await service.UpdateAsync(Renumber("SO-D262", "SO-D269"), AllowMove);
}
catch (ArxDatabaseException exception) when (exception.Code == ArxErrorCode.UniqueViolation)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

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

Expect: 2 items: ORDER#SO-D262 total 120 and ORDER#SO-D269 total 45.

---

# 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.

---

# 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.

---

# DeleteMultiple with ARX.SDK on Amazon DynamoDB

## What a DeleteMultiple does

| Rule | On DynamoDB |
|---|---|
| Many items | DeleteMultiple sends the deletes in one transaction (up to 100), each conditioned on its item existing; one that isn't is left out and the rest sent again, so the count is exact and the deletes all or nothing (one round trip when all are there) |

## The samples

Every DeleteMultiple 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 many records in one call

Basics 1 of DeleteMultiple.

Many items in one call: three SKUs, one of them unknown. The two found go, in one transaction, and the call returns how many.

Good to know:

The deletes are sent conditioned on each item, without a read first, as one `TransactWriteItems` of at most 100 items (more is ARX8001 unless `AllowPartial`). An id no item has isn't an error; it isn't counted. Delete's "How to find the items" sample deletes many by columns a key or an index gives.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var products = new[] { "SKU-D302", "SKU-D303", "SKU-D304" }.Select(sku =>
{
    var product = new ArxEntity("product");
    product["sku"] = sku;
    product["name"] = $"Clearance item {sku}";
    return product;
}).ToList();
await service.CreateMultipleAsync(products);
```

#### 1. Several ids at once

Three SKUs, one of them unknown: the two found go, in one transaction.

In scope: `service` (ArxDynamoDbService).

```csharp
int deleted = await service.DeleteMultipleAsync("product", new[] { "SKU-D302", "SKU-D303", "SKU-D399" });

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

Check it in the database:

```sql
SELECT "sku" FROM "product" WHERE "sku" IN ['SKU-D302', 'SKU-D303', 'SKU-D304']
```

Expect: 1 item, SKU-D304.

---

# Retrieve with ARX.SDK on Amazon DynamoDB

## What a read does

| Rule | On DynamoDB |
|---|---|
| One item | By its full key (`GetItem`), strongly consistent by default; not found is null. By other columns that give a partition key (of the table or an index): a query, and several items are ARX2003 |
| Lookups | A declared lookup reads back as an `ArxEntityReference` whose `Name` is null: DynamoDB makes no joins, so a `.DisplayName(...)` in the catalog isn't read (read the referenced item yourself where you need its name) |
| Attribute names | Case-sensitive in DynamoDB: an attribute the catalog declares (the id, a key, the version, the display name, a declared column) is stored with its declared spelling whatever the case of the entity's name; an undeclared one by its exact spelling (`Name` and `name` are two attributes) |
| Execute | Not offered: DynamoDB has no stored procedures (ARX8001) |

## Reference

### Value types read back

| Stored as | Read back as |
|---|---|
| S (string) | `string`, or the preloaded type (`Guid`, `DateTime`, `DateOnly`, …) |
| N (number) | `decimal` (a `double` past decimal's range), or the preloaded type |
| BOOL | `bool` |
| B (binary) | `byte[]` |
| M (map) | `ArxJsonB` |
| L (list) | `string[]` or `decimal[]` when every element is that kind; otherwise `ArxJsonB` |
| SS, NS, BS (sets) | `string[]`, a number array, `byte[][]` |
| NULL, or no attribute | null |
| a declared lookup | `ArxEntityReference` |

## The samples

Every Retrieve 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 one record

Basics 1 of Retrieve.

Read one item by its key, in three scenarios: a product by its id, an order by its full key with its customer read back as a lookup, and an id no item has.

Good to know:

One `GetItem`, strongly consistent by default (it sees every write that succeeded before it). An order's key is its customer and its number, so give both. The catalog's lookup reads `customer_id` back as an `ArxEntityReference`. Not found is null.

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-D401";
product["name"] = "Hiking pole";
product["price"] = 34.50m;
await service.CreateAsync(product);
var order = new ArxEntity("sales_order");
order["customer_id"] = "C-D401";
order["order_number"] = "SO-D401";
order["status"] = "OPEN";
order["ordered_on"] = "2026-11-08";
await service.CreateAsync(order);
```

#### 1. By id

A product by its SKU, the key and the id.

In scope: `service` (ArxDynamoDbService).

```csharp
ArxEntity? found = await service.RetrieveAsync("product", "SKU-D401");

Console.WriteLine($"{found!["name"]}, {found["price"]}");
```

Check it in the database:

```sql
SELECT "name", "price" FROM "product" WHERE "sku" = 'SKU-D401'
```

Expect: 1 item, Hiking pole, 34.5.

#### 2. By the full key

An order by its customer and number; its customer_id comes back as a reference.

In scope: `service` (ArxDynamoDbService).

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

ArxEntity? found = await service.RetrieveAsync(match);
var customer = (ArxEntityReference)found!["customer_id"]!;

Console.WriteLine($"{found["order_number"]}: {found["status"]}, customer {customer.EntityName} {customer.RecordId!.AsString()}");
```

Check it in the database:

```sql
SELECT "order_number", "status", "customer_id" FROM "crm" WHERE "PK" = 'CUSTOMER#C-D401' AND "SK" = 'ORDER#SO-D401'
```

Expect: 1 item, SO-D401, OPEN, customer_id "C-D401".

#### 3. An id no item has

A SKU no item has: null.

In scope: `service` (ArxDynamoDbService).

```csharp
ArxEntity? missing = await service.RetrieveAsync("product", "SKU-D499");

Console.WriteLine($"found: {missing != null}");
```

Check it in the database:

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

Expect: no items.

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

Advanced case 1 of Retrieve.

The service set up from DynamoDB's description of the lead table, in two scenarios: preload at startup, then read with Strict on, where only the declared attributes may be asked for.

Good to know:

With `Strict`, the preloaded and declared attributes are an allow list for reads too: asking for one outside it is ARX1018, so a typo is caught rather than read back as a missing value.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var lead = new ArxEntity("lead");
lead["email"] = "lead-d481@example.com";
lead["full_name"] = "Ravi Kumar";
lead["score"] = 65;
await service.CreateAsync(lead);
```

#### 1. Preload at startup

Preload the lead table once, at startup.

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

```csharp
var catalog = await new ArxPreLoadTable(new ArxPreLoadTableRegister().PreLoadTable("lead")).LoadAsync(client, model, schema => schema
    .Column("lead", new ArxColumnDefinition("full_name", typeof(string), "S", true))
    .Column("lead", new ArxColumnDefinition("score", typeof(int), "N", true)));

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

return new ArxDynamoDbService(client, model, catalog) { Strict = true };
```

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

#### 2. Read with Strict on

Read the declared attributes; a misspelt one is ARX1018.

In scope: `service` (ArxDynamoDbService).

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

var found = await service.RetrieveAsync(match, new ArxRetrieveOptions { Match = ArxMatch.Key("email"), Columns = new ArxColumnSet("full_name", "score") });
Console.WriteLine($"{found!["full_name"]}, score {found["score"]}");

try
{
    await service.RetrieveAsync(match, new ArxRetrieveOptions { Match = ArxMatch.Key("email"), Columns = new ArxColumnSet("full_name", "scroe") });
}
catch (ArxValidationException exception) when (exception.Code == ArxErrorCode.NotPreloaded)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

```sql
SELECT "full_name", "score" FROM "lead" WHERE "email" = 'lead-d481@example.com'
```

Expect: 1 item, Ravi Kumar, 65.

---

# 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.

---

# ARX.SDK on Amazon DynamoDB: benchmarks

ARX.SDK's DynamoDB provider against the hand-written low-level client code a developer would write (`PutItemRequest`,
`QueryRequest`, `TransactWriteItemsRequest`: the baseline) and the AWS SDK's object persistence model
(`DynamoDBContext`). BenchmarkDotNet, Release, on the `arx-dynamodb` container (DynamoDB Local 2.5.2, Docker Desktop on
Windows), tables `arx_bench_product`, `arx_bench_crm` (with GSI1) and `arx_bench_events` (write-sharded 4 ways). Reports
in `results/`. Ratios are against the raw client; numbers from one machine, so read the ratios, not the times.

| Operation | ARX against the raw client | Notes |
|---|---|---|
| Create, one item (a conditional put) | 1.00× | `DynamoDBContext`'s save (no condition: it overwrites) 1.01× |
| CreateMultiple, 25 and 100 items, one `TransactWriteItems` | 0.98×, 1.14× (spread ±0.13) | `BatchWriteItem` (not atomic): 1.28× and 3.99× |
| UpdateMultiple, 1, 25 and 100 items, conditional updates in one transaction | 0.97×, 0.97×, 1.01× | – |
| Update through an index (25 orders found by status, then updated by key) | 1.03× | against a Query of GSI1 and a transaction of conditional updates |
| Delete, 1 item; DeleteMultiple, 25 and 100 | 0.95×, 1.00×, 1.07× | before tuning 1.48×, 1.13×, 1.74× |
| Retrieve by key (`GetItem`) | 1.16× (spread ±0.10) | the same code read 0.90× to 1.39× in five runs; `DynamoDBContext` 1.01× to 1.48× |
| RetrieveMultiple, 100 and 1,000 items of one partition | 1.01×, 1.18× | the raw baseline only counts the items; `DynamoDBContext`, which builds objects, 0.94× and 1.16× |
| RetrieveMultiple through GSI1 (200 orders) | 0.98× | – |
| Scan of 1,000 items with a filter (`AllowScan`) | 1.03× | – |
| A write-sharded partition, 100 and 1,000 items over 4 shards, in order | 1.10×, 1.12× | against four parallel queries merged by hand; before tuning 2.26×, 1.88× |
| Create and Retrieve against the Document model (`Table.PutItemAsync` with the same condition, `Table.GetItemAsync`) | 1.01×, 1.11× | the Document model 1.05×, 1.15× (one run, 2026-11-09) |
| Retrieve by many keys, 25 and 100: ARX has no single call, so one RetrieveAsync per key in parallel | 1.42×, 0.69× against `BatchGetItem` | the raw client's parallel GetItem 1.88×, 0.56×; `DynamoDBContext` batch get 1.11×, 1.00×. On DynamoDB Local a `BatchGetItem` of 100 is slower than 100 parallel GetItems, which says nothing about AWS; allocations 8× to 10× (one request per key) |
| A move (`AllowKeyChange`): the consistent read, then a delete and a put in one transaction | 1.14× | against the same GetItem and TransactWriteItems by hand |

What the numbers say: writes are level with the raw client, deletes since the tuning; reads cost the dictionary
entity, level with the AWS SDK's own mapper. ARX allocates more: 1.1× to 1.5× the raw client on reads, 2× to 4× on
writes of 25 to 100 items (each item's key, condition and description kept for the error a failed transaction
reports). Single-item timings swing by ±25% between runs on this machine. DynamoDB Local is not DynamoDB: its
transactions take 65 to 90 ms whatever their size, so absolute times say nothing about AWS.

## Reports

| Class | Report |
|---|---|
| `CreateBenchmarks` | [results/…CreateBenchmarks-report-github.md](#report-createbenchmarks) |
| `CreateMultipleBenchmarks` | [results/…CreateMultipleBenchmarks-report-github.md](#report-createmultiplebenchmarks) |
| `UpdateMultipleBenchmarks` | [results/…UpdateMultipleBenchmarks-report-github.md](#report-updatemultiplebenchmarks) |
| `UpdateByIndexBenchmarks` | [results/…UpdateByIndexBenchmarks-report-github.md](#report-updatebyindexbenchmarks) |
| `DeleteMultipleBenchmarks` | [results/…DeleteMultipleBenchmarks-report-github.md](#report-deletemultiplebenchmarks) |
| `RetrieveBenchmarks` | [results/…RetrieveBenchmarks-report-github.md](#report-retrievebenchmarks) |
| `IndexAndScanBenchmarks` | [results/…IndexAndScanBenchmarks-report-github.md](#report-indexandscanbenchmarks) |
| `ShardedReadBenchmarks` | [results/…ShardedReadBenchmarks-report-github.md](#report-shardedreadbenchmarks) |
| `DocumentModelBenchmarks` | [results/…DocumentModelBenchmarks-report-github.md](#report-documentmodelbenchmarks) |
| `BatchGetBenchmarks` | [results/…BatchGetBenchmarks-report-github.md](#report-batchgetbenchmarks) |
| `MoveBenchmarks` | [results/…MoveBenchmarks-report-github.md](#report-movebenchmarks) |

## Not measured yet

Against AWS itself, nothing: these numbers are DynamoDB Local's. A read by many keys in one call has no ARX API yet (a
condition `id In (…)` gives no partition, ARX8001): the `BatchGetBenchmarks` row is what an application does today.

A caution about this machine: the client runs on Windows and reaches DynamoDB Local through Docker Desktop's port
forwarding, which stalls requests of more than about 8 KB by ~50 ms.
The small move transaction above takes 6 ms; the 65 to 90 ms quoted for larger transactions may be that stall rather
than DynamoDB Local. Not re-measured from inside the Docker network yet.
