# ARX.SDK for MongoDB: 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 MongoDB: 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 MongoDB. 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 MongoDB.

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
MongoDB provider, `ARX.SDK.MongoDb`, built on the MongoDB C# driver.

## Instructions for the assistant

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

1. **Use ARX's types for database work.** Create, update, delete and read documents, and run transactions through
   `ArxMongoDbService`, `ArxMongoConnection` and `ArxTransaction`. Don't write driver code (`IMongoCollection`,
   `BsonDocument`, `FilterDefinition`) for work ARX does.
2. **Don't invent API.** Use only the types, methods, properties and parameters listed in `arx-mongodb-types.md`. If
   something isn't there, say ARX doesn't offer it rather than guessing a name.
3. **A document is an `ArxEntity`.** `var account = new ArxEntity("account");` then values by field name:
   `account["name"] = "Northwind Traders";`. The entity's name is the collection. Field names keep their case.
4. **Declare every collection's id once, at startup,** in an `ArxSchemaCatalog`, and give it to the service:
   `ArxSchemaCatalog.Create().Table("account", ArxIdColumn.Generated<string>("account_id")).Build()`, then
   `new ArxMongoDbService(connectionString, schema)`; the connection string names the database
   (`mongodb://host:27017/crm`). The declared id is the document's `_id`: a generated string id is an ObjectId ARX makes
   (returned as its 24-character text), a generated Guid one ARX makes, `ArxIdColumn.Supplied<string>("code")` an id the
   application gives. A generated number id is ARX8001 (no sequences) and `NoIdColumn` is ARX1001 (every document has
   an `_id`). Add business keys with `.Key("account", "account_number")` and back them with unique indexes.
5. **Lookups are declared**, since MongoDB has no foreign keys: `.Lookup("contact", "account_id", "account")`. Write one
   with `new ArxEntityReference("account", id)`; it reads back as an `ArxEntityReference` with the account's display
   name (`.DisplayName("account", "name")`).
6. **Every call is asynchronous** (`CreateAsync`, `CreateMultipleAsync`, `UpdateAsync`, `UpdateMultipleAsync`,
   `DeleteAsync`, `DeleteMultipleAsync`, `RetrieveAsync`, `RetrieveMultipleAsync`); await it, and pass the caller's
   `CancellationToken`. Make one service for the application: it shares one MongoDB client per connection string.
7. **Writes of several documents are all or nothing** (CreateMultiple, UpdateMultiple, DeleteMultiple with checks), in
   a transaction: MongoDB needs a replica set or a sharded cluster for that (one replica set node is enough). On a
   standalone server they are ARX8001.
8. **Update and Delete find documents by the id, a declared key, or fields you name:**
   `new ArxUpdateOptions { Match = ArxMatch.Key("account_number") }`, `ArxMatch.Columns(...)`. An update writes only the
   attributes that are set (`$set`); null stores null. A document's `_id` can't change (ARX8001).
   **To change or delete every document a condition matches** (from 0.1.0-preview.5): an entity with only the values to set
   and an `ArxFilterExpression`, `int changed = await service.UpdateMultipleAsync(values, filter);`, or
   `await service.DeleteMultipleAsync("account", filter)`; one `updateMany` or `deleteMany` in a transaction (a replica set), all or nothing, returning the count. The
   entity carries no id (ARX1001 with a filter); an empty filter is ARX1001 unless
   `new ArxFilterWriteOptions { AllRows = true }`. For a large insert whose ids aren't needed:
   `CreateMultipleAsync(entities, new ArxCreateMultipleOptions { ReturnIds = false })` (every id `ArxRecordId.None`).
9. **Reads.** `RetrieveAsync` returns `null` when nothing matches. `RetrieveMultipleAsync` takes an
   `ArxQueryExpression` (criteria, columns, order, paging, joins as link entities, aggregates) and returns the
   documents in `.Entities`. `Null` matches a stored null and a missing field; NotEqual never matches either. Page by
   `PageInfo.PageNumber`, or pass each page's `PagingCookie` to the next query. Execute is ARX8001: MongoDB has no
   stored procedures.
   **Query cheat sheet** (the exact names; there are no others, such as `AddGroupBy` or `MoreRecords`):
   `var query = new ArxQueryExpression("book");` · 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);`
   · a join: `var author = query.AddLink("author", "author_id", "id", ArxJoinOperator.Inner); author.EntityAlias = "a";
   author.ColumnSet = new ArxColumnSet("name");`, read as `row["a.name"]` · paging:
   `query.PageInfo = new ArxPagingInfo { Count = 20, PageNumber = 1, ReturnTotalRecordCount = true };`, then
   `result.Entities`, `result.HasNextPage`, `result.TotalRecordCount` (or page by `result.PagingCookie`) · grouped counts:
   `query.AggregateInfo = new ArxAggregateInfo(); query.AggregateInfo.AddAggregateColumn("books", "id", ArxAggregateOperator.Count);
   query.AggregateInfo.AddGroupByColumn("author_id");` (Count reads back as a `long`). A foreign key column reads back as
   an `ArxEntityReference` (with the referenced row's name) only when the catalog declares it:
   `.Lookup("book", "author_id", "author")` and `.DisplayName("author", "name")`; otherwise it is the plain id.
10. **Values are never run as queries:** pass them as .NET values. A name with `$` or SQL syntax is ARX1017, so names
    that carry operators are refused, but a valid name is used as given and reaches any collection or field the database
    user can read: map a name from a user to one the application allows (an allow list), or preload the schema and set
    `Strict = true`. Search text is escaped.
11. **Dates:** a BSON date holds milliseconds, in UTC: a `DateTime` is stored as UTC (Local converted, Unspecified taken
    as UTC), a `DateTimeOffset` as its instant (the offset isn't kept), a `DateOnly` as midnight UTC. Never
    `default(DateTime)` (ARX1013: use `null`).
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. `arx-mongodb-error-codes.md` lists every code. Log `error.Message`, not the driver's:
    MongoDB's duplicate key message quotes the value.
13. **Transactions:** `await using var connection = new ArxMongoConnection(service);`,
    `await using var transaction = await connection.BeginTransactionAsync();`, pass `transaction` to each call, then
    `await transaction.CommitAsync();`. Disposing without committing rolls back. Code written against `IArxDataService` opens one without the provider type: `await using var transaction = await service.BeginTransactionAsync();` (from 0.1.0-preview.5; the transaction owns its connection). MongoDB has no savepoints: a call that
    fails at the database aborts the transaction, and 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`; `ArxMongoDbService` and `ArxMongoConnection` in `ARX.SDK.MongoDb.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`.
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`). A linked table's column reads back as `row["alias.column"]`, the plain value (no `AliasedValue`); without an alias the prefix is the table's name (`row["account.name"]`). Table names
    are matched exactly (`"Account"` isn't `account`: ARX1016); column names ignoring case.

## How to use these files

- **One file:** `arx-mongodb-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: MongoDB 7.0, a replica set (tested with 7.0.45, one node); driver: MongoDB.Driver 3.13.0 or later (the lowest version, which the provider is built on; Versions and compatibility lists the versions it is tested on).
- `ARX.SDK.MongoDb` 1.0.0-rc.1: The MongoDB provider. It brings ARX.SDK and MongoDB.Driver 3.13.0 with it. Install: `dotnet add package ARX.SDK.MongoDb --version 1.0.0-rc.1`.
- The samples read the connection string from the `ARX_SAMPLES_MONGODB` environment variable, for example `mongodb://localhost:27017/arx_samples?replicaSet=rs0`. Never put real credentials in code.

## Quick reference

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

### Create: Create one record and get its id, Id in the schema

In scope: `connectionString` (string).

```csharp
// Declare each collection's id once, at startup. A generated string id is an ObjectId.
var schema = ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<string>("account_id"))
    .Build();

var service = new ArxMongoDbService(connectionString, schema);

var entity = new ArxEntity("account");
entity["name"] = "Northwind Traders";
entity["account_number"] = "ACC-M001";
entity["employee_count"] = 240;

// CreateAsync returns the id of the new document: the ObjectId's 24 hexadecimal characters.
ArxRecordId id = await service.CreateAsync(entity);

Console.WriteLine($"created account {id.AsString()}");
```

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

In scope: `service` (ArxMongoDbService).

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

foreach (var (name, number) in new[] { ("Alpine Ski House", "ACC-M101"), ("Blue Yonder Airlines", "ACC-M102"), ("Coho Winery", "ACC-M103") })
{
    var account = new ArxEntity("account");
    account["name"] = name;
    account["account_number"] = number;
    accounts.Add(account);
}

IReadOnlyList<ArxRecordId> ids = await service.CreateMultipleAsync(accounts);

Console.WriteLine($"created {ids.Count} accounts, the first {ids[0].AsString()}");
```

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

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

```csharp
var change = new ArxEntity("account") { RecordId = id };
change["employee_count"] = 55;

int changed = await service.UpdateAsync(change);

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

### UpdateMultiple: Update many records in one call, A value per document

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

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

for (var i = 0; i < ids.Count; i++)
{
    var change = new ArxEntity("account") { RecordId = ids[i] };
    change["employee_count"] = 100 + i;
    changes.Add(change);
}

int changed = await service.UpdateMultipleAsync(changes);

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

### Delete: Delete one record by id, One document

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

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

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

### DeleteMultiple: Delete many records in one call, By ids

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

```csharp
int deleted = await service.DeleteMultipleAsync("account", ids);

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

### Retrieve: Read one record, By id

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

```csharp
ArxEntity? account = await service.RetrieveAsync("account", id);

Console.WriteLine($"{account?["name"]}, {account?["employee_count"]} employees, id {account?["account_id"]}");
```

### RetrieveMultiple: Read many records, Comparisons and an order

In scope: `service` (ArxMongoDbService).

```csharp
var query = Segment();
query.Criteria.AddCondition("score", ArxConditionOperator.GreaterEqual, 60);
query.AddOrder("score", ArxOrderType.Descending);

var page = await service.RetrieveMultipleAsync(query);

Console.WriteLine(string.Join(", ", page.Entities.Select(x => $"{x["first_name"]} {x["score"]}")));
```

Defined in the sample, used above:

```csharp
private static ArxQueryExpression Segment()
{
    var query = new ArxQueryExpression("contact");
    query.ColumnSet.AddColumns("first_name", "last_name", "score");
    query.Criteria.AddCondition("segment", ArxConditionOperator.Equal, "M96");
    return query;
}
```

## The other files

- `arx-mongodb-types.md`: Every public type: its declaration, constructors, properties, methods and their parameters, with what each does.
- `arx-mongodb-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-mongodb-data-types.md`: What each .NET type is stored as in MongoDB, the date and time rules, and what each MongoDB type reads back as.
- `arx-mongodb-error-codes.md`: Every ARX error code: what it means, whether it is safe to retry, and the MongoDB errors it comes from.
- `arx-mongodb-create.md`: What a create does, its rules, and every Create sample: each scenario's code and the mongosh query that checks it.
- `arx-mongodb-create-multiple.md`: What a CreateMultiple does, its rules, and every CreateMultiple sample: each scenario's code and the mongosh query that checks it.
- `arx-mongodb-update.md`: What an update does, its rules, and every Update sample: each scenario's code and the mongosh query that checks it.
- `arx-mongodb-update-multiple.md`: What an UpdateMultiple does, its rules, and every UpdateMultiple sample: each scenario's code and the mongosh query that checks it.
- `arx-mongodb-delete.md`: What a delete does, its rules, and every Delete sample: each scenario's code and the mongosh query that checks it.
- `arx-mongodb-delete-multiple.md`: What a DeleteMultiple does, its rules, and every DeleteMultiple sample: each scenario's code and the mongosh query that checks it.
- `arx-mongodb-retrieve.md`: What a retrieve does, its rules, and every Retrieve sample: each scenario's code and the mongosh query that checks it.
- `arx-mongodb-retrieve-multiple.md`: What a RetrieveMultiple does, its rules, and every RetrieveMultiple sample: each scenario's code and the mongosh query that checks it.
- `arx-mongodb-benchmarks.md`: How ARX compares with the MongoDB driver used directly, measured: the only source for performance claims.
- `arx-mongodb-all.md`: everything in one file.

---

# ARX.SDK types (MongoDB)

Every public type of ARX.SDK and ARX.SDK.MongoDb, 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.

### ArxMongoDbService

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

```csharp
public class ArxMongoDbService
```

The MongoDB service: Create, Update, Delete, Retrieve and RetrieveMultiple against one database, a collection per entity. Give it the connection string (naming the database: `mongodb://…/crm`) and, usually, the application's schema catalog. It shares one `MongoClient` per connection string (the driver's advice: a client is a pool, made once), so one instance serves the whole application.

The table's declared id column is the document's `_id`; the entity names it by the declared column (`entity["account_id"]`). Field names keep their case (MongoDB compares them exactly). Writes of several documents run in a transaction, which needs a replica set or a sharded cluster: on a standalone server they are refused (ARX8001) rather than half done.

Constructors:

- `new ArxMongoDbService(string connectionString)`: A service for the database the connection string names, with no schema catalog.
  - `connectionString`: A MongoDB connection string naming the database: `mongodb://user:password@host:27017/crm?authSource=admin`.
- `new ArxMongoDbService(string connectionString, ArxSchemaCatalog schema)`: A service with the application's schema catalog: the usual way.
  - `connectionString`: A MongoDB connection string naming the database.
  - `schema`: The developer's schema catalog: every collection's id column (the document's `_id`) and keys, lookups, display names, and the selection sets that `ArxSelectionSetValue` attributes are checked against. ARX never reads it from the database by itself.
- `new ArxMongoDbService(string connectionString, string database, ArxSchemaCatalog schema)`: A service for `database`, which overrides the database the connection string names.
  - `connectionString`: A MongoDB connection string.
  - `database`: The database; null for the one the connection string names.
  - `schema`: The developer's schema catalog; null for none.
- `new ArxMongoDbService(IMongoClient client, string database, ArxSchemaCatalog? schema = null)`: A service on a client the application made and owns (dependency injection: register the `IMongoClient` as a singleton, as the driver advises).
  - `client`: The client; ARX never disposes it.
  - `database`: The database.
  - `schema`: The developer's schema catalog; null for none.

Properties:

- `string? ConnectionString { get; }`: The connection string; null when the service was given a client.
- `string DatabaseName { get; }`: The database every call reads and writes.
- `ArxSchemaCatalog Schema { get; }`: The developer's schema catalog; null when none was supplied.
- `IArxSelectionSetCatalog SelectionSetCatalog { get; }`: The catalog `ArxSelectionSetValue` attributes are checked against; null when none was supplied.
- `IArxProviderCapabilities Capabilities { get; }`: What MongoDB supports through ARX: joins (`$lookup`, inner and left), aggregates (`$group`), paging by page number with the total and by cookie, transactions and the multiple-document writes (on a replica set or sharded cluster), full-text search (`$text`), pattern conditions and Or filters. Not Execute (MongoDB has no stored procedures: ARX8001), not upsert, not named indexes.
- `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.
- `bool Strict { get; set; }`: False (the default): a preloaded schema is what ARX knows ahead of time; collections and fields it doesn't list still work. True: the preloaded fields are an allow list, and a call that names a collection or a field the schema doesn't preload is refused before the database with ARX1018 NotPreloaded. Reading every field reads what the documents hold, as without Strict.

Methods:

- `Task<IArxBatchScope> BeginBatchAsync(ArxBatchOptions? options = null, CancellationToken cancellationToken = default)`: Opens a batch scope: one MongoDB transaction across many calls, committed by `CommitAsync`; disposing it without a commit rolls it back. MongoDB has no savepoints, so a failed call rolls the whole scope back; asking for savepoints (`Savepoints = true`) is ARX8001. Needs a replica set or a sharded cluster.
  - `options`: The scope's options; null for the defaults (no savepoints).
  - `cancellationToken`: Cancels the call.
- `Task<ArxTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default)`: Begins a transaction on a connection of its own (a new `ArxMongoConnection`), 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<ArxExecuteResult> ExecuteAsync(ArxProcedureExpression procedure, ArxExecuteOptions? options = null, CancellationToken cancellationToken = default)`: Not offered: MongoDB has no stored procedures or functions that return rows. Always ARX8001; read with RetrieveMultiple (aggregates and joins included), and keep server-side logic in the application.
  - `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<ArxExecuteResult> ExecuteAsync(ArxProcedureExpression procedure, ArxTransaction transaction, ArxExecuteOptions? options = null, CancellationToken cancellationToken = default)`: As `ExecuteAsync`: always ARX8001.
  - `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<ArxRecordId> CreateAsync(ArxEntity entity, CancellationToken cancellationToken = default)`: Inserts one document 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<ArxRecordId> CreateAsync(ArxEntity entity, ArxTransaction transaction, CancellationToken cancellationToken = default)`: Inserts one document in an ARX transaction (`ArxMongoConnection`), as `CreateAsync`.
  - `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 one id per entity in list order, also set on each entity's RecordId. The documents go in ordered `insertMany` batches inside a transaction of ARX's own, which needs a replica set: on a standalone server more than one entity is ARX8001 (see `AllowPartial`). The same entity twice is ARX1001. An empty list returns no ids without touching the database.
  - `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, ArxMongoWriteOptions 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 a standalone server (which has no transactions) write the documents in order, without 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<IReadOnlyList<ArxRecordId>> CreateMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxSchemaCatalog schema, CancellationToken cancellationToken = default)`: As `CreateMultipleAsync`, with the tables declared for this call: `schema` overrides the service's schema per table (selection sets by name).
  - `entities`: The records to create, all or nothing; their ids come back in the same order.
  - `schema`: Declarations for this call only: they replace the service's for the tables they declare.
  - `cancellationToken`: Cancels the call.
