Amazon DynamoDB · Work with AI

AI Instructions

Copyable rules for configuring AI coding assistants to use ARX.SDK accurately and consistently.

An instruction file tells a coding assistant, in every conversation, how the project works. These are for a project that uses ARX.SDK 1.0.0-rc.1 with Amazon DynamoDB (ARX.SDK.DynamoDb): they name the provider, send the assistant to the knowledge files before it writes code, allow only the API those files list, and ask it to build and check its work. Each is generated from the same rules as the knowledge base and the skill; copy the one for your tool into the file named above it.

What the instructions require

  • The right provider: the package, service and knowledge files of Amazon DynamoDB, not another database's.
  • Read before writing: the provider's guide and types file, then the operation's file and its closest verified sample.
  • No invented API: only types, constructors, properties and methods the types file lists; anything else is reported as not offered.
  • Verify: build, run the tests, check every ARX call against the types file, and say what could not be checked.
  • Keep them current: when the ARX.SDK version changes, replace the knowledge files or the skill and update the version in the instructions.

Claude Code

CLAUDE.md: Add to the project's CLAUDE.md. With the arx-sdk skill in .claude/skills/arx-sdk, Claude Code loads the skill when it works on ARX.SDK code; this section makes the project's provider and the verification steps explicit.

CLAUDE.md
## Data access: ARX.SDK

This project uses ARX.SDK 1.0.0-rc.1 with Amazon DynamoDB (package `ARX.SDK.DynamoDb`, service `ArxDynamoDbService`).

- Use the `arx-sdk` skill for any code that reads or writes the database. Its references for this provider are in
  `.claude/skills/arx-sdk/references/dynamodb/`: read `arx-dynamodb-guide.md` and `arx-dynamodb-types.md` first.
- Use only the ARX types and members listed in `arx-dynamodb-types.md`. If something isn't there, say ARX doesn't
  offer it; don't invent a name.
- Start from the closest verified sample in the operation's file (`arx-dynamodb-create.md` and the others).
- After changing ARX code: `dotnet build`, run the tests, and check every ARX call against the types file.
- When the ARX.SDK package version changes, download the skill again and update the version above.

GitHub Copilot

.github/copilot-instructions.md: Copilot's repository-wide instructions, read with every request in the repository.

.github/copilot-instructions.md
## Data access: ARX.SDK

This project uses ARX.SDK 1.0.0-rc.1 with Amazon DynamoDB (package `ARX.SDK.DynamoDb`, service `ArxDynamoDbService`). Its knowledge
files, generated from the library and its verified samples, are in `docs/arx-sdk/references/dynamodb/`; the files named
below are there. Read `arx-dynamodb-guide.md` and `arx-dynamodb-types.md` before writing data access code,
and the operation's file (`arx-dynamodb-create.md`, `-create-multiple.md`, `-update.md`, `-update-multiple.md`,
`-delete.md`, `-delete-multiple.md`, `-retrieve.md`, `-retrieve-multiple.md`, `-execute.md`) for its samples.

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.

Before finishing: `dotnet build`, run the tests, and check every ARX call against `arx-dynamodb-types.md`. When the
ARX.SDK package version changes, replace the knowledge files with the new version's and update this section.

.github/instructions/arx-sdk.instructions.md: Applied to C# files only (applyTo). On GitHub.com, path-specific instructions are used by Copilot's cloud agent and code review; editors such as VS Code use them in chat.

.github/instructions/arx-sdk.instructions.md
---
applyTo: "**/*.cs"
---

# ARX.SDK data access

This project uses ARX.SDK 1.0.0-rc.1 with Amazon DynamoDB (package `ARX.SDK.DynamoDb`, service `ArxDynamoDbService`). Its knowledge
files are in `docs/arx-sdk/references/dynamodb/`; the files named below are there.

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.

Check every ARX call against `arx-dynamodb-types.md`, and build before finishing.

Cursor

.cursor/rules/arx-sdk.mdc: A Cursor project rule (.mdc, in .cursor/rules), attached automatically when a C# file is in context. Cursor also reads AGENTS.md.

.cursor/rules/arx-sdk.mdc
---
description: Writing or changing data access code that uses ARX.SDK (ARX.SDK.DynamoDb)
globs: **/*.cs
alwaysApply: false
---

# ARX.SDK data access

This project uses ARX.SDK 1.0.0-rc.1 with Amazon DynamoDB (package `ARX.SDK.DynamoDb`, service `ArxDynamoDbService`). Its knowledge
files are in `docs/arx-sdk/references/dynamodb/`; the files named below are there. Read `arx-dynamodb-guide.md` and
`arx-dynamodb-types.md` before writing data access code.

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.

Check every ARX call against `arx-dynamodb-types.md`, and build before finishing.

General AI coding assistants

AGENTS.md: For agents that read AGENTS.md (Codex, Cursor, GitHub Copilot's coding agent, Gemini CLI and others; Claude Code too when the project has no CLAUDE.md). Keep the arx-sdk folder from the skill package in the repository where the agent can open it.

AGENTS.md
## Data access: ARX.SDK

This project uses ARX.SDK 1.0.0-rc.1 with Amazon DynamoDB (package `ARX.SDK.DynamoDb`, service `ArxDynamoDbService`). Its knowledge
files, generated from the library and its verified samples, are in `docs/arx-sdk/references/dynamodb/`; the files named
below are there. Read `arx-dynamodb-guide.md` and `arx-dynamodb-types.md` before writing data access code,
and the operation's file (`arx-dynamodb-create.md`, `-create-multiple.md`, `-update.md`, `-update-multiple.md`,
`-delete.md`, `-delete-multiple.md`, `-retrieve.md`, `-retrieve-multiple.md`, `-execute.md`) for its samples.

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.

Before finishing: `dotnet build`, run the tests, and check every ARX call against `arx-dynamodb-types.md`. When the
ARX.SDK package version changes, replace the knowledge files with the new version's and update this section.

An assistant's own custom instructions (a chat assistant's project or profile instructions): paste the rules alone.

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.

Where the knowledge files go

  • Claude Code: install the arx-sdk skill in .claude/skills/arx-sdk/; the CLAUDE.md section points at its references.
  • AGENTS.md, GitHub Copilot and Cursor: the templates expect the skill's folder in docs/arx-sdk/ (its references/ hold the knowledge files). Put it elsewhere if you like and change the path in the instructions.
  • Chat assistants: add the knowledge base files to the project or conversation, with the rules above as its instructions.

Keep them current

The instructions name ARX.SDK 1.0.0-rc.1. When the project moves to a newer version, copy them again from this page (they are regenerated with every release of these pages), replace the knowledge files or the skill, and keep any rules of your own in a section of your own so the next update is a plain replacement.