- `Task<IReadOnlyList<ArxRecordId>> CreateMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxSchemaCatalog schema, ArxTransaction transaction, CancellationToken cancellationToken = default)`: The call-schema form in an ARX transaction.
  - `entities`: The records to create, all or nothing; their ids come back in the same order.
  - `schema`: Declarations for this call only: they replace the service's for the tables they declare.
  - `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<bool> DeleteAsync(string entityName, ArxRecordId id, CancellationToken cancellationToken = default)`: Deletes the document with this id (the one CreateAsync returned); true when one was deleted.
  - `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.
  - `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 documents `entity` identifies (by id by default, or `Match`) and returns how many went. None is a normal outcome (0), unless `RequireSingleRow`: then none is ARX2002 and several ARX2003, and nothing is deleted.
  - `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<TId>(string entityName, IReadOnlyCollection<TId> ids, ArxTransaction transaction, CancellationToken cancellationToken = default)`: Deletes the records with these ids, all or nothing, and returns how many existed, inside an ARX transaction. 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.
  - `transaction`: The ARX transaction to run in. ARX doesn't commit or roll it back.
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxDeleteOptions? options = null, CancellationToken cancellationToken = default)`: Deletes the documents the entities match and returns how many went, all or nothing (in a transaction of ARX's own on a replica set). By id: one `deleteMany` per collection. With RequireSingleRow every entity must match exactly one (ARX2002, ARX2003, nothing deleted). The same entity or match values twice is ARX1001.
  - `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, ArxSchemaCatalog schema, ArxDeleteOptions? options = null, CancellationToken cancellationToken = default)`: With the tables declared for this call.
  - `entities`: The records to delete, all or nothing, each found by its id or by the match's columns.
  - `schema`: The schema catalog: each table's id, keys, lookups and columns, and the selection sets. Null for none.
  - `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)`: 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.
- `Task<int> DeleteMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxSchemaCatalog schema, ArxTransaction transaction, ArxDeleteOptions? options = null, CancellationToken cancellationToken = default)`: The call-schema form in an ARX transaction.
  - `entities`: The records to delete, all or nothing, each found by its id or by the match's columns.
  - `schema`: The schema catalog: each table's id, keys, lookups and columns, and the selection sets. Null for none.
  - `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(ArxEntity values, ArxFilterExpression filter, ArxFilterWriteOptions? options = null, CancellationToken cancellationToken = default)`: Sets `values`' attributes on every document of its collection that `filter` matches, in one `updateMany` with `$set`, and returns how many matched.
  - `values`: The collection (EntityName) and the attributes to set: null stores null.
  - `filter`: The documents to change: conditions on the collection's own fields.
  - `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. A database error aborts the transaction.
  - `values`: The collection (EntityName) and the attributes to set: null stores null.
  - `filter`: The documents to change: conditions on the collection's own fields.
  - `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 document of the collection that `filter` matches, in one `deleteMany`, and returns how many went. The filter's rules are the update's: the collection's own fields, and an empty filter is ARX1001 unless `AllRows` is set.
  - `entityName`: The collection to delete from.
  - `filter`: The documents to delete: conditions on the collection's own fields.
  - `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. A database error aborts the transaction.
  - `entityName`: The collection to delete from.
  - `filter`: The documents to delete: conditions on the collection's own fields.
  - `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 documents, all or nothing. MongoDB's ids are ARX's to make (or supplied), so the call is the same `insertMany` 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. A database error aborts the transaction.
  - `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 document 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(string entityName, ArxRecordId id, ArxTransaction transaction, ArxColumnSet? columns = null, CancellationToken cancellationToken = default)`: By id in an ARX transaction.
  - `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, ArxRetrieveOptions? options = null, CancellationToken cancellationToken = default)`: The one document `match` identifies: by its id by default, or by a declared key or any fields (`Match`). Null when none matches; ARX2003 when several do (a Retrieve answers one record).
  - `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<ArxEntity?> RetrieveAsync(ArxEntity match, ArxTransaction transaction, ArxRetrieveOptions? options = null, CancellationToken cancellationToken = default)`: As `RetrieveAsync`, in an ARX transaction.
  - `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, ArxRetrieveOptions? options = null, CancellationToken cancellationToken = default)`: Runs a query as one aggregation pipeline and returns its documents: filters (equality, comparisons, In, Between, null checks where null also means a missing field, patterns as escaped regular expressions), joins as `$lookup` (inner and left), aggregates as `$group`, order on any fields, and paging by page number (with the total in the same round trip, `$facet`) or by the paging cookie each page returns (a keyset on the order and the id: no documents skipped on the server).
  - `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, ArxTransaction transaction, ArxRetrieveOptions? options = null, CancellationToken cancellationToken = default)`: As `RetrieveMultipleAsync`, in an ARX transaction.
  - `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 ValidateSchemaAsync(CancellationToken cancellationToken = default)`: Compares every collection in the schema catalog with the database: the collection exists (MongoDB makes one on the first insert, so a missing one also means its indexes are missing); a stored document's `_id` has the declared id's type (an ObjectId for a generated string id); and every declared key has a unique index on exactly its fields. ARX never calls this by itself: run it at startup or in a health check. It reads only, and reports every difference at once (ARX5003 SchemaMismatch).
  - `cancellationToken`: Cancels the call.
- `Task<ArxServerCheck> CheckServerAsync(CancellationToken cancellationToken = default)`: What the connected server allows: its version, and the provider's capabilities on it. A standalone server runs no transactions, so it removes `Transactions` and `BulkWrite` (writes of several documents, all or nothing); a replica set or a sharded cluster has both. Call it once at startup.
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateAsync(ArxEntity entity, ArxUpdateOptions? options = null, CancellationToken cancellationToken = default)`: Updates the documents `entity` identifies and returns how many matched.
  - `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.
  - `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, ArxUpdateOptions? options = null, CancellationToken cancellationToken = default)`: Updates every entity, all or nothing, and returns the documents matched. If any entity matches no document, nothing changes and the call throws ARX2002 with how many were missing. The same entity twice, or the same match values twice, is ARX1001. Matches by id go in one ordered `bulkWrite` per collection; matches by a key or fields one `updateMany` each, so each entity's count is known. All in a transaction of ARX's own (a replica set: on a standalone server ARX8001). An empty list returns 0 without touching the database.
  - `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, ArxSchemaCatalog schema, ArxUpdateOptions? options = null, CancellationToken cancellationToken = default)`: As `UpdateMultipleAsync`, with the tables declared for this call.
  - `entities`: The changes, all or nothing: each carries the attributes to write and what finds its row.
  - `schema`: The schema catalog: each table's id, keys, lookups and columns, and the selection sets. Null for none.
  - `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<int> UpdateMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxSchemaCatalog schema, ArxTransaction transaction, ArxUpdateOptions? options = null, CancellationToken cancellationToken = default)`: The call-schema form in an ARX transaction.
  - `entities`: The changes, all or nothing: each carries the attributes to write and what finds its row.
  - `schema`: The schema catalog: each table's id, keys, lookups and columns, and the selection sets. Null for none.
  - `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.

### ArxMongoConnection

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

```csharp
public sealed class ArxMongoConnection : ArxConnection
```

A MongoDB connection for ARX transactions. Application code begins a transaction on it and passes the transaction to `ArxMongoDbService` calls; it never needs a driver type.

It holds a driver session on the service's client. Transactions need a replica set or a sharded cluster: on a standalone server `BeginTransactionAsync` is ARX8001. A transaction reads a snapshot and commits with write concern `majority`. MongoDB has no savepoints, and a failed call aborts the whole transaction on the server, so a failure makes it `IsAborted` (later calls are ARX1015). One transaction at a time, one call at a time; not thread-safe. MongoDB ends a transaction that stays open longer than `transactionLifetimeLimitSeconds` (60 s by default).

Constructors:

- `new ArxMongoConnection(ArxMongoDbService service)`: A connection on the service's client and database.
  - `service`: The service whose calls will take the transaction.
- `new ArxMongoConnection(string connectionString)`: A connection for the database a connection string names (a service is made for it).
  - `connectionString`: A MongoDB connection string naming the database.

### ArxMongoTransaction

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

```csharp
public sealed class ArxMongoTransaction : ArxTransaction
```

A MongoDB transaction begun by `BeginTransactionAsync`. Pass it to `ArxMongoDbService` calls; the state rules are those of `ArxTransaction`.

### 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 ArxMongoDbService(connectionString, 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.

### ArxMongoPreLoad

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

```csharp
public static class ArxMongoPreLoad
```

Loads an `ArxPreLoadTable` from a MongoDB database.

Methods:

- `static Task<ArxSchemaCatalog> LoadAsync(this ArxPreLoadTable preload, string connectionString, Action<ArxSchemaCatalogBuilder>? configure = null, CancellationToken cancellationToken = default)`: Reads the requested collections once (at startup) and returns the schema catalog for `new ArxMongoDbService(connectionString, catalog)`: each collection's id (the `_id`, named "_id": an ObjectId one is a generated string, anything else supplied, by the `$jsonSchema` validator or one stored document), its unique indexes as keys and, in `DataTypes`, the field types its `$jsonSchema` validator declares. MongoDB has no foreign keys, so no lookups or referenced collections are loaded: declare lookups in `configure`.
  - `preload`: The tables to read, and how much of each (ArxPreLoadMode).
  - `connectionString`: The database to read them from; used for this call only.
  - `configure`: Adds what the database can't say: lookups, display names, selection sets, hand-declared collections.
  - `cancellationToken`: Cancels the reads.
- `static Task<ArxSchemaCatalog> LoadAsync(this ArxPreLoadTable preload, string connectionString, string database, Action<ArxSchemaCatalogBuilder>? configure = null, CancellationToken cancellationToken = default)`: As `LoadAsync`, for a named database.
  - `preload`: The tables to read.
  - `connectionString`: The database's connection string.
  - `database`: The database, when the connection string doesn't name it.
  - `configure`: Adds or changes declarations after the read (columns, lookups, versions); null for none.
  - `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<ArxMongoDbService>>(sp => ArxRouter.Create<ArxMongoDbService>(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.

## MongoDB's own types

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

### ArxMongoWriteOptions

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

```csharp
public sealed class ArxMongoWriteOptions
```

Options for MongoDB writes of several documents.

Properties:

- `bool AllowPartial { get; init; }`: False (the default): a write of several documents is all or nothing, in a transaction, and on a standalone server (no transactions) it is refused with ARX8001. True: on a standalone server the documents are written in order without a transaction, and a failure leaves the ones before it stored. On a replica set the write is still all or nothing.

## 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 ArxMongoConnection(connectionString)`. 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.

### 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 ArxMongoDbService(connectionString, 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 MongoDB

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.MongoDb;

/// <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 MongoDB driver 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.MongoDb;

/// <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 ARX service and its schema catalog, created once and shared by the samples.

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

namespace ArxSamples.MongoDb;

/// <summary>The application's ARX service and its schema catalog, shared by the real and advanced samples.</summary>
public sealed partial class SampleContext
{
    private ArxSchemaCatalog? _catalog;
    private ArxMongoDbService? _service;

    /// <summary>The application's schema catalog: collection ids, keys, lookups and selection sets (SampleCatalog.cs).</summary>
    public ArxSchemaCatalog Catalog => _catalog ??= SampleCatalog.Build();

    /// <summary>
    /// One service for the whole application. It shares one MongoDB client per connection string (a client is a
    /// connection pool), so make it once and keep it.
    /// </summary>
    public ArxMongoDbService Service => _service ??= new ArxMongoDbService(ConnectionString, Catalog);
}
```

## SampleCatalog.cs

The application's schema catalog: every table's id column and keys, and its selection sets.

```csharp
using ARX.SDK.Services;

namespace ArxSamples.MongoDb;

/// <summary>An account's industry. Stored in account.industry as the enum's number.</summary>
public enum Industry
{
    Retail = 1,
    Manufacturing = 2,
    Healthcare = 3,
    Finance = 4,
    Technology = 5
}

/// <summary>
/// The application's schema catalog: every collection's id and keys, the lookups between collections, and every
/// picklist (selection set), defined once.
/// </summary>
/// <remarks>
/// <para>
/// <b>Ids.</b> On MongoDB the declared id is the document's <c>_id</c>, named in your code by the column you declare
/// (<c>account["account_id"]</c>). A generated string id is an ObjectId ARX makes before the insert and gives back as its
/// 24-character text; a generated Guid is one ARX makes; a supplied id is yours (a country code, a SKU). MongoDB has no
/// sequences, so a generated number isn't offered, and every document has an <c>_id</c>, so there is no NoIdColumn.
/// </para>
/// <para>
/// <b>Keys.</b> Business keys (an account number; a region and a SKU together) let Update, Delete and Retrieve find
/// documents by them. MongoDB enforces them with unique indexes: the sample database has one for each.
/// </para>
/// <para>
/// <b>Lookups.</b> MongoDB has no foreign keys, so the catalog says which field refers to which collection
/// (<c>.Lookup("contact", "account_id", "account")</c>). ARX stores the referenced id as it is stored there (an ObjectId)
/// and reads the field back as an <see cref="ArxEntityReference"/> with the referenced record's display name.
/// </para>
/// <para>
/// <b>Selection sets.</b> A field that only accepts a fixed list of codes: an account's status, a contact's preferred
/// channels (several codes, stored as an array). ARX checks every <see cref="ArxSelectionSetValue"/> against the catalog
/// before anything is sent and stores the codes themselves.
/// </para>
/// </remarks>
public static class SampleCatalog
{
    public static ArxSchemaCatalog Build() => ArxSchemaCatalog.Create()
        .Table("account", ArxIdColumn.Generated<string>("account_id"))
        .Key("account", "account_number")
        .DisplayName("account", "name")
        .Table("contact", ArxIdColumn.Generated<string>("contact_id"))
        .Key("contact", "email")
        .Lookup("contact", "account_id", "account")
        .DisplayName("contact", new[] { "first_name", "last_name" }, " ")
        .Table("country", ArxIdColumn.Supplied<string>("code"))
        .DisplayName("country", "name")
        .Table("product", ArxIdColumn.Supplied<string>("sku"))
        .DisplayName("product", "name")
        .Table("price_list", ArxIdColumn.Generated<string>("price_id"))
        .Key("price_list", "region", "sku")
        .Lookup("price_list", "region", "country")
        .Table("sales_order", ArxIdColumn.Generated<Guid>("order_id"))
        .Key("sales_order", "order_number")
        .Lookup("sales_order", "account_id", "account")
        .Table("activity_log", ArxIdColumn.Generated<string>("log_id"))
        .AddEnum<Industry>("Industry")
        .Add("AccountStatus", ArxSelectionSetKind.String, ("ACTIVE", "Active"), ("ON_HOLD", "On hold"), ("CLOSED", "Closed"))
        .Add("ContactChannels", ArxSelectionSetKind.String, ArxSelectionSetMultiplicity.Array, ("E", "Email"), ("P", "Phone"), ("S", "SMS"))
        .Build();
}
```

## SecurityGuard.cs

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

```csharp
using ARX.SDK;

namespace ArxSamples.MongoDb;

/// <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 (MongoDB)

## Writing: .NET to MongoDB

| .NET value | Stored as |
|---|---|
| `string`, `char` | string |
| a plain `enum` | refused (ARX1007): write `(int)value` or `value.ToString()`, or use a selection set |
| `int`, `short`, `byte` | 32-bit integer |
| `long`, `uint` | 64-bit integer |
| `decimal` | Decimal128 (exact) |
| `double`, `float` | double (NaN and infinity allowed) |
| `bool` | boolean |
| `Guid` | binary subtype 4 (UUID) |
| `byte[]`, `ArxBytea` | binary |
| `ArxJsonB` | an embedded document or array (its JSON parsed) |
| an array or a list | an array of the values above |
| `ArxSelectionSetValue` | its code, or an array of codes |
| `ArxEntityReference` | the referenced id, as the referenced collection stores it (an ObjectId for a generated string id) |
| `null` | null |

## Dates and times

| .NET value | Stored as |
|---|---|
| `DateTime` (UTC) | a BSON date, to the millisecond: finer ticks are dropped |
| `DateTime` (Local) | converted to UTC |
| `DateTime` (Unspecified) | taken as UTC |
| `DateTimeOffset` | its UTC instant (the offset isn't kept) |
| `DateOnly` | midnight UTC of that day |
| `TimeOnly`, `TimeSpan` | text (`09:30:00.0000000`, `1.02:03:04`) |
| `default(DateTime)`, `default(DateTimeOffset)`, `default(DateOnly)` | refused (ARX1013): they mean "not set" |

## Reading: MongoDB to .NET

| Stored as | Read back as |
|---|---|
| string | `string` |
| 32-bit integer | `int` |
| 64-bit integer | `long` |
| double | `double` |
| Decimal128 | `decimal` (ARX4001 past .NET's range) |
| boolean | `bool` |
| date | `DateTime`, UTC (a `DateOnly` is stored as midnight UTC: read it with `GetAttributeValue<DateOnly>`, which converts a midnight `DateTime` from 0.1.0-preview.5, or preload the field) |
| ObjectId | `string` (24 hexadecimal characters) |
| binary subtype 4 | `Guid` |
| other binary | `byte[]` |
| embedded document | `ArxJsonB` |
| array of one kind of value | a typed array (`string[]`, `int[]`, …); any other array as `ArxJsonB` |
| a declared lookup | `ArxEntityReference` |
| a preloaded field | the preloaded type (`DateOnly` from a date, `TimeOnly` from text, …) |

---

# ARX error codes (MongoDB)

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 MongoDB |
|---|---|---|---|---|
| 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 | `52` a field name starting with '$' (DollarPrefixedFieldName); `56` an empty field name in an update path (EmptyFieldName); `57` a field name with a dot (DottedFieldName); `73` an invalid collection or database name (InvalidNamespace) |
| 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 MongoDB |
|---|---|---|---|---|
| ARX2002 | `RecordNotFound` | The record addressed by key doesn't exist. | no | `ARX's check` an update or delete whose documents weren't all found: checked in ARX's transaction, nothing changed |
| ARX2003 | `MultipleRecordsMatched` | An update or delete with `RequireSingleRow` found more than one row for an entity. Nothing was changed. | no | `ARX's check` an entity that matched several documents when one was required |
| 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 MongoDB |
|---|---|---|---|---|
| ARX3001 | `UniqueViolation` | A unique constraint or key was violated. | no | `11000` duplicate key in a unique index (_id or a unique index); `11001` duplicate key (older form) |
| 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 | `121` the document fails the collection's validator ($jsonSchema) |
| 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 MongoDB |
|---|---|---|---|---|
| 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 | `10334` a document larger than 16 MB; `17280` an index key too large; `17419` a document larger than 16 MB after an update; `17420` a document larger than 16 MB after an update (older form); `driver` a document over 16 MB, refused by the driver before sending |
| ARX4003 | `InvalidValueFormat` | A value can't be parsed for its column (for example invalid JSON, dates or encoding). | no | `2` a bad value in a command (BadValue); `40` two update operators on the same path (ConflictingUpdateOperators); `17287` a regular expression MongoDB can't compile |
| ARX4004 | `DataTypeMismatch` | A value's type doesn't fit the column. | no | `14` an operator applied to a field of the wrong type (TypeMismatch); `28` an update path through a field that isn't a document (PathNotViable); `16837` an update path that crosses a value that isn't a document |

## 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 MongoDB |
|---|---|---|---|---|
| ARX5001 | `UndefinedEntity` | The table or collection doesn't exist. | no | `26` the collection or namespace doesn't exist (NamespaceNotFound) |
| 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 MongoDB |
|---|---|---|---|---|
| ARX6001 | `PermissionDenied` | The database user lacks the required permission. | no | `13` the user lacks a privilege (Unauthorized); `18` authentication failed |

## 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 MongoDB |
|---|---|---|---|---|
| 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 | `112` a write conflict with another transaction (WriteConflict); `244` the transaction was aborted (TransactionAborted); `251` the transaction was aborted on the server (NoSuchTransaction) |
| ARX7003 | `Timeout` | The command or a lock wait timed out. Safe to retry. | yes | `24` a lock wasn't granted in time (LockTimeout); `50` the operation's time limit was exceeded (MaxTimeMSExpired); `290` the transaction exceeded its lifetime limit (TransactionExceededLifetimeLimitSeconds) |
| ARX7004 | `Throttled` | The server is rate-limiting or out of connections. Safe to retry later. | yes | `202` the server's connection pool is exhausted (NetworkInterfaceExceededTimeLimit) |
| ARX7005 | `ConnectionFailure` | The database couldn't be reached, or the connection dropped. Safe to retry. | yes | `6` the host is unreachable; `7` the host wasn't found; `89` a network timeout; `91` the server is shutting down; `189` the primary stepped down; `9001` a socket exception; `10107` the server isn't the primary (NotWritablePrimary); `11600` the server was interrupted at shutdown; `11602` the operation was interrupted by a step-down; `13435` the server isn't the primary and secondaryOk is false; `driver` the driver couldn't select a server (timeout) or the connection broke |

## 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 MongoDB |
|---|---|---|---|---|
| ARX8001 | `NotSupported` | The provider doesn't support the requested operation or feature. | no | `20` an illegal operation (IllegalOperation, for example a transaction on a standalone server); `66` a change to an immutable field such as _id (ImmutableField); `166` a write to a view (CommandNotSupportedOnView); `263` an operation not allowed in a transaction (OperationNotSupportedInTransaction); `292` a sort or group past the memory limit without allowDiskUse (QueryExceededMemoryLimitNoDiskUseAllowed); `16819` a sort past the memory limit; `40324` an unknown pipeline stage; `ARX` a transaction, or a write of several documents, on a standalone server |

## ARX9xxx Provider

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

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

---

# Create with ARX.SDK on MongoDB

## What a create does

| Rule | On MongoDB |
|---|---|
| Where it goes | The entity's name is the collection; MongoDB makes a collection on its first insert |
| The id | The declared id is the document's `_id`, named in your code by its declared column (`account_id`) |
| A generated string id | An ObjectId ARX makes before the insert, returned as its 24-character text |
| A generated Guid id | A version 7 Guid ARX makes (new ids sort by time) |
| A generated number id | Refused (ARX8001): MongoDB has no sequences |
| No id (`NoIdColumn`) | Refused (ARX1001): every MongoDB document has an `_id` |
| A duplicate | ARX3001 UniqueViolation, naming the index; a plain insert never replaces a document |
| Names | ASCII letters, digits and `_`; a `$` or SQL syntax is ARX1017; field names keep their case |
| Without a catalog | Not resolved: a collection has no declared id to read, so a table the catalog (or `entity.IdColumn`) doesn't declare stays ARX1016 IdNotDeclared. The SQL providers resolve it (capability `SchemaResolution`, which MongoDB doesn't declare) |

## Reference

### Value types

| .NET value | Stored as |
|---|---|
| `string`, `char` | string |
| a plain `enum` | refused (ARX1007): write `(int)value` or `value.ToString()`, or use a selection set |
| `int`, `short`, `byte` | 32-bit integer |
| `long`, `uint` | 64-bit integer |
| `decimal` | Decimal128 (exact) |
| `double`, `float` | double (NaN and infinity allowed) |
| `bool` | boolean |
| `Guid` | binary subtype 4 (UUID) |
| `byte[]`, `ArxBytea` | binary |
| `ArxJsonB` | an embedded document or array (its JSON parsed) |
| an array or a list | an array of the values above |
| `ArxSelectionSetValue` | its code, or an array of codes |
| `ArxEntityReference` | the referenced id, as the referenced collection stores it (an ObjectId for a generated string id) |
| `null` | null |

### Dates and times

| .NET value | Stored as |
|---|---|
| `DateTime` (UTC) | a BSON date, to the millisecond: finer ticks are dropped |
| `DateTime` (Local) | converted to UTC |
| `DateTime` (Unspecified) | taken as UTC |
| `DateTimeOffset` | its UTC instant (the offset isn't kept) |
| `DateOnly` | midnight UTC of that day |
| `TimeOnly`, `TimeSpan` | text (`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 MongoDB) and the mongosh query that checks what it did. `service` is the application's `ArxMongoDbService`; 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-mongodb-sample-application.md.

### Create one record and get its id

Basics 1 of Create.

The smallest create: one document, in four scenarios. The usual way first (the id declared in the schema, an ObjectId ARX makes), then the id declared on the entity, then an id you supply, then a Guid id.

Good to know:

The declared id is the document's `_id`. Your code names it by the column you declare (`account_id`); MongoDB stores it as `_id`. A collection that doesn't exist yet is made by MongoDB on the first insert.

Usings:

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

#### 1. Id in the schema

The usual way: the id declared in the schema; ARX makes an ObjectId and returns it as text.

In scope: `connectionString` (string).

```csharp
// Declare each collection's id once, at startup. A generated string id is an ObjectId.
var schema = ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<string>("account_id"))
    .Build();

var service = new ArxMongoDbService(connectionString, schema);

var entity = new ArxEntity("account");
entity["name"] = "Northwind Traders";
entity["account_number"] = "ACC-M001";
entity["employee_count"] = 240;

// CreateAsync returns the id of the new document: the ObjectId's 24 hexadecimal characters.
ArxRecordId id = await service.CreateAsync(entity);

Console.WriteLine($"created account {id.AsString()}");
```

Check it in the database:

```sql
db.account.find({ account_number: "ACC-M001" })
```

Expect: 1 document, _id an ObjectId, name "Northwind Traders", employee_count 240.

#### 2. Id on the entity

The other way: no schema catalog, the id declared on the entity.

In scope: `connectionString` (string).

```csharp
var service = new ArxMongoDbService(connectionString);

var entity = new ArxEntity("account");
entity.IdColumn = ArxIdColumn.Generated<string>("account_id");
entity["name"] = "Contoso Pharmaceuticals";
entity["account_number"] = "ACC-M002";

await service.CreateAsync(entity);

// The id is also set on the entity.
Console.WriteLine($"created account {entity.RecordId} (read from entity.RecordId)");
```

Check it in the database:

```sql
db.account.find({ account_number: "ACC-M002" })
```

Expect: 1 document, name "Contoso Pharmaceuticals".

#### 3. An id you supply

An id you supply: a country's code is its id.

In scope: `connectionString` (string).

```csharp
var schema = ArxSchemaCatalog.Create()
    .Table("country", ArxIdColumn.Supplied<string>("code"))
    .Build();

var service = new ArxMongoDbService(connectionString, schema);

var portugal = new ArxEntity("country");
portugal["code"] = "PT";
portugal["name"] = "Portugal";

ArxRecordId id = await service.CreateAsync(portugal);

Console.WriteLine($"created country {id.AsString()}");
```

Check it in the database:

```sql
db.country.find({ _id: "PT" })
```

Expect: 1 document, name "Portugal".

#### 4. A Guid id

A Guid id ARX makes (version 7: new ids sort by time).

In scope: `connectionString` (string).

```csharp
var schema = ArxSchemaCatalog.Create()
    .Table("sales_order", ArxIdColumn.Generated<Guid>("order_id"))
    .Build();

var service = new ArxMongoDbService(connectionString, schema);

var order = new ArxEntity("sales_order");
order["order_number"] = "SO-M001";
order["total"] = 125.50m;

ArxRecordId id = await service.CreateAsync(order);

Console.WriteLine($"created order {id.AsGuid()}");
```

Check it in the database:

```sql
db.sales_order.find({ order_number: "SO-M001" })
```

Expect: 1 document, _id a UUID, total NumberDecimal("125.50").

### Create a record that refers to another

Basics 2 of Create.

A document that refers to another: a contact of an account, in two scenarios. The reference is an `ArxEntityReference` made from the id CreateAsync returned; one to a collection the schema doesn't declare is refused before anything is sent.

Good to know:

MongoDB has no foreign keys: ARX checks the reference against the schema (the collection is declared, the id has its type) and stores it as the referenced id is stored, an ObjectId here.

Usings:

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

#### 1. A contact of an account

Create the account, then a contact whose account_id refers to it.

In scope: `service` (ArxMongoDbService).

```csharp
var account = new ArxEntity("account");
account["name"] = "Litware, Inc.";
account["account_number"] = "ACC-M201";
ArxRecordId accountId = await service.CreateAsync(account);

var contact = new ArxEntity("contact");
contact["first_name"] = "Ada";
contact["last_name"] = "Lovelace";
contact["email"] = "ada@litware.example";
contact["account_id"] = new ArxEntityReference("account", accountId);

ArxRecordId contactId = await service.CreateAsync(contact);

Console.WriteLine($"created contact {contactId.AsString()} of account {accountId.AsString()}");
```

Check it in the database:

```sql
db.contact.find({ email: "ada@litware.example" })
```

Expect: 1 document, account_id an ObjectId equal to the account's _id.

#### 2. A collection the schema doesn't declare

A reference to a collection the schema doesn't declare: ARX1005, nothing sent.

In scope: `service` (ArxMongoDbService).

```csharp
var contact = new ArxEntity("contact");
contact["email"] = "nobody@litware.example";
contact["account_id"] = new ArxEntityReference("customer", "64b7f0c2a1b2c3d4e5f60718");

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

Check it in the database:

```sql
db.contact.find({ email: "nobody@litware.example" })
```

Expect: no documents.

### Catch the errors a create meets

Basics 3 of Create.

The errors a create meets most, in three scenarios: a duplicate (a unique index), a value ARX refuses before sending anything, and an id MongoDB can't make.

Good to know:

Every error is an `ArxException` with a stable code. The message names the index, never the value (MongoDB's own text quotes it). A plain insert never replaces a document.

Usings:

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

#### 1. A duplicate

A second account with the same number: ARX3001, with the index's name.

In scope: `service` (ArxMongoDbService).

```csharp
var account = new ArxEntity("account");
account["name"] = "Proseware, Inc.";
account["account_number"] = "ACC-M301";
await service.CreateAsync(account);

var again = new ArxEntity("account");
again["name"] = "Proseware copy";
again["account_number"] = "ACC-M301";

try
{
    await service.CreateAsync(again);
}
catch (ArxDatabaseException exception) when (exception.Code == ArxErrorCode.UniqueViolation)
{
    Console.WriteLine(exception.Message);
    Console.WriteLine($"index {exception.ConstraintName}, MongoDB error {exception.NativeCode}");
}
```

Check it in the database:

```sql
db.account.countDocuments({ account_number: "ACC-M301" })
```

Expect: 1.

#### 2. A date that means "not set"

default(DateTime) means "not set": ARX1013 before anything is sent.

In scope: `service` (ArxMongoDbService).

```csharp
var account = new ArxEntity("account");
account["account_number"] = "ACC-M302";
account["created_on"] = default(DateTime);

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

Check it in the database:

```sql
db.account.find({ account_number: "ACC-M302" })
```

Expect: no documents.

#### 3. A generated number id

A generated number id: MongoDB has no sequences, so ARX8001; use an ObjectId or a Guid.

In scope: `connectionString` (string).

```csharp
var schema = ArxSchemaCatalog.Create()
    .Table("activity_log", ArxIdColumn.Generated<long>("log_id"))
    .Build();

var service = new ArxMongoDbService(connectionString, schema);

var entry = new ArxEntity("activity_log");
entry["action"] = "signed in";

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

Check it in the database:

```sql
db.activity_log.find({ action: "signed in" })
```

Expect: no documents.

### Store every kind of value

Real case 1 of Create.

What each .NET value becomes in MongoDB, in three scenarios: numbers, text and flags; dates and times; and the shapes a document can hold, an embedded document, an array and a picklist with several choices.

Good to know:

- `int` is stored as a 32-bit integer, `long` 64-bit, `decimal` as Decimal128 (exact), `double` as a double.
- A `Guid` is binary subtype 4 (UUID), a `byte[]` plain binary.
- A `DateTime` is a BSON date, in UTC and to the millisecond: finer ticks are dropped. A `DateTimeOffset` keeps its instant, not its offset; a `DateOnly` is midnight UTC; a `TimeOnly` is text.
- `ArxJsonB` is an embedded document or array; a list or an array is an array.

Usings:

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

#### 1. Numbers, text and flags

Numbers keep their width; a decimal stays exact.

In scope: `service` (ArxMongoDbService).

```csharp
var account = new ArxEntity("account");
account["name"] = "Tailspin Toys";
account["account_number"] = "ACC-M401";
account["employee_count"] = 320;
account["annual_revenue"] = 12_500_000.75m;
account["credit_score"] = 0.87;
account["visits"] = 9_000_000_000L;
account["is_partner"] = true;
account["external_id"] = Guid.Parse("8f14e45f-ceea-467f-a8f9-1d2a3b4c5d6e");
account["industry"] = new ArxSelectionSetValue("Industry", (int)Industry.Retail);

var id = await service.CreateAsync(account);

Console.WriteLine($"created account {id.AsString()}");
```

Check it in the database:

```sql
db.account.find({ account_number: "ACC-M401" })
```

Expect: employee_count 320, annual_revenue NumberDecimal("12500000.75"), visits NumberLong("9000000000"), external_id UUID("8f14e45f-ceea-467f-a8f9-1d2a3b4c5d6e"), industry 1.

#### 2. Dates and times

Dates are UTC and keep milliseconds; a date alone is midnight UTC.

In scope: `service` (ArxMongoDbService).

```csharp
var order = new ArxEntity("sales_order");
order["order_number"] = "SO-M401";
order["ordered_at"] = new DateTime(2026, 11, 9, 14, 30, 15, 250, DateTimeKind.Utc);
order["promised_at"] = new DateTimeOffset(2026, 11, 12, 9, 0, 0, TimeSpan.FromHours(1));
order["ship_on"] = new DateOnly(2026, 11, 10);
order["delivery_window_start"] = new TimeOnly(9, 30);

await service.CreateAsync(order);

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

Check it in the database:

```sql
db.sales_order.find({ order_number: "SO-M401" })
```

Expect: ordered_at ISODate("2026-11-09T14:30:15.250Z"), promised_at ISODate("2026-11-12T08:00:00Z"), ship_on ISODate("2026-11-10T00:00:00Z"), delivery_window_start "09:30:00.0000000".

#### 3. Documents, arrays and picklists

An embedded address, a list of tags, and several preferred channels from a picklist.

In scope: `service` (ArxMongoDbService).

```csharp
var contact = new ArxEntity("contact");
contact["first_name"] = "Grace";
contact["last_name"] = "Hopper";
contact["email"] = "grace@tailspin.example";
contact["address"] = new ArxJsonB { SerializedValue = "{\"street\":\"1 Navy Way\",\"city\":\"Arlington\",\"country\":\"US\"}" };
contact["tags"] = new[] { "speaker", "vip" };
contact["channels"] = ArxSelectionSetValue.Multiple("ContactChannels", "E", "S");

await service.CreateAsync(contact);

Console.WriteLine("created contact grace@tailspin.example");
```

Check it in the database:

```sql
db.contact.find({ email: "grace@tailspin.example" })
```

Expect: address { street: "1 Navy Way", city: "Arlington", country: "US" }, tags ["speaker", "vip"], channels ["E", "S"].

### Catch an injection attempt

Real case 2 of Create.

A form whose fields become attributes, attacked in three ways: a field named like a MongoDB operator, a field named with SQL in it, and a value that looks like an operator.

Good to know:

A name with `$` or SQL syntax is `ARX1017 SuspectedInjection`, refused before anything is sent; alert on it. Values are never parsed: a value that looks like an operator is stored as the text it is.

Usings:

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

#### 1. An operator as a field name

A posted field named "$where": ARX1017.

In scope: `service` (ArxMongoDbService).

```csharp
var contact = new ArxEntity("contact");
contact["email"] = "mallory@attacker.example";
contact["$where"] = "sleep(5000)";

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

Check it in the database:

```sql
db.contact.find({ email: "mallory@attacker.example" })
```

Expect: no documents.

#### 2. SQL in a field name

A field name carrying SQL (an attack aimed at a SQL backend): ARX1017 too.

In scope: `service` (ArxMongoDbService).

```csharp
var contact = new ArxEntity("contact");
contact["email"] = "eve@attacker.example";
contact["name'; DROP TABLE contact; --"] = "x";

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

Check it in the database:

```sql
db.contact.find({ email: "eve@attacker.example" })
```

Expect: no documents.

#### 3. An operator as a value

A value that looks like an operator is stored as text, never run.

In scope: `service` (ArxMongoDbService).

```csharp
var contact = new ArxEntity("contact");
contact["email"] = "trent@example.example";
contact["first_name"] = "{ \"$ne\": null }";

await service.CreateAsync(contact);

Console.WriteLine("stored, as text");
```

Check it in the database:

```sql
db.contact.find({ email: "trent@example.example" }, { first_name: 1 })
```

Expect: 1 document, first_name the text "{ \"$ne\": null }".

### Create in an ARX transaction

Advanced case 1 of Create.

Several calls in one ARX transaction, in two scenarios: an account and its contacts committed together, and a transaction a failed call aborts, so nothing of it is stored.

Good to know:

`new ArxMongoConnection(service)` gives ARX transactions on the service's database: no driver types in your code. MongoDB runs transactions on a replica set or a sharded cluster; it has no savepoints, so a call that fails at the database aborts the transaction (later calls are ARX1015).

Usings:

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

#### 1. Commit together

An account and two contacts, committed together.

In scope: `service` (ArxMongoDbService).

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

var account = new ArxEntity("account");
account["name"] = "Wide World Importers";
account["account_number"] = "ACC-M501";
var accountId = await service.CreateAsync(account, transaction);

var first = new ArxEntity("contact");
first["email"] = "lena@wideworld.example";
first["account_id"] = new ArxEntityReference("account", accountId);

var second = new ArxEntity("contact");
second["email"] = "omar@wideworld.example";
second["account_id"] = new ArxEntityReference("account", accountId);

await service.CreateMultipleAsync(new[] { first, second }, transaction);

await transaction.CommitAsync();
Console.WriteLine("committed the account and its two contacts");
```

Check it in the database:

```sql
db.contact.countDocuments({ email: /@wideworld\.example$/ })
```

Expect: 2.

#### 2. A failed call aborts it

The second call repeats an account number: the transaction is aborted and the first account goes too.

In scope: `service` (ArxMongoDbService).

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

var account = new ArxEntity("account");
account["name"] = "Woodgrove Bank";
account["account_number"] = "ACC-M502";
await service.CreateAsync(account, transaction);

var duplicate = new ArxEntity("account");
duplicate["name"] = "Repeats ACC-M501";
duplicate["account_number"] = "ACC-M501";

try
{
    await service.CreateAsync(duplicate, transaction);
}
catch (ArxDatabaseException exception)
{
    Console.WriteLine(exception.Message);
}

Console.WriteLine($"transaction aborted: {transaction.IsAborted}");
await transaction.RollbackAsync();
```

Check it in the database:

```sql
db.account.find({ account_number: "ACC-M502" })
```

Expect: no documents.

### A security check around creates: log, block, handle false positives

Advanced case 2 of Create.

A security check around every create (`SecurityGuard`, in the project root), in four scenarios: a normal request passes through; an attack is logged and the user sees a plain message; repeated attacks block the user; and a false positive, a harmless file header flagged as SQL, is triaged and fixed without blocking anyone.

Good to know:

ARX1017 is only raised for a name that is already invalid, so a false positive never stops a call that would otherwise have worked: without the check it would fail with ARX1003. What can be wrong is the label: a harmless name (`E-mail (work)`, `Price (EUR)`, `notes;` pasted into a setting) reported as an attack. Handle it with:

- **The source of the name:** `NameSource.Code` for names from your code or configuration, so an ARX1017 there is logged as a bug and blocks nobody; `NameSource.UserInput` for names built from what a user sent.
- **Strikes, not one event:** a single ARX1017 never blocks anyone.
- **Triage:** the log has ARX's evidence (`contains '('`); if it's harmless, `Unblock` the user and fix what let the name through.
- **The real fix:** map user input to names from a fixed list (headers to attributes, request types to tables), so user input never becomes a name, and neither attacks nor false positives can happen.

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 passes through

A normal request goes through the guard untouched.

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

```csharp
var result = await CreateContactAsync(service, guard, "ana", new Dictionary<string, object>
{
    ["first_name"] = "Ana",
    ["last_name"] = "Guard-C1",
    ["email"] = "guard-ana@example.com",
});

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

Defined in the sample, used above:

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

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

    return guard.RunAsync(user, "create contact", NameSource.UserInput, () => service.CreateAsync(contact));
}
```

Check it in the database:

```sql
db.contact.find({ last_name: "Guard-C1" }, { first_name: 1, email: 1 })
```

Expect: 1 document: Ana, guard-ana@example.com.

#### 2. An attack is logged; the user sees a plain message

A field name carrying SQL: ARX refuses it (ARX1017), the guard logs it and the user sees a plain message.

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

```csharp
var result = await CreateContactAsync(service, guard, "mallory", new Dictionary<string, object>
{
    ["first_name"] = "Mallory",
    ["last_name"] = "Guard-C2",
    ["notes\") VALUES ('x'); DROP TABLE contact; --"] = "x",
});

Console.WriteLine($"mallory sees: \"{result.ForTheUser}\"");
var logged = guard.Events[^1];
Console.WriteLine($"security log: {logged.Kind}, {logged.User}, {logged.Action}");
```

Check it in the database:

```sql
db.contact.countDocuments({ last_name: "Guard-C2" })
```

Expect: 0: nothing created.

#### 3. Repeated attacks block the user

Two more attempts make three strikes: the user is blocked, and even a valid request isn't attempted.

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

```csharp
foreach (var probe in new[] { "last_name; DROP TABLE account", "email UNION SELECT 1" })
{
    await CreateContactAsync(service, guard, "mallory", new Dictionary<string, object> { ["first_name"] = "Mallory", [probe] = "x" });
}

var valid = await CreateContactAsync(service, guard, "mallory", new Dictionary<string, object>
{
    ["first_name"] = "Mallory",
    ["last_name"] = "Guard-C3",
});

Console.WriteLine($"mallory blocked: {guard.IsBlocked("mallory")}; a valid request: {valid.Outcome}, \"{valid.ForTheUser}\"");
```

Check it in the database:

```sql
db.contact.countDocuments({ first_name: "Mallory" })
```

Expect: 0: no request from mallory created anything, the last one wasn't even attempted.

#### 4. A false positive: a harmless file header flagged as SQL

A false positive: a supplier file's header "E-mail (work)" is used as an attribute name. The parentheses make ARX flag it as SQL, though it's harmless. One strike doesn't block anyone; triage reads the log, and the fix is the mapping that should have been there: file headers mapped to known attributes, so user input never becomes a name.

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

```csharp
var header = new[] { "first_name", "last_name", "E-mail (work)" };
var row = new object[] { "Sam", "Guard-C4", "guard-sam@example.com" };

var asIs = header.Zip(row).ToDictionary(x => x.First, x => x.Second);
var refused = await CreateContactAsync(service, guard, "sam", asIs);

Console.WriteLine($"sam: {refused.Outcome}; blocked: {guard.IsBlocked("sam")} (one strike blocks nobody)");
Console.WriteLine($"triage reads: {guard.Events[^1].Detail}");

// The fix: known headers mapped to attributes; anything else is ignored (or reported to the user), never a name.
var headerMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
    ["first_name"] = "first_name",
    ["last_name"] = "last_name",
    ["E-mail (work)"] = "email",
    ["E-mail"] = "email",
};

var mapped = header.Zip(row).Where(x => headerMap.ContainsKey(x.First)).ToDictionary(x => headerMap[x.First], x => x.Second);
var created = await CreateContactAsync(service, guard, "sam", mapped);

Console.WriteLine($"after the fix: {created.Outcome}, contact {created.Value}");
```

Check it in the database:

```sql
db.contact.find({ last_name: "Guard-C4" }, { first_name: 1, email: 1 })
```

Expect: 1 document: Sam, guard-sam@example.com (created once, after the fix).

### Set up the service from the database's schema

Advanced case 3 of Create.

The service set up from the database instead of by hand, in three scenarios: preload a collection at startup, create through the preloaded service, and Strict, where only the preloaded fields may be written.

Good to know:

`ArxPreLoadTable` reads, once, each collection's id (ObjectId ids are generated strings), its unique indexes (as keys) and the field types of its `$jsonSchema` validator. A value of the wrong type is then refused before anything is sent (ARX4004). MongoDB has no foreign keys: declare lookups in the configure callback.

Usings:

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

#### 1. Preload at startup

Preload the lead collection once, at startup.

In scope: `connectionString` (string).

```csharp
var catalog = await new ArxPreLoadTable(new ArxPreLoadTableRegister().PreLoadTable("lead")).LoadAsync(connectionString);

catalog.TryGetTable("lead", out var lead);
Console.WriteLine($"lead: id {lead.IdColumn}, keys {string.Join(", ", lead.Keys)}, fields {string.Join(", ", lead.Columns.Keys)}");

return catalog;
```

Check it in the database:

```sql
db.getCollectionInfos({ name: "lead" })[0].options.validator
```

Expect: the $jsonSchema with email, full_name, score and captured_on.

#### 2. Create through the preloaded service

Create through the preloaded service; a value of the wrong type is refused before MongoDB sees it.

In scope: `connectionString` (string), `catalog` (ArxSchemaCatalog).

```csharp
var service = new ArxMongoDbService(connectionString, catalog);

var lead = new ArxEntity("lead");
lead["email"] = "lead-m701@example.com";
lead["full_name"] = "Kim Abercrombie";
lead["score"] = 72;
lead["captured_on"] = new DateTime(2026, 11, 9, 8, 0, 0, DateTimeKind.Utc);
await service.CreateAsync(lead);
Console.WriteLine("created lead-m701@example.com");

var wrong = new ArxEntity("lead");
wrong["email"] = "lead-m702@example.com";
wrong["score"] = "high";

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

Check it in the database:

```sql
db.lead.find({ email: { $in: ["lead-m701@example.com", "lead-m702@example.com"] } })
```

Expect: 1 document, lead-m701@example.com with score 72.

#### 3. Strict

Strict: a field the preload didn't load is refused (ARX1018), so a typo can't add a field.

In scope: `connectionString` (string), `catalog` (ArxSchemaCatalog).

```csharp
var service = new ArxMongoDbService(connectionString, catalog) { Strict = true };

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

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

Check it in the database:

```sql
db.lead.countDocuments({ email: "lead-m703@example.com" })
```

Expect: 0.

### 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("Fabrikam Robotics", "ACC-M601");

Console.WriteLine($"added Fabrikam Robotics 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("account");
        record["name"] = name;
        record["account_number"] = key;
        return data.CreateAsync(record);
    }
}
```

Check it in the database:

```sql
db.account.find({ account_number: "ACC-M601" }, { name: 1 })
```

Expect: 1 document, Fabrikam Robotics.

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

Check it in the database:

```sql
db.version()
```

Expect: 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("Litware Labs", "ACC-M602");

try
{
    var nobody = new ArxEntity("account") { RecordId = ArxRecordId.Of("account_id", "000000000000000000000000") };
    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
db.account.find({ account_number: "ACC-M602" }, { name: 1 })
```

Expect: 1 document, Litware Labs (the update found nothing to change).

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

Advanced case 5 of Create.

Use documents whose fields are named outside ARX's name rule, in three scenarios: declare an alias for each such field and create documents through them, read and query by the aliases, and see what is still refused: the field's own name in a call, and a field name MongoDB would read as a path.

Good to know:

ARX accepts only names of ASCII letters, digits and underscores. Documents written by other code may have fields such as `first-name` or `Order Total`: declare an alias for each in the schema catalog, `.Alias(table, attribute, databaseName)`. Code uses the attribute; ARX writes, filters and sorts on the field, and reads it back as the attribute. The declared id is the document's `_id`, as always.

Only declared names pass, and a field name may not start with `$` nor contain `.`: MongoDB reads those as an operator or a path.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var schema = ArxSchemaCatalog.Create()
    .Table("legacy_customer", ArxIdColumn.Supplied<string>("customer_code"))
    .Alias("legacy_customer", "first_name", "first-name")
    .Alias("legacy_customer", "last_name", "last-name")
    .Alias("legacy_customer", "order_total", "Order Total")
    .Build();
var service = new ArxMongoDbService(connectionString, schema);
```

#### 1. Create through the aliases

Create two customers: each attribute is written to the field its alias names.

In scope: `service` (ArxMongoDbService).

```csharp
var ana = new ArxEntity("legacy_customer");
ana["customer_code"] = "C-1001";
ana["first_name"] = "Ana";
ana["last_name"] = "Silva";
ana["order_total"] = 120.50m;

var id = await service.CreateAsync(ana);

var ben = new ArxEntity("legacy_customer");
ben["customer_code"] = "C-1002";
ben["first_name"] = "Ben";
ben["order_total"] = 75.00m;

await service.CreateAsync(ben);

Console.WriteLine($"created {id} and C-1002");
```

Check it in the database:

```sql
db.legacy_customer.find({ _id: { $in: ["C-1001", "C-1002"] } }, { "first-name": 1, "Order Total": 1 })
```

Expect: 2 documents: Ana with 120.50, Ben with 75.00.

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

Read a document by id and query by the aliases: every field comes back under its alias.

In scope: `service` (ArxMongoDbService).

```csharp
var ana = await service.RetrieveAsync("legacy_customer", ArxRecordId.Of("customer_code", "C-1001")) ?? throw new InvalidOperationException("The legacy_customer record isn't there.");

Console.WriteLine($"{ana["customer_code"]}: {ana["first_name"]} {ana["last_name"]}, total {ana["order_total"]}");

var query = new ArxQueryExpression("legacy_customer");
query.ColumnSet.AddColumns("first_name", "order_total");
query.Criteria.AddCondition("order_total", ArxConditionOperator.GreaterThan, 50m);
query.AddOrder("first_name", ArxOrderType.Descending);

var customers = await service.RetrieveMultipleAsync(query);

foreach (var customer in customers.Entities)
{
    Console.WriteLine($"{customer["first_name"]}: {customer["order_total"]}");
}
```

Check it in the database:

```sql
db.legacy_customer.find({ "Order Total": { $gt: 50 } }, { "first-name": 1, "Order Total": 1 }).sort({ "first-name": -1 })
```

Expect: Ben, then Ana.

#### 3. What is still refused

The field's own name in a call, and an alias to a field name MongoDB reads as a path, are refused.

In scope: `connectionString` (string), `service` (ArxMongoDbService).

```csharp
var change = new ArxEntity("legacy_customer");
change["customer_code"] = "C-1002";
change["first-name"] = "Benjamin";

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

var dotted = new ArxMongoDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("legacy_customer", ArxIdColumn.Supplied<string>("customer_code"))
    .Alias("legacy_customer", "order_total", "order.total")
    .Build());

var carl = new ArxEntity("legacy_customer");
carl["customer_code"] = "C-1003";
carl["order_total"] = 10m;

try
{
    await dotted.CreateAsync(carl);
}
catch (ArxException exception) when (exception.Code == ArxErrorCode.InvalidIdentifier)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

```sql
db.legacy_customer.find({ _id: { $in: ["C-1002", "C-1003"] } }, { "first-name": 1 })
```

Expect: 1 document, C-1002 with Ben (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 an account: 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 account = new ArxEntity("account");
account["name"] = "Tailspin Toys";
account["account_number"] = "ACC-M611";

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

var query = new ArxQueryExpression("account");
query.Criteria.AddCondition("account_number", ArxConditionOperator.Equal, "ACC-M611");
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
db.account.find({ account_number: "ACC-M611" }, { name: 1 })
```

Expect: 1 document, Tailspin Toys.

#### 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("account") { RecordId = ArxRecordId.Of("account_id", "000000000000000000000000") };
nobody["name"] = "Nobody";

try
{
    await data.UpdateAsync(nobody);
}
catch (ArxException exception) when (exception.Code == ArxErrorCode.RecordNotFound)
{
    logger.LogError(exception, "Renaming account {AccountId} failed: {Code}, transient: {IsTransient}",
        "000000000000000000000000", ArxException.FormatCode(exception.Code), exception.IsTransient);
}
```

Check it in the database:

```sql
db.account.countDocuments({ name: "Nobody" })
```

Expect: 0 (the update found no account 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 account = new ArxEntity("account");
account["name"] = "Wingtip Toys";
account["account_number"] = "ACC-M612";
await data.CreateAsync(account);

Console.WriteLine("created Wingtip Toys: nothing logged");

var gone = new ArxEntity("account") { RecordId = ArxRecordId.Of("account_id", "000000000000000000000000") };
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
db.account.find({ account_number: "ACC-M612" }, { name: 1 })
```

Expect: 1 document, Wingtip Toys.

---

# CreateMultiple with ARX.SDK on MongoDB

## What a CreateMultiple does

| Rule | On MongoDB |
|---|---|
| Many documents | CreateMultiple is all or nothing, in a transaction (a replica set or sharded cluster; ARX8001 on a standalone server) |

## The samples

Every CreateMultiple sample, in reading order. Each scenario shows the code (verified against MongoDB) and the mongosh query that checks what it did. `service` is the application's `ArxMongoDbService`; 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-mongodb-sample-application.md.

### Create many records in one call

Basics 1 of CreateMultiple.

Many documents in one call, all or nothing, with one id per entity in list order, in two scenarios: the call that stores them all, and the call that stores none because one of them fails.

Good to know:

CreateMultiple runs in a transaction, which MongoDB offers on a replica set (one node is enough) or a sharded cluster. On a standalone server it is refused rather than half done.

Usings:

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

#### 1. All stored

Three accounts in one call: the ids come back in the list's order.

In scope: `service` (ArxMongoDbService).

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

foreach (var (name, number) in new[] { ("Alpine Ski House", "ACC-M101"), ("Blue Yonder Airlines", "ACC-M102"), ("Coho Winery", "ACC-M103") })
{
    var account = new ArxEntity("account");
    account["name"] = name;
    account["account_number"] = number;
    accounts.Add(account);
}

IReadOnlyList<ArxRecordId> ids = await service.CreateMultipleAsync(accounts);

Console.WriteLine($"created {ids.Count} accounts, the first {ids[0].AsString()}");
```

Check it in the database:

```sql
db.account.find({ account_number: { $in: ["ACC-M101", "ACC-M102", "ACC-M103"] } })
```

Expect: 3 documents.

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

The second account repeats an account number (a unique index): none of the three is stored.

In scope: `service` (ArxMongoDbService).

```csharp
var first = new ArxEntity("account");
first["name"] = "Fabrikam Inc.";
first["account_number"] = "ACC-M104";

var repeated = new ArxEntity("account");
repeated["name"] = "Fabrikam again";
repeated["account_number"] = "ACC-M104";

var third = new ArxEntity("account");
third["name"] = "Graphic Design Institute";
third["account_number"] = "ACC-M105";

try
{
    await service.CreateMultipleAsync(new[] { first, repeated, third });
}
catch (ArxDatabaseException exception) when (exception.Code == ArxErrorCode.UniqueViolation)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

```sql
db.account.find({ account_number: { $in: ["ACC-M104", "ACC-M105"] } })
```

Expect: no documents.

---

# Update with ARX.SDK on MongoDB

## What an update does

| Rule | On MongoDB |
|---|---|
| What is written | Only the attributes present (`$set`); a field left out keeps its value; null is stored as null |
| Which documents | By the declared id (default), a declared key (`ArxMatch.Key`) or any fields (`ArxMatch.Columns`); match fields are never written |
| Nothing matched | ARX2002 RecordNotFound |
| RequireSingleRow | More than one match is ARX2003 and nothing changes |
| The id | Can't change: MongoDB's `_id` is immutable (ARX8001) |
| Match values | A document or array given as a match value is compared as data (`$eq`), never run as an operator |
| Concurrency | Optional. Declare a version column (`.Version(table, column)` in the schema catalog) and every update checks it in the same statement (a read returns the version with each record, so read, change and update carries it): an entity that carries an old version is ARX2004 RecordChanged and nothing is written, and each update adds one to the version. `.Version(table, column, required: true)` refuses an update that doesn't carry it (ARX1001). Without a version column the last writer wins. |
| 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)`: 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 MongoDB) and the mongosh query that checks what it did. `service` is the application's `ArxMongoDbService`; 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-mongodb-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 a record that isn't there.

Good to know:

Only the attributes you set are written (`$set`); a field left out keeps its value; an attribute set to null is stored as null. Nothing matched is ARX2002. A document's `_id` can't change in MongoDB.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "Fourth Coffee";
account["account_number"] = "ACC-M801";
account["employee_count"] = 40;
account["website"] = "https://fourthcoffee.example";
var id = await service.CreateAsync(account);
```

#### 1. By the id Create returned

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

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

```csharp
var change = new ArxEntity("account") { RecordId = id };
change["employee_count"] = 55;

int changed = await service.UpdateAsync(change);

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

Check it in the database:

```sql
db.account.find({ account_number: "ACC-M801" }, { name: 1, employee_count: 1 })
```

Expect: 1 document, Fourth Coffee, employee_count 55.

#### 2. Clear a value

Null stores null: the field stays, empty.

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

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

await service.UpdateAsync(change);

Console.WriteLine("website cleared");
```

Check it in the database:

```sql
db.account.find({ account_number: "ACC-M801" }, { website: 1 })
```

Expect: 1 document, website null.

#### 3. A record that isn't there

An id no document has: ARX2002.

In scope: `service` (ArxMongoDbService).

```csharp
var change = new ArxEntity("account") { RecordId = ArxRecordId.Of("account_id", "64b7f0c2a1b2c3d4e5f60718") };
change["employee_count"] = 1;

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

Check it in the database:

```sql
db.account.countDocuments({ _id: ObjectId("64b7f0c2a1b2c3d4e5f60718") })
```

Expect: 0.

### Choose how the documents are found

Basics 2 of Update.

The three ways to find the documents to change, in four scenarios: by a declared key, by any fields (several documents), with RequireSingleRow, and many entities at once, all or nothing.

Good to know:

`ArxMatch.Key` needs a key the schema declares; `ArxMatch.Columns` takes any fields. Match fields are never written. UpdateMultiple runs in a transaction: if any entity finds nothing, nothing changes.

Usings:

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

Set up by the sample before its scenarios:

```csharp
foreach (var (name, number) in new[] { ("Adatum", "ACC-M811"), ("Margie's Travel", "ACC-M812"), ("Lucerne Publishing", "ACC-M813") })
{
    var account = new ArxEntity("account");
    account["name"] = name;
    account["account_number"] = number;
    account["region"] = "M81";
    await service.CreateAsync(account);
}
```

#### 1. By a declared key

By the account number, a declared key.

In scope: `service` (ArxMongoDbService).

```csharp
var change = new ArxEntity("account");
change["account_number"] = "ACC-M811";
change["employee_count"] = 120;

await service.UpdateAsync(change, new ArxUpdateOptions { Match = ArxMatch.Key("account_number") });

Console.WriteLine("ACC-M811 changed");
```

Check it in the database:

```sql
db.account.find({ account_number: "ACC-M811" }, { employee_count: 1 })
```

Expect: 1 document, employee_count 120.

#### 2. By any fields

By any fields: every account in region M81 changes, and the call says how many.

In scope: `service` (ArxMongoDbService).

```csharp
var change = new ArxEntity("account");
change["region"] = "M81";
change["status"] = new ArxSelectionSetValue("AccountStatus", "ACTIVE");

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

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

Check it in the database:

```sql
db.account.countDocuments({ region: "M81", status: "ACTIVE" })
```

Expect: 3.

#### 3. Require exactly one

RequireSingleRow: the region matches three documents, so nothing changes (ARX2003).

In scope: `service` (ArxMongoDbService).

```csharp
var change = new ArxEntity("account");
change["region"] = "M81";
change["status"] = new ArxSelectionSetValue("AccountStatus", "CLOSED");

try
{
    await service.UpdateAsync(change, new ArxUpdateOptions { Match = ArxMatch.Columns("region"), RequireSingleRow = true });
}
catch (ArxException exception) when (exception.Code == ArxErrorCode.MultipleRecordsMatched)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

```sql
db.account.countDocuments({ region: "M81", status: "CLOSED" })
```

Expect: 0.

#### 4. Many at once, all or nothing

Many entities at once by their key: one of them has no document, so none changes (ARX2002).

In scope: `service` (ArxMongoDbService).

```csharp
var changes = new[] { "ACC-M812", "ACC-M813", "ACC-M819" }.Select(number =>
{
    var change = new ArxEntity("account");
    change["account_number"] = number;
    change["employee_count"] = 9;
    return change;
}).ToList();

try
{
    await service.UpdateMultipleAsync(changes, new ArxUpdateOptions { Match = ArxMatch.Key("account_number") });
}
catch (ArxException exception) when (exception.Code == ArxErrorCode.RecordNotFound)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

```sql
db.account.countDocuments({ region: "M81", employee_count: 9 })
```

Expect: 0.

### Catch an injection attempt

Real case 1 of Update.

Two attacks on an update endpoint: an operator as a field name, and a match value built to match every document.

Good to know:

A field name with `$` is ARX1017 before anything is sent. A match value that is a document with operator keys (`{ "$ne": null }`) is compared as data: ARX wraps it in `$eq`, so it finds no document instead of all of them.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "Trey Research";
account["account_number"] = "ACC-M821";
account["credit_limit"] = 5000m;
await service.CreateAsync(account);
```

#### 1. An operator as a field name

A posted field named "$set": ARX1017.

In scope: `service` (ArxMongoDbService).

```csharp
var change = new ArxEntity("account");
change["account_number"] = "ACC-M821";
change["$set"] = "{ credit_limit: 1000000 }";

try
{
    await service.UpdateAsync(change, new ArxUpdateOptions { Match = ArxMatch.Key("account_number") });
}
catch (ArxValidationException exception) when (exception.Code == ArxErrorCode.SuspectedInjection)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

```sql
db.account.find({ account_number: "ACC-M821" }, { credit_limit: 1 })
```

Expect: 1 document, credit_limit NumberDecimal("5000").

#### 2. A match built to find everything

An account number posted as JSON `{ "$ne": null }`: compared as data, it matches nothing (ARX2002).

In scope: `service` (ArxMongoDbService).

```csharp
var change = new ArxEntity("account");
change["account_number"] = new ArxJsonB { SerializedValue = "{ \"$ne\": null }" };
change["credit_limit"] = 1_000_000m;

try
{
    await service.UpdateAsync(change, new ArxUpdateOptions { Match = ArxMatch.Columns("account_number") });
}
catch (ArxException exception) when (exception.Code == ArxErrorCode.RecordNotFound)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

```sql
db.account.countDocuments({ credit_limit: NumberDecimal("1000000") })
```

Expect: 0.

### 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 rolled back when the body throws.

Good to know:

A batch scope is one MongoDB transaction across many calls. MongoDB has no savepoints, so a scope opened with the default options has none: a failed call rolls the whole scope back. (Asking for them, with `new ArxBatchOptions { Savepoints = true }`, is refused.)

Usings:

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

#### 1. Committed when the body returns

Create an account, then put it on hold: both or neither.

In scope: `service` (ArxMongoDbService).

```csharp
await service.RunBatchAsync(async scope =>
{
    var account = new ArxEntity("account");
    account["name"] = "VanArsdel, Ltd.";
    account["account_number"] = "ACC-M831";
    var id = await scope.CreateAsync(account);

    var hold = new ArxEntity("account") { RecordId = id };
    hold["status"] = new ArxSelectionSetValue("AccountStatus", "ON_HOLD");
    await scope.UpdateAsync(hold);
});

Console.WriteLine("account created and put on hold");
```

Check it in the database:

```sql
db.account.find({ account_number: "ACC-M831" }, { status: 1 })
```

Expect: 1 document, status "ON_HOLD".

#### 2. Rolled back when the body throws

Your own code fails after the writes: the scope rolls everything back.

In scope: `service` (ArxMongoDbService).

```csharp
try
{
    await service.RunBatchAsync(async scope =>
    {
        var account = new ArxEntity("account");
        account["name"] = "Humongous Insurance";
        account["account_number"] = "ACC-M832";
        await scope.CreateAsync(account);

        throw new InvalidOperationException("the credit check failed");
    });
}
catch (InvalidOperationException exception)
{
    Console.WriteLine($"caught: {exception.Message}");
}
```

Check it in the database:

```sql
db.account.countDocuments({ account_number: "ACC-M832" })
```

Expect: 0.

### A security check around updates: batches, allowed fields, false positives

Advanced case 2 of Update.

A security check around updates (`SecurityGuard`, in the project root), in four scenarios: an API batch of changes passes through; an attack hidden in one item refuses the whole batch and is logged; a list of allowed fields stops unknown names before ARX; and a false positive from your own configuration is reported as a bug, not an attack, so nobody is blocked.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var guard = new SecurityGuard(blockAfter: 3);
var accounts = new List<ArxEntity>();
foreach (var number in new[] { "ACC-G401", "ACC-G402", "ACC-G403" })
{
    var account = new ArxEntity("account");
    account["name"] = $"Guarded {number}";
    account["account_number"] = number;
    account["employee_count"] = 10;
    accounts.Add(account);
}
var ids = await service.CreateMultipleAsync(accounts);
```

#### 1. A batch of changes passes through

A batch of two changes goes through the guard untouched.

In scope: `service` (ArxMongoDbService), `guard` (SecurityGuard), `ids` (IReadOnlyList<ArxRecordId>).

```csharp
var result = await PatchAccountsAsync(service, guard, "ana", new[]
{
    (ids[0], new Dictionary<string, object> { ["employee_count"] = 11 }),
    (ids[1], new Dictionary<string, object> { ["employee_count"] = 12 }),
});

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

Defined in the sample, used above:

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

    foreach (var (id, fields) in items)
    {
        var change = new ArxEntity("account");
        change.RecordId = id;

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

        changes.Add(change);
    }

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

Check it in the database:

```sql
db.account.find({ account_number: { $in: ["ACC-G401", "ACC-G402"] } }, { account_number: 1, employee_count: 1 }).sort({ account_number: 1 })
```

Expect: 2 documents: ACC-G401 11, ACC-G402 12.

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

One item of three carries SQL in a field name: ARX refuses the whole UpdateMultiple, the guard logs it.

In scope: `service` (ArxMongoDbService), `guard` (SecurityGuard), `ids` (IReadOnlyList<ArxRecordId>).

```csharp
var result = await PatchAccountsAsync(service, guard, "mallory", new[]
{
    (ids[0], new Dictionary<string, object> { ["employee_count"] = 0 }),
    (ids[1], new Dictionary<string, object> { ["employee_count\" = 0, \"name"] = "pwned" }),
    (ids[2], new Dictionary<string, object> { ["employee_count"] = 0 }),
});

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

Check it in the database:

```sql
db.account.countDocuments({ account_number: { $in: ["ACC-G401", "ACC-G402", "ACC-G403"] }, employee_count: 0 })
```

Expect: 0: not even the two harmless items were stored.

#### 3. Allowed fields stop unknown names before ARX

Defence in depth: the endpoint checks the field names against its allowed list before calling ARX. An unknown field is a client error (a 400), not a security event, and ARX never sees it.

In scope: `service` (ArxMongoDbService), `guard` (SecurityGuard), `ids` (IReadOnlyList<ArxRecordId>).

```csharp
var posted = new Dictionary<string, object> { ["credit_limit"] = 1_000_000m, ["employee_count"] = 13 };
var unknown = posted.Keys.Where(x => !AllowedFields.Contains(x)).ToList();

if (unknown.Count > 0)
{
    Console.WriteLine($"400 Bad Request: fields not allowed: {string.Join(", ", unknown)}; security log still has {guard.Events.Count} entries");
}
else
{
    await PatchAccountsAsync(service, guard, "ana", new[] { (ids[2], posted) });
}
```

Defined in the sample, used above:

```csharp
private static readonly HashSet<string> AllowedFields = new(StringComparer.Ordinal) { "name", "website", "employee_count" };

private static Task<GuardResult<int>> PatchAccountsAsync(ArxMongoDbService service, SecurityGuard guard, string user, IReadOnlyList<(ArxRecordId Id, Dictionary<string, object> Fields)> items)
{
    var changes = new List<ArxEntity>();

    foreach (var (id, fields) in items)
    {
        var change = new ArxEntity("account");
        change.RecordId = id;

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

        changes.Add(change);
    }

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

Check it in the database:

```sql
db.account.find({ account_number: "ACC-G403" }, { credit_limit: 1, employee_count: 1 })
```

Expect: 1 document: no credit_limit field, employee_count 10 (the request was refused before ARX).

#### 4. A false positive from your own configuration

A false positive from your own configuration: a nightly job reads the column to reset from appsettings, and someone pasted it with a trailing ";". ARX refuses it as SQL (ARX1017), but the names came from code (`Code`), so the guard logs a bug, blocks nobody, and the job's owner fixes the setting.

In scope: `service` (ArxMongoDbService), `guard` (SecurityGuard), `ids` (IReadOnlyList<ArxRecordId>).

```csharp
var columnFromSettings = "website;";

foreach (var column in new[] { columnFromSettings, columnFromSettings.TrimEnd(';') })
{
    var reset = new ArxEntity("account");
    reset.RecordId = ids[2];
    reset[column] = "https://example.com/guarded";

    var result = await guard.RunAsync("nightly-job", "reset website", NameSource.Code, () => service.UpdateAsync(reset));

    Console.WriteLine($"'{column}': {result.Outcome}" + (result.Outcome == GuardOutcome.Refused ? $", logged as a {guard.Events[^1].Kind}; job blocked: {guard.IsBlocked("nightly-job")}" : ""));
}
```

Check it in the database:

```sql
db.account.find({ account_number: "ACC-G403" }, { website: 1 })
```

Expect: 1 document: https://example.com/guarded (written once the setting was fixed).

### Set up the service from the database's schema

Advanced case 3 of Update.

The service set up from the database instead of by hand, then updating through it, in two scenarios: preload the lead collection at startup, and updating with Strict on, where only the preloaded fields may be named.

Good to know:

`ArxPreLoadTable` reads, once, each collection's id, its unique indexes (as keys) and the field types of its `$jsonSchema` validator. With `Strict`, a field the preload didn't load is ARX1018 before anything is sent.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var lead = new ArxEntity("lead");
lead["email"] = "lead-m841@example.com";
lead["full_name"] = "Nuno Teixeira";
lead["score"] = 40;
await service.CreateAsync(lead);
```

#### 1. Preload at startup

Preload the lead collection once, at startup, and make the service Strict.

In scope: `connectionString` (string).

```csharp
var catalog = await new ArxPreLoadTable(new ArxPreLoadTableRegister().PreLoadTable("lead")).LoadAsync(connectionString);
var service = new ArxMongoDbService(connectionString, catalog) { Strict = true };

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

return service;
```

Check it in the database:

```sql
db.lead.getIndexes()
```

Expect: _id_ and the unique email_1 index.

#### 2. Update by the preloaded key

Update by the preloaded key; a misspelt field is refused (ARX1018), the right one is written.

In scope: `service` (ArxMongoDbService).

```csharp
var typo = new ArxEntity("lead");
typo["email"] = "lead-m841@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-m841@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
db.lead.find({ email: "lead-m841@example.com" }, { score: 1, scroe: 1 })
```

Expect: 1 document, score 90 and no scroe field.

---

# UpdateMultiple with ARX.SDK on MongoDB

## What an UpdateMultiple does

| Rule | On MongoDB |
|---|---|
| Many entities | UpdateMultiple is all or nothing, in a transaction; the same entity or the same match values twice is ARX1001 |
| By a filter | `UpdateMultipleAsync(values, filter)` and `DeleteMultipleAsync(table, filter)`: one `updateMany` or `deleteMany` in a transaction, returning the count; no id in the values, an empty filter needs `AllRows` (Basic2). From 0.1.0-preview.5 |

## The samples

Every UpdateMultiple sample, in reading order. Each scenario shows the code (verified against MongoDB) and the mongosh query that checks what it did. `service` is the application's `ArxMongoDbService`; 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-mongodb-sample-application.md.

### Update many records in one call

Basics 1 of UpdateMultiple.

Many documents in one call, all or nothing, in two scenarios: a different value for each document, and a list with one id no document has, where none of them changes.

Good to know:

UpdateMultiple runs in a transaction, which MongoDB offers on a replica set (one node is enough) or a sharded cluster; on a standalone server it is refused rather than half done. Each entity writes only the fields it sets.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var accounts = new List<ArxEntity>();
foreach (var number in new[] { "ACC-M921", "ACC-M922", "ACC-M923" })
{
    var account = new ArxEntity("account");
    account["name"] = "Many updates " + number;
    account["account_number"] = number;
    account["employee_count"] = 10;
    accounts.Add(account);
}
var ids = await service.CreateMultipleAsync(accounts);
```

#### 1. A value per document

One entity per document, each with its own value: the call returns how many changed.

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

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

for (var i = 0; i < ids.Count; i++)
{
    var change = new ArxEntity("account") { RecordId = ids[i] };
    change["employee_count"] = 100 + i;
    changes.Add(change);
}

int changed = await service.UpdateMultipleAsync(changes);

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

Check it in the database:

```sql
db.account.find({ account_number: { $in: ["ACC-M921", "ACC-M922", "ACC-M923"] } }, { account_number: 1, employee_count: 1 }).sort({ account_number: 1 })
```

Expect: 3 documents, employee_count 100, 101, 102.

#### 2. All or nothing

The second id belongs to no document: RecordNotFound, and the first document keeps its value too.

In scope: `service` (ArxMongoDbService), `existing` (ArxRecordId).

```csharp
var first = new ArxEntity("account") { RecordId = existing };
first["employee_count"] = 999;

var missing = new ArxEntity("account");
missing["account_id"] = "65a000000000000000000000";
missing["employee_count"] = 999;

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
db.account.find({ account_number: "ACC-M921" }, { employee_count: 1 })
```

Expect: employee_count 100 (not 999).

### Update or delete every document a filter matches

Basics 2 of UpdateMultiple.

Every document a filter matches, changed or deleted in one call: one updateMany (or deleteMany) with the filter as a query document, in a transaction, so a document refused half-way leaves every document as it was.

Good to know:

The filter takes the operators of a query on the collection's own fields; values are never read as query operators.

The entity carries only the values to set: an id or _id is refused (ARX1001), and so is an empty filter unless new ArxFilterWriteOptions { AllRows = true } is passed.

Needs ARX.SDK 0.1.0-preview.5 or later, and a replica set (one node is enough).

Usings:

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

Set up by the sample before its scenarios:

```csharp
var accounts = new List<ArxEntity>();
for (var i = 1; i <= 5; i++)
{
    var account = new ArxEntity("account");
    account["name"] = $"Filter update {i}";
    account["account_number"] = $"ACC-MF0{i}";
    account["employee_count"] = i * 5;
    account["status"] = "ACTIVE";
    accounts.Add(account);
}
await service.CreateMultipleAsync(accounts);
```

#### 1. Update by a filter

Small accounts put on hold with one updateMany.

In scope: `service` (ArxMongoDbService).

```csharp
var change = new ArxEntity("account");
change["status"] = "ON_HOLD";

var filter = new ArxFilterExpression();
filter.AddCondition("account_number", ArxConditionOperator.BeginsWith, "ACC-MF0");
filter.AddCondition("employee_count", ArxConditionOperator.LessThan, 15);

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

Console.WriteLine($"{changed} accounts put on hold");
```

Check it in the database:

```sql
db.account.find({ account_number: /^ACC-MF0/ }, { account_number: 1, employee_count: 1, status: 1 }).sort({ account_number: 1 })
```

Expect: 5 documents; ACC-MF01 and ACC-MF02 (5 and 10 employees) ON_HOLD, the other three ACTIVE.

#### 2. Delete by a filter

The accounts on hold deleted with one deleteMany; the call returns how many went.

In scope: `service` (ArxMongoDbService).

```csharp
var filter = new ArxFilterExpression();
filter.AddCondition("account_number", ArxConditionOperator.BeginsWith, "ACC-MF0");
filter.AddCondition("status", ArxConditionOperator.Equal, "ON_HOLD");

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

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

Check it in the database:

```sql
db.account.find({ account_number: /^ACC-MF0/ }, { account_number: 1, status: 1 }).sort({ account_number: 1 })
```

Expect: 3 documents, ACC-MF03 to ACC-MF05, all ACTIVE.

---

# Delete with ARX.SDK on MongoDB

## What a delete does

| Rule | On MongoDB |
|---|---|
| By id | `DeleteAsync(collection, id)` returns whether a document went; nothing to delete is false, not an error |
| By an entity | By the declared id, a key or any fields; returns how many went (0 is a normal outcome) |
| RequireSingleRow | None is ARX2002, several ARX2003, and nothing is deleted |
| Match values | A document or array given as a match value is compared as data, never run as an operator |
| 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)`: 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 MongoDB) and the mongosh query that checks what it did. `service` is the application's `ArxMongoDbService`; 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-mongodb-sample-application.md.

### Delete one record by id

Basics 1 of Delete.

Delete by id, in two scenarios: one document, and one that is already gone (not an error).

Good to know:

`DeleteAsync(collection, id)` returns whether a document went. Many ids in one call are DeleteMultiple's.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "To delete ACC-M901";
account["account_number"] = "ACC-M901";
var id = await service.CreateAsync(account);
```

#### 1. One document

Delete the account by the id Create returned.

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

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

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

Check it in the database:

```sql
db.account.countDocuments({ account_number: "ACC-M901" })
```

Expect: 0.

#### 2. Already gone

The same id again: nothing to delete, false, no error.

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

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

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

Check it in the database:

```sql
db.account.countDocuments({ account_number: "ACC-M901" })
```

Expect: 0.

### Choose how the documents are found

Basics 2 of Delete.

Delete by a key or by any fields, in three scenarios: a compound key, every document with a field's value, and RequireSingleRow, which refuses to delete several.

Good to know:

Finding nothing is a normal outcome (0) unless RequireSingleRow is on; then it is ARX2002, and several ARX2003.

Usings:

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

Set up by the sample before its scenarios:

```csharp
foreach (var (region, sku) in new[] { ("PT", "SKU-M911"), ("PT", "SKU-M912"), ("ES", "SKU-M911") })
{
    var price = new ArxEntity("price_list");
    price["region"] = region;
    price["sku"] = sku;
    price["unit_price"] = 10m;
    price["batch"] = "M91";
    await service.CreateAsync(price);
}
```

#### 1. By a compound key

The price of SKU-M911 in PT, by the region and SKU together.

In scope: `service` (ArxMongoDbService).

```csharp
var match = new ArxEntity("price_list");
match["region"] = "PT";
match["sku"] = "SKU-M911";

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

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

Check it in the database:

```sql
db.price_list.find({ batch: "M91" }, { region: 1, sku: 1 })
```

Expect: 2 documents: PT SKU-M912, ES SKU-M911.

#### 2. Require exactly one

RequireSingleRow with a field two documents share: ARX2003, nothing deleted.

In scope: `service` (ArxMongoDbService).

```csharp
var match = new ArxEntity("price_list");
match["batch"] = "M91";

try
{
    await service.DeleteAsync(match, new ArxDeleteOptions { Match = ArxMatch.Columns("batch"), RequireSingleRow = true });
}
catch (ArxException exception) when (exception.Code == ArxErrorCode.MultipleRecordsMatched)
{
    Console.WriteLine(exception.Message);
}
```

Check it in the database:

```sql
db.price_list.countDocuments({ batch: "M91" })
```

Expect: 2.

#### 3. By any fields

Every price of the batch.

In scope: `service` (ArxMongoDbService).

```csharp
var match = new ArxEntity("price_list");
match["batch"] = "M91";

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

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

Check it in the database:

```sql
db.price_list.countDocuments({ batch: "M91" })
```

Expect: 0.

### Catch an injection attempt

Real case 1 of Delete.

Two attacks on a "delete by" endpoint: a match field named like an operator, and a match value built to match every document. Neither deletes anything.

Good to know:

A document with operator keys given as a match value would run as a query in MongoDB's shorthand equality; ARX wraps it in `$eq`, so it is compared as data and finds nothing.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var contact = new ArxEntity("contact");
contact["email"] = "victim-m921@example.com";
contact["segment"] = "M92";
await service.CreateAsync(contact);
```

#### 1. An operator as a match field

"Delete where $where": ARX1017 before anything is sent.

In scope: `service` (ArxMongoDbService).

```csharp
var match = new ArxEntity("contact");
match["$where"] = "true";

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

Check it in the database:

```sql
db.contact.countDocuments({ segment: "M92" })
```

Expect: 1.

#### 2. A match built to find everything

A segment posted as `{ "$exists": true }`: compared as data, it deletes nothing.

In scope: `service` (ArxMongoDbService).

```csharp
var match = new ArxEntity("contact");
match["segment"] = new ArxJsonB { SerializedValue = "{ \"$exists\": true }" };

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

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

Check it in the database:

```sql
db.contact.countDocuments({ segment: "M92" })
```

Expect: 1: the victim is still there.

### 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 half way and leaves the old order in place.

Good to know:

MongoDB can't change a document's `_id`, so a new id means a new document: delete and create, in one transaction, so a reader never sees neither or both.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var order = new ArxEntity("sales_order");
order["order_number"] = "SO-M931";
order["total"] = 40m;
var oldId = await service.CreateAsync(order);
```

#### 1. Replace

Delete SO-M931 and create SO-M932 in its place, committed together.

In scope: `service` (ArxMongoDbService), `oldId` (ArxRecordId).

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

await service.DeleteAsync("sales_order", oldId, transaction);

var replacement = new ArxEntity("sales_order");
replacement["order_number"] = "SO-M932";
replacement["total"] = 45m;
await service.CreateAsync(replacement, transaction);

await transaction.CommitAsync();
Console.WriteLine("SO-M931 replaced by SO-M932");
```

Check it in the database:

```sql
db.sales_order.find({ order_number: { $in: ["SO-M931", "SO-M932"] } }, { order_number: 1 })
```

Expect: 1 document, SO-M932.

#### 2. A failed replacement

The new order repeats an existing number: the transaction is rolled back, SO-M932 stays.

In scope: `service` (ArxMongoDbService).

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

var match = new ArxEntity("sales_order");
match["order_number"] = "SO-M932";
await service.DeleteAsync(match, transaction, new ArxDeleteOptions { Match = ArxMatch.Key("order_number") });

var first = new ArxEntity("sales_order");
first["order_number"] = "SO-M933";
var second = new ArxEntity("sales_order");
second["order_number"] = "SO-M933";

try
{
    await service.CreateMultipleAsync(new[] { first, second }, transaction);
    await transaction.CommitAsync();
}
catch (ArxDatabaseException exception)
{
    Console.WriteLine(exception.Message);
    await transaction.RollbackAsync();
}
```

Check it in the database:

```sql
db.sales_order.find({ order_number: { $in: ["SO-M932", "SO-M933"] } }, { order_number: 1 })
```

Expect: 1 document, SO-M932: the delete was rolled back with the rest.

### A security check around deletes: probing, blocking, unblocking a false positive

Advanced case 2 of Delete.

A security check around deletes (`SecurityGuard`, in the project root), in four scenarios: a delete through an allowed list of types passes; an endpoint that forgot the list is probed, and the attacker is blocked after three attempts; and a false positive, an old client sending a harmless type name, is triaged: support unblocks the user and the endpoint maps the old name.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var guard = new SecurityGuard(blockAfter: 3);
var accounts = new List<ArxEntity>();
foreach (var number in new[] { "ACC-G301", "ACC-G302", "ACC-G303", "ACC-G304" })
{
    var account = new ArxEntity("account");
    account["name"] = $"Guarded delete {number}";
    account["account_number"] = number;
    accounts.Add(account);
}
var ids = await service.CreateMultipleAsync(accounts);
```

#### 1. A delete through the allowed types

`DELETE /api/accounts/{id}` through the allowed types: the account is deleted.

In scope: `service` (ArxMongoDbService), `guard` (SecurityGuard), `id` (ArxRecordId).

```csharp
var result = await DeleteThroughTheListAsync(service, guard, "ana", "accounts", id);
var unknown = await DeleteThroughTheListAsync(service, guard, "ana", "account; DROP TABLE account", id);

Console.WriteLine($"ana: {result.Outcome}, deleted = {result.Value}; an unknown type: \"{unknown.ForTheUser}\" (never reaches ARX)");
```

Defined in the sample, used above:

```csharp
private static readonly Dictionary<string, string> Types = new(StringComparer.OrdinalIgnoreCase)
{
    ["accounts"] = "account",
    ["contacts"] = "contact",
};

private static Task<GuardResult<bool>> DeleteThroughTheListAsync(ArxMongoDbService service, SecurityGuard guard, string user, string type, ArxRecordId id)
    => Types.TryGetValue(type, out var table)
        ? guard.RunAsync(user, $"delete {type}", NameSource.Code, () => service.DeleteAsync(table, id))
        : Task.FromResult(new GuardResult<bool>(GuardOutcome.Refused, false, $"404 Not Found: no type '{type}'."));
```

Check it in the database:

```sql
db.account.countDocuments({ account_number: "ACC-G301" })
```

Expect: 0: deleted.

#### 2. Probing the legacy endpoint gets the user blocked

The legacy endpoint is probed three times: each probe is refused by ARX (ARX1017) and logged; then the user is blocked.

In scope: `service` (ArxMongoDbService), `guard` (SecurityGuard), `id` (ArxRecordId).

```csharp
foreach (var probe in new[] { "account WHERE 1=1; --", "account\"; TRUNCATE account; --", "account UNION SELECT 1" })
{
    await DeleteDirectlyAsync(service, guard, "mallory", probe, id);
}

var valid = await DeleteDirectlyAsync(service, guard, "mallory", "account", id);

Console.WriteLine($"mallory: {guard.Events.Count(x => x.User == "mallory")} events logged, blocked: {guard.IsBlocked("mallory")}; a valid delete: {valid.Outcome}");
```

Defined in the sample, used above:

```csharp
private static Task<GuardResult<bool>> DeleteDirectlyAsync(ArxMongoDbService service, SecurityGuard guard, string user, string type, ArxRecordId id)
    => guard.RunAsync(user, $"delete {type}", NameSource.UserInput, () => service.DeleteAsync(type, id));
```

Check it in the database:

```sql
db.account.countDocuments({ account_number: "ACC-G302" })
```

Expect: 1: nothing was deleted, not even by the valid request after the block.

#### 3. A false positive is triaged: unblock, then map the old name

A false positive: an old mobile client sends the type as "account (archived)". The parentheses make ARX flag it as SQL, the client retries, and after three strikes the user is blocked. Support reads the log, sees the same harmless name three times, unblocks the user, and the endpoint maps the old name to its table.

In scope: `service` (ArxMongoDbService), `guard` (SecurityGuard), `first` (ArxRecordId), `second` (ArxRecordId).

```csharp
for (var retry = 0; retry < 3; retry++)
{
    await DeleteDirectlyAsync(service, guard, "sam", "account (archived)", first);
}

Console.WriteLine($"sam blocked: {guard.IsBlocked("sam")}; the log shows: {string.Join(" | ", guard.Events.Where(x => x.User == "sam").Select(x => x.Action).Distinct())}");

// Triage: the same harmless name three times, from a known old client. Unblock, and map the old name.
guard.Unblock("sam");
Types["account (archived)"] = "account";

var afterTheFix = await DeleteThroughTheListAsync(service, guard, "sam", "account (archived)", second);

Console.WriteLine($"after the fix: {afterTheFix.Outcome}, deleted = {afterTheFix.Value}");
```

Defined in the sample, used above:

```csharp
private static readonly Dictionary<string, string> Types = new(StringComparer.OrdinalIgnoreCase)
{
    ["accounts"] = "account",
    ["contacts"] = "contact",
};

private static Task<GuardResult<bool>> DeleteThroughTheListAsync(ArxMongoDbService service, SecurityGuard guard, string user, string type, ArxRecordId id)
    => Types.TryGetValue(type, out var table)
        ? guard.RunAsync(user, $"delete {type}", NameSource.Code, () => service.DeleteAsync(table, id))
        : Task.FromResult(new GuardResult<bool>(GuardOutcome.Refused, false, $"404 Not Found: no type '{type}'."));

private static Task<GuardResult<bool>> DeleteDirectlyAsync(ArxMongoDbService service, SecurityGuard guard, string user, string type, ArxRecordId id)
    => guard.RunAsync(user, $"delete {type}", NameSource.UserInput, () => service.DeleteAsync(type, id));
```

Check it in the database:

```sql
db.account.find({ account_number: { $in: ["ACC-G303", "ACC-G304"] } }, { account_number: 1 })
```

Expect: 1 document: ACC-G303 (the blocked attempts deleted nothing); ACC-G304 was deleted after the fix.

### Set up the service from the database's schema

Advanced case 3 of Delete.

The service set up from the database instead of by hand, then deleting through it, in two scenarios: preload the lead collection at startup, and deleting with Strict on, where only the preloaded fields may be named.

Good to know:

`ArxPreLoadTable` reads, once, each collection's id, its unique indexes (as keys) and the field types of its `$jsonSchema` validator. With `Strict`, a field the preload didn't load is ARX1018 before anything is sent.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var lead = new ArxEntity("lead");
lead["email"] = "lead-m851@example.com";
lead["full_name"] = "Nuno Teixeira";
lead["score"] = 40;
await service.CreateAsync(lead);
```

#### 1. Preload at startup

Preload the lead collection once, at startup, and make the service Strict.

In scope: `connectionString` (string).

```csharp
var catalog = await new ArxPreLoadTable(new ArxPreLoadTableRegister().PreLoadTable("lead")).LoadAsync(connectionString);
var service = new ArxMongoDbService(connectionString, catalog) { Strict = true };

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

return service;
```

Check it in the database:

```sql
db.lead.getIndexes()
```

Expect: _id_ and the unique email_1 index.

#### 2. Delete by the preloaded key

Delete by the preloaded key; a field the preload doesn't know is refused (ARX1018).

In scope: `service` (ArxMongoDbService).

```csharp
var unknown = new ArxEntity("lead");
unknown["source"] = "fair";

try
{
    await service.DeleteAsync(unknown, new ArxDeleteOptions { Match = ArxMatch.Columns("source") });
}
catch (ArxValidationException exception) when (exception.Code == ArxErrorCode.NotPreloaded)
{
    Console.WriteLine(exception.Message);
}

var match = new ArxEntity("lead");
match["email"] = "lead-m851@example.com";
int deleted = await service.DeleteAsync(match, new ArxDeleteOptions { Match = ArxMatch.Key("email") });
Console.WriteLine($"{deleted} lead deleted");
```

Check it in the database:

```sql
db.lead.countDocuments({ email: "lead-m851@example.com" })
```

Expect: 0.

---

# DeleteMultiple with ARX.SDK on MongoDB

## What a DeleteMultiple does

| Rule | On MongoDB |
|---|---|
| By many ids | One `deleteMany` with `$in`; returns how many went |
| Many entities | DeleteMultiple is all or nothing when it checks; the same entity or match values twice is ARX1001 |

## The samples

Every DeleteMultiple sample, in reading order. Each scenario shows the code (verified against MongoDB) and the mongosh query that checks what it did. `service` is the application's `ArxMongoDbService`; 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-mongodb-sample-application.md.

### Delete many records in one call

Basics 1 of DeleteMultiple.

Many documents in one call, in two scenarios: by the ids Create returned, and by a list of entities found by the declared key (the account number).

Good to know:

By ids, ARX sends one `deleteMany` with `$in`. By entities, the deletes run in one transaction (a replica set or a sharded cluster). Either way the call returns how many documents went: an id or a key no document has isn't an error.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var ids = new List<ArxRecordId>();
foreach (var number in new[] { "ACC-M902", "ACC-M903", "ACC-M904", "ACC-M905" })
{
    var account = new ArxEntity("account");
    account["name"] = "To delete " + number;
    account["account_number"] = number;
    ids.Add(await service.CreateAsync(account));
}
```

#### 1. By ids

Two ids in one call.

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

```csharp
int deleted = await service.DeleteMultipleAsync("account", ids);

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

Check it in the database:

```sql
db.account.countDocuments({ account_number: { $in: ["ACC-M902", "ACC-M903"] } })
```

Expect: 0.

#### 2. By the declared key

A list of account numbers, one of them unknown: the two found go.

In scope: `service` (ArxMongoDbService).

```csharp
var requests = new[] { "ACC-M904", "ACC-M905", "ACC-M999" }.Select(number =>
{
    var request = new ArxEntity("account");
    request["account_number"] = number;
    return request;
}).ToList();

int deleted = await service.DeleteMultipleAsync(requests, new ArxDeleteOptions { Match = ArxMatch.Key("account_number") });

Console.WriteLine($"{deleted} of {requests.Count} documents deleted");
```

Check it in the database:

```sql
db.account.countDocuments({ account_number: { $in: ["ACC-M904", "ACC-M905"] } })
```

Expect: 0.

---

# Retrieve with ARX.SDK on MongoDB

## What a read does

| Rule | On MongoDB |
|---|---|
| One record | By id, or by a key or fields; not found is null; several matches are ARX2003 |
| Lookups | A declared lookup reads back as an `ArxEntityReference` with the referenced record's display name |
| Field names | Case-sensitive in MongoDB: a field the catalog declares is read and written with its declared spelling whatever the attribute's case; an undeclared field by its exact spelling (`Name` and `name` are two fields) |
| Functions and Execute | Not offered: MongoDB has no stored procedures or functions that return rows (ARX8001) |

## Reference

### Value types read back

| Stored as | Read back as |
|---|---|
| string | `string` |
| 32-bit integer | `int` |
| 64-bit integer | `long` |
| double | `double` |
| Decimal128 | `decimal` (ARX4001 past .NET's range) |
| boolean | `bool` |
| date | `DateTime`, UTC (a `DateOnly` is stored as midnight UTC: read it with `GetAttributeValue<DateOnly>`, which converts a midnight `DateTime` from 0.1.0-preview.5, or preload the field) |
| ObjectId | `string` (24 hexadecimal characters) |
| binary subtype 4 | `Guid` |
| other binary | `byte[]` |
| embedded document | `ArxJsonB` |
| array of one kind of value | a typed array (`string[]`, `int[]`, …); any other array as `ArxJsonB` |
| a declared lookup | `ArxEntityReference` |
| a preloaded field | the preloaded type (`DateOnly` from a date, `TimeOnly` from text, …) |

### Aggregates

| Aggregate | On MongoDB | Reads back as |
|---|---|---|
| Count, CountBig, CountDistinct, CountBigDistinct | `$sum` of 1 (or of the documents where the field is set), `$addToSet` then its size, as a 64-bit integer | `long` |
| Sum | `$sum`: of 32-bit integers, a 32-bit integer while it fits and a 64-bit one past it; of doubles, a double; of Decimal128, a Decimal128. A field no document has sums to 0 | `int`, `long`, `double` or `decimal` |
| Avg | `$avg`: a double (a Decimal128 for Decimal128 values) | `double` or `decimal` |
| Min, Max | `$min`, `$max` | the field's type |

## The samples

Every Retrieve sample, in reading order. Each scenario shows the code (verified against MongoDB) and the mongosh query that checks what it did. `service` is the application's `ArxMongoDbService`; 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-mongodb-sample-application.md.

### Read one record

Basics 1 of Retrieve.

Read one document, in three scenarios: by its id with every field, with chosen fields, and by a key; an id no document has reads null.

Good to know:

The id comes back under the declared column's name (`account_id`) and as `RecordId`.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "Consolidated Messenger";
account["account_number"] = "ACC-M951";
account["employee_count"] = 75;
var id = await service.CreateAsync(account);
```

#### 1. By id

By the id Create returned: every field.

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

```csharp
ArxEntity? account = await service.RetrieveAsync("account", id);

Console.WriteLine($"{account?["name"]}, {account?["employee_count"]} employees, id {account?["account_id"]}");
```

Check it in the database:

```sql
db.account.find({ account_number: "ACC-M951" })
```

Expect: 1 document, Consolidated Messenger, employee_count 75.

#### 2. Chosen fields

Only the fields you name (the id always comes too).

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

```csharp
ArxEntity? account = await service.RetrieveAsync("account", id, new ArxColumnSet("name"));

Console.WriteLine($"fields read: {string.Join(", ", account!.Attributes.Keys)}");
```

Check it in the database:

```sql
db.account.find({ account_number: "ACC-M951" }, { name: 1 })
```

Expect: 1 document, _id and name.

#### 3. By a key

By the account number (a declared key), and a number no account has: null.

In scope: `service` (ArxMongoDbService).

```csharp
var match = new ArxEntity("account");
match["account_number"] = "ACC-M951";
ArxEntity? found = await service.RetrieveAsync(match, new ArxRetrieveOptions { Match = ArxMatch.Key("account_number") });

match["account_number"] = "ACC-M959";
ArxEntity? missing = await service.RetrieveAsync(match, new ArxRetrieveOptions { Match = ArxMatch.Key("account_number") });

Console.WriteLine($"ACC-M951: {found?["name"]}; ACC-M959: {(missing == null ? "null" : "found")}");
```

Check it in the database:

```sql
db.account.countDocuments({ account_number: "ACC-M959" })
```

Expect: 0.

---

# RetrieveMultiple with ARX.SDK on MongoDB

## What a RetrieveMultiple does

| Rule | On MongoDB |
|---|---|
| A query | One aggregation pipeline: `$match`, `$lookup` for links, `$group` for aggregates, `$sort`, the page |
| **Aggregates** | Count is a `long` on every provider. Sum and Avg read back in the database's own type (the Retrieve page's Aggregates table): code that runs on several providers reads them with `Convert.ToDecimal(row["total"])`. |
| **Distinct** | `query.Distinct = true` returns rows that differ in no selected column once. The id isn't added to a distinct (or aggregate) query, since it would make every row distinct, so its rows have no `RecordId`: select the id column if you need it |
| Conditions | Equality, comparisons, In, Between, patterns (escaped regular expressions; Contains, BeginsWith and EndsWith ignore case) |
| Field paths | A dotted name in a condition, an order or a column set is a link's field when its first part is the link's alias (`acc.name`), and otherwise a path into an embedded document (`address.city`), each part checked by the name rules. The SQL providers refuse a dotted name that names no link (ARX1001) |
| Null | `Null` matches a stored null and a missing field; NotEqual and the other negations never match either |
| Joins | Inner and left (`$lookup`); a right join is ARX8001; link fields come back as `alias.field` |
| Paging | By page number (with the total in the same round trip), or by the paging cookie each page returns |
| 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) |

## The samples

Every RetrieveMultiple sample, in reading order. Each scenario shows the code (verified against MongoDB) and the mongosh query that checks what it did. `service` is the application's `ArxMongoDbService`; 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-mongodb-sample-application.md.

### Read many records

Basics 1 of RetrieveMultiple.

Read many documents with conditions and an order, in three scenarios: comparisons and an order, text patterns, and what null means on MongoDB.

Good to know:

Contains, BeginsWith and EndsWith ignore case, and their text is never read as a regular expression. `Null` matches a stored null and a missing field; NotEqual and the other negations never match either, as in SQL.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var contacts = new List<ArxEntity>();
foreach (var (first, last, score) in new[] { ("Ana", "Silva", (int?)80), ("Bruno", "Costa", 55), ("Carla", "Sá", null), ("Diogo", "Silva (Jr.)", 91) })
{
    var contact = new ArxEntity("contact");
    contact["first_name"] = first;
    contact["last_name"] = last;
    contact["segment"] = "M96";

    if (score != null)
    {
        contact["score"] = score;
    }

    contacts.Add(contact);
}
await service.CreateMultipleAsync(contacts);
```

#### 1. Comparisons and an order

A score of 60 or more, best first.

In scope: `service` (ArxMongoDbService).

```csharp
var query = Segment();
query.Criteria.AddCondition("score", ArxConditionOperator.GreaterEqual, 60);
query.AddOrder("score", ArxOrderType.Descending);

var page = await service.RetrieveMultipleAsync(query);

Console.WriteLine(string.Join(", ", page.Entities.Select(x => $"{x["first_name"]} {x["score"]}")));
```

Defined in the sample, used above:

```csharp
private static ArxQueryExpression Segment()
{
    var query = new ArxQueryExpression("contact");
    query.ColumnSet.AddColumns("first_name", "last_name", "score");
    query.Criteria.AddCondition("segment", ArxConditionOperator.Equal, "M96");
    return query;
}
```

Check it in the database:

```sql
db.contact.find({ segment: "M96", score: { $gte: 60 } }, { first_name: 1, score: 1 }).sort({ score: -1 })
```

Expect: 2 documents: Diogo 91, Ana 80.

#### 2. Text patterns

Last names containing "silva", any case; "(Jr.)" is plain text, not a pattern.

In scope: `service` (ArxMongoDbService).

```csharp
var silva = Segment();
silva.Criteria.AddCondition("last_name", ArxConditionOperator.Contains, "silva");

var junior = Segment();
junior.Criteria.AddCondition("last_name", ArxConditionOperator.EndsWith, "(Jr.)");

Console.WriteLine($"contains silva: {(await service.RetrieveMultipleAsync(silva)).Entities.Count}; ends with (Jr.): {(await service.RetrieveMultipleAsync(junior)).Entities.Count}");
```

Check it in the database:

```sql
db.contact.countDocuments({ segment: "M96", last_name: /silva/i })
```

Expect: 2.

#### 3. What null means

Carla has no score: Null finds her; NotEqual 80 doesn't.

In scope: `service` (ArxMongoDbService).

```csharp
var noScore = Segment();
noScore.Criteria.AddCondition("score", ArxConditionOperator.Null);

var notEighty = Segment();
notEighty.Criteria.AddCondition("score", ArxConditionOperator.NotEqual, 80);

Console.WriteLine($"no score: {string.Join(", ", (await service.RetrieveMultipleAsync(noScore)).Entities.Select(x => x["first_name"]))}; "
    + $"score not 80: {string.Join(", ", (await service.RetrieveMultipleAsync(notEighty)).Entities.Select(x => x["first_name"]))}");
```

Check it in the database:

```sql
db.contact.find({ segment: "M96", score: null }, { first_name: 1 })
```

Expect: 1 document, Carla (her document has no score field).

### Page through results

Basics 2 of RetrieveMultiple.

Page through a result, in two scenarios: by page number with the total, and by the paging cookie each page returns.

Good to know:

A page number skips the earlier documents on the server; the total comes in the same round trip. A cookie says where the next page starts (the last document's sort values and id), so no document is skipped or read twice even when values repeat; pass it back unchanged, with the same query.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var products = Enumerable.Range(1, 7).Select(i =>
{
    var product = new ArxEntity("product");
    product["sku"] = $"SKU-M97{i}";
    product["name"] = $"Product {i}";
    product["category"] = "M97";
    product["price"] = i % 3 * 5m;
    return product;
}).ToList();
await service.CreateMultipleAsync(products);
```

#### 1. By page number

Page 2 of 3, with the total.

In scope: `service` (ArxMongoDbService).

```csharp
var query = Category();
query.PageInfo = new ArxPagingInfo { Count = 3, PageNumber = 2, ReturnTotalRecordCount = true };

var page = await service.RetrieveMultipleAsync(query);

Console.WriteLine($"page {page.CurrentPage} of {page.TotalPageCount} ({page.TotalRecordCount} products): {string.Join(", ", page.Entities.Select(x => x["name"]))}");
```

Defined in the sample, used above:

```csharp
private static ArxQueryExpression Category()
{
    var query = new ArxQueryExpression("product");
    query.ColumnSet.AddColumns("name", "price");
    query.Criteria.AddCondition("category", ArxConditionOperator.Equal, "M97");
    query.AddOrder("price", ArxOrderType.Ascending);
    return query;
}
```

Check it in the database:

```sql
db.product.countDocuments({ category: "M97" })
```

Expect: 7.

#### 2. By cookie

Every page by cookie: pass back the one the previous page gave, until there is none.

In scope: `service` (ArxMongoDbService).

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

do
{
    var query = Category();
    query.PageInfo = new ArxPagingInfo { Count = 3, PagingCookie = cookie };
    var page = await service.RetrieveMultipleAsync(query);
    seen.AddRange(page.Entities.Select(x => (string)x["sku"]!));
    cookie = page.PagingCookie;
}
while (cookie != null);

Console.WriteLine($"read {seen.Count} products, {seen.Distinct().Count()} different");
```

Check it in the database:

```sql
db.product.find({ category: "M97" }, { name: 1, price: 1 }).sort({ price: 1, _id: 1 })
```

Expect: 7 documents, in the order the pages read them.

### Read an account with its contacts

Real case 1 of RetrieveMultiple.

An account and its contacts, three ways: lookups read back as references with the account's name, a join (`$lookup`) from contacts to their account, and a left join that keeps contacts without one.

Good to know:

A declared lookup (`.Lookup("contact", "account_id", "account")`) reads back as an `ArxEntityReference` whose `Name` is the account's display name. A link entity is a `$lookup`: inner or left; its fields come back as `alias.field`.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "City Power & Light";
account["account_number"] = "ACC-M981";
var accountId = await service.CreateAsync(account);
foreach (var (first, last, hasAccount) in new[] { ("Ivo", "Matos", true), ("Joana", "Reis", true), ("Kai", "Lopes", false) })
{
    var contact = new ArxEntity("contact");
    contact["first_name"] = first;
    contact["last_name"] = last;
    contact["segment"] = "M98";

    if (hasAccount)
    {
        contact["account_id"] = new ArxEntityReference("account", accountId);
    }

    await service.CreateAsync(contact);
}
```

#### 1. Lookups with the account's name

Read the contacts: account_id comes back as a reference named after the account.

In scope: `service` (ArxMongoDbService).

```csharp
var query = new ArxQueryExpression("contact");
query.ColumnSet.AddColumns("first_name", "account_id");
query.Criteria.AddCondition("segment", ArxConditionOperator.Equal, "M98");
query.Criteria.AddCondition("account_id", ArxConditionOperator.NotNull);
query.AddOrder("first_name", ArxOrderType.Ascending);

foreach (var contact in (await service.RetrieveMultipleAsync(query)).Entities)
{
    var reference = (ArxEntityReference)contact["account_id"]!;
    Console.WriteLine($"{contact["first_name"]} works at {reference.Name}");
}
```

Check it in the database:

```sql
db.contact.find({ segment: "M98", account_id: { $ne: null } }, { first_name: 1, account_id: 1 })
```

Expect: 2 documents, Ivo and Joana, account_id the account's ObjectId.

#### 2. A join to the account

An inner join: only contacts with an account, with its name and number.

In scope: `service` (ArxMongoDbService).

```csharp
var query = new ArxQueryExpression("contact");
query.ColumnSet.AddColumn("first_name");
query.Criteria.AddCondition("segment", ArxConditionOperator.Equal, "M98");
var link = query.AddLink("account", "account_id", "account_id", ArxJoinOperator.Inner);
link.EntityAlias = "acc";
link.ColumnSet.AddColumns("name", "account_number");
query.AddOrder("first_name", ArxOrderType.Ascending);

foreach (var contact in (await service.RetrieveMultipleAsync(query)).Entities)
{
    Console.WriteLine($"{contact["first_name"]}: {contact["acc.name"]} ({contact["acc.account_number"]})");
}
```

Check it in the database:

```sql
db.contact.aggregate([{ $match: { segment: "M98" } }, { $lookup: { from: "account", localField: "account_id", foreignField: "_id", as: "acc" } }, { $unwind: "$acc" }])
```

Expect: 2 documents, Ivo and Joana, each with the account City Power & Light.

#### 3. A left join

A left join keeps Kai, who has no account: his account fields read null.

In scope: `service` (ArxMongoDbService).

```csharp
var query = new ArxQueryExpression("contact");
query.ColumnSet.AddColumn("first_name");
query.Criteria.AddCondition("segment", ArxConditionOperator.Equal, "M98");
var link = query.AddLink("account", "account_id", "account_id", ArxJoinOperator.Left);
link.EntityAlias = "acc";
link.ColumnSet.AddColumn("name");
query.AddOrder("first_name", ArxOrderType.Ascending);

Console.WriteLine(string.Join(", ", (await service.RetrieveMultipleAsync(query)).Entities.Select(x => $"{x["first_name"]}: {x["acc.name"] ?? "(none)"}")));
```

Check it in the database:

```sql
db.contact.countDocuments({ segment: "M98" })
```

Expect: 3.

### A report by industry

Real case 2 of RetrieveMultiple.

A report from aggregates (`$group`), in two scenarios: counts and sums by industry, and the distinct regions.

Good to know:

Group-by columns and aggregate aliases are the result's attributes; order by either. A count of a field counts the documents that have a value for it, as SQL's COUNT(column).

Usings:

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

Set up by the sample before its scenarios:

```csharp
var accounts = new[] { (Industry.Retail, 120, "North"), (Industry.Retail, 80, "South"), (Industry.Finance, 300, "North"), (Industry.Technology, 45, "North") }
    .Select((x, i) =>
    {
        var account = new ArxEntity("account");
        account["name"] = $"Report account {i}";
        account["account_number"] = $"ACC-M99{i}";
        account["industry"] = new ArxSelectionSetValue("Industry", (int)x.Item1);
        account["employee_count"] = x.Item2;
        account["region"] = x.Item3;
        account["report"] = "M99";
        return account;
    })
    .ToList();
await service.CreateMultipleAsync(accounts);
```

#### 1. Counts and sums by industry

Accounts and employees by industry, the largest first.

In scope: `service` (ArxMongoDbService).

```csharp
var query = new ArxQueryExpression("account");
query.Criteria.AddCondition("report", ArxConditionOperator.Equal, "M99");
query.AggregateInfo = new ArxAggregateInfo();
query.AggregateInfo.AddGroupByColumn("industry");
query.AggregateInfo.AddAggregateColumn("accounts", "", ArxAggregateOperator.Count);
query.AggregateInfo.AddAggregateColumn("employees", "employee_count", ArxAggregateOperator.Sum);
query.AddOrder("employees", ArxOrderType.Descending);

foreach (var row in (await service.RetrieveMultipleAsync(query)).Entities)
{
    Console.WriteLine($"{(Industry)Convert.ToInt32(row["industry"])}: {row["accounts"]} accounts, {row["employees"]} employees");
}
```

Check it in the database:

```sql
db.account.aggregate([{ $match: { report: "M99" } }, { $group: { _id: "$industry", accounts: { $sum: 1 }, employees: { $sum: "$employee_count" } } }, { $sort: { employees: -1 } }])
```

Expect: 3 groups: Finance 1 / 300, Retail 2 / 200, Technology 1 / 45.

#### 2. Distinct regions

The regions the report's accounts are in, once each.

In scope: `service` (ArxMongoDbService).

```csharp
var query = new ArxQueryExpression("account") { Distinct = true };
query.ColumnSet.AddColumn("region");
query.Criteria.AddCondition("report", ArxConditionOperator.Equal, "M99");
query.AddOrder("region", ArxOrderType.Ascending);

Console.WriteLine(string.Join(", ", (await service.RetrieveMultipleAsync(query)).Entities.Select(x => x["region"])));
```

Check it in the database:

```sql
db.account.distinct("region", { report: "M99" })
```

Expect: ["North", "South"].

### Catch an injection attempt

Real case 3 of RetrieveMultiple.

A search endpoint attacked three ways: an operator as the field to search, a regular expression as the text to find, and a sort field built from user input.

Good to know:

Names with `$` or SQL syntax are ARX1017 before anything is sent. Search text is escaped, so `.*` finds the two characters ".*", not everything.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var contact = new ArxEntity("contact");
contact["first_name"] = "Private";
contact["segment"] = "M90";
await service.CreateAsync(contact);
```

#### 1. An operator as the field

Search field "$where": ARX1017.

In scope: `service` (ArxMongoDbService).

```csharp
var query = new ArxQueryExpression("contact");
query.Criteria.AddCondition("$where", ArxConditionOperator.Equal, "this.first_name != ''");

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

Check it in the database:

```sql
db.contact.countDocuments({ segment: "M90" })
```

Expect: 1: still there, and nothing was read.

#### 2. A regular expression as the text

Search text ".*": escaped, it matches no name.

In scope: `service` (ArxMongoDbService).

```csharp
var query = new ArxQueryExpression("contact");
query.Criteria.AddCondition("segment", ArxConditionOperator.Equal, "M90");
query.Criteria.AddCondition("first_name", ArxConditionOperator.Contains, ".*");

Console.WriteLine($"{(await service.RetrieveMultipleAsync(query)).Entities.Count} contacts found");
```

Check it in the database:

```sql
db.contact.countDocuments({ segment: "M90", first_name: { $regex: "\\.\\*", $options: "i" } })
```

Expect: 0.

#### 3. A sort field from the user

A sort field from the query string, with SQL in it: ARX1017.

In scope: `service` (ArxMongoDbService).

```csharp
var query = new ArxQueryExpression("contact");
query.AddOrder("first_name; DROP TABLE contact", ArxOrderType.Ascending);

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

Check it in the database:

```sql
db.getCollectionNames().includes("contact")
```

Expect: true.

### Read your writes in a transaction

Advanced case 1 of RetrieveMultiple.

Reads inside an ARX transaction see its own writes; reads outside don't until the commit. Two scenarios: before and after the commit.

Good to know:

A transaction reads one snapshot of the database, plus its own writes.

Usings:

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

Set up by the sample before its scenarios:

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

#### 1. Before the commit

Two contacts written in the transaction: it reads both, a read outside reads none.

In scope: `service` (ArxMongoDbService), `transaction` (ArxTransaction).

```csharp
var contacts = new[] { "Lia", "Rui" }.Select(name =>
{
    var contact = new ArxEntity("contact");
    contact["first_name"] = name;
    contact["segment"] = "M94";
    return contact;
}).ToList();

await service.CreateMultipleAsync(contacts, transaction);

var inside = (await service.RetrieveMultipleAsync(Imported(), transaction)).Entities.Count;
var outside = (await service.RetrieveMultipleAsync(Imported())).Entities.Count;

Console.WriteLine($"inside the transaction: {inside}; outside: {outside}");
```

Defined in the sample, used above:

```csharp
private static ArxQueryExpression Imported()
{
    var query = new ArxQueryExpression("contact");
    query.Criteria.AddCondition("segment", ArxConditionOperator.Equal, "M94");
    return query;
}
```

Check it in the database:

```sql
db.contact.countDocuments({ segment: "M94" })
```

Expect: 0 while the transaction is open.

#### 2. After the commit

After the commit everyone reads them.

In scope: `service` (ArxMongoDbService), `transaction` (ArxTransaction).

```csharp
await transaction.CommitAsync();

Console.WriteLine($"outside, after the commit: {(await service.RetrieveMultipleAsync(Imported())).Entities.Count}");
```

Check it in the database:

```sql
db.contact.countDocuments({ segment: "M94" })
```

Expect: 2.

### A security check around a search endpoint

Advanced case 2 of RetrieveMultiple.

A security check around a search endpoint (`SecurityGuard`, in the project root), in four scenarios: a normal search passes; a sort field carrying SQL is logged and blocked after three strikes; a field list checked first keeps unknown names away from ARX; and a false positive (a column label with parentheses) is triaged and mapped, without blocking anyone.

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);
foreach (var number in new[] { "ACC-R741", "ACC-R742" })
{
    var account = new ArxEntity("account");
    account["name"] = "Guarded search " + number;
    account["account_number"] = number;
    await service.CreateAsync(account);
}
```

#### 1. A normal search passes through

A normal search goes through the guard untouched.

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

```csharp
var result = await SearchAsync(service, guard, "ana", "account_number");
Console.WriteLine($"ana: {result.Outcome}, {string.Join(", ", result.Value!.Entities.Select(x => x["account_number"]))}");
```

Defined in the sample, used above:

```csharp
private static Task<GuardResult<ArxEntityCollection>> SearchAsync(ArxMongoDbService service, SecurityGuard guard, string user, string sort)
{
    var query = new ArxQueryExpression("account");
    query.ColumnSet.AddColumn("account_number");
    query.Criteria.AddCondition("account_number", ArxConditionOperator.BeginsWith, "ACC-R74");
    query.AddOrder(sort, ArxOrderType.Ascending);

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

Check it in the database:

```sql
db.account.find({ account_number: /^ACC\-R74/ }, { account_number: 1 }).sort({ account_number: 1 })
```

Expect: 2 documents, ACC-R741, ACC-R742.

#### 2. An attacker is logged, then blocked

Three sort fields carrying SQL: each refused by ARX (ARX1017) and logged; then the user is blocked.

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

```csharp
foreach (var probe in new[] { "name; DROP TABLE account; --", "(SELECT password FROM users)", "name UNION SELECT 1" })
{
    await SearchAsync(service, guard, "mallory", probe);
}

var valid = await SearchAsync(service, guard, "mallory", "name");
Console.WriteLine($"mallory: {guard.Events.Count(x => x.User == "mallory")} events, blocked {guard.IsBlocked("mallory")}; a valid search: {valid.Outcome}, \"{valid.ForTheUser}\"");
```

Check it in the database:

```sql
db.getCollectionNames().includes("account")
```

Expect: true.

#### 3. The field list comes first

The well-written endpoint maps the request's field through its list: an unknown field is a 400, not a security event.

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

```csharp
var requested = "password";

if (SortFields.TryGetValue(requested, out var column))
{
    await SearchAsync(service, guard, "ben", column);
}
else
{
    Console.WriteLine($"400 Bad Request: can't sort by '{requested}'; security log unchanged ({guard.Events.Count} events)");
}
```

Defined in the sample, used above:

```csharp
private static readonly Dictionary<string, string> SortFields = new(StringComparer.OrdinalIgnoreCase)
{
    ["name"] = "name",
    ["number"] = "account_number",
};

private static Task<GuardResult<ArxEntityCollection>> SearchAsync(ArxMongoDbService service, SecurityGuard guard, string user, string sort)
{
    var query = new ArxQueryExpression("account");
    query.ColumnSet.AddColumn("account_number");
    query.Criteria.AddCondition("account_number", ArxConditionOperator.BeginsWith, "ACC-R74");
    query.AddOrder(sort, ArxOrderType.Ascending);

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

Check it in the database:

```sql
db.account.countDocuments({ password: { $exists: true } })
```

Expect: 0: no document has such a field, and nothing asked the database.

#### 4. A false positive: a column label with parentheses

A false positive: a reporting tool sends the sort field as its column label "Number (legal)". The parentheses make ARX flag it (ARX1017), one strike, nobody blocked; triage reads the log, and the label is added to the list.

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

```csharp
var refused = await SearchAsync(service, guard, "reporting-tool", "Number (legal)");
Console.WriteLine($"reporting-tool: {refused.Outcome}; blocked {guard.IsBlocked("reporting-tool")}; triage reads: {guard.Events[^1].Detail}");

SortFields["Number (legal)"] = "account_number";
var fixedSearch = await SearchAsync(service, guard, "reporting-tool", SortFields["Number (legal)"]);
Console.WriteLine($"after the mapping: {fixedSearch.Outcome}, {fixedSearch.Value!.Entities.Count} accounts");
```

Check it in the database:

```sql
db.account.countDocuments({ account_number: /^ACC\-R74/ })
```

Expect: 2, what the mapped search read.

### Set up the service from the database's schema

Advanced case 3 of RetrieveMultiple.

The service set up from the database instead of by hand, then reading through it, in two scenarios: preload the lead collection at startup, and reading with Strict on, where only the preloaded fields may be named.

Good to know:

`ArxPreLoadTable` reads, once, each collection's id, its unique indexes (as keys) and the field types of its `$jsonSchema` validator. With `Strict`, a field the preload didn't load is ARX1018 before anything is sent.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var lead = new ArxEntity("lead");
lead["email"] = "lead-m861@example.com";
lead["full_name"] = "Nuno Teixeira";
lead["score"] = 40;
await service.CreateAsync(lead);
```

#### 1. Preload at startup

Preload the lead collection once, at startup, and make the service Strict.

In scope: `connectionString` (string).

```csharp
var catalog = await new ArxPreLoadTable(new ArxPreLoadTableRegister().PreLoadTable("lead")).LoadAsync(connectionString);
var service = new ArxMongoDbService(connectionString, catalog) { Strict = true };

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

return service;
```

Check it in the database:

```sql
db.lead.getIndexes()
```

Expect: _id_ and the unique email_1 index.

#### 2. Read with the preloaded types

Read with the preloaded types: score comes back as an int; a field the preload doesn't know is refused.

In scope: `service` (ArxMongoDbService).

```csharp
var query = new ArxQueryExpression("lead");
query.ColumnSet.AddColumns("email", "score");
query.Criteria.AddCondition("email", ArxConditionOperator.Equal, "lead-m861@example.com");
var lead = (await service.RetrieveMultipleAsync(query)).Entities.Single();
Console.WriteLine($"score {lead["score"]} ({lead["score"]?.GetType().Name})");

var unknown = new ArxQueryExpression("lead");
unknown.ColumnSet.AddColumn("password");

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

Check it in the database:

```sql
db.lead.find({ email: "lead-m861@example.com" }, { score: 1 })
```

Expect: 1 document, score 40.

### Protect paging cookies

Advanced case 4 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 values the page is ordered by, of its last document: here a contact'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.MongoDb.Services;
using ARX.SDK.Query;
using ARX.SDK.Services;
using System.Text;
```

Set up by the sample before its scenarios:

```csharp
var contacts = new[] { "ana", "ben", "cy", "dee" }.Select(name =>
{
    var contact = new ArxEntity("contact");
    contact["first_name"] = name;
    contact["last_name"] = "M991";
    contact["email"] = $"{name}.m991@example.com";
    return contact;
}).ToList();
await service.CreateMultipleAsync(contacts);
```

#### 1. Sealed by default

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

In scope: `service` (ArxMongoDbService).

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

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

Defined in the sample, used above:

```csharp
private static ArxQueryExpression Query(string? cookie = null)
{
    var query = new ArxQueryExpression("contact");
    query.ColumnSet.AddColumns("email");
    query.Criteria.AddCondition("last_name", ArxConditionOperator.Equal, "M991");
    query.AddOrder("email", ArxOrderType.Ascending);
    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
db.contact.find({ last_name: "M991" }, { email: 1 }).sort({ email: 1 })
```

Expect: 4 documents; the second page holds cy.m991@example.com and dee.m991@example.com.

#### 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: `connectionString` (string).

```csharp
// In production the key comes from configuration or a secret store, the same on every instance.
var sharedKey = ArxPagingCookie.NewKey();
var instanceA = new ArxMongoDbService(connectionString, SampleCatalog.Build()) { PagingCookieKey = sharedKey };
var instanceB = new ArxMongoDbService(connectionString, SampleCatalog.Build()) { 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["email"]))}");

var withItsOwnKey = new ArxMongoDbService(connectionString, SampleCatalog.Build()) { 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("contact");
    query.ColumnSet.AddColumns("email");
    query.Criteria.AddCondition("last_name", ArxConditionOperator.Equal, "M991");
    query.AddOrder("email", ArxOrderType.Ascending);
    query.PageInfo = new ArxPagingInfo { Count = 2, PagingCookie = cookie };
    return query;
}
```

Check it in the database:

```sql
db.contact.countDocuments({ last_name: "M991" })
```

Expect: 4 (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: `connectionString` (string).

```csharp
var service = new ArxMongoDbService(connectionString, SampleCatalog.Build()) { PagingCookieProtection = ArxPagingCookieProtection.Plain };
var first = await service.RetrieveMultipleAsync(Query());
Console.WriteLine($"the cookie shows the email: {Readable(first.PagingCookie).Contains("ben.m991@example.com")}");
```

Check it in the database:

```sql
db.contact.countDocuments({ last_name: "M991" })
```

Expect: 4.

---

# ARX.SDK on MongoDB: benchmarks

ARX.SDK's MongoDB provider against the hand-written driver code a developer would write (`BsonDocument`, the baseline)
and the driver's typed API (POCO classes). BenchmarkDotNet job Standard (3 warm-ups, 8 iterations), Release, on the
`arx-mongo` container (MongoDB 7.0, a single-node replica set, Docker Desktop on Windows), database `arx_bench`.
Reports in `results/`. Ratios are against the raw driver; numbers from one machine, so read the ratios, not the times.

| Operation | ARX against the raw driver | Against the typed driver |
|---|---|---|
| Create, one document | 0.92× (level) | level |
| CreateMultiple, 100 to 10,000 documents, all in one transaction | 0.90–0.97× (level) | level to 1.3× |
| UpdateMultiple by id, 100 and 1,000 documents, in a transaction, each checked found | 1.16×, 1.07× | – |
| Update one document | 0.64× (ARX skips the transaction for one document) | – |
| DeleteMultiple by id, 100 to 10,000 | 0.93–1.24× (inside the spread: the single-operation runs vary ±40%) | – |
| Retrieve by id | 1.16× (0.17 ms) | 1.06× |
| RetrieveMultiple, 100 / 1,000 / 10,000 documents | 0.98× / 1.17× / 1.35× | the typed driver is 0.61× the raw one at 10,000 |

What the numbers say: writes are level with the driver; reads cost the dictionary entity (2.2× the raw driver's
allocations), as on PostgreSQL, SQL Server and MySQL.
