# ARX.SDK for SQL Server: 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, Execute, Benchmarks.

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

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
SQL Server provider, `ARX.SDK.SQL`.

## Instructions for the assistant

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

1. **Use ARX's types for database work.** Create, update, delete and read rows, call procedures and functions, and run
   transactions through `ArxMsSqlDbService`, `ArxMsSqlConnection` and `ArxTransaction`. Don't write
   Microsoft.Data.SqlClient code (`SqlConnection`, `SqlCommand`) or SQL text for work ARX does.
2. **Don't invent API.** Use only the types, methods, properties and parameters listed in `arx-sql-types.md`. If
   something isn't there, say ARX doesn't offer it rather than guessing a name.
3. **A row is an `ArxEntity`.** `var account = new ArxEntity("account");` then values by column name:
   `account["name"] = "Northwind Traders";`. Table and column names are the database's, as strings.
4. **Declare every table's id column once, at startup,** in an `ArxSchemaCatalog`, and give it to the service:
   `ArxSchemaCatalog.Create().Table("account", ArxIdColumn.Generated<long>("id")).Build()`, then
   `new ArxMsSqlDbService(connectionString, schema)`. Use `ArxIdColumn.Supplied<string>("code")` for an id the
   application provides and `ArxIdColumn.NoIdColumn` for a table without one; add business keys with
   `.Key("account", "account_number")`. ARX never guesses an id: an undeclared table is ARX1016. ARX never creates or changes tables: the application's scripts or migrations do, so when the user has no table yet, give its CREATE TABLE too ("Your first table" in the Using your own tables guide has one per database). A service created
   without a catalog (`new ArxMsSqlDbService(connectionString)`) reads each table's primary key and column types from the
   database before every call instead: much slower, only for simple CRUD in small applications (set
   `RememberResolvedTables = true` to read each table once); a table without a usable primary key is then ARX1019. Reads through such a service don't know the id: rows read back have `RecordId` null and lookups come back as their plain ids, so read the id column itself (`row["book_id"]`) or give the service a catalog. An `IDENTITY` id is
   `Generated`: SQL Server refuses a value for it (ARX5002).
5. **Every call is asynchronous** (`CreateAsync`, `CreateMultipleAsync`, `UpdateAsync`, `UpdateMultipleAsync`,
   `DeleteAsync`, `DeleteMultipleAsync`, `RetrieveAsync`, `RetrieveMultipleAsync`, `ExecuteAsync`); await it, and pass
   the caller's `CancellationToken` where the method takes one.
6. **Create returns the id.** `ArxRecordId id = await service.CreateAsync(entity);` (also set on `entity.RecordId`);
   `CreateMultipleAsync` returns one id per entity, in list order. Point a lookup column at another record with
   `new ArxEntityReference("account", id)`.
7. **Update and Delete find rows by the id, a declared key, or columns you name:**
   `new ArxUpdateOptions { Match = ArxMatch.Key("account_number") }`, `ArxMatch.Columns(...)`. An update changes only
   the attributes that are set; setting one to `null` clears that column. Text is matched as SQL Server compares it
   (case-insensitive with the default collation).
   **To change or delete every row 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 statement (`SqlBulkCopy` for a create of 1,000 rows or more of one shape), 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`).
8. **Reads.** `RetrieveAsync` returns `null` when nothing matches. `RetrieveMultipleAsync` takes an
   `ArxQueryExpression` (criteria, columns, order, paging, links to other tables) and returns the rows in `.Entities`.
   A paged query needs an order: the declared id is used when none is given.
   **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` · 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.
9. **Values are always parameters:** pass them as .NET values and never build SQL with them. A table or column name
   that carries SQL is refused with ARX1017, but a valid name is used as given: a name typed by a user can read any
   table or column the database user can (`sys.tables`, `INFORMATION_SCHEMA.COLUMNS`). Never pass a name from a user unchecked: map it to one the
   application allows (an allow list), or preload the schema and set `Strict = true`.
10. **Dates:** `DateTimeOffset` for a moment (ARX writes it as its UTC instant, `+00:00`, so it is the same moment in a
    `datetimeoffset` or a `datetime2` column; the offset itself isn't kept, as on PostgreSQL and MySQL), a
    `DateTime` for a wall-clock time (`datetime2`; SQL Server keeps no zone, so write UTC when the column means a moment), `DateOnly` (`date`),
    `TimeOnly` (`time`). A `TimeSpan` goes to `time` and must be under 24 hours: store a duration as a number. Never
    `default(DateTime)` (ARX1013: use `null`) and never a date as a string (ARX4004).
11. **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 (a deadlock, 1205, is). `arx-sql-error-codes.md` lists every code. Log
    `error.Message`, not the inner `SqlException`'s: SQL Server's own message can quote the value.
12. **Transactions:** `await using var connection = new ArxMsSqlConnection(connectionString);`,
    `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). For many calls as one unit,
    `await service.RunBatchAsync(async batch => { … });` commits when the body returns. A connection with an open
    transaction must be given that transaction (ARX1008 otherwise).
13. **Procedures** run with `ExecuteAsync(new ArxProcedureExpression("dbo.my_procedure") …)`: arguments by name,
    OUTPUT parameters declared with their type, the RETURN value in `Outputs["RETURN_VALUE"]`, result sets by
    position. SQL Server has no cursor parameters or VARIADIC (ARX8001).
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`; `ArxMsSqlDbService` and `ArxMsSqlConnection` in `ARX.SDK.SQL.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-sql-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: SQL Server 2022 (tested with 16.0.4225.2); driver: Microsoft.Data.SqlClient 7.1.1 or later (the lowest version, which the provider is built on; Versions and compatibility lists the versions it is tested on).
- `ARX.SDK.SQL` 1.0.0-rc.1: The SQL Server provider. It brings ARX.SDK and Microsoft.Data.SqlClient 7.1.1 with it. Install: `dotnet add package ARX.SDK.SQL --version 1.0.0-rc.1`.
- The samples read the connection string from the `ARX_SAMPLES_SQLSERVER` environment variable, for example `Server=localhost,1433;Database=arx_samples;User Id=sa;Password=Your-password1;TrustServerCertificate=True`. 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 column in the schema

In scope: `connectionString` (string).

```csharp
// Declare each table's id column once, at startup. ARX never guesses it.
var schema = ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id"))
    .Build();

var service = new ArxMsSqlDbService(connectionString, schema);

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

// CreateAsync returns the id of the new record.
ArxRecordId id = await service.CreateAsync(entity);

// The account's id is a bigint column, so read it as a number.
long accountId = id.AsInt64();

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

### CreateMultiple: Create many records in one call and get their ids, Id column in the schema

In scope: `connectionString` (string).

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

var service = new ArxMsSqlDbService(connectionString, schema);

var first = new ArxEntity("account");
first["name"] = "Contoso Ltd.";
first["account_number"] = "ACC-0002";

var second = new ArxEntity("account");
second["name"] = "Fabrikam Inc.";
second["account_number"] = "ACC-0003";

var third = new ArxEntity("account");
third["name"] = "Litware Inc.";
third["account_number"] = "ACC-0004";

var accounts = new List<ArxEntity> { first, second, third };

// CreateMultipleAsync returns one id per entity, in the list's order: ids[0] is first's id, and so on.
IReadOnlyList<ArxRecordId> ids = await service.CreateMultipleAsync(accounts);

Console.WriteLine($"created accounts {string.Join(", ", ids)}");
```

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

In scope: `connectionString` (string).

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

var service = new ArxMsSqlDbService(connectionString, schema);

var account = new ArxEntity("account");
account["name"] = "Fabrikam Outdoors";
account["account_number"] = "ACC-U101";
account["employee_count"] = 40;
ArxRecordId id = await service.CreateAsync(account);

// Only the attributes you set are written: the name and account number stay as they are.
var change = new ArxEntity("account");
change.RecordId = id;
change["employee_count"] = 55;
change["website"] = "https://fabrikam.example";

int changed = await service.UpdateAsync(change);

Console.WriteLine($"account {id.AsInt64()}, {changed} row changed");
```

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

In scope: `connectionString` (string).

```csharp
var service = Service(connectionString);
var ids = await CreateAccountsAsync(service, 1, 5);

var changes = new List<ArxEntity>();

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

int changed = await service.UpdateMultipleAsync(changes);

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

Defined in the sample, used above:

```csharp
private static ArxMsSqlDbService Service(string connectionString) => new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id"))
    .Build());

private static async Task<IReadOnlyList<ArxRecordId>> CreateAccountsAsync(ArxMsSqlDbService service, int first, int count)
{
    var accounts = new List<ArxEntity>();

    for (var i = 0; i < count; i++)
    {
        var account = new ArxEntity("account");
        account["name"] = $"Basic update {first + i}";
        account["account_number"] = $"ACC-U2{first + i:00}";
        account["employee_count"] = 10;
        accounts.Add(account);
    }

    return await service.CreateMultipleAsync(accounts);
}
```

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

In scope: `connectionString` (string).

```csharp
var service = Service(connectionString);

var account = new ArxEntity("account");
account["name"] = "Temporary account";
account["account_number"] = "ACC-D101";
ArxRecordId id = await service.CreateAsync(account);

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

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

Defined in the sample, used above:

```csharp
private static ArxMsSqlDbService Service(string connectionString) => new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id"))
    .Table("country", ArxIdColumn.Supplied<string>("code"))
    .Build());
```

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

In scope: `service` (ArxMsSqlDbService).

```csharp
var ids = await CreateAccountsAsync(service, 1, 5);

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

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

Defined in the sample, used above:

```csharp
private static async Task<IReadOnlyList<ArxRecordId>> CreateAccountsAsync(ArxMsSqlDbService service, int first, int count)
{
    var accounts = Enumerable.Range(first, count).Select(i =>
    {
        var account = new ArxEntity("account");
        account["name"] = $"Delete many {i}";
        account["account_number"] = $"ACC-D2{i:00}";
        return account;
    }).ToList();

    return await service.CreateMultipleAsync(accounts);
}
```

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

In scope: `connectionString` (string).

```csharp
var service = Service(connectionString);

var account = new ArxEntity("account");
account["name"] = "Northwind Traders";
account["account_number"] = "ACC-R101";
account["employee_count"] = 120;
ArxRecordId id = await service.CreateAsync(account);

ArxEntity read = await service.RetrieveAsync("account", id, new ArxColumnSet("name", "employee_count")) ?? throw new InvalidOperationException("The account record isn't there.");

// The id is always read too, so read.RecordId can update or delete this record later.
Console.WriteLine($"{read["name"]}, {read["employee_count"]} employees, RecordId {read.RecordId}");
```

Defined in the sample, used above:

```csharp
private static ArxMsSqlDbService Service(string connectionString) => new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id"))
    .Table("country", ArxIdColumn.Supplied<string>("code"))
    .Build());
```

### RetrieveMultiple: Retrieve many records with a query, A condition and an order

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = new ArxQueryExpression("account");
query.ColumnSet.AddColumns("name", "employee_count");
query.Criteria.AddCondition("name", ArxConditionOperator.BeginsWith, "Basic B");
query.AddOrder("name", ArxOrderType.Ascending);

ArxEntityCollection result = await service.RetrieveMultipleAsync(query);

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

### Execute: Call a stored procedure, Named arguments

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
var procedure = new ArxProcedureExpression("sp_rename_account");
procedure.AddParameter("p_account_id", accountId.AsInt64());
procedure.AddParameter("p_name", "New Name Ltd");
procedure.AddParameter("p_reason", "rebranding");

var result = await service.ExecuteAsync(procedure);
Console.WriteLine($"renamed; outputs: {result.Outputs.Attributes.Count()}, result sets: {result.ResultSets.Count}");
```

## The other files

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

---

# ARX.SDK types (SQL Server)

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

### ArxMsSqlDbService

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

```csharp
public class ArxMsSqlDbService
```

The SQL Server service: Create, Update, Delete, Retrieve, RetrieveMultiple and Execute against one database. Give it the connection string and, usually, the application's schema catalog. It holds no connection (each call takes one from the pool), so one instance serves the whole application.

Constructors:

- `new ArxMsSqlDbService(string connectionString)`: A service with no schema catalog: before a call, it reads from the database what the call needs to know about each table (its id column, keys and column types), so simple CRUD works with only a connection string. That read costs a round trip per call unless `RememberResolvedTables` keeps it; a table with no usable id (none, or a compound one) is ARX1019 when the call needs one. Pass a schema catalog instead to declare the tables once.
  - `connectionString`: The Microsoft.Data.SqlClient connection string.
- `new ArxMsSqlDbService(string connectionString, ArxSchemaCatalog schema)`: A service with the application's schema catalog: the usual way.
  - `connectionString`: The Microsoft.Data.SqlClient connection string.
  - `schema`: The developer's schema catalog: every table's id column and keys, and the selection sets that `ArxSelectionSetValue` attributes are checked against. ARX never reads ids, keys or option tables from the database by itself. Without it, creating a record fails with ARX1016 unless the entity declares its own `IdColumn`.

Properties:

- `string ConnectionString { get; }`: The Microsoft.Data.SqlClient connection string each call opens its connection with.
- `ArxSchemaCatalog Schema { get; }`: The developer's schema catalog (table keys and selection sets); null when none was supplied.
- `IArxSelectionSetCatalog SelectionSetCatalog { get; }`: The catalog `ArxSelectionSetValue` attributes are checked against; null when none was supplied.
- `IArxProviderCapabilities Capabilities { get; }`: What SQL Server supports through ARX: joins, aggregates, paging by page number with the total, transactions and batch scopes, the multiple-row writes, Execute, full-text search, pattern conditions and Or filters. Not token paging (`PageInfo.PagingCookie` is ARX8001: page by page number), not upsert, not named secondary indexes (the database chooses its indexes).
- `const string RETURN_VALUE`: The name a procedure's RETURN value is read under, in `Outputs`.
- `bool RememberResolvedTables { get; set; }`: Without a schema catalog, keep what was resolved about each table for this service's lifetime instead of reading it again on every call. Off by default: every call then reads its tables again, always current, at the cost of extra round trips. On: each table is read once, the first time a call needs it; a schema change after that needs a new service. Has no effect on a service with a catalog, which never resolves.
- `bool Strict { get; set; }`: False (the default): a preloaded schema is what ARX knows ahead of time; tables and columns it doesn't list still work. True: the preloaded columns are an allow list. A call that names a table or a column the schema doesn't preload (written, matched, read, filtered, ordered, grouped, joined) is refused before the database with ARX1018 NotPreloaded, and reading all columns reads only the preloaded ones. Function row sources and Execute aren't covered: their columns are the routine's.

Methods:

- `Task<IArxBatchScope> BeginBatchAsync(ArxBatchOptions? options = null, CancellationToken cancellationToken = default)`: Opens a connection and a transaction for a batch scope with manual commit. For automatic commit, use `RunBatchAsync`.
  - `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 `ArxMsSqlConnection`), for code that works through `IArxDataService` and so can't name the provider's connection type. Pass it to the calls, then commit it; disposing it without committing rolls it back, and closes its connection: `await using var transaction = await service.BeginTransactionAsync();`
  - `cancellationToken`: Cancels the call.
- `Task<ArxRecordId> CreateAsync(ArxEntity entity, CancellationToken cancellationToken = default)`: Inserts one entity and returns its id, which is also set on `RecordId`. A table declared NoIdColumn gives `None`.
  - `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 entity inside an ARX transaction and returns its id, which is also set on `RecordId`. Nothing is visible to other connections until the transaction commits.
  - `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> entityList, CancellationToken cancellationToken = default)`: Inserts all entities and returns their ids: one per entity, in list order, the same instances that are set on each entity's `RecordId`. A table declared NoIdColumn gives `None`.
  - `entityList`: 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, ArxTransaction transaction, CancellationToken cancellationToken = default)`: Inserts all entities inside an ARX transaction, all or nothing, and returns their ids, as `CreateMultipleAsync` does. Nothing is visible to other connections until the transaction commits.
  - `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)`: Inserts all entities with `schema` overriding the service's schema for this call only, and returns their ids, as `CreateMultipleAsync` does.
  - `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)`: `CreateMultipleAsync` inside an ARX transaction: `schema` declares the tables for this call, and the entities mustn't.
  - `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<IReadOnlyList<ArxRecordId>> CreateMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxCreateMultipleOptions options, CancellationToken cancellationToken = default)`: Creates the records, all or nothing. With `ReturnIds` false every id returned is `None`, and a group of 1,000 rows or more of one shape goes in by `SqlBulkCopy` (one per group, in one transaction, with constraints checked and triggers fired, as an INSERT); smaller groups take the ordinary statement, which was as fast or faster below that size. With ReturnIds true, the same as `CreateMultipleAsync`.
  - `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. A database error aborts the transaction.
  - `entities`: The records to create, all or nothing.
  - `options`: ReturnIds false to skip reading the ids back.
  - `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 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<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, ArxDeleteOptions? options = null, CancellationToken cancellationToken = default)`: Deletes the rows `entity` matches and returns how many went: by default by the table's id (RecordId or the id attribute), or by a declared key or any columns (`Match`), whose values come from the entity's attributes. Attributes outside the match are ignored, so an entity read or created earlier can be passed as it is.
  - `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)`: `DeleteAsync` inside an ARX transaction. A database error or a failed RequireSingleRow check aborts the 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 rows the entities match, all or nothing, and returns how many went. Every entity uses the same match (`Match`): by id, by a declared key, or by any columns, whose values come from the entity's attributes; other attributes are ignored. Compound keys go in one statement (`DELETE … FROM … JOIN` a `VALUES` list or an `OPENJSON` source). Not strict by default: duplicates count once and nothing matching is 0. With RequireSingleRow each entity must delete exactly one row (ARX2002 or ARX2003 otherwise, nothing deleted).
  - `entities`: The records to delete, all or nothing, each found by its id or by the match's columns.
  - `options`: How the rows are found (`Match`) and checked; null for the defaults (by id).
  - `cancellationToken`: Cancels the call.
- `Task<int> DeleteMultipleAsync(IReadOnlyList<ArxEntity> entities, ArxSchemaCatalog schema, ArxDeleteOptions? options = null, CancellationToken cancellationToken = default)`: DeleteMultiple with `schema` overriding the service's schema for this call, as for CreateMultiple. Entities in the call mustn't declare their own id column or keys (ARX1001).
  - `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)`: DeleteMultiple by entities inside 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)`: DeleteMultiple by entities with a schema for this call, inside 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<ArxExecuteResult> ExecuteAsync(ArxProcedureExpression procedure, ArxExecuteOptions? options = null, CancellationToken cancellationToken = default)`: Calls a stored procedure or a function the database owns on ARX's own connection. A procedure runs as `EXEC` outside any transaction, so it may begin and commit its own; its OUTPUT values and its RETURN value (`RETURN_VALUE`) come back in `Outputs`, the tables it returns in `ResultSets`, by position. A table-valued function's rows are `ResultSets[0]` (ordered and paged when asked); a scalar function's value is `result.Value<T>()`.
  - `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)`: Calls a routine inside an ARX transaction: its writes are part of it, and a failed call aborts it. A procedure that commits or rolls back the transaction is 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<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 `UPDATE … SET … FROM … WHERE …`, and 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> 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 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 (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 row of the table that `filter` matches, in one `DELETE … FROM … WHERE …`, and returns how many went. The filter's rules are the update'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. A database error aborts the transaction.
  - `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 (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<ArxEntity?> RetrieveAsync(string entityName, ArxRecordId id, ArxColumnSet? columns = null, CancellationToken cancellationToken = default)`: Reads one record by the id Create returned; null when not found.
  - `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)`: Retrieve by the id Create returned inside 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)`: Reads the one record `match` identifies: by its id (the default), a declared key or any columns (`Match`, an `ArxMatch`), with the match values taken from its attributes. Null when nothing matches; ARX2003 when a key or columns match more than one row.
  - `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)`: Retrieve by a match inside 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 on a table, view, materialized view or function (`Source`): its columns (including `column._lk.table.columns` direct joins), criteria, link entities, order, paging, distinct and aggregates. Every name is validated and quoted, every value sent as a parameter.
  - `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)`: RetrieveMultiple inside an ARX transaction: it sees the transaction's own uncommitted writes.
  - `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 table in the schema catalog with the database: the table exists; its id column exists with a compatible type, identifies one row on its own (it is the primary key, or has a unique constraint or index of its own), and has a default or is an identity column when declared generated; and every declared key's columns exist and have a unique constraint or index on exactly those columns. Tables declared `NoIdColumn` skip the id checks.
  - `cancellationToken`: Cancels the call.
- `Task<ArxServerCheck> CheckServerAsync(CancellationToken cancellationToken = default)`: What the connected server allows: connects once and returns the server's version and the provider's capabilities on it. Every capability of this provider works on any supported server, so the check finds no limits; a server it can't reach is the error a call would get (ARX7005, ARX6001, …). Call it once at startup.
  - `cancellationToken`: Cancels the call.
- `Task<int> UpdateAsync(ArxEntity entity, ArxUpdateOptions? options = null, CancellationToken cancellationToken = default)`: Updates the row `entity` identifies and returns how many rows changed.
  - `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)`: `UpdateAsync` inside an ARX transaction. A validation error or ARX2002 when nothing matched leaves the transaction usable; a database error, or a failed RequireSingleRow check, aborts it.
  - `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 rows changed.
  - `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)`: UpdateMultiple with `schema` overriding the service's schema for this call (per table and per selection set, as for CreateMultiple). Entities in the call mustn't declare their own id column or keys (ARX1001).
  - `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)`: UpdateMultiple inside an ARX transaction. A database error or a failed check aborts the 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)`: UpdateMultiple with a schema for this call, inside 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.

### ArxMsSqlConnection

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

```csharp
public sealed class ArxMsSqlConnection : ArxConnection
```

A SQL Server connection for ARX transactions. Application code begins a transaction on it and passes the transaction to `ArxMsSqlDbService` calls; it never needs a Microsoft.Data.SqlClient type.

It wraps an `SqlConnection`, which is sealed, so ARX can't derive from it. The connection opens on the first `BeginTransactionAsync`, with the session defaults of every connection ARX opens (SQL Server has no session time zone; `XACT_ABORT` stays off so a failed statement leaves the transaction usable for batch-scope savepoints), and closes on dispose. One transaction at a time, and one call at a time. Not thread-safe.

Constructors:

- `new ArxMsSqlConnection(string connectionString)`: A connection for ARX transactions. It opens with the first BeginTransactionAsync.
  - `connectionString`: The Microsoft.Data.SqlClient connection string.

### ArxMsSqlTransaction

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

```csharp
public sealed class ArxMsSqlTransaction : ArxTransaction
```

A SQL Server transaction begun by `BeginTransactionAsync`. Pass it to `ArxMsSqlDbService` 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 ArxMsSqlDbService(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.

### ArxMsSqlPreLoad

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

```csharp
public static class ArxMsSqlPreLoad
```

Loads an `ArxPreLoadTable` from a SQL Server database.

Methods:

- `static Task<ArxSchemaCatalog> LoadAsync(this ArxPreLoadTable preload, string connectionString, Action<ArxSchemaCatalogBuilder>? configure = null, CancellationToken cancellationToken = default)`: Reads the requested tables from the database once (call it at startup, at service registration) and returns the schema catalog for `new ArxMsSqlDbService(connectionString, catalog)`: each table's id, business keys, lookups and, in `DataTypes`, its columns' types; the tables the loaded columns reference are added with the reason (`Reason`). Operations never read the catalog again.
  - `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: display names, selection sets, hand-declared tables.
  - `cancellationToken`: Cancels the reads.

### 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<ArxMsSqlDbService>>(sp => ArxRouter.Create<ArxMsSqlDbService>(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.

## 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 ArxMsSqlConnection(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 ArxMsSqlDbService(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 SQL Server

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

/// <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 SqlClient 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.MSSQL;

/// <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.SQL.Services;
using ARX.SDK.Services;

namespace ArxSamples.MSSQL;

/// <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 ArxMsSqlDbService? _service;

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

    /// <summary>
    /// One service for the whole application. It holds no connection: every call opens one from the SqlClient pool and
    /// closes it, unless you pass your own connection or transaction.
    /// </summary>
    public ArxMsSqlDbService Service => _service ??= new ArxMsSqlDbService(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;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace ArxSamples.MSSQL;

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

/// <summary>
/// The application's schema catalog: every table's id column and keys, and every picklist (selection set),
/// defined once.
/// </summary>
/// <remarks>
/// <para>
/// <b>Ids.</b> Every table the application writes is declared with its one id column: its name, its .NET type, and
/// whether the database generates it (<c>ArxIdColumn.Generated</c>: identity, sequence, default) or the caller supplies
/// it (<c>ArxIdColumn.Supplied</c>: a natural id, a client-generated Guid). A table with no id column, such as a log or a
/// table identified by several columns together, is declared <c>ArxIdColumn.NoIdColumn</c>. ARX never reads ids from
/// the database: the developer knows the schema, and an undeclared table is refused (ARX1016).
/// </para>
/// <para>
/// <b>Keys.</b> Optional business keys: one or more columns whose values identify a record, such as an account number,
/// or a region and a SKU together. Create doesn't use them (the database's unique constraints do the enforcing); Update
/// and Delete will be able to find rows by them. <c>ValidateSchemaAsync</c> checks ids and keys against the database
/// when you ask it to.
/// </para>
/// <para>
/// <b>Selection sets.</b> /// A selection set (picklist, option set) is a column that only accepts a fixed list of codes, each with a display
/// text: an account's status (ACTIVE, ON_HOLD, CLOSED), a contact's preferred channel (E, P, S). The catalog tells ARX,
/// for each set:
/// </para>
/// <list type="bullet">
/// <item><b>Name</b>: what an <see cref="ArxSelectionSetValue"/> refers to, for example <c>new ArxSelectionSetValue("AccountStatus", "ON_HOLD")</c>.</item>
/// <item><b>Kind</b>: what the codes are, matching the column: Integer, String, Character, Boolean, Decimal or Guid.
/// (SQL Server has no enum types: a fixed list of text values is a String set, and a CHECK can guard the column.)</item>
/// <item><b>Multiplicity</b>: Single (one code), or several codes stored as bit flags (Flags), a delimited string
/// (Delimited) or a JSON array (Json). SQL Server has no array columns.</item>
/// <item><b>Options</b>: each code with its display text and order. A retired option (<c>IsActive: false</c>) stays
/// readable in old rows, but can't be written any more.</item>
/// </list>
/// <para>
/// Build the catalog once at startup and give it to the service: <c>new ArxMsSqlDbService(connectionString, SampleCatalog.Build())</c>.
/// ARX checks every <see cref="ArxSelectionSetValue"/> against it in memory, before any SQL is sent, and stores the code
/// itself in the column: no lookup table, no extra query. A plain value (a string or number that isn't an
/// ArxSelectionSetValue) isn't checked.
/// </para>
/// <para>
/// The catalog is plain code, so the options can come from wherever you keep them: written out here, taken from a .NET
/// enum (<c>AddEnum</c>), loaded from JSON (<see cref="SetsJson"/> and <see cref="FromJson"/> below), or loaded at
/// startup from your own option table and passed to <c>new ArxSelectionSetDefinition(name, kind, options)</c>. ARX
/// never reads option tables or files itself.
/// </para>
/// </remarks>
public static class SampleCatalog
{
    /// <summary>
    /// Selection sets kept as JSON, as an application might keep them in a file, a settings store or a database column.
    /// Here the JSON is in the class itself; in an application, read it from wherever you keep it
    /// (<c>File.ReadAllText("selection-sets.json")</c>, configuration, an HTTP call) and pass it to <see cref="FromJson"/>.
    /// "multiplicity" and "active" are optional (Single and true).
    /// </summary>
    public const string SetsJson = """
        [
          {
            "name": "LeadSource",
            "kind": "String",
            "options": [
              { "value": "WEB",       "text": "Website",    "order": 0 },
              { "value": "REFERRAL",  "text": "Referral",   "order": 1 },
              { "value": "EVENT",     "text": "Trade show", "order": 2 },
              { "value": "COLD_CALL", "text": "Cold call",  "order": 3, "active": false }
            ]
          },
          {
            "name": "ContactRating",
            "kind": "Integer",
            "multiplicity": "Single",
            "options": [
              { "value": 1, "text": "Poor" },
              { "value": 2, "text": "Fair" },
              { "value": 3, "text": "Good" },
              { "value": 4, "text": "Very good" },
              { "value": 5, "text": "Excellent" }
            ]
          }
        ]
        """;

    public static ArxSchemaCatalog Build()
    {
        var builder = ArxSchemaCatalog.Create()

            // ---------- tables: id column and keys ----------
            // Generated bigint ids (IDENTITY): the database fills them in. The keys are the unique columns that also
            // identify a record.
            .Table("account", ArxIdColumn.Generated<long>("id")).Key("account", "account_number")
            .Table("contact", ArxIdColumn.Generated<long>("id")).Key("contact", "email")

            // Retrieve: a contact's account_id reads back as a reference to its account, named by the account's
            // name; a contact is named by its first and last names.
            .Lookup("contact", "account_id", "account")
            .DisplayName("account", "name")
            .DisplayName("contact", new[] { "first_name", "last_name" }, " ")

            .Table("product_category", ArxIdColumn.Generated<long>("id")).Key("product_category", "name")
            .Table("product", ArxIdColumn.Generated<long>("id")).Key("product", "sku")
            .Table("sales_order", ArxIdColumn.Generated<long>("id")).Key("sales_order", "order_number")
            .Table("order_line", ArxIdColumn.Generated<long>("id"))
            .Table("data_type_showcase", ArxIdColumn.Generated<long>("id"))

            // A natural id: the caller gives the ISO country code.
            .Table("country", ArxIdColumn.Supplied<string>("code"))

            // A generated uuid id (DEFAULT gen_random_uuid()).
            .Table("device", ArxIdColumn.Generated<Guid>("id")).Key("device", "serial_number")

            // No id column: a price is identified by its region and SKU together, a compound key.
            .Table("price_list", ArxIdColumn.NoIdColumn).Key("price_list", "region", "sku")

            // No primary key at all: the same action can be logged twice.
            .Table("activity_log", ArxIdColumn.NoIdColumn)

            // A partitioned table without an id: a visit is a fact, never updated (Retrieve Advanced8).
            .Table("web_visit", ArxIdColumn.NoIdColumn)

            // ---------- selection sets ----------

            // From a .NET enum: the set is named after the enum ("Industry"), the codes are its numbers (Retail = 1, …),
            // the display texts are its member names. Column: account.industry smallint.
            .AddEnum<Industry>()

            // Written out: String codes with display texts, single choice. Column: account.status varchar(10).
            .Add("AccountStatus", ArxSelectionSetKind.String, ("ACTIVE", "Active"), ("ON_HOLD", "On hold"), ("CLOSED", "Closed"))

            // Multiple choice, stored as delimited text ("ENTERPRISE,PUBLIC"). RESELLER is retired: rows that have it still
            // read back, but new writes can't use it. Column: account.segments nvarchar(200).
            .Add(new ArxSelectionSetDefinition("CustomerSegment", ArxSelectionSetKind.String, new[]
            {
                new ArxSelectionSetOption("SMB", "Small business", 0),
                new ArxSelectionSetOption("ENTERPRISE", "Enterprise", 1),
                new ArxSelectionSetOption("PUBLIC", "Public sector", 2),
                new ArxSelectionSetOption("RESELLER", "Reseller", 3, IsActive: false)
            }, ArxSelectionSetMultiplicity.Delimited))

            // Single-character codes. Column: contact.preferred_channel char(1).
            .Add("ContactChannel", ArxSelectionSetKind.Character, ('E', "Email"), ('P', "Phone"), ('S', "Text message"))

            // A fixed list of text values the column's CHECK also guards (SQL Server has no enum types).
            // Column: data_type_showcase.mood nvarchar(10).
            .Add(new ArxSelectionSetDefinition("Mood", ArxSelectionSetKind.String, new[]
            {
                new ArxSelectionSetOption("happy", "Happy", 0),
                new ArxSelectionSetOption("neutral", "Neutral", 1),
                new ArxSelectionSetOption("sad", "Sad", 2)
            }));

        // Loaded from JSON: LeadSource (contact.lead_source text) and ContactRating (contact.rating smallint).
        foreach (var set in FromJson(SetsJson))
        {
            builder.Add(set);
        }

        return builder.Build();
    }

    /// <summary>Reads selection set definitions from JSON (the shape of <see cref="SetsJson"/>).</summary>
    public static IEnumerable<ArxSelectionSetDefinition> FromJson(string json)
    {
        var options = new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true,
            Converters = { new JsonStringEnumConverter() }   // "String", "Integer", "Array", … as names
        };

        var sets = JsonSerializer.Deserialize<List<JsonSet>>(json, options)
            ?? throw new InvalidOperationException("The selection set JSON is empty.");

        foreach (var set in sets)
        {
            yield return new ArxSelectionSetDefinition(
                set.Name,
                set.Kind,
                set.Options.Select(x => new ArxSelectionSetOption(Code(x.Value, set.Kind), x.Text, x.Order, x.Active)),
                set.Multiplicity,
                set.DatabaseTypeName);
        }
    }

    /// <summary>A JSON value as a code of the set's kind: JSON has no char, Guid or 64-bit integer of its own.</summary>
    private static object Code(JsonElement value, ArxSelectionSetKind kind) => kind switch
    {
        ArxSelectionSetKind.Integer => value.GetInt64(),
        ArxSelectionSetKind.Decimal => value.GetDecimal(),
        ArxSelectionSetKind.Boolean => value.GetBoolean(),
        ArxSelectionSetKind.Guid => value.GetGuid(),
        ArxSelectionSetKind.Character => value.GetString()![0],
        _ => value.GetString()!   // String and DatabaseEnum
    };

    private sealed record JsonSet(
        string Name,
        ArxSelectionSetKind Kind,
        List<JsonOption> Options,
        ArxSelectionSetMultiplicity Multiplicity = ArxSelectionSetMultiplicity.Single,
        string? DatabaseTypeName = null);

    private sealed record JsonOption(JsonElement Value, string Text, int Order = 0, bool Active = true);
}
```

## SecurityGuard.cs

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

```csharp
using ARX.SDK;

namespace ArxSamples.MSSQL;

/// <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 (SQL Server)

## Writing: .NET to SQL Server

| .NET value | Column | Stored as |
|---|---|---|
| `string` | `nvarchar(n)`, `varchar(n)`, `nchar(n)`, `nvarchar(max)` | as written (`nchar(n)` pads with spaces) |
| `byte` / `short` / `int` / `long` | `tinyint` / `smallint` / `int` / `bigint` | `200` / `32000` / `2000000000` / `9000000000000` |
| `decimal` | `decimal(p, s)`, `money` | `12345.678901` |
| `float` / `double` | `real` / `float` | `3.25` / `2.718281828459045` |
| `bool` | `bit` | `1` |
| `Guid` | `uniqueidentifier` | `6F9619FF-8B86-D011-B42D-00C04FC964FF` |
| `DateTimeOffset` | `datetimeoffset` | the moment, in UTC: `22:30 +08:00` is stored as `2026-11-07 14:30:00 +00:00`. The same with a hand-written catalog, a preload or none: like PostgreSQL and MySQL, ARX keeps the moment, not the offset (store the offset in a column of its own if the application needs it) |
| `DateTimeOffset` | `datetime2`, `datetime` | the moment in UTC (`2026-11-07 14:30:00`), with or without a catalog (SQL Server alone would drop the offset and keep `22:30`, eight hours off) |
| `DateTime` | `datetime2`, `datetime` | as written: `2026-11-07 09:00:00` (`datetime` rounds to 1/300 of a second) |
| `DateOnly` / `TimeOnly` | `date` / `time` | `2026-11-07` / `17:45:30` |
| `byte[]`, `ArxBytea` | `varbinary(n)`, `varbinary(max)` | `0xCAFE` |
| `ArxJsonB` | `nvarchar(max)`, with `CHECK (ISJSON(column) = 1)` | the JSON text (invalid JSON fails the check: `ARX3004`) |
| `ArxEntityReference` | the lookup column | the referenced id |
| `ArxSelectionSetValue` | the set's column | the option's code |
| `null` | any | `NULL` |
| `sbyte` / `ushort` / `uint` / `ulong` | converted to `smallint` / `int` / `bigint` / `decimal` | `-100` / `65000` / `4000000000` / `18446744073709551615` |
| `char` | text | `X` |
| a plain `enum` | refused (ARX1007) | write `(int)value` or `value.ToString()`, or use a selection set, which stores the code its set declares |
| `DateTime` with Kind `Utc` | `datetimeoffset` | the moment, offset `+00:00` |
| `DateTime` with Kind `Local` | `datetime2`, `datetimeoffset` | converted to UTC first |

Refused before anything is sent: any other type (`ARX1007 UnsupportedValueType`: write `uri.ToString()` rather than a
`Uri`); arrays (`ARX8001`: SQL Server has no array columns, so store a list as delimited text, JSON or rows of its own);
`NaN` and the infinities, and a `TimeSpan` below zero or of 24 hours or more (`ARX4001`: `time` holds a time of day, so
store a duration as a number).

With the column types preloaded (`ArxPreLoadMode.DataTypes`), a number with a fraction written to a whole-number column is refused before SQL Server (`ARX4001 ValueOutOfRange`) on a create, an update and a write by a filter, where SQL Server would round or truncate it; a condition in a read (`qty < 7.5`) isn't refused. A `NaN` or an infinity written to a declared `decimal` column is refused the same way.

SQL Server converts some values silently that other databases refuse: `"42"` into an `int`, a number into a `bit`, a
`decimal` into an `int` by cutting off the fraction. A preload with column types (`ArxPreLoadMode.DataTypes`) refuses
those before the database. `RealCase2_EveryDataType` writes every row of this table and prints what SQL Server stored.

## Dates and times

| You have | Use | Column |
|---|---|---|
| A moment (an order was placed) | `DateTimeOffset` | `datetimeoffset` |
| A moment, in a column without an offset | `DateTime` with Kind `Utc` | `datetime2` (store UTC, read it back as UTC) |
| A wall-clock time with no zone | `DateTime` with Kind `Unspecified` | `datetime2` |
| A calendar date | `DateOnly` | `date` |
| A time of day | `TimeOnly` | `time` |
| A duration | a number (seconds, minutes) | `int`, `bigint` |

SQL Server keeps no time zone: `datetime2` stores the clock time it is given, and `datetimeoffset` the clock time and
its offset. ARX writes a `DateTimeOffset` as its UTC instant and converts a local `DateTime` to UTC first, so a moment is
never stored hours off. Refused: `default(DateTime)` (`ARX1013`: it means "not set",
and SQL Server would store `0001-01-01`; use `null`). ARX never parses a string as a date, but SQL Server converts
one it can read, by the session's date format (`01/07/1992` is January 7 under `us_english`), and refuses one it
can't (`ARX4003`); with a schema preloaded with data types, ARX refuses every date string first (`ARX4004`). Parse
dates yourself. Full rules: [Data types](/csharp/sql/data-types).

## Reading: SQL Server to .NET

| SQL Server column | Reads back as | Notes |
|---|---|---|
| `nvarchar(n)`, `varchar(n)`, `nchar(n)`, `nvarchar(max)` | `string` | `nchar(n)` keeps its padding spaces; `""` stays `""` |
| `tinyint` / `smallint` / `int` / `bigint` | `byte` / `short` / `int` / `long` | |
| `decimal(p, s)`, `money` | `decimal` | More than 28 significant digits: `ARX4001 ValueOutOfRange` |
| `real` / `float` | `float` / `double` | |
| `bit` | `bool` | |
| `uniqueidentifier` | `Guid` | |
| `datetimeoffset` | `DateTimeOffset` | The clock time and the offset it was stored with |
| `datetime2`, `datetime`, `smalldatetime` | `DateTime`, Kind `Unspecified` | The clock time as stored: SQL Server keeps no zone |
| `date` / `time` | `DateOnly` / `TimeOnly` | |
| `varbinary(n)`, `varbinary(max)`, `rowversion` | `byte[]` | |
| JSON in `nvarchar(max)` | `string` | SQL Server 2022 has no JSON type: the text as stored |
| `sql_variant` | the value's own type | |
| An alias type (`CREATE TYPE … FROM nvarchar(320)`) | Its base type | |
| A declared lookup (`.Lookup(...)`) | `ArxEntityReference` | `Name` from the referenced table's `.DisplayName(...)` |
| A selection set column | Its stored code | `entity.GetSelectionSet(column, set, catalog)` gives the option |
| NULL | `null` | The attribute is present, with a null value |

`RealCase1_EveryDataTypeReadBack` writes one row of every type with Create and prints each value's .NET type as it
reads back.

---

# ARX error codes (SQL Server)

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

## ARX2xxx NotFound

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

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

## ARX3xxx Constraint

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

| Code | Name | Meaning | Safe to retry | From SQL Server |
|---|---|---|---|---|
| ARX3001 | `UniqueViolation` | A unique constraint or key was violated. | no | `2601` duplicate key in a unique index; `2627` unique constraint violation |
| ARX3002 | `ForeignKeyViolation` | A referenced record is missing, or a record is still referenced. | no | `547` foreign key or check constraint conflict (told apart by the message) |
| ARX3003 | `NotNullViolation` | A required attribute has no value. | no | `515` NULL into a NOT NULL column |
| ARX3004 | `CheckViolation` | A check constraint or validation rule failed. | no | `547` check constraint conflict (the same number as a foreign key conflict; ARX reads which) |
| 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 | `50000+` THROW or RAISERROR with your own message or number |

## 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 SQL Server |
|---|---|---|---|---|
| ARX4001 | `ValueOutOfRange` | A number is outside the column's range. | no | `220` arithmetic overflow for a small integer type; `232` arithmetic overflow for type; `242` date or time out of the type's range; `517` date arithmetic overflow; `8115` arithmetic overflow converting a value |
| ARX4002 | `ValueTooLong` | Text or binary data is longer than the column allows. | no | `2628` string or binary data would be truncated; `8152` string or binary data would be truncated (older form) |
| ARX4003 | `InvalidValueFormat` | A value can't be parsed for its column (for example invalid JSON, dates or encoding). | no | `235` a text value can't be converted to money; `241` date or time conversion failed; `245` conversion failed; `8114` error converting a parameter's data type; `8169` conversion to uniqueidentifier failed; `13609` JSON text isn't properly formatted |
| ARX4004 | `DataTypeMismatch` | A value's type doesn't fit the column. | no | `201` a procedure parameter wasn't supplied; `206` operand type clash; `257` implicit conversion not allowed; `402` data types incompatible in an operator; `8144` too many arguments for a procedure or function; `8145` a named argument the procedure doesn't have |

## 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 SQL Server |
|---|---|---|---|---|
| ARX5001 | `UndefinedEntity` | The table or collection doesn't exist. | no | `208` invalid object name; `2812` stored procedure not found |
| ARX5002 | `UndefinedAttribute` | The column doesn't exist or can't be written. | no | `207` invalid column name; `271` a computed column can't be modified; `544` a value into an IDENTITY column; `8101` an explicit value for an IDENTITY column; `8102` an identity column can't be updated; `8120` a column neither grouped nor aggregated |
| 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 SQL Server |
|---|---|---|---|---|
| ARX6001 | `PermissionDenied` | The database user lacks the required permission. | no | `229` permission denied on an object; `230` permission denied on a column; `262` permission denied in the database; `18456` login failed; `33504` a row-level security block predicate refused the change |

## 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 SQL Server |
|---|---|---|---|---|
| ARX7001 | `Deadlock` | The operation was chosen as a deadlock victim. Safe to retry. | yes | `1205` chosen as the deadlock victim |
| ARX7002 | `SerializationFailure` | The operation conflicted with a concurrent transaction. Safe to retry. | yes | `3960` snapshot isolation update conflict |
| ARX7003 | `Timeout` | The command or a lock wait timed out. Safe to retry. | yes | `-2` the command timed out (client); `1222` lock request timeout |
| ARX7004 | `Throttled` | The server is rate-limiting or out of connections. Safe to retry later. | yes | `10928` resource limit reached; `10929` too busy; `40501` the service is busy |
| ARX7005 | `ConnectionFailure` | The database couldn't be reached, or the connection dropped. Safe to retry. | yes | `2` the server can't be reached; `53` network path not found; `233` no process on the other end of the pipe; `4060` cannot open the database; `10053` the connection was aborted; `10054` the connection was reset; `10060` the connection timed out; `10061` the connection was refused; `40613` database not currently available; `severity 20+` a fatal error: the connection is closed |

## 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 SQL Server |
|---|---|---|---|---|
| ARX8001 | `NotSupported` | The provider doesn't support the requested operation or feature. | no | `102` incorrect syntax; `156` incorrect syntax near a keyword; `266` a procedure changed the transaction count (committed or rolled back the caller's transaction); `1934` the operation needs other SET options; `2809` a function called as a procedure (a table-valued function: EXEC refuses it); `3902` COMMIT with no matching BEGIN TRANSACTION; `3903` ROLLBACK with no matching BEGIN TRANSACTION; `4403` a view with DISTINCT, GROUP BY or aggregates can't be updated; `4405` a view's change would affect several base tables; `4406` a view's derived field can't be updated; `4413` a view or function can't be used; `7601` full-text search on a column without a full-text index |

## ARX9xxx Provider

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

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

---

# Create with ARX.SDK on SQL Server

## What a create does

| | |
|---|---|
| **Names** | The entity name is the table; each attribute name is a column. Only ASCII letters, digits and underscores (at most 128 characters): `ARX1017 SuspectedInjection` when a name carries SQL, `ARX1003 InvalidIdentifier` for a plain bad name, both before anything is sent. With the default, case-insensitive collation SQL Server treats `Account` and `account` as one table; the schema catalog matches names exactly |
| **Values** | Always parameters: `Robert'); DROP TABLE contact;--`, `O'Brien`, emoji and backslashes are stored exactly as typed |
| **The id column** | Every table declares its one id column, or `ArxIdColumn.NoIdColumn`: in the schema catalog given to the service (the usual way), or on the entity (`entity.IdColumn`) when the service has no schema; never both. Undeclared: `ARX1016 IdNotDeclared`. `Generated` means the database fills the id in when you leave it out (an `IDENTITY` column refuses one you set, `ARX5002`; a `uniqueidentifier` with a default takes it); `Supplied` means you always give it (missing or null: `ARX1001`) |
| **What comes back** | `CreateAsync` returns the new id as an `ArxRecordId` (`AsInt64()`, `AsInt32()`, `AsString()`, `AsGuid()`, or `Value`); the same id is set on `entity.RecordId`. A table with no id column gives `ArxRecordId.None` (`HasValue` is false) |
| **Keys** | Optional (`.Key(table, columns…)`). Create doesn't use them: the database's unique constraints enforce them. Update and Delete find rows by them |
| **Defaults** | Columns you don't set get their database defaults. `new ArxIgnore()` leaves an attribute out |
| **Errors** | The class says where the error was found, the code what happened: `ArxValidationException` for a request ARX refuses before the database (1xxx, and the 4xxx value checks ARX makes itself); `ArxDatabaseException` for an error the database reports (3xxx, 7xxx, and the 4xxx and 5xxx it reports); `ArxNotSupportedException` for ARX8001. The outcome codes (2xxx: nothing matched, several matched, a changed record) and a value read back as a type the schema doesn't expect (ARX5003) can come as the base `ArxException`. All of them are `ArxException`, with `Code`, `Category`, `EntityName` and `AttributeName`: catch `ArxException` and branch on the code or category, never on the class or the message. ARX's messages name tables, columns and constraints, never values, so they are safe to log. SQL Server's own message (the `SqlException` in `InnerException`) can quote the value: log ARX's |
| **Without a catalog** | A service created with only the connection string reads each table's primary key and column types from the database before the call: the id is generated when the key has a default or identity, supplied otherwise; one more round trip per call, so much slower than with a catalog: meant for simple CRUD and small applications. `RememberResolvedTables = true` reads each table once per service. No primary key, a compound one or one of an unusable type is `ARX1019 TableNotResolvable` (declare it in a catalog or with `entity.IdColumn`); a service with a catalog never resolves (Basic6, Advanced10) |

## Reference

### Where a table is declared

| The service has | The call passes a schema | The entity declares | Result |
|---|---|---|---|
| A schema | No | Nothing | The service's schema |
| A schema | Yes | Nothing | The call's schema for the tables it declares; the service's for the others (selection sets the same way, by name) |
| No schema | Yes | Nothing | The call's schema |
| No schema | No | `IdColumn` (and `Keys`) | The entity's declaration |
| A schema, or the call passes one | | `IdColumn` or `Keys` | Refused: `ARX1001`, before anything is sent |
| No schema | No | Nothing | Read from the database before the call (a service without a catalog): the table's primary key is its id; `ARX1019 TableNotResolvable` when the call needs an id the table doesn't give (no primary key, a compound one, a type ARX can't use) |
| A schema that doesn't declare the table | No | Nothing | Refused: `ARX1016 IdNotDeclared`, naming the line to add |

Entities with the same declaration share one statement, even when each gets its own `ArxIdColumn` object.

### Ids by kind of table

| The table has | Declared as | What you do |
|---|---|---|
| A number the database generates (`IDENTITY`) | `Generated<long>("id")` | Leave it out; read the returned id (or `entity.RecordId`) after Create |
| A uuid the database generates (`DEFAULT NEWID()`) | `Generated<Guid>("id")` | Leave it out, or set it to write your own (`device["id"] = Guid.NewGuid()`) |
| A natural id (code, SKU) | `Supplied<string>("code")` | Always set it; missing or null is `ARX1001` |
| No id column (a log, or rows identified by several columns) | `NoIdColumn`, plus `.Key(table, columns…)` if columns identify a row | Set the columns; Create returns `ArxRecordId.None`, and the database enforces the key |

Id types: `short`, `int`, `long`, `decimal`, `string`, `Guid`, `DateOnly`, `DateTime`. ARX reads the new ids back with
`OUTPUT`, in list order; on a table with a trigger (where SQL Server allows only `OUTPUT INTO`) it switches to a table
variable on its own and remembers the table.

### Lookups

`new ArxEntityReference(table, id, name?)`: the referenced **table first, then the id** (of any id type, or the
`ArxRecordId` Create returned), then an optional display name.

| Id type of the referenced table | Constructor |
|---|---|
| `short`, `int`, `long` | `new ArxEntityReference("account", 42L)` |
| `string` | `new ArxEntityReference("country", "PT")` |
| `Guid` | `new ArxEntityReference("device", deviceId)` |
| `decimal`, `DateOnly`, `DateTime` | `new ArxEntityReference("t", value)` |
| Any: the id Create returned | `new ArxEntityReference("account", accountId)` |

- **Checked before anything is sent** (`ARX1005 InvalidLookup`, naming the attribute): the reference has an id (not
  missing, zero, negative, an empty Guid or empty text); with a schema, the table is declared, has an id column, and
  the id has its type (integer widths are interchangeable).
- **Checked by the database:** the referenced record exists, through the foreign key (`ARX3002 ForeignKeyViolation`).
  Without a foreign key, an id that matches nothing is stored as given.

### Value types

| .NET value | Column | Stored as |
|---|---|---|
| `string` | `nvarchar(n)`, `varchar(n)`, `nchar(n)`, `nvarchar(max)` | as written (`nchar(n)` pads with spaces) |
| `byte` / `short` / `int` / `long` | `tinyint` / `smallint` / `int` / `bigint` | `200` / `32000` / `2000000000` / `9000000000000` |
| `decimal` | `decimal(p, s)`, `money` | `12345.678901` |
| `float` / `double` | `real` / `float` | `3.25` / `2.718281828459045` |
| `bool` | `bit` | `1` |
| `Guid` | `uniqueidentifier` | `6F9619FF-8B86-D011-B42D-00C04FC964FF` |
| `DateTimeOffset` | `datetimeoffset` | the moment, in UTC: `22:30 +08:00` is stored as `2026-11-07 14:30:00 +00:00`. The same with a hand-written catalog, a preload or none: like PostgreSQL and MySQL, ARX keeps the moment, not the offset (store the offset in a column of its own if the application needs it) |
| `DateTimeOffset` | `datetime2`, `datetime` | the moment in UTC (`2026-11-07 14:30:00`), with or without a catalog (SQL Server alone would drop the offset and keep `22:30`, eight hours off) |
| `DateTime` | `datetime2`, `datetime` | as written: `2026-11-07 09:00:00` (`datetime` rounds to 1/300 of a second) |
| `DateOnly` / `TimeOnly` | `date` / `time` | `2026-11-07` / `17:45:30` |
| `byte[]`, `ArxBytea` | `varbinary(n)`, `varbinary(max)` | `0xCAFE` |
| `ArxJsonB` | `nvarchar(max)`, with `CHECK (ISJSON(column) = 1)` | the JSON text (invalid JSON fails the check: `ARX3004`) |
| `ArxEntityReference` | the lookup column | the referenced id |
| `ArxSelectionSetValue` | the set's column | the option's code |
| `null` | any | `NULL` |
| `sbyte` / `ushort` / `uint` / `ulong` | converted to `smallint` / `int` / `bigint` / `decimal` | `-100` / `65000` / `4000000000` / `18446744073709551615` |
| `char` | text | `X` |
| a plain `enum` | refused (ARX1007) | write `(int)value` or `value.ToString()`, or use a selection set, which stores the code its set declares |
| `DateTime` with Kind `Utc` | `datetimeoffset` | the moment, offset `+00:00` |
| `DateTime` with Kind `Local` | `datetime2`, `datetimeoffset` | converted to UTC first |

Refused before anything is sent: any other type (`ARX1007 UnsupportedValueType`: write `uri.ToString()` rather than a
`Uri`); arrays (`ARX8001`: SQL Server has no array columns, so store a list as delimited text, JSON or rows of its own);
`NaN` and the infinities, and a `TimeSpan` below zero or of 24 hours or more (`ARX4001`: `time` holds a time of day, so
store a duration as a number).

With the column types preloaded (`ArxPreLoadMode.DataTypes`), a number with a fraction written to a whole-number column is refused before SQL Server (`ARX4001 ValueOutOfRange`) on a create, an update and a write by a filter, where SQL Server would round or truncate it; a condition in a read (`qty < 7.5`) isn't refused. A `NaN` or an infinity written to a declared `decimal` column is refused the same way.

SQL Server converts some values silently that other databases refuse: `"42"` into an `int`, a number into a `bit`, a
`decimal` into an `int` by cutting off the fraction. A preload with column types (`ArxPreLoadMode.DataTypes`) refuses
those before the database. `RealCase2_EveryDataType` writes every row of this table and prints what SQL Server stored.

### Dates and times

| You have | Use | Column |
|---|---|---|
| A moment (an order was placed) | `DateTimeOffset` | `datetimeoffset` |
| A moment, in a column without an offset | `DateTime` with Kind `Utc` | `datetime2` (store UTC, read it back as UTC) |
| A wall-clock time with no zone | `DateTime` with Kind `Unspecified` | `datetime2` |
| A calendar date | `DateOnly` | `date` |
| A time of day | `TimeOnly` | `time` |
| A duration | a number (seconds, minutes) | `int`, `bigint` |

SQL Server keeps no time zone: `datetime2` stores the clock time it is given, and `datetimeoffset` the clock time and
its offset. ARX writes a `DateTimeOffset` as its UTC instant and converts a local `DateTime` to UTC first, so a moment is
never stored hours off. Refused: `default(DateTime)` (`ARX1013`: it means "not set",
and SQL Server would store `0001-01-01`; use `null`). ARX never parses a string as a date, but SQL Server converts
one it can read, by the session's date format (`01/07/1992` is January 7 under `us_english`), and refuses one it
can't (`ARX4003`); with a schema preloaded with data types, ARX refuses every date string first (`ARX4004`). Parse
dates yourself. Full rules: [Data types](/csharp/sql/data-types).

### Selection sets

A selection set (picklist, option set) is a column that accepts only a fixed list of codes, each with a display text.
The **catalog** lists them once, in code (or loaded from JSON, a table or configuration at startup), and the service
checks every `ArxSelectionSetValue` against it in memory, before any SQL is sent: no lookup table, no extra query.

| Part | What it says | Example |
|---|---|---|
| Name | What an `ArxSelectionSetValue` refers to | `"AccountStatus"` |
| Kind | What the codes are, matching the column | `String`, `Integer`, `Character`, `Boolean`, `Decimal`, `Guid` (`DatabaseEnum` is `ARX8001`: SQL Server has no enum types; a `CHECK` on a text column does that job) |
| Multiplicity | One code, or several | `Single`, or several stored as `Flags`, `Delimited` or `Json` (`Array` is `ARX8001`) |
| Options | Each code with its display text and order | `("ON_HOLD", "On hold")` |
| Active | A retired option stays readable in old rows, but can't be written | `IsActive: false` |

Refused before the database: a code that isn't in the set, a retired option, several values for a single-choice set
(`ARX1011`), a set the catalog doesn't have (`ARX1012`). A plain value (not an `ArxSelectionSetValue`) isn't checked.
`catalog.TryGetSet(name, out var set)` gives a set's options, for example for a drop-down list.

### Database errors as user messages

| What the user did | Code | Details on the exception |
|---|---|---|
| Reused an account number | `ARX3001 UniqueViolation` | `ConstraintName` |
| Left the name empty | `ARX3003 NotNullViolation` | `AttributeName` |
| Entered a negative employee count | `ARX3004 CheckViolation` | `ConstraintName` |
| Pasted a 220-character website into `nvarchar(200)` | `ARX4002 ValueTooLong` | `AttributeName` |
| Entered 3,000,000,000 employees into an `int` | `ARX4001 ValueOutOfRange` | – |
| Typed "forty" into a number | `ARX4003 InvalidValueFormat` | – (SQL Server doesn't say which column) |
| A field the table doesn't have | `ARX5002 UndefinedAttribute` | the column, in the message |
| A table name with SQL in it | `ARX1017 SuspectedInjection`, before the database | the name, in the message |

`ArxDatabaseException` keeps SQL Server's error number as `NativeCode` (`2627`) and the `SqlException` as
`InnerException`; `IsTransient` is true for errors worth retrying (deadlocks, timeouts, dropped connections). All
codes: [the error codes](/csharp/sql/error-codes).

### Transactions, batch scopes and services

- **One service per database, a singleton.** It holds no connection; every call takes one from the
  Microsoft.Data.SqlClient pool. Inject `IArxBatchDataService` where code only needs batch scopes.
- **An ARX transaction** (`new ArxMsSqlConnection(cs)`, then `BeginTransactionAsync()`) is for code that decides, step
  by step, whether the work is kept. A failed database call aborts it:

  | After | `IsAborted` | Next call | `CommitAsync()` |
  |---|---|---|---|
  | A database error (constraint, type, connection, cancellation) | `true` | `ARX1015 TransactionAborted` | rolls back, throws `ARX1015` |
  | A validation error (before the database) | `false` | works | commits |

- **A batch scope** gives each call its own savepoint (`SAVE TRANSACTION`), so a failed batch is undone alone
  (`BeginBatchAsync` to decide yourself, `RunBatchAsync` to commit when the body returns). With `Savepoints = false`,
  any failure rolls back everything. Nothing a scope writes is visible to other connections until it commits; under
  SQL Server's default `READ COMMITTED`, another connection that reads those rows waits for the commit.
- **Two databases can't share a transaction.** Write the source of truth first and commit; then write the copy, made
  safe to repeat (a unique key there).
- *Interop only:* `CreateAsync(entity, IDbConnection, IDbTransaction)` accepts a `SqlConnection` and its
  `SqlTransaction`, to share them with another library; ARX never commits, rolls back or closes them. A connection with
  an open transaction must be given that transaction (`ARX1008`).

### Checking declarations at startup

`ValidateSchemaAsync` compares each declaration with the database and reports every difference at once
(`ARX5003 SchemaMismatch`): the table exists; the id column exists with a compatible type, is generated (`IDENTITY` or
a default) when declared so, and identifies one row on its own; every key's columns have a primary key or unique index
on exactly those columns (a unique index filtered `WHERE column IS NOT NULL` counts: it is SQL Server's way to make a
nullable column unique). A drifted declaration that reaches a write is `ARX5003` too, never a raw .NET exception:
without a transaction the row is already committed, in an ARX transaction nothing is kept, in a batch scope that call
is rolled back to its savepoint.

### Preloading the schema at service setup

| Choose | Mode | What you get | Trade-off |
|---|---|---|---|
| **Convenience** | `ArxPreLoadMode.KeysOnly` | The table keys, as you would declare them: the id, business keys (unique constraints and indexes), lookups (foreign keys) | Values go to the database as they are, and SQL Server converts what it can (`"42"` into an `int`) |
| **Typed** (the default) | `ArxPreLoadMode.DataTypes` | The keys plus every loaded column's type: a wrong type refused before the database (ARX4004) | A little more to load at startup |
| **An allow list** | `DataTypes` + `Strict = true` | Only the preloaded tables and columns can be used; anything else is `ARX1018 NotPreloaded`, before the database | Every column a call uses must be listed; functions and Execute aren't covered |

A table a loaded column references is added with its reason (`"added: referenced by account.primary_contact_id"`).
Display names and selection sets aren't discovered: add them in `LoadAsync(cs, configure => …)`.

## The samples

Every Create sample, in reading order. Each scenario shows the code (verified against SQL Server) and the SQL that checks what it did. `service` is the application's `ArxMsSqlDbService`; 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-sql-sample-application.md.

### Create one record and get its id

Basics 1 of Create.

The smallest create: one record, in four scenarios. Read them in order: the usual way first (the id column declared in the schema), then the other way (declared on the entity), then two tables with no id column: one identified by a compound key, one with no key at all.

Usings:

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

#### 1. Id column in the schema

The usual way: the id column declared in the schema, the id read from what CreateAsync returns.

In scope: `connectionString` (string).

```csharp
// Declare each table's id column once, at startup. ARX never guesses it.
var schema = ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id"))
    .Build();

var service = new ArxMsSqlDbService(connectionString, schema);

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

// CreateAsync returns the id of the new record.
ArxRecordId id = await service.CreateAsync(entity);

// The account's id is a bigint column, so read it as a number.
long accountId = id.AsInt64();

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

Check it in the database:

```sql
SELECT id, name, account_number, employee_count
FROM account
WHERE account_number = 'ACC-0001';
```

Expect: 1 row, Northwind Traders, employee_count 240.

#### 2. Id column on the entity

The other way: the id column declared on the entity, the id read from the entity.

In scope: `connectionString` (string).

```csharp
// No schema catalog: the entity says what its id column is, for this record only.
var service = new ArxMsSqlDbService(connectionString);

var entity = new ArxEntity("account");
entity.IdColumn = ArxIdColumn.Generated<long>("id");
entity["name"] = "Contoso Pharmaceuticals";
entity["account_number"] = "ACC-0006";
entity["employee_count"] = 85;

await service.CreateAsync(entity);

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

Check it in the database:

```sql
SELECT id, name, account_number, employee_count
FROM account
WHERE account_number = 'ACC-0006';
```

Expect: 1 row, Contoso Pharmaceuticals, employee_count 85.

#### 3. Compound key

A table with no id column, where a compound key (region + sku) identifies each record.

In scope: `connectionString` (string).

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

var service = new ArxMsSqlDbService(connectionString, schema);

// A price's region must be a country that exists, so add the country first.
var italy = new ArxEntity("country");
italy["code"] = "IT";
italy["name"] = "Italy";
await service.CreateAsync(italy);

var price = new ArxEntity("price_list");
price["region"] = "IT";
price["sku"] = "SKU-000001";
price["unit_price"] = 9.50m;
price["valid_from"] = new DateOnly(2026, 1, 1);

// There is no id column, so there is no id to return: ArxRecordId.None. The region and SKU identify the record.
ArxRecordId id = await service.CreateAsync(price);

Console.WriteLine($"created price region {price["region"]}, sku {price["sku"]} (id {id})");
```

Check it in the database:

```sql
SELECT p.region, c.name AS country, p.sku, p.unit_price, p.valid_from
FROM price_list p
JOIN country c ON c.code = p.region
WHERE p.region = 'IT' AND p.sku = 'SKU-000001';
```

Expect: 1 row, Italy, unit_price 9.50.

#### 4. No key

A table with no id column and no key: rows may repeat, like a log.

In scope: `connectionString` (string).

```csharp
var schema = ArxSchemaCatalog.Create()
    .Table("activity_log", ArxIdColumn.NoIdColumn)
    .Build();

var service = new ArxMsSqlDbService(connectionString, schema);

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

ArxRecordId id = await service.CreateAsync(entry);

Console.WriteLine($"logged \"{entry["action"]}\" (id {id})");
```

Check it in the database:

```sql
SELECT action, logged_at
FROM activity_log
WHERE action = 'signed in';
```

Expect: 1 row (the table has no id column).

### Create a record that points at another one

Basics 2 of Create.

A record that points at another one (a lookup), in four scenarios: by the referenced entity's RecordId, by the id CreateAsync returned, to a record whose id is text, and what happens when the lookup is wrong.

Usings:

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

#### 1. By the entity's RecordId

The usual way: point at the account by the RecordId set on it when it was created.

In scope: `connectionString` (string).

```csharp
var service = Service(connectionString);

var account = new ArxEntity("account");
account["name"] = "Adventure Works";
await service.CreateAsync(account);

var contact = new ArxEntity("contact");
contact["first_name"] = "Ana";
contact["last_name"] = "Silva";
contact["email"] = "ana.silva@adventure-works.example";
contact["account_id"] = new ArxEntityReference("account", account.RecordId);   // the table, then the id

await service.CreateAsync(contact);

Console.WriteLine($"created contact {contact.RecordId} for account {account.RecordId}");
```

Defined in the sample, used above:

```csharp
private static ArxMsSqlDbService Service(string connectionString) => new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id"))
    .Table("contact", ArxIdColumn.Generated<long>("id"))
    .Table("country", ArxIdColumn.Supplied<string>("code"))
    .Table("price_list", ArxIdColumn.NoIdColumn).Key("price_list", "region", "sku")
    .Build());
```

Check it in the database:

```sql
SELECT c.id, c.first_name, c.last_name, c.account_id, a.name AS account
FROM contact c
JOIN account a ON a.id = c.account_id
WHERE c.email = 'ana.silva@adventure-works.example';
```

Expect: 1 row, Ana Silva, account Adventure Works.

#### 2. By the id Create returned

The other way: keep the id CreateAsync returned, and point at the account with it.

In scope: `connectionString` (string).

```csharp
var service = Service(connectionString);

var account = new ArxEntity("account");
account["name"] = "Proseware Labs";

ArxRecordId accountId = await service.CreateAsync(account);

var contact = new ArxEntity("contact");
contact["first_name"] = "Ben";
contact["last_name"] = "Okafor";
contact["email"] = "ben.okafor@proseware-labs.example";
contact["account_id"] = new ArxEntityReference("account", accountId);    // an ArxRecordId works for any id type

await service.CreateAsync(contact);

Console.WriteLine($"created contact {contact.RecordId} for account {accountId}");
```

Check it in the database:

```sql
SELECT c.id, c.first_name, c.last_name, c.account_id, a.name AS account
FROM contact c
JOIN account a ON a.id = c.account_id
WHERE c.email = 'ben.okafor@proseware-labs.example';
```

Expect: 1 row, Ben Okafor, account Proseware Labs.

#### 3. To a text id

A record whose id is text: a price points at its country by the country code.

In scope: `connectionString` (string).

```csharp
var service = Service(connectionString);

var belgium = new ArxEntity("country");
belgium["code"] = "BE";
belgium["name"] = "Belgium";
await service.CreateAsync(belgium);

var price = new ArxEntity("price_list");
price["region"] = new ArxEntityReference("country", "BE");               // a text id
price["sku"] = "SKU-000001";
price["unit_price"] = 2.40m;
price["valid_from"] = new DateOnly(2026, 1, 1);

await service.CreateAsync(price);

Console.WriteLine("created a price for country BE");
```

Check it in the database:

```sql
SELECT p.region, c.name AS country, p.sku, p.unit_price
FROM price_list p
JOIN country c ON c.code = p.region
WHERE p.region = 'BE';
```

Expect: 1 row, Belgium, SKU-000001, unit_price 2.40.

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

When the lookup is wrong: a record that doesn't exist is refused by the database's foreign key (ARX3002); a table the schema doesn't declare is refused by ARX before anything is sent (ARX1005).

In scope: `connectionString` (string).

```csharp
var service = Service(connectionString);

var noSuchAccount = new ArxEntity("contact");
noSuchAccount["first_name"] = "Nobody";
noSuchAccount["last_name"] = "Here";
noSuchAccount["account_id"] = new ArxEntityReference("account", 999_999L);

try
{
    await service.CreateAsync(noSuchAccount);
}
catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.ForeignKeyViolation)
{
    Console.WriteLine($"{error.Code} (the database's foreign key)");
}

var typo = new ArxEntity("contact");
typo["first_name"] = "Typo";
typo["last_name"] = "Table";
typo["account_id"] = new ArxEntityReference("acount", 1L);              // "acount": not a declared table

try
{
    await service.CreateAsync(typo);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.InvalidLookup)
{
    Console.WriteLine($"A table the schema lacks: {error.Code}, before anything was sent: {error.Message}");
}
```

Check it in the database:

```sql
SELECT count(*)
FROM contact
WHERE first_name IN ('Nobody', 'Typo');
```

Expect: 0: both contacts were refused.

### Catch an error by its code

Basics 3 of Create.

Catching errors, in three scenarios: a database error by its code, several database errors by their category, and a mistake ARX refuses before anything is sent to the database.

Usings:

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

#### 1. By its code

The usual way: the database refuses a record, and the ARX code says why. Here, a duplicate account number.

In scope: `connectionString` (string).

```csharp
var service = Service(connectionString);

var original = new ArxEntity("account");
original["name"] = "Tailspin Toys";
original["account_number"] = "ACC-0005";
await service.CreateAsync(original);

// Account numbers are unique, so the database refuses this one.
var duplicate = new ArxEntity("account");
duplicate["name"] = "Tailspin Toys (copy)";
duplicate["account_number"] = "ACC-0005";

try
{
    await service.CreateAsync(duplicate);
}
catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.UniqueViolation)
{
    Console.WriteLine($"not created: {error.Message}");
}
```

Defined in the sample, used above:

```csharp
private static ArxMsSqlDbService Service(string connectionString) => new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id"))
    .Table("country", ArxIdColumn.Supplied<string>("code"))
    .Build());
```

Check it in the database:

```sql
SELECT name, account_number
FROM account
WHERE account_number = 'ACC-0005';
```

Expect: 1 row, Tailspin Toys: the copy was refused.

#### 2. By its category

The other way: catch by category when several codes need the same handling. Every constraint the database enforces (unique, not null, check, foreign key) is the Constraint category.

In scope: `connectionString` (string).

```csharp
var service = Service(connectionString);

var numberTaken = new ArxEntity("account");
numberTaken["name"] = "Tailspin Toys (another copy)";
numberTaken["account_number"] = "ACC-0005";

var noName = new ArxEntity("account");
noName["account_number"] = "ACC-0013";   // name is required (NOT NULL)

foreach (var account in new[] { numberTaken, noName })
{
    try
    {
        await service.CreateAsync(account);
    }
    catch (ArxDatabaseException error) when (error.Category == ArxErrorCategory.Constraint)
    {
        Console.WriteLine($"{error.Category}: {error.Code}");
    }
}
```

Check it in the database:

```sql
SELECT account_number, count(*)
FROM account
WHERE account_number IN ('ACC-0005', 'ACC-0013')
GROUP BY account_number;
```

Expect: only ACC-0005, count 1: neither refused account was stored.

#### 3. Before the database

A mistake in the request itself is refused by ARX before anything is sent: an ArxValidationException, not an ArxDatabaseException. Here, a country without its code, which the schema declares as supplied.

In scope: `connectionString` (string).

```csharp
var service = Service(connectionString);

var noCode = new ArxEntity("country");
noCode["name"] = "Narnia";

try
{
    await service.CreateAsync(noCode);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.InvalidArgument)
{
    Console.WriteLine($"{error.Code}: {error.Message}");
}
```

Check it in the database:

```sql
SELECT count(*)
FROM country
WHERE name = 'Narnia';
```

Expect: 0: refused before anything was sent.

### Ids that aren't numbers: text and uuid

Basics 4 of Create.

Ids that aren't numbers, in three scenarios: a text id you supply (a country code), a uuid the database generates, and a uuid your code generates.

Usings:

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

#### 1. Text id you supply

A text id you supply: the country code. Supplied means you always set it.

In scope: `connectionString` (string).

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

var service = new ArxMsSqlDbService(connectionString, schema);

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

ArxRecordId id = await service.CreateAsync(country);

// A text id is read with AsString().
Console.WriteLine($"created country {id.AsString()}");
```

Check it in the database:

```sql
SELECT code, name
FROM country
WHERE code = 'PT';
```

Expect: 1 row, Portugal.

#### 2. Uuid the database makes

A uuid the database generates (DEFAULT NEWID()): leave the id out.

In scope: `connectionString` (string).

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

var service = new ArxMsSqlDbService(connectionString, schema);

var device = new ArxEntity("device");
device["serial_number"] = "DEV-0001";

ArxRecordId id = await service.CreateAsync(device);

// A uuid is read with AsGuid().
Console.WriteLine($"created device {id.AsGuid()}");
```

Check it in the database:

```sql
SELECT id, serial_number
FROM device
WHERE serial_number = 'DEV-0001';
```

Expect: 1 row, with the uuid the database generated.

#### 3. Uuid your code makes

A uuid your code generates: set the id, and it's written as given.

In scope: `connectionString` (string).

```csharp
// The same declaration as scenario 2: Generated means the database CAN fill it in, not that it must.
var schema = ArxSchemaCatalog.Create()
    .Table("device", ArxIdColumn.Generated<Guid>("id"))
    .Build();

var service = new ArxMsSqlDbService(connectionString, schema);

var myId = Guid.NewGuid();

var device = new ArxEntity("device");
device["id"] = myId;
device["serial_number"] = "DEV-0002";

ArxRecordId id = await service.CreateAsync(device);

Console.WriteLine($"created device {id.AsGuid()} (the same as myId: {id.AsGuid() == myId})");
```

Check it in the database:

```sql
SELECT id, serial_number
FROM device
WHERE serial_number = 'DEV-0002';
```

Expect: 1 row, with the uuid the sample printed (myId).

### Where to declare: on the entity, or a schema for one call, never both

Basics 5 of Create.

Where a table's id column and keys are declared, in three scenarios: on the entity (for a service with no schema), in a schema passed to one call (which overrides the service's schema for that call), and the rule that a table is declared in one place only.

Usings:

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

#### 1. On the entity

A service with no schema: each entity says its id column (none for the price) and its key.

In scope: `connectionString` (string).

```csharp
// No schema catalog: the country and the price below declare themselves.
var service = new ArxMsSqlDbService(connectionString);

// A price's region must be a country that exists, so add the country first.
var austria = new ArxEntity("country");
austria.IdColumn = ArxIdColumn.Supplied<string>("code");
austria["code"] = "AT";
austria["name"] = "Austria";
await service.CreateAsync(austria);

var price = new ArxEntity("price_list");
price.IdColumn = ArxIdColumn.NoIdColumn;
price.Keys = new[] { new ArxKey("region", "sku") };
price["region"] = new ArxEntityReference("country", "AT");   // a lookup to the country, by its code
price["sku"] = "SKU-000042";
price["unit_price"] = 4.90m;
price["valid_from"] = new DateOnly(2026, 1, 1);

await service.CreateAsync(price);

Console.WriteLine($"created price region {austria["code"]}, sku {price["sku"]}");
```

Check it in the database:

```sql
SELECT p.region, c.name AS country, p.sku, p.unit_price
FROM price_list p
JOIN country c ON c.code = p.region
WHERE p.region = 'AT';
```

Expect: 1 row, Austria, SKU-000042, unit_price 4.90.

#### 2. A schema for one call

The service's schema says the database can generate a device's uuid. This import wants every device to bring its own, so it passes a schema for this one call that declares the id Supplied: it overrides the service's declaration of that table, and ARX then refuses a device without an id.

In scope: `connectionString` (string).

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

var service = new ArxMsSqlDbService(connectionString, schema);

// Only for this import: the same table, declared stricter.
var importSchema = ArxSchemaCatalog.Create()
    .Table("device", ArxIdColumn.Supplied<Guid>("id"))
    .Build();

var noId = new ArxEntity("device");
noId["serial_number"] = "IMPORT-0001";

try
{
    await service.CreateMultipleAsync(new List<ArxEntity> { noId }, importSchema);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.InvalidArgument)
{
    Console.WriteLine($"refused before anything was sent ({error.AttributeName} is required)");
}

var withId = new ArxEntity("device");
withId["id"] = Guid.NewGuid();
withId["serial_number"] = "IMPORT-0002";

var ids = await service.CreateMultipleAsync(new List<ArxEntity> { withId }, importSchema);

Console.WriteLine($"created device {ids[0]}");
```

Check it in the database:

```sql
SELECT id, serial_number
FROM device
WHERE serial_number IN ('IMPORT-0001', 'IMPORT-0002');
```

Expect: 1 row, IMPORT-0002: IMPORT-0001 had no id and was refused.

#### 3. One place only

One place only: when there is a schema (the service's, or one passed to the call), an entity that also declares its own id column or keys is refused, so a table is never declared twice.

In scope: `connectionString` (string).

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

var service = new ArxMsSqlDbService(connectionString, schema);

var device = new ArxEntity("device");
device.IdColumn = ArxIdColumn.Generated<Guid>("id");   // declared again on the entity: not allowed
device["serial_number"] = "TWICE-0001";

try
{
    await service.CreateAsync(device);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.InvalidArgument)
{
    Console.WriteLine($"refused ({error.Code}): the service has a schema, so the entity can't declare its own");
}
```

Check it in the database:

```sql
SELECT count(*)
FROM device
WHERE serial_number = 'TWICE-0001';
```

Expect: 0: refused before anything was sent.

### Create without a catalog

Basics 6 of Create.

A service created with only a connection string, in four scenarios: ARX reads the table's id column and column types from the database before the create, for a generated id and for a supplied text id; then the two tables it can't resolve an id for, one with no primary key and one with a compound primary key, refused with ARX1019, and the one line that fixes each.

Good to know:

Without a catalog, every call first reads the tables it writes from the database: one more round trip per call, so it is much slower than a service with a catalog. It is meant for simple CRUD in small applications and scripts; for anything more, declare the catalog or preload it (Create Advanced6), or remember what was read (Create Advanced10).

ARX still never guesses: it uses the primary key the table declares. A table with no primary key, a compound one, or one of a type an id can't be is ARX1019 TableNotResolvable when the call needs the id, with the reason and the fix in the message.

A service created with a catalog never resolves: a table missing from it is still ARX1016 IdNotDeclared.

Usings:

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

Set up by the sample before its scenarios:

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

#### 1. A generated id, read from the database

The usual case: account's primary key is a bigint IDENTITY, so ARX knows the id is generated and returns it.

In scope: `service` (ArxMsSqlDbService).

```csharp
var entity = new ArxEntity("account");
entity["name"] = "Adatum Corporation";
entity["account_number"] = "ACC-N001";
entity["employee_count"] = 120;

ArxRecordId id = await service.CreateAsync(entity);

Console.WriteLine($"created account {id.AsInt64()} (id column \"{id.Column}\")");
```

Check it in the database:

```sql
SELECT id, name, employee_count
FROM account
WHERE account_number = 'ACC-N001';
```

Expect: 1 row, Adatum Corporation, employee_count 120.

#### 2. A supplied text id

country's primary key is a char(2) with no default, so the id is supplied: set it like any attribute.

In scope: `service` (ArxMsSqlDbService).

```csharp
var norway = new ArxEntity("country");
norway["code"] = "NO";
norway["name"] = "Norway";

ArxRecordId id = await service.CreateAsync(norway);

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

Check it in the database:

```sql
SELECT code, name
FROM country
WHERE code = 'NO';
```

Expect: 1 row, Norway.

#### 3. A table with no primary key

activity_log has no primary key: ARX1019, and the fix, declaring that on the entity.

In scope: `service` (ArxMsSqlDbService).

```csharp
var entry = new ArxEntity("activity_log");
entry["action"] = "exported the price list";

try
{
    await service.CreateAsync(entry);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.TableNotResolvable)
{
    Console.WriteLine($"refused: {error.Message}");
}

// Say so on the entity (or in a catalog): the row is written and there is no id to return.
entry.IdColumn = ArxIdColumn.NoIdColumn;
ArxRecordId none = await service.CreateAsync(entry);

Console.WriteLine($"logged \"{entry["action"]}\" (id {none})");
```

Check it in the database:

```sql
SELECT action, logged_at
FROM activity_log
WHERE action = 'exported the price list';
```

Expect: 1 row (the refused call wrote nothing).

#### 4. A compound primary key

price_list's primary key is two columns (region, sku): no single id, so ARX1019 too, with that reason.

In scope: `service` (ArxMsSqlDbService).

```csharp
var price = new ArxEntity("price_list");
price["region"] = "NO";
price["sku"] = "SKU-N00001";
price["unit_price"] = 12.00m;
price["valid_from"] = new DateOnly(2026, 1, 1);

try
{
    await service.CreateAsync(price);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.TableNotResolvable)
{
    Console.WriteLine($"refused: {error.Message}");
}

// The region and SKU identify the row: declare no id column, and the two as its key.
price.IdColumn = ArxIdColumn.NoIdColumn;
price.Keys = new[] { new ArxKey("region", "sku") };
await service.CreateAsync(price);

Console.WriteLine($"created price region {price["region"]}, sku {price["sku"]}");
```

Check it in the database:

```sql
SELECT region, sku, unit_price
FROM price_list
WHERE sku = 'SKU-N00001';
```

Expect: 1 row, NO, unit_price 12.00.

### A sign-up form creates an account

Real case 1 of Create.

A sign-up form creates a customer account, in three scenarios: the account from the form (columns left out get their database defaults), a subsidiary that points at its parent, and form fields the table doesn't have (ArxIgnore).

Usings:

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

#### 1. Create the account from the form

The account from the sign-up form. guid, status and created_on aren't set: the database fills them in.

In scope: `service` (ArxMsSqlDbService).

```csharp
var account = new ArxEntity("account");
account["name"] = "Northwind Traders";
account["account_number"] = "ACC-10001";
account["industry"] = new ArxSelectionSetValue("Industry", Industry.Retail);
account["website"] = "https://northwind.example";
account["annual_revenue"] = 12_500_000.00m;
account["employee_count"] = 240;
account["is_key_account"] = true;
account["founded_on"] = new DateOnly(1998, 4, 1);
account["modified_on"] = DateTime.UtcNow;

var id = await service.CreateAsync(account);
Console.WriteLine($"account {id} (also on account.RecordId)");

return account;
```

Check it in the database:

```sql
SELECT id, name, account_number, industry, website, annual_revenue, guid, status, created_on
FROM account
WHERE account_number = 'ACC-10001';
```

Expect: 1 row; guid, status and created_on are the database defaults.

#### 2. Add a subsidiary that points at its parent

A subsidiary account points at its parent with a lookup.

In scope: `service` (ArxMsSqlDbService), `parent` (ArxEntity).

```csharp
var subsidiary = new ArxEntity("account");
subsidiary["name"] = "Northwind Traders (Canada)";
subsidiary["account_number"] = "ACC-10002";
subsidiary["parent_account_id"] = new ArxEntityReference("account", parent.RecordId);

var id = await service.CreateAsync(subsidiary);
```

Check it in the database:

```sql
SELECT s.name, s.account_number, s.parent_account_id, p.name AS parent
FROM account s
JOIN account p ON p.id = s.parent_account_id
WHERE s.account_number = 'ACC-10002';
```

Expect: 1 row, Northwind Traders (Canada), parent Northwind Traders.

#### 3. Form fields the table doesn't have are marked ArxIgnore

A form field the table doesn't have stays on the entity, marked ArxIgnore, and isn't written.

In scope: `service` (ArxMsSqlDbService).

```csharp
var fromForm = new ArxEntity("account");
fromForm["name"] = "Contoso Pharmaceuticals";
fromForm["industry"] = new ArxSelectionSetValue("Industry", Industry.Healthcare);
fromForm["display_label"] = new ArxIgnore();

// The id column "id" is declared Generated, and the form doesn't set it, so the database fills it in. An id you do
// set is written as given, so leave the id attribute out unless you mean it.
var id = await service.CreateAsync(fromForm);
Console.WriteLine($"account {id}: display_label wasn't sent, and the database chose the id");
```

Check it in the database:

```sql
SELECT id, name, industry
FROM account
WHERE name = 'Contoso Pharmaceuticals' AND account_number IS NULL;
```

Expect: 1 row, industry 3 (Healthcare); there is no display_label column.

### Every supported .NET type, and what SQL Server stores

Real case 2 of Create.

Every kind of value the SQL Server provider writes, into data_type_showcase, and what SQL Server stored, in four scenarios: every supported type, types converted to one that holds every value, null, and a type that's refused. The comment on each line is the stored value as SQL Server prints it.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var owner = new ArxEntity("account");
owner["name"] = "Showcase owner";
var ownerId = await service.CreateAsync(owner);
```

#### 1. One row with a value of every supported kind

One row with a value of every supported kind.

In scope: `service` (ArxMsSqlDbService), `ownerId` (ArxRecordId).

```csharp
var row = new ArxEntity("data_type_showcase");
row["text_value"] = "Hello, ARX";                                                        // Hello, ARX
row["varchar_value"] = "Short text";                                                     // Short text
row["char_value"] = "AB";                                                                // AB (nchar(3) pads to "AB ")
row["tinyint_value"] = (byte)200;                                                        // 200
row["smallint_value"] = (short)32_000;                                                   // 32000
row["int_value"] = 2_000_000_000;                                                        // 2000000000
row["bigint_value"] = 9_000_000_000_000L;                                                // 9000000000000
row["decimal_value"] = 12_345.678901m;                                                   // 12345.678901
row["money_value"] = 19.99m;                                                             // 19.99
row["real_value"] = 3.25f;                                                               // 3.25
row["float_value"] = 2.718281828459045;                                                  // 2.718281828459045
row["bit_value"] = true;                                                                 // 1
row["uniqueidentifier_value"] = Guid.Parse("6f9619ff-8b86-d011-b42d-00c04fc964ff");      // 6F9619FF-8B86-D011-B42D-00C04FC964FF
row["datetimeoffset_value"] = new DateTimeOffset(2026, 11, 7, 22, 30, 0, TimeSpan.FromHours(8)); // 2026-11-07T22:30:00+08:00 (offset kept)
row["datetime2_value"] = new DateTime(2026, 11, 7, 9, 0, 0, DateTimeKind.Unspecified);   // 2026-11-07T09:00:00
row["datetime_value"] = new DateTime(2026, 11, 7, 9, 0, 0, 1);                           // 2026-11-07T09:00:00 (datetime rounds to 1/300 s)
row["date_value"] = new DateOnly(2026, 11, 7);                                           // 2026-11-07
row["time_value"] = new TimeOnly(17, 45, 30);                                            // 17:45:30
row["varbinary_value"] = new byte[] { 0xCA, 0xFE };                                      // 0xCAFE
row["json_value"] = new ArxJsonB { SerializedValue = "{\"plan\": \"gold\", \"seats\": 25}" };  // {"plan": "gold", "seats": 25} (text, checked by ISJSON)
row["mood"] = new ArxSelectionSetValue("Mood", "happy");                                 // happy (text the CHECK allows)
row["account_id"] = new ArxEntityReference("account", ownerId);                          // the account's id
row["industry"] = new ArxSelectionSetValue("Industry", Industry.Technology);             // 5

await service.CreateAsync(row);
```

Check it in the database:

```sql
SELECT text_value, char_value, tinyint_value, smallint_value, int_value, bigint_value, decimal_value, money_value,
       real_value, float_value, bit_value, uniqueidentifier_value, datetimeoffset_value, datetime2_value,
       datetime_value, date_value, time_value, varbinary_value, json_value, mood, industry
FROM data_type_showcase
WHERE text_value = 'Hello, ARX';
```

Expect: 1 row with the values in the comments next to each attribute.

#### 2. Types without a SQL Server type of their own are converted to one that holds every value

Types without a SQL Server type of their own are converted to one that holds every value.

In scope: `service` (ArxMsSqlDbService).

```csharp
var converted = new ArxEntity("data_type_showcase");
converted["smallint_value"] = (sbyte)-100;                                               // -100 (sbyte → smallint)
converted["int_value"] = (ushort)65_000;                                                 // 65000 (ushort → int)
converted["bigint_value"] = 4_000_000_000u;                                              // 4000000000 (uint → bigint)
converted["decimal_value"] = ulong.MaxValue;                                             // 18446744073709551615.000000 (ulong → decimal)
converted["char_value"] = 'X';                                                           // X (char → text)
converted["text_value"] = Industry.Finance.ToString();                                   // Finance (an enum: its name, or (int) for its number)
converted["datetimeoffset_value"] = new DateTime(2026, 11, 7, 14, 30, 0, DateTimeKind.Utc); // 2026-11-07T14:30:00Z (a UTC DateTime: offset +00:00)
converted["varbinary_value"] = new ArxBytea { Data = new byte[] { 1, 2 } };              // 0x0102

await service.CreateAsync(converted);
```

Check it in the database:

```sql
SELECT smallint_value, int_value, bigint_value, decimal_value, char_value, text_value, datetimeoffset_value, varbinary_value
FROM data_type_showcase
WHERE text_value = 'Finance';
```

Expect: 1 row: -100, 65000, 4000000000, 18446744073709551615.000000, X, Finance, 2026-11-07 14:30:00.0000000 +00:00, 0x0102.

#### 3. null is stored as NULL

null is stored as NULL, whatever the column's type.

In scope: `service` (ArxMsSqlDbService).

```csharp
var empty = new ArxEntity("data_type_showcase");
empty["text_value"] = null;
empty["int_value"] = null;
empty["json_value"] = null;

await service.CreateAsync(empty);
```

Check it in the database:

```sql
SELECT id, text_value, int_value, json_value
FROM data_type_showcase
WHERE text_value IS NULL AND int_value IS NULL AND json_value IS NULL
  AND time_value IS NULL AND mood IS NULL AND uniqueidentifier_value IS NULL;
```

Expect: 1 row, all NULL.

#### 4. A type the provider can't write is refused before anything is sent

A type the provider can't write is refused before anything is sent.

In scope: `service` (ArxMsSqlDbService).

```csharp
var unsupported = new ArxEntity("data_type_showcase");
unsupported["text_value"] = new Uri("https://northwind.example");

try
{
    await service.CreateAsync(unsupported);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.UnsupportedValueType)
{
    Console.WriteLine(error.Message);
    Console.WriteLine("write uri.ToString() instead");
}
```

Check it in the database:

```sql
SELECT count(*)
FROM data_type_showcase
WHERE text_value LIKE 'https://northwind%';
```

Expect: 0: the Uri was refused before anything was sent.

### Instants, local times, dates and times of day

Real case 3 of Create.

Dates and times, in four scenarios: an order placed in Manila written three ways (the same instant, stored in UTC), calendar values (a birthday, an opening time, a delivery window, a local event time), and what is refused: default(DateTime), and a date given as a string SQL Server can't read.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "Tailspin Toys";
var accountId = await service.CreateAsync(account);
```

#### 1. The same instant written three ways into sales_order.ordered_on (datetimeoffset)

An instant goes into datetimeoffset. These three orders were placed at the same moment, written three ways.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
var fromOffset = new ArxEntity("sales_order");
fromOffset["order_number"] = "SO-1001";
fromOffset["account_id"] = new ArxEntityReference("account", accountId);
fromOffset["ordered_on"] = new DateTimeOffset(2026, 11, 7, 18, 0, 0, TimeSpan.FromHours(8));            // Manila time

var fromUtc = new ArxEntity("sales_order");
fromUtc["order_number"] = "SO-1002";
fromUtc["account_id"] = new ArxEntityReference("account", accountId);
fromUtc["ordered_on"] = new DateTime(2026, 11, 7, 10, 0, 0, DateTimeKind.Utc);

var fromLocal = new ArxEntity("sales_order");
fromLocal["order_number"] = "SO-1003";
fromLocal["account_id"] = new ArxEntityReference("account", accountId);
fromLocal["ordered_on"] = new DateTime(2026, 11, 7, 10, 0, 0, DateTimeKind.Utc).ToLocalTime();          // ARX converts Local to UTC

var orders = new List<ArxEntity> { fromOffset, fromUtc, fromLocal };
var ids = await service.CreateMultipleAsync(orders);
```

Check it in the database:

```sql
SELECT order_number, ordered_on, SWITCHOFFSET(ordered_on, '+00:00') AS in_utc
FROM sales_order
WHERE order_number IN ('SO-1001', 'SO-1002', 'SO-1003')
ORDER BY order_number;
```

Expect: 3 rows, ordered_on and in_utc all 2026-11-07 10:00:00 +00:00: a DateTimeOffset goes in as its UTC instant (18:00 +08:00 is 10:00 UTC), so the same value would be right in a datetime2 column too.

#### 2. A birthday, an opening time, a delivery window and a local event time

Calendar values have their own types. A wall-clock time with no zone (an Unspecified DateTime) goes into datetime2.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
var contact = new ArxEntity("contact");
contact["account_id"] = new ArxEntityReference("account", accountId);
contact["first_name"] = "Maria";
contact["last_name"] = "Santos";
contact["birthday"] = new DateOnly(1990, 2, 28);
var contactId = await service.CreateAsync(contact);

var schedule = new ArxEntity("data_type_showcase");
schedule["time_value"] = new TimeOnly(9, 0);
schedule["datetime2_value"] = new DateTime(2026, 12, 24, 18, 0, 0, DateTimeKind.Unspecified);
var scheduleId = await service.CreateAsync(schedule);
```

Check it in the database:

```sql
SELECT first_name, last_name, birthday
FROM contact
WHERE first_name = 'Maria' AND last_name = 'Santos';
SELECT time_value, datetime2_value
FROM data_type_showcase
WHERE datetime2_value = '2026-12-24 18:00:00';
```

Expect: birthday 1990-02-28; 09:00:00, 2026-12-24 18:00:00.

#### 3. default(DateTime) is refused: use null for "no value"

default(DateTime) means "not set", and SQL Server would store 0001-01-01: refused before anything is sent.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
var unset = new ArxEntity("sales_order");
unset["order_number"] = "SO-1004";
unset["account_id"] = new ArxEntityReference("account", accountId);
unset["ordered_on"] = default(DateTime);

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

Check it in the database:

```sql
SELECT count(*)
FROM sales_order
WHERE order_number = 'SO-1004';
```

Expect: 0: default(DateTime) was refused before anything was sent.

#### 4. A day-first date given as a string is refused; DateOnly.ParseExact works

ARX never parses a string as a date, but SQL Server converts one it can read, by the session's date format (month first under the default us_english: 01/07/1992 would be stored as January 7). This day-first date can't be read, so it is refused. Parse dates yourself, with the format you mean.

In scope: `service` (ArxMsSqlDbService).

```csharp
var fromCsv = new ArxEntity("contact");
fromCsv["first_name"] = "Leo";
fromCsv["last_name"] = "Park";
fromCsv["birthday"] = "13/07/1992";

try
{
    await service.CreateAsync(fromCsv);
}
catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.InvalidValueFormat)
{
    Console.WriteLine(error.Message);
}

fromCsv["birthday"] = DateOnly.ParseExact("13/07/1992", "dd/MM/yyyy", System.Globalization.CultureInfo.InvariantCulture);
var id = await service.CreateAsync(fromCsv);
Console.WriteLine($"stored contact {id}");
```

Check it in the database:

```sql
SELECT first_name, last_name, birthday
FROM contact
WHERE first_name = 'Leo' AND last_name = 'Park';
```

Expect: 1 row, birthday 1992-07-13 (the second attempt, with DateOnly.ParseExact).

### Picklist values checked against the application's catalog

Real case 4 of Create.

Selection sets (picklists, option sets), in seven scenarios: the catalog, a catalog without the sets, an account's industry, status and segments, a contact's preferred channel and a survey mood, sets loaded from JSON, values the catalog refuses, and a plain string that skips the catalog.

Good to know:

**The catalog** is the list of every selection set your application has: for each set, its name, what kind of code it uses (a number, a string, a character, …), whether one or several codes can be chosen, and its options (code + display text, and whether the option is still active). You build it once at startup, in code, and give it to the service: `new ArxMsSqlDbService(connectionString, catalog)`. This sample's catalog is in SampleCatalog.cs.

When you write an `ArxSelectionSetValue`, ARX looks up its set in the catalog, checks the code, and writes the code itself into the column. The check happens in memory, before any SQL is sent: no lookup table and no extra query. The database never sees a code your catalog doesn't allow.

Usings:

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

#### 1. The application's catalog: one entry per selection set

The catalog, built once at startup (SampleCatalog.Build()) and passed to the service: what it holds.

In scope: `catalog` (ArxSchemaCatalog).

```csharp
foreach (var set in catalog.SelectionSets.Sets)
{
    var options = string.Join(", ", set.Options.Select(x => $"{x.Value} = {x.DisplayText}{(x.IsActive ? "" : " (retired)")}"));
    Console.WriteLine($"{set.Name,-16} {set.Kind,-12} {set.Multiplicity,-7} {options}");
}

// A set can be looked up by name, for example to fill a drop-down list in your UI.
if (catalog.TryGetSet("AccountStatus", out var statusSet))
{
    Console.WriteLine($"drop-down for AccountStatus: {string.Join(" | ", statusSet.Options.Where(x => x.IsActive).Select(x => x.DisplayText))}");
}
```

Check it in the database:

```sql
-- Nothing is stored: the catalog lives in memory (SampleCatalog.cs). The columns its sets write to:
SELECT table_name, column_name, data_type, character_maximum_length
FROM information_schema.columns
WHERE table_name + '.' + column_name IN ('account.industry', 'account.status', 'account.segments',
      'contact.preferred_channel', 'contact.lead_source', 'contact.rating', 'data_type_showcase.mood')
ORDER BY table_name, column_name;
```

Expect: 7 rows.

#### 2. Without the selection sets in the catalog, a selection set value is refused

A service whose catalog declares the tables but no selection sets can't write selection set values: there is nothing to check them against.

In scope: `connectionString` (string).

```csharp
var tablesOnly = ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id"))
    .Build();
var noCatalog = new ArxMsSqlDbService(connectionString, tablesOnly);

var notChecked = new ArxEntity("account");
notChecked["name"] = "No catalog Ltd.";
notChecked["industry"] = new ArxSelectionSetValue("Industry", Industry.Retail);

try
{
    await noCatalog.CreateAsync(notChecked);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.UnknownSelectionSet)
{
    Console.WriteLine(error.Message);
}
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE name = 'No catalog Ltd.';
```

Expect: 0: refused, the service had no selection sets to check against.

#### 3. Industry (an enum), status (a string) and segments (several strings)

An account's industry (an enum), status (a string) and segments (several strings).

In scope: `service` (ArxMsSqlDbService).

```csharp
var account = new ArxEntity("account");
account["name"] = "Adventure Works";
account["industry"] = new ArxSelectionSetValue("Industry", Industry.Manufacturing);
account["status"] = new ArxSelectionSetValue("AccountStatus", "ON_HOLD");
account["segments"] = ArxSelectionSetValue.Multiple("CustomerSegment", "ENTERPRISE", "PUBLIC");
var id = await service.CreateAsync(account);

return id;
```

Check it in the database:

```sql
SELECT name, industry, status, segments
FROM account
WHERE name = 'Adventure Works' AND status = 'ON_HOLD';
```

Expect: 1 row: industry 2, status ON_HOLD, segments ENTERPRISE,PUBLIC.

#### 4. A preferred channel (a character) and a mood (text values a CHECK guards)

A contact's preferred channel (a character) and a survey's mood (text values a CHECK guards).

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
var contact = new ArxEntity("contact");
contact["account_id"] = new ArxEntityReference("account", accountId);
contact["first_name"] = "Kenji";
contact["last_name"] = "Mori";
contact["preferred_channel"] = new ArxSelectionSetValue("ContactChannel", 'S');
var contactId = await service.CreateAsync(contact);

var survey = new ArxEntity("data_type_showcase");
survey["mood"] = new ArxSelectionSetValue("Mood", "neutral");
var surveyId = await service.CreateAsync(survey);
```

Check it in the database:

```sql
SELECT first_name, last_name, preferred_channel
FROM contact
WHERE first_name = 'Kenji' AND last_name = 'Mori';
SELECT mood
FROM data_type_showcase
WHERE mood = 'neutral';
```

Expect: preferred_channel S; mood neutral.

#### 5. A lead source and a rating, from the sets the catalog loaded from JSON

Sets loaded from JSON (SampleCatalog.SetsJson) are used exactly like the ones written in code.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
var lead = new ArxEntity("contact");
lead["account_id"] = new ArxEntityReference("account", accountId);
lead["first_name"] = "Sofia";
lead["last_name"] = "Rossi";
lead["lead_source"] = new ArxSelectionSetValue("LeadSource", "EVENT");
lead["rating"] = new ArxSelectionSetValue("ContactRating", 4);
var leadId = await service.CreateAsync(lead);

var coldCall = new ArxEntity("contact");
coldCall["first_name"] = "Max";
coldCall["last_name"] = "Weber";
coldCall["lead_source"] = new ArxSelectionSetValue("LeadSource", "COLD_CALL");   // "active": false in the JSON

try
{
    await service.CreateAsync(coldCall);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.SelectionSetValueNotAllowed)
{
    Console.WriteLine(error.Message);
}
```

Check it in the database:

```sql
SELECT first_name, last_name, lead_source, rating
FROM contact
WHERE (first_name = 'Sofia' AND last_name = 'Rossi') OR (first_name = 'Max' AND last_name = 'Weber');
```

Expect: 1 row, Sofia Rossi, EVENT, 4: Max Weber's retired lead source was refused.

#### 6. Values the catalog refuses

Values the catalog refuses: not an option, a retired option, two codes for a single choice, an unknown set.

In scope: `service` (ArxMsSqlDbService).

```csharp
var notAnOption = new ArxEntity("account");
notAnOption["name"] = "Bad status";
notAnOption["status"] = new ArxSelectionSetValue("AccountStatus", "DELETED");

var retiredOption = new ArxEntity("account");
retiredOption["name"] = "Retired segment";
retiredOption["segments"] = ArxSelectionSetValue.Multiple("CustomerSegment", "SMB", "RESELLER");

var twoForOne = new ArxEntity("account");
twoForOne["name"] = "Two industries";
twoForOne["industry"] = ArxSelectionSetValue.Multiple("Industry", Industry.Retail, Industry.Finance);

var unknownSet = new ArxEntity("account");
unknownSet["name"] = "Typo";
unknownSet["industry"] = new ArxSelectionSetValue("Industries", Industry.Retail);

foreach (var refused in new[] { notAnOption, retiredOption, twoForOne, unknownSet })
{
    try
    {
        await service.CreateAsync(refused);
    }
    catch (ArxValidationException error)
    {
        Console.WriteLine(error.Message);
    }
}
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE name IN ('Bad status', 'Retired segment', 'Two industries', 'Typo');
```

Expect: 0: all four were refused before anything was sent.

#### 7. A plain string skips the catalog

A plain value (not an ArxSelectionSetValue) isn't checked against the catalog; only the table's own constraints apply. Here the CHECK on status refuses it.

In scope: `service` (ArxMsSqlDbService).

```csharp
var plain = new ArxEntity("account");
plain["name"] = "Plain value";
plain["status"] = "DELETED";

try
{
    await service.CreateAsync(plain);
}
catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.CheckViolation)
{
    Console.WriteLine(error.Message);
}
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE name = 'Plain value';
```

Expect: 0: the table's CHECK on status refused it.

### Turn ARX error codes into messages for the user

Real case 5 of Create.

An "add account" screen that turns ARX errors into messages for the user, in four scenarios: constraints the database enforces, values the column can't hold, requests that don't fit the table, and the native details kept on a database error. Every error has a stable code (the same on every database), a category, and the entity, attribute or constraint involved. Messages name things, never values, so they are safe to log.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var existing = new ArxEntity("account");
existing["name"] = "Litware Inc.";
existing["account_number"] = "ACC-20001";
await service.CreateAsync(existing);
```

#### 1. Constraints the database enforces

Constraints the database enforces: a unique number, a required name, a CHECK on the employee count.

In scope: `service` (ArxMsSqlDbService).

```csharp
var numberTaken = new ArxEntity("account");
numberTaken["name"] = "Litware Europe";
numberTaken["account_number"] = "ACC-20001";

var noName = new ArxEntity("account");
noName["website"] = "https://no-name.example";

var negativeCount = new ArxEntity("account");
negativeCount["name"] = "Shrinking Co.";
negativeCount["employee_count"] = -5;

foreach (var attempt in new[] { numberTaken, noName, negativeCount })
{
    await SaveAndExplainAsync(service, attempt);
}
```

Defined in the sample, used above:

```csharp
private static async Task SaveAndExplainAsync(ArxMsSqlDbService service, ArxEntity attempt)
{
    Console.WriteLine($"save {attempt["name"] ?? "(no name)"}");

    try
    {
        await service.CreateAsync(attempt);
    }
    catch (ArxException error)
    {
        // Handle by Code or Category, never by parsing the message.
        var forTheUser = error.Code switch
        {
            ArxErrorCode.UniqueViolation => "That account number is already in use.",
            ArxErrorCode.NotNullViolation => $"Please fill in {error.AttributeName}.",
            ArxErrorCode.CheckViolation => "One of the values isn't allowed.",
            ArxErrorCode.ValueTooLong => "One of the values is too long.",
            ArxErrorCode.ValueOutOfRange => "One of the numbers is too large.",
            ArxErrorCode.DataTypeMismatch or ArxErrorCode.InvalidValueFormat => "One of the values has the wrong format.",
            _ when error.IsTransient => "The database is busy. Please try again.",
            _ => "Something went wrong. The error was logged."
        };

        Console.WriteLine($"  {error.Code} ({error.Category}), constraint {error.ConstraintName ?? "-"}, attribute {error.AttributeName ?? "-"}");
        Console.WriteLine($"  logged:    {error.Message}");
        Console.WriteLine($"  user sees: {forTheUser}");
    }
}
```

Check it in the database:

```sql
SELECT name, account_number, employee_count
FROM account
WHERE account_number = 'ACC-20001' OR name IN ('Litware Europe', 'Shrinking Co.') OR website = 'https://no-name.example';
```

Expect: 1 row, Litware Inc.: the three attempts were refused.

#### 2. Values the column can't hold

Values the column can't hold: too long, out of range, the wrong type.

In scope: `service` (ArxMsSqlDbService).

```csharp
var longWebsite = new ArxEntity("account");
longWebsite["name"] = "Long URL Ltd.";
longWebsite["website"] = "https://example.com/" + new string('a', 200);

var hugeCount = new ArxEntity("account");
hugeCount["name"] = "Huge Corp.";
hugeCount["employee_count"] = 3_000_000_000L;

var textForNumber = new ArxEntity("account");
textForNumber["name"] = "Typo Corp.";
textForNumber["employee_count"] = "forty";

foreach (var attempt in new[] { longWebsite, hugeCount, textForNumber })
{
    await SaveAndExplainAsync(service, attempt);
}
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE name IN ('Long URL Ltd.', 'Huge Corp.', 'Typo Corp.');
```

Expect: 0.

#### 3. Requests that don't fit the table

Requests that don't fit the table: a column it doesn't have, a table name that isn't a plain identifier.

In scope: `service` (ArxMsSqlDbService).

```csharp
var unknownColumn = new ArxEntity("account");
unknownColumn["name"] = "Extra Field Corp.";
unknownColumn["fax_number"] = "555-0100";

var badTableName = new ArxEntity("account; DROP TABLE account");
badTableName["name"] = "Bad Table Name Corp.";

foreach (var attempt in new[] { unknownColumn, badTableName })
{
    await SaveAndExplainAsync(service, attempt);
}
```

Check it in the database:

```sql
SELECT count(*) AS stored, CASE WHEN OBJECT_ID('dbo.account') IS NOT NULL THEN 1 ELSE 0 END AS account_table_still_there
FROM account
WHERE name IN ('Extra Field Corp.', 'Bad Table Name Corp.');
```

Expect: 0, 1: nothing stored, and the hostile table name changed nothing.

#### 4. Native details on a database error

ArxValidationException is found before the database is called; ArxDatabaseException is reported by SQL Server, with the native details kept.

In scope: `service` (ArxMsSqlDbService).

```csharp
var numberTaken = new ArxEntity("account");
numberTaken["name"] = "Litware Europe";
numberTaken["account_number"] = "ACC-20001";

try
{
    await service.CreateAsync(numberTaken);
}
catch (ArxDatabaseException error)
{
    Console.WriteLine($"NativeCode {error.NativeCode}, InnerException {error.InnerException?.GetType().Name}, IsTransient {error.IsTransient}");
}
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number = 'ACC-20001';
```

Expect: 1: still only Litware Inc.

### Values and names that come from users or files

Real case 6 of Create.

Saving what users typed and what files contain, in three scenarios: a public "contact us" form full of SQL-looking text, a CSV header used directly as attribute names, and the better way, mapping the file's columns. Values are always sent as parameters, so any text is stored exactly as typed. Names are validated and quoted, so a name can't change the SQL.

Usings:

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

#### 1. A form submission full of SQL-looking text is stored exactly as typed

A form submission full of SQL-looking text is stored exactly as typed.

In scope: `service` (ArxMsSqlDbService).

```csharp
var form = new ContactForm("Robert'); DROP TABLE contact;--", "O'Brien", "Ünïcödé, emoji 🚀, quotes \" ' and a backslash \\");

var contact = new ArxEntity("contact");
contact["first_name"] = form.FirstName;
contact["last_name"] = form.LastName;
contact["notes"] = form.Message;
var id = await service.CreateAsync(contact);
```

Defined in the sample, used above:

```csharp
private sealed record ContactForm(string FirstName, string LastName, string Message);
```

Check it in the database:

```sql
SELECT first_name, last_name, notes
FROM contact
WHERE last_name = 'O''Brien';
```

Expect: 1 row, stored exactly as typed: Robert'); DROP TABLE contact;-- and the Unicode note.

#### 2. A CSV header used directly as attribute names

Column names from a file header: only ASCII letters, digits and underscores (at most 63 characters) are accepted, and the check happens before any SQL is built. A header with quotes in it is SQL, not a typo, so it is refused as ARX1017 SuspectedInjection (RealCase10_CatchAnInjectionAttempt shows how to handle it).

In scope: `service` (ArxMsSqlDbService).

```csharp
var header = new[] { "first_name", "last_name", "email\" text, \"x" };
var values = new[] { "Ivy", "Chen", "ivy.chen@example.com" };

var imported = new ArxEntity("contact");

for (var i = 0; i < header.Length; i++)
{
    imported[header[i]] = values[i];
}

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

Check it in the database:

```sql
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'contact' AND column_name LIKE 'email%';
```

Expect: 1 row, email: the header with quotes was refused before any SQL was built.

#### 3. Better: map the file's columns to the attributes you expect

The better way: map the file's columns to the attributes you expect.

In scope: `service` (ArxMsSqlDbService).

```csharp
var csvRow = new Dictionary<string, string> { ["First Name"] = "Ivy", ["Last Name"] = "Chen", ["E-mail"] = "ivy.chen@example.com", ["Fax"] = "ignored" };

var mapped = new ArxEntity("contact");
mapped["first_name"] = csvRow["First Name"];
mapped["last_name"] = csvRow["Last Name"];
mapped["email"] = csvRow["E-mail"];
var id = await service.CreateAsync(mapped);

Console.WriteLine($"stored contact {id}");
```

Check it in the database:

```sql
SELECT first_name, last_name, email
FROM contact
WHERE email = 'ivy.chen@example.com';
```

Expect: 1 row, Ivy Chen.

### Natural ids, uuid ids and a table with a compound key

Real case 7 of Create.

Ids that aren't a generated number, and a table with no id column, in five scenarios: countries by their ISO code, devices with uuid ids, a regional price list identified by region + SKU, and the two mistakes SQL Server catches: the same region and SKU again, and one part of the key missing. All declared in SampleCatalog.cs.

Good to know:

A supplied id is an attribute like any other: `country["code"] = "ES"`. Create returns the id as an `ArxRecordId` (also on `entity.RecordId`). A table declared NoIdColumn returns `None`; its declared key (region, sku) is what Update and Delete will find a price by.

Usings:

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

#### 1. Countries by their ISO code (a supplied text id)

Countries by their ISO code: a text id the caller supplies.

In scope: `service` (ArxMsSqlDbService).

```csharp
var spain = new ArxEntity("country");
spain["code"] = "ES";
spain["name"] = "Spain";

var brazil = new ArxEntity("country");
brazil["code"] = "BR";
brazil["name"] = "Brazil";

var philippines = new ArxEntity("country");
philippines["code"] = "PH";
philippines["name"] = "Philippines";

var countryIds = await service.CreateMultipleAsync(new List<ArxEntity> { spain, brazil, philippines });
Console.WriteLine($"ids: {string.Join(", ", countryIds.Select(x => x.AsString()))}");
```

Check it in the database:

```sql
SELECT code, name
FROM country
WHERE code IN ('ES', 'BR', 'PH')
ORDER BY code;
```

Expect: 3 rows, Brazil, Spain, Philippines.

#### 2. Devices with uuid ids: generated by the database, or generated by your code

Devices with uuid ids: generated by the database, or generated by your code.

In scope: `service` (ArxMsSqlDbService).

```csharp
var tablet = new ArxEntity("device");
tablet["serial_number"] = "TAB-0001";

var phone = new ArxEntity("device");
phone["id"] = Guid.NewGuid();   // a client-generated id is written as given
phone["serial_number"] = "PHN-0001";

var deviceIds = await service.CreateMultipleAsync(new List<ArxEntity> { tablet, phone });
Console.WriteLine($"tablet {deviceIds[0].AsGuid()} (from the database), phone {deviceIds[1].AsGuid()} (from the code)");
```

Check it in the database:

```sql
SELECT id, serial_number
FROM device
WHERE serial_number IN ('TAB-0001', 'PHN-0001');
```

Expect: 2 rows, with the uuids the sample printed.

#### 3. A regional price list: 3 regions x 1,000 SKUs, no id column, identified by (region, sku)

A regional price list with no id column: each price is identified by its region and SKU together.

In scope: `service` (ArxMsSqlDbService).

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

foreach (var region in new[] { "ES", "BR", "PH" })
{
    for (var i = 1; i <= 1_000; i++)
    {
        var price = new ArxEntity("price_list");
        price["region"] = new ArxEntityReference("country", region);   // a lookup to the country, by its code
        price["sku"] = $"SKU-{i:D6}";
        price["unit_price"] = region == "BR" ? 9.90m + i % 50 : 12.50m + i % 40;
        price["valid_from"] = new DateOnly(2026, 1, 1);
        prices.Add(price);
    }
}

var priceIds = await service.CreateMultipleAsync(prices);
```

Check it in the database:

```sql
SELECT region, count(*) AS prices
FROM price_list
WHERE region IN ('ES', 'BR', 'PH')
GROUP BY region
ORDER BY region;
```

Expect: 3 rows, 1000 prices each.

#### 4. The same region and SKU again: the table's primary key refuses it

The same region and SKU again: the table's primary key refuses it.

In scope: `service` (ArxMsSqlDbService).

```csharp
var duplicate = new ArxEntity("price_list");
duplicate["region"] = new ArxEntityReference("country", "ES");
duplicate["sku"] = "SKU-000001";
duplicate["unit_price"] = 1.00m;
duplicate["valid_from"] = new DateOnly(2026, 6, 1);

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

Check it in the database:

```sql
SELECT region, sku, unit_price, valid_from
FROM price_list
WHERE region = 'ES' AND sku = 'SKU-000001';
```

Expect: 1 row, the original: 13.50 from 2026-01-01 (the duplicate was refused).

#### 5. One part of the key missing: the NOT NULL column refuses it

One part of the key missing: the NOT NULL column refuses it (Create doesn't use keys; the database enforces them).

In scope: `service` (ArxMsSqlDbService).

```csharp
var noRegion = new ArxEntity("price_list");
noRegion["sku"] = "SKU-999999";
noRegion["unit_price"] = 5.00m;
noRegion["valid_from"] = new DateOnly(2026, 6, 1);

try
{
    await service.CreateAsync(noRegion);
}
catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.NotNullViolation)
{
    Console.WriteLine(error.Message);
}
```

Check it in the database:

```sql
SELECT count(*)
FROM price_list
WHERE sku = 'SKU-999999';
```

Expect: 0: refused by the NOT NULL region column.

### Catch an SQL injection attempt on create

Real case 8 of Create.

Catching an SQL injection attempt on create, in four scenarios. ARX refuses a name (table or attribute) that carries SQL with its own code, ARX1017 SuspectedInjection, before anything is sent to the database, so you can log it as a security event instead of a bug. Values are never refused: they are parameters, and SQL in a value is just text.

Usings:

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

#### 1. A field name from a form carries SQL

A form whose field names are copied straight into attributes (the mistake), and an attacker who adds a field named with SQL. The whole create is refused with ARX1017; a plain typo in a field name is ARX1003.

In scope: `service` (ArxMsSqlDbService).

```csharp
var posted = new Dictionary<string, object>
{
    ["first_name"] = "Mallory",
    ["last_name"] = "Injection-C1",
    ["notes\") VALUES ('x'); DROP TABLE contact; --"] = "x",
};

var typo = new Dictionary<string, object>
{
    ["first_name"] = "Ted",
    ["last name"] = "Typo-C1",
};

foreach (var form in new[] { posted, typo })
{
    var contact = new ArxEntity("contact");

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

    try
    {
        await service.CreateAsync(contact);
    }
    catch (ArxValidationException error) when (error.Code is ArxErrorCode.SuspectedInjection or ArxErrorCode.InvalidIdentifier)
    {
        Report(error);
    }
}
```

Defined in the sample, used above:

```csharp
private static void Report(ArxValidationException error)
{
    if (error.Code == ArxErrorCode.SuspectedInjection)
    {
        Console.WriteLine($"SECURITY: {error.Message}");
    }
    else
    {
        Console.WriteLine($"bug, not an attack: {error.Message}");
    }
}
```

Check it in the database:

```sql
SELECT count(*)
FROM contact
WHERE last_name = 'Injection-C1' OR first_name = 'Ted';
```

Expect: 0: neither contact was created, and the contact table is still there.

#### 2. A table name from the request carries SQL

A table name taken from the request (`?type=…`). ARX refuses the SQL in it; the fix is to map the request's word to a table from a fixed list, so user input never becomes a name.

In scope: `service` (ArxMsSqlDbService).

```csharp
var requested = "contact; DROP TABLE account; --";

var fromTheRequest = new ArxEntity(requested);
fromTheRequest["first_name"] = "Mallory";
fromTheRequest["last_name"] = "Injection-C2";

try
{
    await service.CreateAsync(fromTheRequest);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.SuspectedInjection)
{
    Report(error);
}

// The fix: a fixed list. Anything else is refused by the application, before ARX.
string? table = requested switch
{
    "person" => "contact",
    "company" => "account",
    _ => null,
};

Console.WriteLine(table == null ? $"the allow-list refused type '{requested}'" : $"would create in {table}");
```

Check it in the database:

```sql
SELECT CASE WHEN OBJECT_ID('dbo.account') IS NOT NULL THEN 1 ELSE 0 END AS account_exists,
       (SELECT count(*) FROM contact WHERE last_name = 'Injection-C2') AS created;
```

Expect: account_exists 1, created 0.

#### 3. One bad row in an import refuses the whole CreateMultiple

An import of three contacts where one row's column header carries SQL: the whole call is refused, nothing is written.

In scope: `service` (ArxMsSqlDbService).

```csharp
var first = new ArxEntity("contact");
first["first_name"] = "Ann";
first["last_name"] = "Import-C3";

var bad = new ArxEntity("contact");
bad["first_name"] = "Bob";
bad["last_name\" text, \"x"] = "Import-C3";

var third = new ArxEntity("contact");
third["first_name"] = "Cy";
third["last_name"] = "Import-C3";

try
{
    await service.CreateMultipleAsync(new List<ArxEntity> { first, bad, third });
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.SuspectedInjection)
{
    Report(error);
}
```

Check it in the database:

```sql
SELECT count(*)
FROM contact
WHERE last_name = 'Import-C3';
```

Expect: 0: Ann and Cy weren't created either.

#### 4. SQL in a value is stored as plain text

SQL in a value isn't an attack on ARX: values are parameters, so it's stored exactly as typed and nothing is refused.

In scope: `service` (ArxMsSqlDbService).

```csharp
var contact = new ArxEntity("contact");
contact["first_name"] = "Robert'); DROP TABLE contact; --";
contact["last_name"] = "Value-C4";
var id = await service.CreateAsync(contact);

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

Check it in the database:

```sql
SELECT first_name
FROM contact
WHERE last_name = 'Value-C4';
```

Expect: 1 row: Robert'); DROP TABLE contact; -- (stored as typed; the contact table is still there).

### Register ARX in a DI container and inject it

Advanced case 1 of Create.

Registering ARX in a dependency injection container (Microsoft.Extensions.DependencyInjection, as in ASP.NET Core and worker services) and using it from an application service, in two scenarios: register, then use.

Good to know:

Register one ArxMsSqlDbService as a singleton: it holds no connection, so it is shared safely across requests; each call takes a connection from the SqlClient pool. Register the catalog once too. Inject ArxMsSqlDbService itself for Create; inject IArxBatchDataService where the code only needs batch scopes. Code that should not depend on the provider can take IArxDataService instead: see "Set the service up for production".

Usings:

```csharp
using ARX.SDK.SQL.Services;
using ARX.SDK.Services;
using Microsoft.Extensions.DependencyInjection;
```

#### 1. Register the schema catalog and the service (in ASP.NET Core: builder.Services)

Register the schema catalog and the service once, as singletons (in ASP.NET Core: builder.Services).

In scope: `connectionString` (string).

```csharp
var services = new ServiceCollection();

services.AddSingleton(SampleCatalog.Build());   // the schema catalog: table ids, keys and selection sets
services.AddSingleton(provider => new ArxMsSqlDbService(connectionString, provider.GetRequiredService<ArxSchemaCatalog>()));
services.AddSingleton<IArxBatchDataService>(provider => provider.GetRequiredService<ArxMsSqlDbService>());
services.AddScoped<CustomerOnboarding>();

return services.BuildServiceProvider();
```

Defined in the sample, used above:

```csharp
private sealed class CustomerOnboarding
{
    private readonly ArxMsSqlDbService _service;

    public CustomerOnboarding(ArxMsSqlDbService service)
    {
        _service = service;
    }

    /// <summary>Creates an account and its primary contact together: both or neither.</summary>
    public async Task<ArxRecordId> SignUpAsync(string company, string accountNumber, string firstName, string lastName, string email)
    {
        ArxRecordId accountId = null!;

        await _service.RunBatchAsync(async batch =>
        {
            var account = new ArxEntity("account");
            account["name"] = company;
            account["account_number"] = accountNumber;
            account["status"] = new ArxSelectionSetValue("AccountStatus", "ACTIVE");
            accountId = await batch.CreateAsync(account);

            var contact = new ArxEntity("contact");
            contact["account_id"] = new ArxEntityReference("account", accountId);
            contact["first_name"] = firstName;
            contact["last_name"] = lastName;
            contact["email"] = email;
            await batch.CreateAsync(contact);
        });

        return accountId;
    }
}
```

Note: Nothing is stored: this scenario only registers the catalog and the service.

#### 2. Resolve the application service in a scope (in ASP.NET Core: one per request) and use it

Resolve the application service in a scope (in ASP.NET Core: one per request) and use it.

In scope: `provider` (ServiceProvider).

```csharp
using var scope = provider.CreateScope();
var onboarding = scope.ServiceProvider.GetRequiredService<CustomerOnboarding>();

var accountId = await onboarding.SignUpAsync("Proseware Inc.", "ACC-30001", "Olivia", "Grant", "olivia.grant@proseware.example");

Console.WriteLine($"signed up account {accountId} with its primary contact");
Console.WriteLine($"same service instance everywhere: {ReferenceEquals(provider.GetRequiredService<ArxMsSqlDbService>(), provider.GetRequiredService<IArxBatchDataService>())}");
```

Check it in the database:

```sql
SELECT a.name, a.account_number, a.status, c.first_name, c.last_name, c.email
FROM account a
JOIN contact c ON c.account_id = a.id
WHERE a.account_number = 'ACC-30001';
```

Expect: 1 row, Proseware Inc. with Olivia Grant: both or neither.

### Several calls in one ARX transaction: commit or roll back

Advanced case 2 of Create.

Several ARX calls in one transaction, with ARX types only, in three scenarios: commit, roll back, and a failed call that aborts the transaction. Open an `ArxMsSqlConnection`, begin a transaction on it, pass the transaction to each call, then commit or roll back. No SqlClient type appears in application code.

Good to know:

Use a transaction when your code decides, step by step, whether the work is kept: create the account, create its contacts, check something, then commit or roll back. For imports in batches and processes ARX runs from start to finish, the batch scope samples (3 and 4) do the same with less code.

Usings:

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

#### 1. Commit: an account and its contacts, kept together

Commit: an account and its contacts, kept together.

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

```csharp
// The connection is ARX's own: it opens on BeginTransactionAsync and closes when disposed.
await using (var connection = new ArxMsSqlConnection(connectionString))
await using (var transaction = await connection.BeginTransactionAsync())
{
    var account = new ArxEntity("account");
    account["name"] = "Woodgrove Bank";
    account["industry"] = new ArxSelectionSetValue("Industry", Industry.Finance);

    // Pass the transaction to each call: the rows are written inside it.
    var accountId = await service.CreateAsync(account, transaction);

    var ines = new ArxEntity("contact");
    ines["account_id"] = new ArxEntityReference("account", accountId);
    ines["first_name"] = "Ines";
    ines["last_name"] = "Moreau";

    var tom = new ArxEntity("contact");
    tom["account_id"] = new ArxEntityReference("account", accountId);
    tom["first_name"] = "Tom";
    tom["last_name"] = "Becker";

    await service.CreateMultipleAsync(new List<ArxEntity> { ines, tom }, transaction);

    await transaction.CommitAsync();
}
```

Check it in the database:

```sql
SELECT a.name, a.industry, c.first_name, c.last_name
FROM contact c
JOIN account a ON a.id = c.account_id
WHERE c.last_name IN ('Moreau', 'Becker');
```

Expect: 2 rows, Ines Moreau and Tom Becker of Woodgrove Bank, industry 4 (Finance).

#### 2. Roll back: your own check fails after the account was created

Roll back: your own check fails after the account was created.

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

```csharp
await using (var connection = new ArxMsSqlConnection(connectionString))
await using (var transaction = await connection.BeginTransactionAsync())
{
    var account = new ArxEntity("account");
    account["name"] = "Margie's Travel";
    account["credit_limit"] = 50_000m;
    await service.CreateAsync(account, transaction);

    var creditCheckPassed = false;   // say your credit check service says no

    if (creditCheckPassed)
    {
        await transaction.CommitAsync();
    }
    else
    {
        await transaction.RollbackAsync();
    }

    // Leaving the block without committing would roll back too: disposing an active transaction rolls it back.
}
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE name = 'Margie''s Travel';
```

Expect: 0: rolled back.

#### 3. A failed call aborts the transaction: ARX refuses to commit it

In SQL Server, a statement that fails usually leaves the transaction open with the earlier rows in it (some errors end it), so a later COMMIT would keep half the work. ARX treats every database failure alike: the transaction is marked aborted, further calls are refused, and CommitAsync rolls back and throws instead of committing part of it.

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

```csharp
await using (var connection = new ArxMsSqlConnection(connectionString))
await using (var transaction = await connection.BeginTransactionAsync())
{
    var account = new ArxEntity("account");
    account["name"] = "Alpine Ski House";
    account["account_number"] = "ACC-40001";
    await service.CreateAsync(account, transaction);

    var duplicate = new ArxEntity("account");
    duplicate["name"] = "Alpine Ski House (copy)";
    duplicate["account_number"] = "ACC-40001";

    try
    {
        await service.CreateAsync(duplicate, transaction);
    }
    catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.UniqueViolation)
    {
        Console.WriteLine(error.Message);
        Console.WriteLine($"transaction.IsAborted = {transaction.IsAborted}");
    }

    try
    {
        await transaction.CommitAsync();
    }
    catch (ArxValidationException error) when (error.Code == ArxErrorCode.TransactionAborted)
    {
        Console.WriteLine(error.Message);
    }
}
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number = 'ACC-40001';
```

Expect: 0: the failed call aborted the transaction, so nothing was committed.

### Load ids and keys from JSON and validate them at startup

Advanced case 3 of Create.

Preloading the schema catalog, in three scenarios: each table's id column and keys come from JSON, as an application might keep them in a file or a settings store; they are checked against the database at startup with ValidateSchemaAsync; and a declaration that drifted is found before any write.

Good to know:

The ids and keys are always the developer's declaration: ARX reads them from the database only when asked to preload them (Advanced6). Loading them from JSON is plain application code (below), like the selection sets in SampleCatalog. ValidateSchemaAsync is the explicit check: ARX never runs it by itself. Run it at startup or in a health check, so a declaration that drifted from the schema is found before any write.

Usings:

```csharp
using ARX.SDK;
using ARX.SDK.SQL.Services;
using ARX.SDK.Services;
using System.Text.Json;
```

#### 1. Build the schema catalog from JSON: ids and keys, plus the selection sets loaded the same way

Build the schema catalog from JSON: ids and keys, plus the selection sets loaded the same way.

```csharp
var builder = ArxSchemaCatalog.Create();
AddTablesFromJson(builder, TablesJson);

foreach (var set in SampleCatalog.FromJson(SampleCatalog.SetsJson))
{
    builder.Add(set);
}

var schema = builder.Build();

foreach (var table in schema.Tables)
{
    var keys = table.Keys.Count == 0 ? "no keys" : "keys " + string.Join(" ", table.Keys);
    Console.WriteLine($"{table.Name,-12} id {table.IdColumn}; {keys}");
}

return schema;
```

Defined in the sample, used above:

```csharp
private static void AddTablesFromJson(ArxSchemaCatalogBuilder builder, string json)
{
    var tables = JsonSerializer.Deserialize<List<JsonTable>>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
        ?? throw new InvalidOperationException("The table JSON is empty.");

    foreach (var table in tables)
    {
        builder.Table(table.Name, table.Id == null ? ArxIdColumn.NoIdColumn : IdColumn(table.Id.Column, table.Id.Type, table.Id.Generated));

        foreach (var key in table.Keys ?? new List<string[]>())
        {
            builder.Key(table.Name, key);
        }
    }
}

private static ArxIdColumn IdColumn(string name, string type, bool generated) => (type.ToLowerInvariant(), generated) switch
{
    ("long", true) => ArxIdColumn.Generated<long>(name),
    ("long", false) => ArxIdColumn.Supplied<long>(name),
    ("int", true) => ArxIdColumn.Generated<int>(name),
    ("int", false) => ArxIdColumn.Supplied<int>(name),
    ("guid", true) => ArxIdColumn.Generated<Guid>(name),
    ("guid", false) => ArxIdColumn.Supplied<Guid>(name),
    ("string", true) => ArxIdColumn.Generated<string>(name),
    ("string", false) => ArxIdColumn.Supplied<string>(name),
    ("date", _) => ArxIdColumn.Supplied<DateOnly>(name),
    _ => throw new InvalidOperationException($"Id column '{name}' has an unknown type '{type}' in the JSON.")
};

private sealed record JsonTable(string Name, JsonIdColumn Id, List<string[]> Keys);

private sealed record JsonIdColumn(string Column, string Type, bool Generated = false);
```

Note: Nothing is stored: the catalog is built in memory from JSON.

#### 2. At startup, check the declarations against the database

At startup, check the declarations against the database, then use the catalog.

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

```csharp
var service = new ArxMsSqlDbService(connectionString, schema);
await service.ValidateSchemaAsync();
Console.WriteLine("ValidateSchemaAsync: every declared id and key matches its table");

var device = new ArxEntity("device");
device["serial_number"] = "LOADED-0001";
var deviceId = await service.CreateAsync(device);
Console.WriteLine($"created device {deviceId} through the loaded catalog");
```

Check it in the database:

```sql
SELECT id, serial_number
FROM device
WHERE serial_number = 'LOADED-0001';
```

Expect: 1 row, created through the loaded catalog.

#### 3. A declaration that drifted from the schema is found before any write

Someone changed the JSON by mistake: the uuid id is declared long, and a key names a column that isn't unique.

In scope: `connectionString` (string).

```csharp
const string drifted = """
    [
      { "name": "device",  "id": { "column": "id", "type": "long", "generated": true } },
      { "name": "account", "id": { "column": "id", "type": "long", "generated": true }, "keys": [ [ "name" ] ] }
    ]
    """;

var driftedBuilder = ArxSchemaCatalog.Create();
AddTablesFromJson(driftedBuilder, drifted);

try
{
    await new ArxMsSqlDbService(connectionString, driftedBuilder.Build()).ValidateSchemaAsync();
}
catch (ArxException error) when (error.Code == ArxErrorCode.SchemaMismatch)
{
    Console.WriteLine(error.Message);
}
```

Check it in the database:

```sql
-- Nothing is stored. What ValidateSchemaAsync compared the drifted JSON with:
SELECT table_name, column_name, data_type
FROM information_schema.columns
WHERE table_name + '.' + column_name IN ('device.id', 'account.name');
SELECT i.name, STRING_AGG(c.name, ', ') WITHIN GROUP (ORDER BY ic.key_ordinal) AS columns
FROM sys.indexes i
JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id
JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
WHERE i.object_id = OBJECT_ID('account') AND i.is_unique = 1
GROUP BY i.name;
```

Expect: device.id is uniqueidentifier (declared long in the JSON); no unique index on account.name alone.

### Write to two databases, each with its own service

Advanced case 4 of Create.

Two databases in one application, in three scenarios: one service per database, an order placed in the CRM (the source of truth) and recorded in a reporting database (an event log), and a retry of the copy. One `ArxMsSqlDbService` per database, each with its own connection string and schema catalog, registered as keyed services and injected by key.

Good to know:

Each database has its own transactions: ARX can't make a write to two databases atomic (there's no distributed transaction). Write the source of truth first and commit it, then write the copy. Make the copy safe to repeat (here the event is unique per order number), so a failed copy can simply be retried.

Usings:

```csharp
using ARX.SDK;
using ARX.SDK.SQL.Services;
using ARX.SDK.Services;
using Microsoft.Extensions.DependencyInjection;
```

Set up by the sample before its scenarios:

```csharp
await dbContext.Direct.EnsureDatabaseExistsAsync(REPORTING_DATABASE);
var reportingDirect = dbContext.Direct.ForDatabase(REPORTING_DATABASE);
await reportingDirect.ExecuteAsync(
    "DROP TABLE IF EXISTS order_event; "
    + "CREATE TABLE order_event (id bigint IDENTITY(1, 1) PRIMARY KEY, order_number varchar(20) NOT NULL UNIQUE, account_name nvarchar(200) NOT NULL, "
    + "total decimal(14, 2) NOT NULL, recorded_at datetimeoffset NOT NULL DEFAULT SYSDATETIMEOFFSET())");
using var scope = provider.CreateScope();
var desk = scope.ServiceProvider.GetRequiredService<OrderDesk>();
```

#### 1. Register one service per database, by key (in ASP.NET Core: builder.Services)

Register one service per database, by key (in ASP.NET Core: builder.Services).

In scope: `connectionString` (string).

```csharp
var services = new ServiceCollection();

// Each database has its own schema catalog: only the tables that database has.
services.AddKeyedSingleton("crm", (_, _) => new ArxMsSqlDbService(connectionString, SampleCatalog.Build()));
services.AddScoped<OrderDesk>();

return services.BuildServiceProvider();
```

Defined in the sample, used above:

```csharp
private sealed class OrderDesk
{
    private readonly ArxMsSqlDbService _crm;
    private readonly ArxMsSqlDbService _reporting;

    public OrderDesk([FromKeyedServices("crm")] ArxMsSqlDbService crm, [FromKeyedServices("reporting")] ArxMsSqlDbService reporting)
    {
        _crm = crm;
        _reporting = reporting;
    }

    /// <summary>The source of truth first, in one CRM transaction; then the copy in reporting.</summary>
    public async Task<ArxRecordId> PlaceOrderAsync(string accountName, string orderNumber, decimal total)
    {
        ArxRecordId orderId = null!;

        await _crm.RunBatchAsync(async batch =>
        {
            var account = new ArxEntity("account");
            account["name"] = accountName;
            await batch.CreateAsync(account);

            var order = new ArxEntity("sales_order");
            order["order_number"] = orderNumber;
            order["account_id"] = new ArxEntityReference("account", account.RecordId);
            order["ordered_on"] = DateTime.UtcNow;
            order["total"] = total;
            orderId = await batch.CreateAsync(order);
        });

        // Committed in the CRM. If this next write fails, the order still stands: retry the event later.
        await RecordEventAsync(accountName, orderNumber, total);
        return orderId;
    }

    /// <summary>Records the order in reporting. Returns false when it was already recorded (safe to call again).</summary>
    public async Task<bool> RecordEventAsync(string accountName, string orderNumber, decimal total)
    {
        var orderEvent = new ArxEntity("order_event");
        orderEvent["order_number"] = orderNumber;
        orderEvent["account_name"] = accountName;
        orderEvent["total"] = total;

        try
        {
            await _reporting.CreateAsync(orderEvent);
            return true;
        }
        catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.UniqueViolation)
        {
            return false;
        }
    }
}
```

Check it in the database:

```sql
SELECT name
FROM sys.databases
WHERE name IN ('arx_samples', 'arx_sdk_reporting');
```

Expect: 2 rows: the CRM database and the reporting database.

#### 2. Place an order in the CRM, then record it in reporting

The source of truth first, in one CRM transaction; then the copy in reporting.

In scope: `desk` (OrderDesk).

```csharp
var orderId = await desk.PlaceOrderAsync("Fourth Coffee", "SO-6001", 149.70m);
```

Check it in the database:

```sql
SELECT o.order_number, o.total, a.name AS account
FROM sales_order o
JOIN account a ON a.id = o.account_id
WHERE o.order_number = 'SO-6001';
-- In the arx_sdk_reporting database:
SELECT order_number, account_name, total, recorded_at
FROM arx_sdk_reporting.dbo.order_event
WHERE order_number = 'SO-6001';
```

Expect: the order in the CRM (Fourth Coffee, 149.70), and its event in reporting.

#### 3. Recording the same event again (a retry after a failure) is harmless

Recording the same event again (a retry after a failure) is harmless: the order number is unique there.

In scope: `desk` (OrderDesk).

```csharp
var recordedAgain = await desk.RecordEventAsync("Fourth Coffee", "SO-6001", 149.70m);
```

Check it in the database:

```sql
-- In the arx_sdk_reporting database:
SELECT count(*)
FROM arx_sdk_reporting.dbo.order_event
WHERE order_number = 'SO-6001';
```

Expect: 1: the retry didn't record a second event.

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

Advanced case 5 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` (ArxMsSqlDbService), `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(ArxMsSqlDbService 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
SELECT first_name, email
FROM contact
WHERE last_name = 'Guard-C1';
```

Expect: 1 row: 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` (ArxMsSqlDbService), `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
SELECT count(*)
FROM contact
WHERE 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` (ArxMsSqlDbService), `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
SELECT count(*)
FROM contact
WHERE 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` (ArxMsSqlDbService), `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
SELECT first_name, email
FROM contact
WHERE last_name = 'Guard-C4';
```

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

### Preload the schema at service setup: keys only, data types, Strict

Advanced case 6 of Create.

The schema preloaded when the service is set up, in four scenarios: what a preload of "account" finds, including the contact table it added because account.primary_contact_id references it (with that reason, in the JSON); the convenient setup, keys only; the typed setup, data types for the columns you name; and Strict, where the preloaded columns are an allow list.

Good to know:

`new ArxPreLoadTable(tables)` names the tables: an empty array loads every column, a list loads only those columns (the id is always loaded). `LoadAsync` reads them from the database once, at startup, and returns an ordinary `ArxSchemaCatalog`; no operation reads the database's catalog afterwards.

Two modes, the developer's choice. **KeysOnly** (convenient): the id, the business keys and the lookups, what you would otherwise declare by hand. **DataTypes** (the default, typed): those plus every loaded column's type, so a value of the wrong type is refused before the database (SQL Server would otherwise convert many of them silently: '42' into an int, a number into a bit). `Strict` needs DataTypes.

Usings:

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

#### 1. Preload "account": what it found, and the table it added

Preload "account" with every column: contact comes too, because a column of account references it.

In scope: `connectionString` (string).

```csharp
var tables = new ArxPreLoadTableRegister()
    .PreLoadTable("account");

var preload = new ArxPreLoadTable(tables);
var schema = await preload.LoadAsync(connectionString);

foreach (var table in schema.Tables)
{
    Console.WriteLine($"{table.Name,-8} {table.Reason}");
    Console.WriteLine($"         id {table.IdColumn}; keys {string.Join(" ", table.Keys)}; {table.Columns.Count} columns");
    Console.WriteLine($"         lookups {string.Join(", ", table.Lookups.Select(x => $"{x.Key} -> {x.Value}"))}");
}

// The catalog as JSON: keep it in a file or a settings store and reload it with AddJson (RetrieveMultiple Advanced10).
var json = schema.ToJson();
Console.WriteLine($"ToJson: {json.Length:N0} characters; contact's entry starts:");

foreach (var line in json.Split('\n').SkipWhile(x => !x.Contains("\"name\": \"contact\"")).Take(3))
{
    Console.WriteLine("  " + line.TrimEnd());
}
```

Check it in the database:

```sql
SELECT name, OBJECT_NAME(parent_object_id) AS from_table, OBJECT_NAME(referenced_object_id) AS to_table
FROM sys.foreign_keys
WHERE name = 'account_primary_contact_fkey';
```

Expect: 1 row, account -> contact: the reference the preload followed to add contact (reason "added: referenced by account.primary_contact_id").

#### 2. The convenient setup: ArxPreLoadMode.KeysOnly

Convenient: keys only. The ids, keys and lookups you would otherwise declare by hand.

In scope: `connectionString` (string).

```csharp
var tables = new ArxPreLoadTableRegister()
    .PreLoadTable("account");

var schema = await new ArxPreLoadTable(tables, ArxPreLoadMode.KeysOnly).LoadAsync(connectionString);
var service = new ArxMsSqlDbService(connectionString, schema);

var contact = new ArxEntity("contact");
contact["first_name"] = "Paz";
contact["last_name"] = "Preload";
contact["email"] = "paz.preload@example.com";
var contactId = await service.CreateAsync(contact);

// primary_contact_id is a lookup the preload found: the reference is checked against contact's id type first.
var account = new ArxEntity("account");
account["name"] = "Preloaded Pantry";
account["account_number"] = "ACC-PRE81";
account["primary_contact_id"] = new ArxEntityReference("contact", contactId);
var accountId = await service.CreateAsync(account);
Console.WriteLine($"created contact {contactId} and account {accountId}, its primary contact");
```

Check it in the database:

```sql
SELECT a.name, c.first_name, c.last_name
FROM account a JOIN contact c ON c.id = a.primary_contact_id
WHERE a.account_number = 'ACC-PRE81';
```

Expect: 1 row: Preloaded Pantry, Paz Preload.

#### 3. The typed setup: ArxPreLoadMode.DataTypes for four columns

Typed: data types for the columns you name. A wrong type is refused before the database.

In scope: `connectionString` (string).

```csharp
var tables = new ArxPreLoadTableRegister()
    .PreLoadTable("account", ["name", "account_number", "employee_count", "founded_on"]);

var schema = await new ArxPreLoadTable(tables).LoadAsync(connectionString);
var service = new ArxMsSqlDbService(connectionString, schema);

schema.TryGetTable("account", out var table);
Console.WriteLine("loaded: " + string.Join(", ", table!.Columns.Values));

var account = new ArxEntity("account");
account["name"] = "Typed Tailors";
account["account_number"] = "ACC-PRE82";
account["employee_count"] = 12;
account["founded_on"] = new DateOnly(2019, 4, 1);
Console.WriteLine($"created account {await service.CreateAsync(account)}");

var wrong = new ArxEntity("account");
wrong["name"] = "Wrongly Typed";
wrong["account_number"] = "ACC-PRE83";
wrong["employee_count"] = "twelve";

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

Check it in the database:

```sql
SELECT account_number, employee_count, founded_on
FROM account
WHERE account_number IN ('ACC-PRE82', 'ACC-PRE83');
```

Expect: 1 row, ACC-PRE82, 12, 2019-04-01. ACC-PRE83 was refused before the database (ARX4004).

#### 4. Strict: the preloaded columns are an allow list

Strict: a column that wasn't preloaded is refused (ARX1018); the same service not Strict writes it.

In scope: `connectionString` (string).

```csharp
// A sign-up endpoint may write these three columns, nothing else.
var tables = new ArxPreLoadTableRegister()
    .PreLoadTable("account", ["name", "account_number", "website"]);

var schema = await new ArxPreLoadTable(tables).LoadAsync(connectionString);
var strict = new ArxMsSqlDbService(connectionString, schema) { Strict = true };

var signUp = new ArxEntity("account");
signUp["name"] = "Strict Stationers";
signUp["account_number"] = "ACC-PRE84";
signUp["website"] = "https://stationers.example.com";
signUp["credit_limit"] = 1_000_000m;

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

// Not Strict (the default): the preload is what ARX knows ahead of time, and other columns still work.
var relaxed = new ArxMsSqlDbService(connectionString, schema);
Console.WriteLine($"not Strict: created account {await relaxed.CreateAsync(signUp)} with its credit limit");
```

Check it in the database:

```sql
SELECT account_number, website, credit_limit
FROM account
WHERE account_number = 'ACC-PRE84';
```

Expect: 1 row, credit_limit 1000000.00: written by the service that isn't Strict; the Strict one refused credit_limit before the database (ARX1018).

### Set the service up for production

Advanced case 7 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-60001");

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
SELECT name FROM account WHERE account_number = 'ACC-60001';
```

Expect: 1 row, 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
SELECT SERVERPROPERTY('ProductVersion');
```

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

try
{
    var nobody = new ArxEntity("account") { RecordId = ArxRecordId.Of("id", 999999999L) };
    nobody["name"] = "Nobody";
    await data.UpdateAsync(nobody);
}
catch (ArxException exception) when (exception.Code == ArxErrorCode.RecordNotFound)
{
    Console.WriteLine(exception.Message);
}

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

Check it in the database:

```sql
SELECT name FROM account WHERE account_number = 'ACC-60002';
```

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

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

Advanced case 8 of Create.

Use a table whose columns are named outside ARX's name rule, in three scenarios: declare an alias for each such column and create a record through them, read and query by the aliases, and see that the database's own name is still refused when a call uses it.

Good to know:

ARX accepts only names of ASCII letters, digits and underscores, so no name can break out of its quotes. A table made outside ARX may have columns such as `first-name`, `Customer Code` or `order.total`: declare an alias for each in the schema catalog, `.Alias(table, attribute, databaseName)`. Code uses the attribute; ARX sends the column's name, quoted by SQL Server's rules, and reads the column back as the attribute. The id and keys are declared by their attributes.

Only declared names pass: the database name never comes from a call. A link entity's columns keep the name rule in this version.

Usings:

```csharp
using ARX.SDK;
using ARX.SDK.Query;
using ARX.SDK.SQL.Services;
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", "customer_code", "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 ArxMsSqlDbService(connectionString, schema);
```

#### 1. Create through the aliases

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

In scope: `service` (ArxMsSqlDbService).

```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
SELECT [first-name], [order.total] FROM legacy_customer WHERE [Customer Code] IN ('C-1001', 'C-1002');
```

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

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

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

In scope: `service` (ArxMsSqlDbService).

```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
SELECT [first-name], [order.total] FROM legacy_customer WHERE [order.total] > 50 ORDER BY [first-name] DESC;
```

Expect: Ben, then Ana.

#### 3. The database name itself is refused

A call that uses the column's own name is refused before the database: only declared aliases pass.

In scope: `service` (ArxMsSqlDbService).

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

Check it in the database:

```sql
SELECT [first-name] FROM legacy_customer WHERE [Customer Code] = 'C-1002';
```

Expect: Ben (the update was refused).

### Log every call through ILogger

Advanced case 9 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-61001";

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

var query = new ArxQueryExpression("account");
query.Criteria.AddCondition("account_number", ArxConditionOperator.Equal, "ACC-61001");
await data.RetrieveMultipleAsync(query);
```

Defined in the sample, used above:

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

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

        ActivitySource.AddActivityListener(_listener);
    }

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

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

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

Check it in the database:

```sql
SELECT name FROM account WHERE account_number = 'ACC-61001';
```

Expect: 1 row, 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("id", 999999999L) };
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}",
        999999999L, ArxException.FormatCode(exception.Code), exception.IsTransient);
}
```

Check it in the database:

```sql
SELECT count(*) FROM account WHERE 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-61002";
await data.CreateAsync(account);

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

var gone = new ArxEntity("account") { RecordId = ArxRecordId.Of("id", 999999999L) };
gone["name"] = "Gone";

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

Check it in the database:

```sql
SELECT name FROM account WHERE account_number = 'ACC-61002';
```

Expect: 1 row, Wingtip Toys.

### Remember resolved tables, or preload them

Advanced case 10 of Create.

The three ways to set up a service, from the least code to the fastest, in three scenarios: without a catalog, reading the tables on every call; without a catalog, remembering what was read (RememberResolvedTables); and the tables preloaded into a catalog once, at startup.

Good to know:

**Re-read (the default).** Every call reads its tables from the database first: always current, one more round trip per call. Measured on PostgreSQL, a create or an update by id took 5–7 times as long as with a catalog (the benchmark project's NoCatalog results).

**Remembered.** `RememberResolvedTables = true`: each table is read the first time a call needs it, then kept for the service's lifetime, so later calls were level with a catalog. A schema change after that needs a new service; keep one service for the application (a singleton) so the tables are read once.

**Preloaded.** `ArxPreLoadTable` reads the tables once, at startup, into a catalog: no call reads the database's catalog, and a table left out is refused (ARX1016) instead of resolved. It is the setup for an application that grows past simple CRUD (Create Advanced6 shows its options).

Usings:

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

#### 1. Re-read on every call (the default)

The default: both creates read the account table first, so each sees the table as it is now.

In scope: `connectionString` (string).

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

var first = await service.CreateAsync(Account("Re-read 1", "ACC-N081"));
var second = await service.CreateAsync(Account("Re-read 2", "ACC-N082"));

Console.WriteLine($"created {first} and {second}; the table was read twice");
```

Defined in the sample, used above:

```csharp
private static ArxEntity Account(string name, string number)
{
    var account = new ArxEntity("account");
    account["name"] = name;
    account["account_number"] = number;
    return account;
}
```

Check it in the database:

```sql
SELECT account_number, name
FROM account
WHERE account_number IN ('ACC-N081', 'ACC-N082');
```

Expect: 2 rows.

#### 2. Remember what was read

Remembered: the first call reads the account table, the later ones use what it read.

In scope: `connectionString` (string).

```csharp
// One service for the application's lifetime, so the tables are read once.
var service = new ArxMsSqlDbService(connectionString) { RememberResolvedTables = true };

var id = await service.CreateAsync(Account("Remembered", "ACC-N083"));

var change = new ArxEntity("account");
change.RecordId = id;
change["employee_count"] = 12;
await service.UpdateAsync(change);

var read = await service.RetrieveAsync("account", id, new ArxColumnSet("name", "employee_count")) ?? throw new InvalidOperationException("The account record isn't there.");

Console.WriteLine($"{read["name"]}, {read["employee_count"]} employees; the table was read once, by the create");
```

Check it in the database:

```sql
SELECT name, employee_count
FROM account
WHERE account_number = 'ACC-N083';
```

Expect: 1 row, Remembered, 12.

#### 3. Preload a catalog at startup

Preloaded: the tables read once, at startup, into a catalog the service is created with.

In scope: `connectionString` (string).

```csharp
var tables = new ArxPreLoadTableRegister()
    .PreLoadTable("account");

ArxSchemaCatalog schema = await new ArxPreLoadTable(tables).LoadAsync(connectionString);

var service = new ArxMsSqlDbService(connectionString, schema);

var id = await service.CreateAsync(Account("Preloaded", "ACC-N084"));

Console.WriteLine($"created {id}; no call reads the database's catalog");
```

Check it in the database:

```sql
SELECT account_number, name
FROM account
WHERE account_number = 'ACC-N084';
```

Expect: 1 row, Preloaded.

---

# CreateMultiple with ARX.SDK on SQL Server

## What a CreateMultiple does

| | |
|---|---|
| **Many records** | `CreateMultipleAsync` returns one id per entity, in list order (`ids[i]` is `list[i]`'s), each also set on its entity. All or nothing: if the call fails, nothing is stored, nothing is returned, and no entity gets an id. The same `ArxEntity` object twice in one list is `ARX1001` (one entity holds one id). Rows with the same attributes and value types are grouped: 20 or more go in as one JSON parameter read by `OPENJSON`, smaller groups as a list of values under SQL Server's limit of 2,100 parameters, all in one transaction |
| **Without a catalog** | Every table in the list is read once, before the first row is written: one more round trip per call, not per row; still slower than with a catalog (Basic2) |
| **Without ids** | `new ArxCreateMultipleOptions { ReturnIds = false }` reads no id back: a group of 1,000 rows or more of one shape goes in by `SqlBulkCopy` (smaller ones by the ordinary statement, faster below that size), with constraints and triggers checked; a shape that writes an IDENTITY column, or a `datetime` or `xml` column, takes the ordinary `INSERT`; all or nothing, and `ArxRecordId.None` for each entity (Basic3). From 0.1.0-preview.5 |

## The samples

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

### Create many records in one call and get their ids

Basics 1 of CreateMultiple.

Several records in one call, in seven scenarios. Three ways to declare the id column (in the service's schema, on each entity, in a schema for this one call), then two tables with no id column, then two things that only happen with many records: several tables in one list, and all or nothing when one record is refused.

Usings:

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

#### 1. Id column in the schema

The usual way: the id column declared in the schema, the ids read from the list CreateMultipleAsync returns.

In scope: `connectionString` (string).

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

var service = new ArxMsSqlDbService(connectionString, schema);

var first = new ArxEntity("account");
first["name"] = "Contoso Ltd.";
first["account_number"] = "ACC-0002";

var second = new ArxEntity("account");
second["name"] = "Fabrikam Inc.";
second["account_number"] = "ACC-0003";

var third = new ArxEntity("account");
third["name"] = "Litware Inc.";
third["account_number"] = "ACC-0004";

var accounts = new List<ArxEntity> { first, second, third };

// CreateMultipleAsync returns one id per entity, in the list's order: ids[0] is first's id, and so on.
IReadOnlyList<ArxRecordId> ids = await service.CreateMultipleAsync(accounts);

Console.WriteLine($"created accounts {string.Join(", ", ids)}");
```

Check it in the database:

```sql
SELECT id, name, account_number
FROM account
WHERE account_number IN ('ACC-0002', 'ACC-0003', 'ACC-0004')
ORDER BY id;
```

Expect: 3 rows, Contoso Ltd., Fabrikam Inc., Litware Inc., ids in list order.

#### 2. Id column on each entity

The other way: the id column declared on each entity, the ids read from the entities.

In scope: `connectionString` (string).

```csharp
// No schema catalog: each entity says what its id column is.
var service = new ArxMsSqlDbService(connectionString);

// Declared once, outside the loop: every entity shares the same object.
var accountId = ArxIdColumn.Generated<long>("id");

var accounts = new List<ArxEntity>();

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

await service.CreateMultipleAsync(accounts);

// Each id is also set on its own entity.
foreach (var account in accounts)
{
    Console.WriteLine($"{account["name"]} got id {account.RecordId}");
}
```

Check it in the database:

```sql
SELECT id, name, account_number
FROM account
WHERE account_number IN ('ACC-0007', 'ACC-0008', 'ACC-0009')
ORDER BY id;
```

Expect: 3 rows, Alpine Ski House, Blue Yonder Airlines, Coho Winery.

#### 3. A schema for this call

A third way: a schema for this one call. The entities declare nothing; the schema passed to CreateMultipleAsync declares the table for this call only, in place of the service's.

In scope: `connectionString` (string).

```csharp
// No schema catalog on the service: the call brings its own.
var service = new ArxMsSqlDbService(connectionString);

var schema = ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id"))
    .Build();

var accounts = new List<ArxEntity>();

foreach (var (name, number) in new[] { ("Wide World Importers", "ACC-0011"), ("Woodgrove Bank", "ACC-0012") })
{
    var account = new ArxEntity("account");
    account["name"] = name;
    account["account_number"] = number;
    accounts.Add(account);
}

// The schema declares the table, so the entities mustn't: one with its own IdColumn or Keys is refused (ARX1001).
var ids = await service.CreateMultipleAsync(accounts, schema);

Console.WriteLine($"created accounts {string.Join(", ", ids)}");
```

Check it in the database:

```sql
SELECT id, name, account_number
FROM account
WHERE account_number IN ('ACC-0011', 'ACC-0012')
ORDER BY id;
```

Expect: 2 rows, Wide World Importers, Woodgrove Bank.

#### 4. Compound key

A table with no id column, where a compound key (region + sku) identifies each record.

In scope: `connectionString` (string).

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

var service = new ArxMsSqlDbService(connectionString, schema);

// A price's region must be a country that exists, so add the country first.
var netherlands = new ArxEntity("country");
netherlands["code"] = "NL";
netherlands["name"] = "Netherlands";
await service.CreateAsync(netherlands);

var espresso = new ArxEntity("price_list");
espresso["region"] = "NL";
espresso["sku"] = "SKU-000002";
espresso["unit_price"] = 1.20m;
espresso["valid_from"] = new DateOnly(2026, 1, 1);

var cappuccino = new ArxEntity("price_list");
cappuccino["region"] = "NL";
cappuccino["sku"] = "SKU-000003";
cappuccino["unit_price"] = 1.80m;
cappuccino["valid_from"] = new DateOnly(2026, 1, 1);

// There is no id column, so each id in the list is ArxRecordId.None.
var ids = await service.CreateMultipleAsync(new List<ArxEntity> { espresso, cappuccino });

Console.WriteLine($"created {ids.Count} prices; their ids: {string.Join(", ", ids)}");
```

Check it in the database:

```sql
SELECT region, sku, unit_price
FROM price_list
WHERE region = 'NL'
ORDER BY sku;
```

Expect: 2 rows, SKU-000002 (1.20) and SKU-000003 (1.80).

#### 5. No key

A table with no id column and no key: the same row can go in twice.

In scope: `connectionString` (string).

```csharp
var schema = ArxSchemaCatalog.Create()
    .Table("activity_log", ArxIdColumn.NoIdColumn)
    .Build();

var service = new ArxMsSqlDbService(connectionString, schema);

var first = new ArxEntity("activity_log");
first["action"] = "report exported";

var again = new ArxEntity("activity_log");
again["action"] = "report exported";

var ids = await service.CreateMultipleAsync(new List<ArxEntity> { first, again });

Console.WriteLine($"logged \"report exported\" {ids.Count} times");
```

Check it in the database:

```sql
SELECT action, count(*)
FROM activity_log
WHERE action = 'report exported'
GROUP BY action;
```

Expect: count 2 (the same row went in twice).

#### 6. Several tables

Several tables in one call: the ids come back in list order, each of its own table's type.

In scope: `connectionString` (string).

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

var service = new ArxMsSqlDbService(connectionString, schema);

var france = new ArxEntity("country");
france["code"] = "FR";
france["name"] = "France";

var kiosk = new ArxEntity("device");
kiosk["serial_number"] = "KIOSK-0001";

var germany = new ArxEntity("country");
germany["code"] = "DE";
germany["name"] = "Germany";

var ids = await service.CreateMultipleAsync(new List<ArxEntity> { france, kiosk, germany });

Console.WriteLine($"country {ids[0].AsString()}, device {ids[1].AsGuid()}, country {ids[2].AsString()}");
```

Check it in the database:

```sql
SELECT 'country' AS source, code AS id, name FROM country WHERE code IN ('FR', 'DE')
UNION ALL
SELECT 'device', CAST(id AS nvarchar(max)), serial_number FROM device WHERE serial_number = 'KIOSK-0001';
```

Expect: 3 rows: France, Germany and the kiosk device with its uuid.

#### 7. All or nothing

All or nothing: when the database refuses one record, none of the list is stored and no entity gets an id.

In scope: `connectionString` (string).

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

var service = new ArxMsSqlDbService(connectionString, schema);

var fine = new ArxEntity("account");
fine["name"] = "Margie's Travel";
fine["account_number"] = "ACC-0010";

// Account numbers are unique, and ACC-0002 was created in scenario 1.
var taken = new ArxEntity("account");
taken["name"] = "Contoso (again)";
taken["account_number"] = "ACC-0002";

try
{
    await service.CreateMultipleAsync(new List<ArxEntity> { fine, taken });
}
catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.UniqueViolation)
{
    Console.WriteLine($"refused ({error.Code}); Margie's Travel wasn't stored either, and has no id ({(fine.RecordId == null ? "RecordId is null" : fine.RecordId)})");
}
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number = 'ACC-0010';
```

Expect: 0: the list was refused, so Margie's Travel wasn't stored either.

### Create many records without a catalog

Basics 2 of CreateMultiple.

Many records in one call on a service created with only a connection string: accounts and a country in one list. ARX reads both tables from the database once, before it writes the first row, then returns one id per entity.

Good to know:

A call over many rows resolves every table it touches in one go, so the extra round trip is paid once per call, not once per row. It is still slower than a service with a catalog; for imports, declare the catalog or preload it.

Usings:

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

#### 1. Two tables in one call

Two tables in one list: each table is read once, then every row is written, all or nothing.

In scope: `connectionString` (string).

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

var first = new ArxEntity("account");
first["name"] = "Litware Inc.";
first["account_number"] = "ACC-N011";

var second = new ArxEntity("account");
second["name"] = "Proseware Inc.";
second["account_number"] = "ACC-N012";

var estonia = new ArxEntity("country");
estonia["code"] = "EE";
estonia["name"] = "Estonia";

// One id per entity, in list order: two generated numbers, then the code that was supplied.
IReadOnlyList<ArxRecordId> ids = await service.CreateMultipleAsync(new List<ArxEntity> { first, second, estonia });

Console.WriteLine($"created {string.Join(", ", ids)}");
```

Check it in the database:

```sql
SELECT account_number, name
FROM account
WHERE account_number IN ('ACC-N011', 'ACC-N012');
```

Expect: 2 rows, Litware Inc. and Proseware Inc. SELECT code, name FROM country WHERE code = 'EE'; 1 row, Estonia.

### Create many records without reading their ids

Basics 3 of CreateMultiple.

Many records created when the program doesn't need their ids: ReturnIds = false reads none back, and SQL Server loads a group of 1,000 rows or more of one shape with SqlBulkCopy, constraints and triggers checked. Still all or nothing.

Good to know:

The call returns one ArxRecordId.None per entity, so code that switches between the two forms keeps its type.

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

Usings:

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

#### 1. A load without ids

A thousand accounts in one call; no id comes back.

In scope: `connectionString` (string).

```csharp
var service = new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id"))
    .Build());

var accounts = new List<ArxEntity>();

for (var i = 1; i <= 1000; i++)
{
    var account = new ArxEntity("account");
    account["name"] = $"Bulk account {i}";
    account["account_number"] = $"ACC-F3{i:0000}";
    account["employee_count"] = i % 50;
    accounts.Add(account);
}

var ids = await service.CreateMultipleAsync(accounts, new ArxCreateMultipleOptions { ReturnIds = false });

Console.WriteLine($"{ids.Count} accounts created; ids returned: {ids.Count(x => x.HasValue)}");
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number LIKE 'ACC-F3%';
```

Expect: 1000.

### Save an account's contacts in one call

Real case 1 of CreateMultiple.

A new customer's contacts, entered on one screen and saved in one call, in two scenarios: three contacts with different attributes, and a second save where one row fails, so none is stored.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "Fabrikam Manufacturing";
account["industry"] = new ArxSelectionSetValue("Industry", Industry.Manufacturing);
await service.CreateAsync(account);
```

#### 1. Three contacts in one CreateMultipleAsync

Three contacts in one call. Rows don't need the same attributes: ARX groups them by shape.

In scope: `service` (ArxMsSqlDbService), `account` (ArxEntity).

```csharp
var ana = new ArxEntity("contact");
ana["account_id"] = new ArxEntityReference("account", account.RecordId);
ana["first_name"] = "Ana";
ana["last_name"] = "Silva";
ana["job_title"] = "Purchasing manager";
ana["email"] = "ana.silva@fabrikam.example";
ana["preferred_channel"] = new ArxSelectionSetValue("ContactChannel", 'E');
ana["consent_given_at"] = new DateTimeOffset(2026, 11, 2, 9, 30, 0, TimeSpan.FromHours(8));

var ben = new ArxEntity("contact");
ben["account_id"] = new ArxEntityReference("account", account.RecordId);
ben["first_name"] = "Ben";
ben["last_name"] = "Okafor";
ben["job_title"] = "Plant director";
ben["email"] = "ben.okafor@fabrikam.example";
ben["preferred_channel"] = new ArxSelectionSetValue("ContactChannel", 'P');

// No email or channel for this one, but a birthday and a note.
var chen = new ArxEntity("contact");
chen["account_id"] = new ArxEntityReference("account", account.RecordId);
chen["first_name"] = "Chen";
chen["last_name"] = "Wei";
chen["job_title"] = "Accounts payable";
chen["birthday"] = new DateOnly(1985, 9, 14);
chen["notes"] = "Prefers calls before 10am.";

var ids = await service.CreateMultipleAsync(new List<ArxEntity> { ana, ben, chen });
Console.WriteLine($"Ana {ids[0]}, Ben {ids[1]}, Chen {ids[2]}");
```

Check it in the database:

```sql
SELECT c.first_name, c.last_name, c.email, c.preferred_channel, c.birthday, c.notes
FROM contact c
JOIN account a ON a.id = c.account_id
WHERE a.name = 'Fabrikam Manufacturing'
ORDER BY c.id;
```

Expect: 3 rows: Ana (E), Ben (P), Chen (a birthday and a note, no email).

#### 2. A second save where one email is already taken: nothing is stored

A second save where one email is already taken: the whole call is refused, and no entity gets an id.

In scope: `service` (ArxMsSqlDbService), `account` (ArxEntity).

```csharp
var dana = new ArxEntity("contact");
dana["account_id"] = new ArxEntityReference("account", account.RecordId);
dana["first_name"] = "Dana";
dana["last_name"] = "Ruiz";
dana["email"] = "dana.ruiz@fabrikam.example";

var anaAgain = new ArxEntity("contact");
anaAgain["account_id"] = new ArxEntityReference("account", account.RecordId);
anaAgain["first_name"] = "Ana";
anaAgain["last_name"] = "Silva";
anaAgain["email"] = "ana.silva@fabrikam.example";

try
{
    await service.CreateMultipleAsync(new List<ArxEntity> { dana, anaAgain });
}
catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.UniqueViolation)
{
    Console.WriteLine(error.Message);
}
```

Check it in the database:

```sql
SELECT count(*)
FROM contact
WHERE email = 'dana.ruiz@fabrikam.example';
```

Expect: 0: the whole call was refused, Dana included.

### Import a 20,000-item product feed in one call

Real case 2 of CreateMultiple.

A supplier's product feed of 20,000 items, imported in one call, in three scenarios: the categories first, the feed itself (you don't split the list: ARX does, and keeps the call all or nothing), and next week's feed with a bad row, which refuses the whole feed.

Usings:

```csharp
using ARX.SDK;
using ARX.SDK.Models;
using ARX.SDK.Services;
using System.Text.Json;
```

#### 1. The categories first, so products can point at them

The categories first, so the products can point at them.

In scope: `service` (ArxMsSqlDbService).

```csharp
var beverages = new ArxEntity("product_category");
beverages["name"] = "Beverages";

var condiments = new ArxEntity("product_category");
condiments["name"] = "Condiments";

var ids = await service.CreateMultipleAsync(new List<ArxEntity> { beverages, condiments });
Console.WriteLine($"Beverages {ids[0]}, Condiments {ids[1]}");

return (ids[0], ids[1]);
```

Check it in the database:

```sql
SELECT id, name
FROM product_category
WHERE name IN ('Beverages', 'Condiments')
ORDER BY id;
```

Expect: 2 rows.

#### 2. 20,000 products in one CreateMultipleAsync

20,000 products in one call, with JSON and delimited tag columns.

In scope: `service` (ArxMsSqlDbService), `beverages` (ArxRecordId), `condiments` (ArxRecordId).

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

for (var i = 1; i <= 20_000; i++)
{
    var product = new ArxEntity("product");
    product["sku"] = $"SKU-{i:D6}";
    product["name"] = $"Product {i}";
    product["category_id"] = new ArxEntityReference("product_category", i % 2 == 0 ? beverages : condiments);
    product["unit_price"] = 1.50m + i % 400 * 0.25m;
    product["weight_kg"] = 0.25 + i % 40 * 0.05;
    product["stock_quantity"] = i % 500;
    product["tags"] = i % 3 == 0 ? "imported,organic" : "imported,standard";
    product["attributes"] = new ArxJsonB { SerializedValue = JsonSerializer.Serialize(new { origin = i % 2 == 0 ? "PT" : "BR", shelfLifeDays = 180 }) };
    product["launched_on"] = new DateOnly(2024, 1, 1).AddDays(i % 700);
    products.Add(product);
}

var ids = await service.CreateMultipleAsync(products);
```

Check it in the database:

```sql
SELECT count(*) AS products, min(sku) AS first_sku, max(sku) AS last_sku
FROM product
WHERE sku LIKE 'SKU-%';
SELECT sku, unit_price, tags, attributes
FROM product
WHERE sku = 'SKU-000043';
```

Expect: 20000 products, SKU-000001 to SKU-020000; SKU-000043 has tags imported,standard.

#### 3. Next week's feed has a negative price in its last item: nothing from it is stored

Next week's feed has a negative price in its last item: nothing from it is stored.

In scope: `service` (ArxMsSqlDbService).

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

for (var i = 1; i <= 5_000; i++)
{
    var product = new ArxEntity("product");
    product["sku"] = $"NEXT-{i:D6}";
    product["name"] = $"New product {i}";
    product["unit_price"] = i == 5_000 ? -1.00m : 9.99m;
    nextFeed.Add(product);
}

try
{
    await service.CreateMultipleAsync(nextFeed);
}
catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.CheckViolation)
{
    Console.WriteLine(error.Message);
}
```

Check it in the database:

```sql
SELECT count(*)
FROM product
WHERE sku LIKE 'NEXT-%';
```

Expect: 0: one negative price refused the whole feed.

### Import 5,000 contacts in batches, in one transaction

Advanced case 1 of CreateMultiple.

A 5,000-contact import in batches of 500, in one transaction that ARX manages (batch scopes), in three scenarios: automatic commit with RunBatchAsync; manual commit with BeginBatchAsync when you want to decide after a failed batch; and savepoints off when any failure must stop the whole import.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "Wide World Importers";
var accountId = await service.CreateAsync(account);
```

#### 1. Automatic commit: 10 batches, one commit at the end

RunBatchAsync commits when your code returns, and rolls back if anything in it throws.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
await service.RunBatchAsync(async scope =>
{
    foreach (var batch in ReadContactFile(accountId, "first"))
    {
        await scope.CreateMultipleAsync(batch);
    }
});
```

Defined in the sample, used above:

```csharp
private static IEnumerable<List<ArxEntity>> ReadContactFile(ArxRecordId accountId, string file, int? badRowInBatch = null)
{
    for (var batchNumber = 1; batchNumber <= 10; batchNumber++)
    {
        var batch = new List<ArxEntity>();

        for (var row = 1; row <= 500; row++)
        {
            var contact = new ArxEntity("contact");
            contact["account_id"] = new ArxEntityReference("account", accountId);
            contact["first_name"] = $"Customer {batchNumber}-{row}";
            contact["last_name"] = batchNumber == badRowInBatch && row == 250 ? null : "Imported";
            contact["email"] = $"{file}.{batchNumber}.{row}@wideworld.example";
            batch.Add(contact);
        }

        yield return batch;
    }
}
```

Check it in the database:

```sql
SELECT count(*)
FROM contact
WHERE email LIKE 'first.%';
```

Expect: 5000.

#### 2. Manual commit: batch 8 has a contact without a last name

Each call has its own savepoint (asked for with Savepoints = true), so a failed batch is undone alone and the scope carries on. The error says which batch failed; you decide what to do.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
await using (var scope = await service.BeginBatchAsync(new ArxBatchOptions { Savepoints = true }))
{
    foreach (var batch in ReadContactFile(accountId, "second", badRowInBatch: 8))
    {
        try
        {
            await scope.CreateMultipleAsync(batch);
        }
        catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.NotNullViolation)
        {
            Console.WriteLine($"batch {error.BatchNumber} skipped: {error.Message}");
        }
    }

    // Your rule for this import: one bad batch is acceptable, more is not.
    if (scope.FailedBatches.Count <= 1)
    {
        await scope.CommitAsync();
    }

    // Leaving the block without CommitAsync() rolls back, even when an exception is passing through.
}
```

Check it in the database:

```sql
SELECT count(*) AS contacts, SUM(CASE WHEN email LIKE 'second.8.%' THEN 1 ELSE 0 END) AS from_batch_8
FROM contact
WHERE email LIKE 'second.%';
```

Expect: 4500, 0: batch 8 was undone alone, the other nine committed.

#### 3. Savepoints off: one failed batch rolls back the whole import

With savepoints off, one failed batch rolls back the whole import.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
await using (var scope = await service.BeginBatchAsync(new ArxBatchOptions { Savepoints = false }))
{
    try
    {
        foreach (var batch in ReadContactFile(accountId, "third", badRowInBatch: 2))
        {
            await scope.CreateMultipleAsync(batch);
        }

        await scope.CommitAsync();
    }
    catch (ArxDatabaseException error)
    {
        Console.WriteLine($"batch {error.BatchNumber} failed; the scope is {scope.State}");
    }
}
```

Check it in the database:

```sql
SELECT count(*)
FROM contact
WHERE email LIKE 'third.%';
```

Expect: 0: with savepoints off, one failed batch rolled back the whole import.

### An order and its lines: all steps or none

Advanced case 2 of CreateMultiple.

A process of several steps in one batch scope: create the order, create its lines, then check the payment. In two scenarios: the payment is declined, so neither the order nor its lines are kept (any step that fails, including your own code, undoes them all); and the payment is accepted, so both are committed together.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "Coho Winery";
var accountId = await service.CreateAsync(account);
var redWine = new ArxEntity("product");
redWine["sku"] = "WINE-RED";
redWine["name"] = "Red wine";
redWine["unit_price"] = 19.99m;
var whiteWine = new ArxEntity("product");
whiteWine["sku"] = "WINE-WHITE";
whiteWine["name"] = "White wine";
whiteWine["unit_price"] = 17.50m;
var productIds = await service.CreateMultipleAsync(new List<ArxEntity> { redWine, whiteWine });
```

#### 1. Payment declined: nothing is kept

Your own code throws after the order and its lines were created: the scope rolls them all back.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId), `productIds` (IReadOnlyList<ArxRecordId>).

```csharp
try
{
    await PlaceOrderAsync(service, accountId, productIds, paymentAccepted: false);
}
catch (InvalidOperationException error)
{
    Console.WriteLine($"the process failed: {error.Message}");
}
```

Defined in the sample, used above:

```csharp
private static async Task PlaceOrderAsync(ArxMsSqlDbService service, ArxRecordId accountId, IReadOnlyList<ArxRecordId> productIds, bool paymentAccepted)
{
    await service.RunBatchAsync(async scope =>
    {
        var order = new ArxEntity("sales_order");
        order["order_number"] = "SO-3001";
        order["account_id"] = new ArxEntityReference("account", accountId);
        order["ordered_on"] = DateTime.UtcNow;
        order["total"] = 57.48m;
        var orderId = await scope.CreateAsync(order);

        var redLine = new ArxEntity("order_line");
        redLine["order_id"] = new ArxEntityReference("sales_order", orderId);
        redLine["product_id"] = new ArxEntityReference("product", productIds[0]);
        redLine["quantity"] = 2;
        redLine["unit_price"] = 19.99m;

        var whiteLine = new ArxEntity("order_line");
        whiteLine["order_id"] = new ArxEntityReference("sales_order", orderId);
        whiteLine["product_id"] = new ArxEntityReference("product", productIds[1]);
        whiteLine["quantity"] = 1;
        whiteLine["unit_price"] = 17.50m;

        await scope.CreateMultipleAsync(new List<ArxEntity> { redLine, whiteLine });

        if (!paymentAccepted)
        {
            throw new InvalidOperationException("Payment declined.");   // the order and its lines are rolled back
        }
    });
}
```

Check it in the database:

```sql
SELECT count(DISTINCT o.id) AS orders, count(l.order_id) AS lines
FROM sales_order o
LEFT JOIN order_line l ON l.order_id = o.id
WHERE o.order_number = 'SO-3001';
```

Expect: 1 order, 2 lines: the declined attempt left nothing; the order is the one scenario 2 committed.

#### 2. Payment accepted: the order and its lines are committed

Every step succeeds: the order and its lines are committed together.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId), `productIds` (IReadOnlyList<ArxRecordId>).

```csharp
await PlaceOrderAsync(service, accountId, productIds, paymentAccepted: true);
```

Check it in the database:

```sql
SELECT o.order_number, o.total, p.name AS product, l.quantity, l.unit_price
FROM sales_order o
JOIN order_line l ON l.order_id = o.id
JOIN product p ON p.id = l.product_id
WHERE o.order_number = 'SO-3001';
```

Expect: 2 rows, red wine x2 and white wine x1.

---

# Update with ARX.SDK on SQL Server

## What an update does

| | |
|---|---|
| **What is written** | Only the attributes you set. An attribute left out keeps its value; set to null, the column is cleared; `ArxIgnore` leaves it out |
| **Which rows** | By default the table's declared id: `entity.RecordId` (what `CreateAsync` returned) or the id attribute. `ArxUpdateOptions.Match` finds rows by a declared key (`ArxMatch.Key("account_number")`) or by any columns (`ArxMatch.Columns("lead_source")`), which may match many rows. Match columns are never written. Text is matched as SQL Server compares it: with the default collation, case-insensitively and without trailing spaces (`'abc'` finds `'ABC '`) |
| **What comes back** | The number of rows changed. An `INSTEAD OF` trigger that skips a row still counts it: SQL Server reports the rows the statement was for |
| **Nothing matched** | `ARX2002 RecordNotFound`. In `UpdateMultipleAsync`, one entity matching nothing refuses the whole call: nothing changes |
| **Strict** | `RequireSingleRow = true`: each entity must change exactly one row, else ARX2002 or `ARX2003 MultipleRecordsMatched`, and nothing changes |
| **Values** | Exactly as Create writes them: types, selection sets checked against the catalog, lookups, dates |
| 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. |
| **Without a catalog** | By the id, the table's primary key is read from the database first (one more round trip per call, so much slower than with a catalog: meant for simple CRUD and small applications); `ARX1019` when it has none ARX can use. By a key or columns no id is needed, so a table without a primary key works (Basic5) |
| **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`. |

## Reference

### Error codes an update can raise

| Code | When |
|---|---|
| ARX1001 InvalidArgument | No id or match value, a NULL match value, an id of the wrong type or one that can't exist (0, negative, empty), a key the table doesn't declare, a NoIdColumn table matched by id, nothing to write, the same entity or match values twice in one call (compared as SQL Server compares them), an entity declaring its own id or keys while a schema exists |
| ARX1002, ARX1003, ARX1004 | Missing table name, an invalid name, an attribute twice |
| ARX1017 SuspectedInjection | A table, attribute or match column name carrying SQL (a quote, `;`, a comment, `=`, an attack word after a space); nothing sent to the database (RealCase4) |
| ARX1005, ARX1006, ARX1011, ARX1012, ARX1013 | Lookups, selection sets and dates, as for Create |
| ARX1008 InvalidConnection | A connection with an open transaction, given without that transaction |
| ARX1016 IdNotDeclared | Matching by id or key on a table nobody declared (a match by columns needs no declaration) |
| ARX2002 RecordNotFound | Nothing matched; in UpdateMultiple, any entity matched nothing (nothing changed) |
| ARX2003 MultipleRecordsMatched | RequireSingleRow, and an entity matched several rows (nothing changed) |
| ARX3001 to ARX3004 | Unique, foreign key (including a natural id still referenced), not null, check |
| ARX4001 to ARX4004 | Out of range, too long, text that doesn't convert to the column's type (`'forty'` into an `int`), wrong type (including a match value of another type than its column) |
| ARX5001, ARX5002 | A table or column that doesn't exist, or an `IDENTITY` or computed column written |
| ARX6001 | No permission, or a row-level security block predicate refusing the change |
| ARX7001 to ARX7003 | Deadlock, snapshot update conflict, lock or command timeout (transient; SQL Server has no statement timeout of its own, so the client's command timeout applies) |
| ARX8001 NotSupported | A view SQL Server can't update (DISTINCT, GROUP BY or aggregates, or a change over several base tables) |

## The samples

Every Update sample, in reading order. Each scenario shows the code (verified against SQL Server) and the SQL that checks what it did. `service` is the application's `ArxMsSqlDbService`; 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-sql-sample-application.md.

### Update one record

Basics 1 of Update.

The smallest update: change one record, in four scenarios. Read them in order: the usual way first (the id that CreateAsync returned), then the other way (the id as an attribute), then a text id, then a table with no id column, whose record is found by its compound key.

Usings:

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

#### 1. By the id Create returned

The usual way: keep the id CreateAsync returned, and give it to the entity you update.

In scope: `connectionString` (string).

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

var service = new ArxMsSqlDbService(connectionString, schema);

var account = new ArxEntity("account");
account["name"] = "Fabrikam Outdoors";
account["account_number"] = "ACC-U101";
account["employee_count"] = 40;
ArxRecordId id = await service.CreateAsync(account);

// Only the attributes you set are written: the name and account number stay as they are.
var change = new ArxEntity("account");
change.RecordId = id;
change["employee_count"] = 55;
change["website"] = "https://fabrikam.example";

int changed = await service.UpdateAsync(change);

Console.WriteLine($"account {id.AsInt64()}, {changed} row changed");
```

Check it in the database:

```sql
SELECT name, employee_count, website
FROM account
WHERE account_number = 'ACC-U101';
```

Expect: 1 row, Fabrikam Outdoors, employee_count 55, website https://fabrikam.example.

#### 2. By the id attribute

The other way: the id as an attribute, for example a value that came from a form or an API.

In scope: `connectionString` (string).

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

var service = new ArxMsSqlDbService(connectionString, schema);

var account = new ArxEntity("account");
account["name"] = "Wide World Importers";
account["account_number"] = "ACC-U102";
long accountId = (await service.CreateAsync(account)).AsInt64();

// The id attribute finds the row; it is never written, because it is the match.
var change = new ArxEntity("account");
change["id"] = accountId;
change["name"] = "Wide World Importers Ltd.";

await service.UpdateAsync(change);

Console.WriteLine($"account {accountId} renamed");
```

Check it in the database:

```sql
SELECT name
FROM account
WHERE account_number = 'ACC-U102';
```

Expect: 1 row, Wide World Importers Ltd.

#### 3. A text id

A text id: a country, found by its ISO code.

In scope: `connectionString` (string).

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

var service = new ArxMsSqlDbService(connectionString, schema);

var sweden = new ArxEntity("country");
sweden["code"] = "SE";
sweden["name"] = "Swden";
await service.CreateAsync(sweden);

// Fix the typo: the code is the id, the name is written.
var fix = new ArxEntity("country");
fix["code"] = "SE";
fix["name"] = "Sweden";
await service.UpdateAsync(fix);

Console.WriteLine("country SE renamed to Sweden");
```

Check it in the database:

```sql
SELECT code, name
FROM country
WHERE code = 'SE';
```

Expect: 1 row, Sweden.

#### 4. By a compound key

A table with no id column: a price is found by its region and SKU, the compound key.

In scope: `connectionString` (string).

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

var service = new ArxMsSqlDbService(connectionString, schema);

var denmark = new ArxEntity("country");
denmark["code"] = "DK";
denmark["name"] = "Denmark";
await service.CreateAsync(denmark);

var price = new ArxEntity("price_list");
price["region"] = "DK";
price["sku"] = "SKU-U401";
price["unit_price"] = 12.00m;
price["valid_from"] = new DateOnly(2026, 1, 1);
await service.CreateAsync(price);

// There is no id, so say how to find the row: by the declared key.
var newPrice = new ArxEntity("price_list");
newPrice["region"] = "DK";
newPrice["sku"] = "SKU-U401";
newPrice["unit_price"] = 13.50m;

await service.UpdateAsync(newPrice, new ArxUpdateOptions { Match = ArxMatch.Key("region", "sku") });

Console.WriteLine("price DK / SKU-U401 is now 13.50");
```

Check it in the database:

```sql
SELECT region, sku, unit_price, valid_from
FROM price_list
WHERE region = 'DK' AND sku = 'SKU-U401';
```

Expect: 1 row, unit_price 13.50, valid_from 2026-01-01 (not written, so unchanged).

### Clear a value, keep the others

Basics 2 of Update.

What an update writes, in three scenarios: an attribute set to null clears the column; an attribute you leave out keeps its value; `ArxIgnore` leaves out an attribute that is on the entity anyway. Each scenario has its own account.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var service = new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id"))
    .Build());
```

#### 1. Null clears

Set an attribute to null to clear the column.

In scope: `service` (ArxMsSqlDbService).

```csharp
var id = await CreateAccountAsync(service, "ACC-U301");

var change = new ArxEntity("account");
change.RecordId = id;
change["website"] = null;

await service.UpdateAsync(change);

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

Defined in the sample, used above:

```csharp
private static async Task<ArxRecordId> CreateAccountAsync(ArxMsSqlDbService service, string number)
{
    var account = new ArxEntity("account");
    account["name"] = "Proseware Labs";
    account["account_number"] = number;
    account["website"] = "https://proseware.example";
    account["employee_count"] = 12;
    return await service.CreateAsync(account);
}
```

Check it in the database:

```sql
SELECT name, website, employee_count
FROM account
WHERE account_number = 'ACC-U301';
```

Expect: 1 row, Proseware Labs, website NULL, employee_count 12.

#### 2. Left out keeps its value

An attribute you don't set isn't touched: this update changes the name only.

In scope: `service` (ArxMsSqlDbService).

```csharp
var id = await CreateAccountAsync(service, "ACC-U302");

var change = new ArxEntity("account");
change.RecordId = id;
change["name"] = "Proseware Laboratories";

await service.UpdateAsync(change);

Console.WriteLine("name changed, website and employee_count kept");
```

Check it in the database:

```sql
SELECT name, website, employee_count
FROM account
WHERE account_number = 'ACC-U302';
```

Expect: 1 row, Proseware Laboratories, https://proseware.example, employee_count 12.

#### 3. ArxIgnore

An entity you reuse may hold attributes you don't want written: mark them ArxIgnore.

In scope: `service` (ArxMsSqlDbService).

```csharp
var id = await CreateAccountAsync(service, "ACC-U303");

var change = new ArxEntity("account");
change.RecordId = id;
change["employee_count"] = 14;
change["name"] = new ArxIgnore();

await service.UpdateAsync(change);

Console.WriteLine("employee_count changed, name not written");
```

Check it in the database:

```sql
SELECT name, employee_count
FROM account
WHERE account_number = 'ACC-U303';
```

Expect: 1 row, Proseware Labs, employee_count 14.

### Catch a record that doesn't exist

Basics 3 of Update.

An update of a record that doesn't exist is an error, ARX2002 RecordNotFound, never silently nothing. Two scenarios: one record, and a list where one is missing.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var service = new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id"))
    .Build());
```

#### 1. One record

The usual case: the id doesn't exist (deleted meanwhile, or a wrong id).

In scope: `service` (ArxMsSqlDbService).

```csharp
var change = new ArxEntity("account");
change["id"] = 999_999_998L;
change["name"] = "Nobody";

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

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE name = 'Nobody';
```

Expect: 0.

#### 2. One of many

In a list, the error says how many entities matched no row, and nothing in the list was changed.

In scope: `service` (ArxMsSqlDbService).

```csharp
var account = new ArxEntity("account");
account["name"] = "Margie's Travel";
account["account_number"] = "ACC-U401";
var id = await service.CreateAsync(account);

var real = new ArxEntity("account");
real.RecordId = id;
real["name"] = "Margie's Travel Agency";

var ghost = new ArxEntity("account");
ghost["id"] = 999_999_997L;
ghost["name"] = "Ghost";

try
{
    await service.UpdateMultipleAsync(new[] { real, ghost });
}
catch (ArxException error) when (error.Code == ArxErrorCode.RecordNotFound)
{
    Console.WriteLine($"{error.Message}");
}
```

Check it in the database:

```sql
SELECT name
FROM account
WHERE account_number = 'ACC-U401';
```

Expect: 1 row, Margie's Travel: the real account wasn't renamed either.

### Find rows by id, by key or by columns

Basics 4 of Update.

How an update finds its rows (ArxMatch), in four scenarios: by id (the default), by a declared key, by any columns (which may match many rows), and strict: exactly one row, or nothing changes.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var service = new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id")).Key("account", "account_number")
    .Table("contact", ArxIdColumn.Generated<long>("id")).Key("contact", "email")
    .Build());
```

#### 1. By id

The default: by the table's id. The other ways below are for when you don't have it.

In scope: `service` (ArxMsSqlDbService).

```csharp
var account = new ArxEntity("account");
account["name"] = "Lucerne Publishing";
account["account_number"] = "ACC-U501";
var id = await service.CreateAsync(account);

var change = new ArxEntity("account");
change.RecordId = id;
change["employee_count"] = 30;
await service.UpdateAsync(change);

Console.WriteLine("ACC-U501 updated by its id");
```

Check it in the database:

```sql
SELECT name, employee_count
FROM account
WHERE account_number = 'ACC-U501';
```

Expect: 1 row, employee_count 30.

#### 2. By a key

By a declared key: you know the account number, not the id.

In scope: `service` (ArxMsSqlDbService).

```csharp
var account = new ArxEntity("account");
account["name"] = "Coho Winery";
account["account_number"] = "ACC-U502";
await service.CreateAsync(account);

var change = new ArxEntity("account");
change["account_number"] = "ACC-U502";
change["employee_count"] = 18;

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

Console.WriteLine("ACC-U502 updated by its account number");
```

Check it in the database:

```sql
SELECT name, employee_count
FROM account
WHERE account_number = 'ACC-U502';
```

Expect: 1 row, Coho Winery, employee_count 18.

#### 3. By columns

By any columns: every contact from one import is marked "do not email". Many rows change.

In scope: `service` (ArxMsSqlDbService).

```csharp
var contacts = Enumerable.Range(1, 4).Select(i =>
{
    var contact = new ArxEntity("contact");
    contact["first_name"] = "Imported";
    contact["last_name"] = $"Contact {i}";
    contact["email"] = $"import-u5-{i}@example.com";
    contact["lead_source"] = "Expo U5";
    return contact;
}).ToList();
await service.CreateMultipleAsync(contacts);

var change = new ArxEntity("contact");
change["lead_source"] = "Expo U5";
change["do_not_email"] = true;

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

Console.WriteLine($"{changed} contacts from Expo U5 marked do not email");
```

Check it in the database:

```sql
SELECT count(*)
FROM contact
WHERE lead_source = 'Expo U5' AND do_not_email = 1;
```

Expect: 4.

#### 4. Strict

Strict: RequireSingleRow refuses an update that would change more than one row, and changes nothing.

In scope: `service` (ArxMsSqlDbService).

```csharp
var change = new ArxEntity("contact");
change["lead_source"] = "Expo U5";
change["job_title"] = "Buyer";

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

Check it in the database:

```sql
SELECT count(*)
FROM contact
WHERE lead_source = 'Expo U5' AND job_title = 'Buyer';
```

Expect: 0: four contacts matched, so none was changed.

### Update without a catalog

Basics 5 of Update.

Updates on a service created with only a connection string, in two scenarios: by the id, which ARX reads from the table's primary key; and a table with no primary key, found by columns, which needs no id at all.

Good to know:

Every call reads its table from the database first, which makes it much slower than a service with a catalog (Create Basic6 says when that is fine).

An update by the id (the default) on a table with no usable primary key is ARX1019 TableNotResolvable. By a key or by columns, the table needs no id, so the update runs.

Usings:

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

Set up by the sample before its scenarios:

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

#### 1. By the id

The usual way: the id Create returned, on an entity with only the attributes to change.

In scope: `service` (ArxMsSqlDbService).

```csharp
var account = new ArxEntity("account");
account["name"] = "Blue Yonder Airlines";
account["account_number"] = "ACC-N021";
account["employee_count"] = 40;
ArxRecordId id = await service.CreateAsync(account);

var change = new ArxEntity("account");
change.RecordId = id;
change["employee_count"] = 45;

int changed = await service.UpdateAsync(change);

Console.WriteLine($"account {id.AsInt64()}: {changed} row changed");
```

Check it in the database:

```sql
SELECT name, employee_count
FROM account
WHERE account_number = 'ACC-N021';
```

Expect: 1 row, employee_count 45.

#### 2. A table with no primary key, by columns

activity_log has no primary key: find the rows by a column instead, and every matching row changes.

In scope: `service` (ArxMsSqlDbService).

```csharp
for (var i = 0; i < 2; i++)
{
    var entry = new ArxEntity("activity_log") { IdColumn = ArxIdColumn.NoIdColumn };
    entry["action"] = "imported the rate card";
    await service.CreateAsync(entry);
}

// "action" finds the rows; logged_at is the value written.
var change = new ArxEntity("activity_log");
change["action"] = "imported the rate card";
change["logged_at"] = new DateTimeOffset(2026, 3, 1, 9, 0, 0, TimeSpan.Zero);

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

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

Check it in the database:

```sql
SELECT action, logged_at
FROM activity_log
WHERE action = 'imported the rate card';
```

Expect: 2 rows, both logged_at 2026-03-01 09:00:00.0000000 +00:00.

### An edit form changes some account details

Real case 1 of Update.

An "edit account" form, in three scenarios: the fields the user changed are written and the rest kept; a field the user emptied is cleared; the form sends the id it was opened with.

Usings:

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

#### 1. Write only the fields the user changed

The form posts only what the user changed. Each posted field becomes an attribute; nothing else is touched.

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

```csharp
var posted = new Dictionary<string, object>
{
    ["employee_count"] = 75,
    ["annual_revenue"] = 4_800_000.00m
};

var change = new ArxEntity("account");
change.RecordId = id;

foreach (var (field, value) in posted)
{
    change[field] = value;
}

var changed = await service.UpdateAsync(change);
Console.WriteLine($"{changed} row changed: {string.Join(", ", posted.Keys)}");
```

Check it in the database:

```sql
SELECT name, website, employee_count, annual_revenue
FROM account
WHERE account_number = 'ACC-U601';
```

Expect: 1 row, Alpine Ski House, https://alpineskihouse.example, employee_count 75, annual_revenue 4800000.00.

#### 2. An emptied field is cleared

The user emptied the website box: the form sends null, and the column is cleared.

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

```csharp
var change = new ArxEntity("account");
change.RecordId = id;
change["website"] = null;
await service.UpdateAsync(change);

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

Check it in the database:

```sql
SELECT website
FROM account
WHERE account_number = 'ACC-U602';
```

Expect: 1 row, NULL.

#### 3. The id comes back with the form

The form was opened with the account's id in a hidden field: it comes back as the id attribute.

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

```csharp
var hiddenField = id.AsInt64().ToString();

var change = new ArxEntity("account");
change["id"] = long.Parse(hiddenField);
change["name"] = "Alpine Ski House Ltd.";
await service.UpdateAsync(change);

Console.WriteLine($"account {hiddenField} renamed");
```

Check it in the database:

```sql
SELECT name
FROM account
WHERE account_number = 'ACC-U603';
```

Expect: 1 row, Alpine Ski House Ltd.

### Turn update errors into messages

Real case 2 of Update.

An edit screen turns update errors into messages, in two scenarios: what the database refuses (a taken account number, a negative employee count, a website too long), and what ARX refuses before the database (a NULL to find the row by, nothing to write). Each failed update leaves the account as it was.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var taken = new ArxEntity("account");
taken["name"] = "Errors: taken number";
taken["account_number"] = "ACC-U901";
await service.CreateAsync(taken);
var account = new ArxEntity("account");
account["name"] = "Errors: the account being edited";
account["account_number"] = "ACC-U902";
account["employee_count"] = 20;
var id = await service.CreateAsync(account);
```

#### 1. What the database refuses

The database refuses: each error has a code to choose the message by.

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

```csharp
var attempts = new (string Field, object Value)[]
{
    ("account_number", "ACC-U901"),
    ("employee_count", -3),
    ("website", "https://example.com/" + new string('x', 250))
};

foreach (var (field, value) in attempts)
{
    var change = new ArxEntity("account");
    change.RecordId = id;
    change[field] = value;

    try
    {
        await service.UpdateAsync(change);
    }
    catch (ArxDatabaseException error)
    {
        Console.WriteLine($"{field}: {error.Code}, the user sees \"{ForTheUser(error)}\"");
    }
}
```

Defined in the sample, used above:

```csharp
private static string ForTheUser(ArxException error) => error.Code switch
{
    ArxErrorCode.RecordNotFound => "This account no longer exists.",
    ArxErrorCode.UniqueViolation => "That account number is already in use.",
    ArxErrorCode.CheckViolation => "One of the values isn't allowed.",
    ArxErrorCode.ValueTooLong => "One of the values is too long.",
    _ => "The change couldn't be saved."
};
```

Check it in the database:

```sql
SELECT account_number, employee_count, website
FROM account
WHERE name = 'Errors: the account being edited';
```

Expect: 1 row, ACC-U902, employee_count 20, website NULL: none of the three changes was stored.

#### 2. What ARX refuses before the database

ARX refuses before the database: a NULL to find the row by, or an update with nothing to write.

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

```csharp
var byNullNumber = new ArxEntity("account");
byNullNumber["account_number"] = null;
byNullNumber["employee_count"] = 1;

var nothingToWrite = new ArxEntity("account");
nothingToWrite.RecordId = id;

foreach (var (change, options) in new[] { (byNullNumber, new ArxUpdateOptions { Match = ArxMatch.Key("account_number") }), (nothingToWrite, (ArxUpdateOptions?)null) })
{
    try
    {
        await service.UpdateAsync(change, options);
    }
    catch (ArxValidationException error)
    {
        Console.WriteLine(error.Message);
    }
}
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number IS NULL AND employee_count = 1;
```

Expect: 0.

### Change prices found by a compound key

Real case 3 of Update.

The price list has no id column; each price is found by its region and SKU. Three scenarios: change one price by the compound key; move a price to another SKU (changing part of the key, found by the old values through columns); rename a natural id that other rows still use, which the foreign key refuses.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var norway = new ArxEntity("country");
norway["code"] = "NO";
norway["name"] = "Norway";
await service.CreateAsync(norway);
var prices = new[] { "SKU-U501", "SKU-U502" }.Select(sku =>
{
    var price = new ArxEntity("price_list");
    price["region"] = "NO";
    price["sku"] = sku;
    price["unit_price"] = 20.00m;
    price["valid_from"] = new DateOnly(2026, 1, 1);
    return price;
}).ToList();
await service.CreateMultipleAsync(prices);
```

#### 1. One price, by region and SKU

Region and SKU together find the price: ArxMatch.Key with both columns.

In scope: `service` (ArxMsSqlDbService).

```csharp
var price = new ArxEntity("price_list");
price["region"] = "NO";
price["sku"] = "SKU-U501";
price["unit_price"] = 24.90m;
await service.UpdateAsync(price, new ArxUpdateOptions { Match = ArxMatch.Key("region", "sku") });

Console.WriteLine("NO / SKU-U501 is now 24.90");
```

Check it in the database:

```sql
SELECT sku, unit_price
FROM price_list
WHERE region = 'NO' AND sku = 'SKU-U501';
```

Expect: 1 row, 24.90.

#### 2. Renumber a SKU: find the row by other columns

The SKU itself changes (the product was renumbered). The key columns can't both find the row and be written, so find it by other columns: here the region and its current price, with RequireSingleRow so a second match refuses it.

In scope: `service` (ArxMsSqlDbService).

```csharp
var move = new ArxEntity("price_list");
move["region"] = "NO";
move["unit_price"] = 20.00m;
move["sku"] = "SKU-U502-NEW";
await service.UpdateAsync(move, new ArxUpdateOptions { Match = ArxMatch.Columns("region", "unit_price"), RequireSingleRow = true });

Console.WriteLine("SKU-U502 renumbered to SKU-U502-NEW");
```

Check it in the database:

```sql
SELECT sku, unit_price
FROM price_list
WHERE region = 'NO'
ORDER BY sku;
```

Expect: SKU-U501 24.90, SKU-U502-NEW 20.00.

#### 3. Rename a natural id that prices still use

Renaming the country code NO would orphan its prices: the foreign key refuses it (ARX3002).

In scope: `service` (ArxMsSqlDbService).

```csharp
var rename = new ArxEntity("country");
rename["name"] = "Norway";
rename["code"] = "NW";

try
{
    await service.UpdateAsync(rename, new ArxUpdateOptions { Match = ArxMatch.Columns("name") });
}
catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.ForeignKeyViolation)
{
    Console.WriteLine(error.Message);
}
```

Check it in the database:

```sql
SELECT code, name
FROM country
WHERE name = 'Norway';
```

Expect: 1 row, NO: unchanged.

### Catch an SQL injection attempt on update

Real case 4 of Update.

Catching an SQL injection attempt on update, in four scenarios. A name that carries SQL (a field to change, the column to find rows by) is refused with ARX1017 SuspectedInjection before the database. SQL in a value, the classic `' OR '1'='1`, is a parameter: it matches nothing instead of every row.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var first = new ArxEntity("account");
first["name"] = "Injection: first";
first["account_number"] = "ACC-I201";
first["employee_count"] = 10;
var second = new ArxEntity("account");
second["name"] = "Injection: second";
second["account_number"] = "ACC-I202";
second["employee_count"] = 10;
var ids = await service.CreateMultipleAsync(new List<ArxEntity> { first, second });
```

#### 1. A field name from the edit form carries SQL

The edit form's field names are copied into the change, and one of them carries SQL: the update is refused, the account is unchanged.

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

```csharp
var posted = new Dictionary<string, object>
{
    ["employee_count"] = 11,
    ["name\" = 'pwned', \"employee_count"] = 0,
};

var change = new ArxEntity("account");
change.RecordId = id;

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

try
{
    await service.UpdateAsync(change);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.SuspectedInjection)
{
    SecurityLog(error);
}
```

Defined in the sample, used above:

```csharp
private static void SecurityLog(ArxValidationException error) => Console.WriteLine($"SECURITY: {error.Message}");
```

Check it in the database:

```sql
SELECT name, employee_count
FROM account
WHERE account_number = 'ACC-I201';
```

Expect: 1 row: Injection: first, 10 (not even employee_count changed).

#### 2. The column to find rows by, from the request, carries SQL

The column to find rows by comes from the request (`?by=…`). ARX refuses the SQL in it; the fix is a fixed list of the columns a request may find rows by.

In scope: `service` (ArxMsSqlDbService).

```csharp
var by = "account_number = account_number OR 1=1 --";

var change = new ArxEntity("account");
change[by] = "anything";
change["employee_count"] = 0;

try
{
    await service.UpdateAsync(change, new ArxUpdateOptions { Match = ArxMatch.Columns(by) });
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.SuspectedInjection)
{
    SecurityLog(error);
}

// The fix: only the columns you allow.
var allowed = new HashSet<string> { "account_number" };
Console.WriteLine(allowed.Contains(by) ? $"would find rows by {by}" : "the allow-list refused the column to find rows by");
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE employee_count = 0;
```

Expect: 0: no account was changed.

#### 3. SQL in the value to find by matches nothing

The classic: the account number to find comes from the user as `ACC-I202' OR '1'='1`. Concatenated into SQL it would change every account; as a parameter it is just an account number that doesn't exist (ARX2002).

In scope: `service` (ArxMsSqlDbService).

```csharp
var change = new ArxEntity("account");
change["account_number"] = "ACC-I202' OR '1'='1";
change["employee_count"] = 999;

try
{
    await service.UpdateAsync(change, new ArxUpdateOptions { Match = ArxMatch.Key("account_number") });
}
catch (ArxException error) when (error.Code == ArxErrorCode.RecordNotFound)
{
    Console.WriteLine($"no account has that number: {error.Code}");
}
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number IN ('ACC-I201', 'ACC-I202') AND employee_count = 999;
```

Expect: 0: the text was compared as a value, so it matched no account (concatenated, it would have changed both).

#### 4. One bad entity refuses the whole UpdateMultiple

UpdateMultiple with one entity carrying SQL in a name: the whole call is refused, the other account is unchanged too.

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

```csharp
var good = new ArxEntity("account");
good.RecordId = ids[0];
good["employee_count"] = 50;

var bad = new ArxEntity("account");
bad.RecordId = ids[1];
bad["employee_count; DELETE FROM account; --"] = 50;

try
{
    await service.UpdateMultipleAsync(new List<ArxEntity> { good, bad });
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.SuspectedInjection)
{
    SecurityLog(error);
}
```

Check it in the database:

```sql
SELECT account_number, employee_count
FROM account
WHERE account_number IN ('ACC-I201', 'ACC-I202')
ORDER BY account_number;
```

Expect: 2 rows, both with employee_count 10: the good change wasn't stored either.

### Updates in one ARX transaction: commit, roll back, abort

Advanced case 1 of Update.

Updates inside an ARX transaction, in three scenarios: transfer credit between two accounts and commit; roll back when your own check fails; and an update the database refuses, which aborts the transaction. ARX types only: `ArxMsSqlConnection`, `ArxTransaction`.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var from = new ArxEntity("account");
from["name"] = "Transfer: parent";
from["account_number"] = "ACC-U1001";
from["credit_limit"] = 50_000.00m;
var to = new ArxEntity("account");
to["name"] = "Transfer: subsidiary";
to["account_number"] = "ACC-U1002";
to["credit_limit"] = 0.00m;
var ids = await service.CreateMultipleAsync(new List<ArxEntity> { from, to });
```

#### 1. Two updates, one commit

Move 10,000 of credit from the parent to the subsidiary: both updates or neither.

In scope: `service` (ArxMsSqlDbService), `connectionString` (string), `from` (ArxRecordId), `to` (ArxRecordId).

```csharp
await using (var connection = new ArxMsSqlConnection(connectionString))
await using (var transaction = await connection.BeginTransactionAsync())
{
    await service.UpdateAsync(Credit(from, 40_000.00m), transaction);
    await service.UpdateAsync(Credit(to, 10_000.00m), transaction);
    await transaction.CommitAsync();
}

Console.WriteLine("credit moved: 40,000 and 10,000");
```

Defined in the sample, used above:

```csharp
private static ArxEntity Credit(ArxRecordId id, decimal limit)
{
    var change = new ArxEntity("account");
    change.RecordId = id;
    change["credit_limit"] = limit;
    return change;
}
```

Check it in the database:

```sql
SELECT account_number, credit_limit
FROM account
WHERE account_number IN ('ACC-U1001', 'ACC-U1002')
ORDER BY account_number;
```

Expect: ACC-U1001 40000.00, ACC-U1002 10000.00.

#### 2. Roll back when your own check fails

Your own rule says no (a limit above 45,000 needs approval): roll back, and neither update is kept.

In scope: `service` (ArxMsSqlDbService), `connectionString` (string), `from` (ArxRecordId), `to` (ArxRecordId).

```csharp
await using (var connection = new ArxMsSqlConnection(connectionString))
await using (var transaction = await connection.BeginTransactionAsync())
{
    await service.UpdateAsync(Credit(from, 0.00m), transaction);
    var newLimit = 50_000.00m;
    await service.UpdateAsync(Credit(to, newLimit), transaction);

    if (newLimit > 45_000.00m)
    {
        await transaction.RollbackAsync();
        Console.WriteLine("limit needs approval: rolled back");
    }
}
```

Check it in the database:

```sql
SELECT account_number, credit_limit
FROM account
WHERE account_number IN ('ACC-U1001', 'ACC-U1002')
ORDER BY account_number;
```

Expect: unchanged since scenario 1: 40000.00 and 10000.00.

#### 3. A refused update aborts the transaction

A database error aborts the transaction: later calls and the commit are refused (ARX1015); only a rollback is possible.

In scope: `service` (ArxMsSqlDbService), `connectionString` (string), `from` (ArxRecordId).

```csharp
await using var connection = new ArxMsSqlConnection(connectionString);
await using var transaction = await connection.BeginTransactionAsync();

var negative = new ArxEntity("account");
negative.RecordId = from;
negative["employee_count"] = -1;

try
{
    await service.UpdateAsync(negative, transaction);
}
catch (ArxDatabaseException error)
{
    Console.WriteLine(error.Message);
}

try
{
    await transaction.CommitAsync();
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.TransactionAborted)
{
    Console.WriteLine(error.Message);
}
```

Check it in the database:

```sql
SELECT account_number, employee_count
FROM account
WHERE account_number = 'ACC-U1001';
```

Expect: 1 row, employee_count NULL.

### Create an order and update the stock, all or nothing

Advanced case 2 of Update.

Placing an order creates rows and updates others, all or nothing, in two scenarios: the order, its lines and the stock in one batch scope; and an order for a product that no longer exists, which keeps nothing.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var customer = new ArxEntity("account");
customer["name"] = "Order process customer";
customer["account_number"] = "ACC-U1101";
var customerId = await service.CreateAsync(customer);
var widget = new ArxEntity("product");
widget["sku"] = "SKU-U1101";
widget["name"] = "Widget";
widget["unit_price"] = 4.50m;
widget["stock_quantity"] = 100;
var widgetId = await service.CreateAsync(widget);
```

#### 1. Order, line and stock in one scope

The order, its line and the new stock level: one transaction, committed when the code returns.

In scope: `service` (ArxMsSqlDbService), `customerId` (ArxRecordId), `widgetId` (ArxRecordId).

```csharp
await service.RunBatchAsync(scope => PlaceOrderAsync(scope, customerId, "SO-U1101", widgetId, quantity: 3, stockLeft: 97));

Console.WriteLine("order SO-U1101 placed, stock 97");
```

Defined in the sample, used above:

```csharp
private static async Task PlaceOrderAsync(IArxBatchScope scope, ArxRecordId customerId, string orderNumber, ArxRecordId productId, int quantity, int stockLeft)
{
    var order = new ArxEntity("sales_order");
    order["order_number"] = orderNumber;
    order["account_id"] = new ArxEntityReference("account", customerId);
    order["ordered_on"] = DateTime.UtcNow;
    order["total"] = quantity * 4.50m;
    var orderId = await scope.CreateAsync(order);

    var line = new ArxEntity("order_line");
    line["order_id"] = new ArxEntityReference("sales_order", orderId);
    line["product_id"] = new ArxEntityReference("product", productId);
    line["quantity"] = quantity;
    line["unit_price"] = 4.50m;
    await scope.CreateAsync(line);

    var stock = new ArxEntity("product");
    stock.RecordId = productId;
    stock["stock_quantity"] = stockLeft;
    await scope.UpdateAsync(stock);
}
```

Check it in the database:

```sql
SELECT o.order_number, o.total, l.quantity, p.stock_quantity
FROM sales_order o
JOIN order_line l ON l.order_id = o.id
JOIN product p ON p.id = l.product_id
WHERE o.order_number = 'SO-U1101';
```

Expect: 1 row, total 13.50, quantity 3, stock_quantity 97.

#### 2. A product that no longer exists: nothing kept

The product was deleted meanwhile: its line refers to nothing, so nothing of the order is kept.

In scope: `service` (ArxMsSqlDbService), `customerId` (ArxRecordId).

```csharp
var gone = ArxRecordId.Of("id", 999_999_996L);

try
{
    await service.RunBatchAsync(scope => PlaceOrderAsync(scope, customerId, "SO-U1102", gone, quantity: 1, stockLeft: 0));
}
catch (ArxException error)
{
    Console.WriteLine(error.Message);
}
```

Check it in the database:

```sql
SELECT count(*)
FROM sales_order
WHERE order_number = 'SO-U1102';
```

Expect: 0: the order created before the failed line was rolled back with it.

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

Advanced case 3 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` (ArxMsSqlDbService), `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(ArxMsSqlDbService 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
SELECT account_number, employee_count
FROM account
WHERE account_number IN ('ACC-G401', 'ACC-G402')
ORDER BY account_number;
```

Expect: 2 rows: 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` (ArxMsSqlDbService), `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
SELECT count(*)
FROM account
WHERE account_number IN ('ACC-G401', 'ACC-G402', 'ACC-G403') AND 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` (ArxMsSqlDbService), `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(ArxMsSqlDbService 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
SELECT credit_limit, employee_count
FROM account
WHERE account_number = 'ACC-G403';
```

Expect: 1 row: credit_limit NULL, 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` (ArxMsSqlDbService), `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
SELECT website
FROM account
WHERE account_number = 'ACC-G403';
```

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

### Update on a preloaded schema: keys only, data types, Strict

Advanced case 4 of Update.

Update on a schema preloaded at service setup, in four scenarios: the convenient setup (keys only) updating by the business key the preload found; the typed setup refusing a value of the wrong type before the database; Strict as an allow list of the columns an endpoint may change; and the table a preload added because it is referenced, used by its own key.

Good to know:

The preload runs once, when the service is set up: the keys, lookups and column types are known before the first call, and no Update reads the database's catalog. Create Advanced6 shows the preload itself and its JSON.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "Preload Plumbing";
account["account_number"] = "ACC-PRE51";
account["website"] = "https://old.plumbing.example.com";
account["credit_limit"] = 5_000m;
await service.CreateAsync(account);
var contact = new ArxEntity("contact");
contact["first_name"] = "Uma";
contact["last_name"] = "Update";
contact["email"] = "uma.update@example.com";
contact["job_title"] = "Buyer";
await service.CreateAsync(contact);
```

#### 1. KeysOnly: update by the business key the preload found

Keys only: account_number is unique in the database, so the preload declared it a key.

In scope: `connectionString` (string).

```csharp
var tables = new ArxPreLoadTableRegister()
    .PreLoadTable("account");

var schema = await new ArxPreLoadTable(tables, ArxPreLoadMode.KeysOnly).LoadAsync(connectionString);
var service = new ArxMsSqlDbService(connectionString, schema);

schema.TryGetTable("account", out var table);
Console.WriteLine($"account keys: {string.Join(" ", table!.Keys)}");

var change = new ArxEntity("account");
change["account_number"] = "ACC-PRE51";
change["website"] = "https://plumbing.example.com";
var changed = await service.UpdateAsync(change, new ArxUpdateOptions { Match = ArxMatch.Key("account_number") });
Console.WriteLine($"updated {changed} row");
```

Check it in the database:

```sql
SELECT account_number, website
FROM account
WHERE account_number = 'ACC-PRE51';
```

Expect: 1 row, website https://plumbing.example.com (until scenario 3 changes it).

#### 2. DataTypes: a wrong type is refused before the database

Data types: employee_count is an int column, so "lots" never reaches the database.

In scope: `connectionString` (string).

```csharp
var tables = new ArxPreLoadTableRegister()
    .PreLoadTable("account", ["account_number", "employee_count", "founded_on"]);

var schema = await new ArxPreLoadTable(tables).LoadAsync(connectionString);
var service = new ArxMsSqlDbService(connectionString, schema);

var wrong = new ArxEntity("account");
wrong["account_number"] = "ACC-PRE51";
wrong["employee_count"] = "lots";

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

var right = new ArxEntity("account");
right["account_number"] = "ACC-PRE51";
right["employee_count"] = 40;
right["founded_on"] = new DateOnly(2001, 9, 15);
Console.WriteLine($"updated {await service.UpdateAsync(right, new ArxUpdateOptions { Match = ArxMatch.Key("account_number") })} row with the right types");
```

Check it in the database:

```sql
SELECT employee_count, founded_on
FROM account
WHERE account_number = 'ACC-PRE51';
```

Expect: 40, 2001-09-15.

#### 3. Strict: only the columns an endpoint may change

Strict: a profile endpoint may change the website, never the credit limit.

In scope: `connectionString` (string).

```csharp
var tables = new ArxPreLoadTableRegister()
    .PreLoadTable("account", ["account_number", "website"]);

var schema = await new ArxPreLoadTable(tables).LoadAsync(connectionString);
var profileEndpoint = new ArxMsSqlDbService(connectionString, schema) { Strict = true };

// A request that tries to raise its own credit limit along with its website.
var request = new ArxEntity("account");
request["account_number"] = "ACC-PRE51";
request["website"] = "https://www.plumbing.example.com";
request["credit_limit"] = 9_999_999m;

try
{
    await profileEndpoint.UpdateAsync(request, new ArxUpdateOptions { Match = ArxMatch.Key("account_number") });
}
catch (ArxValidationException exception) when (exception.Code == ArxErrorCode.NotPreloaded)
{
    Console.WriteLine(exception.Message);
}

var allowed = new ArxEntity("account");
allowed["account_number"] = "ACC-PRE51";
allowed["website"] = "https://www.plumbing.example.com";
Console.WriteLine($"the allowed change: updated {await profileEndpoint.UpdateAsync(allowed, new ArxUpdateOptions { Match = ArxMatch.Key("account_number") })} row");
```

Check it in the database:

```sql
SELECT website, credit_limit
FROM account
WHERE account_number = 'ACC-PRE51';
```

Expect: https://www.plumbing.example.com, 5000.00: the credit limit was never sent.

#### 4. The table the preload added, updated by its own key

The preload of account added contact (account.primary_contact_id references it), with contact's own keys.

In scope: `connectionString` (string).

```csharp
var tables = new ArxPreLoadTableRegister()
    .PreLoadTable("account");

var schema = await new ArxPreLoadTable(tables).LoadAsync(connectionString);
var service = new ArxMsSqlDbService(connectionString, schema);

schema.TryGetTable("contact", out var contact);
Console.WriteLine($"contact: {contact!.Reason}; keys {string.Join(" ", contact.Keys)}");

var change = new ArxEntity("contact");
change["email"] = "uma.update@example.com";
change["job_title"] = "Head of purchasing";
Console.WriteLine($"updated {await service.UpdateAsync(change, new ArxUpdateOptions { Match = ArxMatch.Key("email") })} row");
```

Check it in the database:

```sql
SELECT first_name, job_title
FROM contact
WHERE email = 'uma.update@example.com';
```

Expect: Uma, Head of purchasing.

---

# UpdateMultiple with ARX.SDK on SQL Server

## What an UpdateMultiple does

| | |
|---|---|
| **Many rows** | Rows with the same attributes and value types go in one statement (`UPDATE … FROM … JOIN` the new values), wherever they are in the list: 20 or more as one JSON parameter read by `OPENJSON`, smaller groups as a list of values. The checks run in the same round trip, and a failed one undoes the whole call |
| **Without a catalog** | The table is read once for the whole list, then every row is changed, all or nothing (Basic2) |
| **By a filter** | One entity with the values to set and an `ArxFilterExpression`: one `UPDATE … SET … FROM … WHERE …` for every row the filter matches, returning the count (Basic3). The entity carries no id (ARX1001); an empty filter needs `AllRows`; a version column goes up by one on each changed row. From 0.1.0-preview.5 |

## The samples

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

### Update many records in one call

Basics 1 of UpdateMultiple.

Many records in one call, in three scenarios: a different value for each row, rows that change different attributes, and all or nothing when one record doesn't exist.

Usings:

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

#### 1. A value per row

The usual way: one entity per record, each with its own value. The call returns the rows changed.

In scope: `connectionString` (string).

```csharp
var service = Service(connectionString);
var ids = await CreateAccountsAsync(service, 1, 5);

var changes = new List<ArxEntity>();

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

int changed = await service.UpdateMultipleAsync(changes);

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

Defined in the sample, used above:

```csharp
private static ArxMsSqlDbService Service(string connectionString) => new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id"))
    .Build());

private static async Task<IReadOnlyList<ArxRecordId>> CreateAccountsAsync(ArxMsSqlDbService service, int first, int count)
{
    var accounts = new List<ArxEntity>();

    for (var i = 0; i < count; i++)
    {
        var account = new ArxEntity("account");
        account["name"] = $"Basic update {first + i}";
        account["account_number"] = $"ACC-U2{first + i:00}";
        account["employee_count"] = 10;
        accounts.Add(account);
    }

    return await service.CreateMultipleAsync(accounts);
}
```

Check it in the database:

```sql
SELECT account_number, employee_count
FROM account
WHERE account_number BETWEEN 'ACC-U201' AND 'ACC-U205'
ORDER BY account_number;
```

Expect: 5 rows, employee_count 100, 101, 102, 103, 104.

#### 2. Different attributes

Rows don't have to change the same attributes: each one writes only what it sets.

In scope: `connectionString` (string).

```csharp
var service = Service(connectionString);
var ids = await CreateAccountsAsync(service, 11, 2);

var renamed = new ArxEntity("account");
renamed.RecordId = ids[0];
renamed["name"] = "Basic update 11 (renamed)";

var resized = new ArxEntity("account");
resized.RecordId = ids[1];
resized["employee_count"] = 999;

await service.UpdateMultipleAsync(new[] { renamed, resized });

Console.WriteLine("one renamed, one resized");
```

Check it in the database:

```sql
SELECT account_number, name, employee_count
FROM account
WHERE account_number IN ('ACC-U211', 'ACC-U212')
ORDER BY account_number;
```

Expect: ACC-U211 renamed with employee_count 10; ACC-U212 still named Basic update 12, employee_count 999.

#### 3. All or nothing

All or nothing: if one record doesn't exist, no record changes, and the error says how many were missing.

In scope: `connectionString` (string).

```csharp
var service = Service(connectionString);
var ids = await CreateAccountsAsync(service, 21, 3);

var changes = ids.Select(id =>
{
    var change = new ArxEntity("account");
    change.RecordId = id;
    change["employee_count"] = 7;
    return change;
}).ToList();

// An id no account has: maybe the account was deleted in the meantime.
var missing = new ArxEntity("account");
missing["id"] = 999_999_999L;
missing["employee_count"] = 7;
changes.Add(missing);

try
{
    await service.UpdateMultipleAsync(changes);
}
catch (ArxException error) when (error.Code == ArxErrorCode.RecordNotFound)
{
    Console.WriteLine($"{error.Message}");
}
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number BETWEEN 'ACC-U221' AND 'ACC-U223' AND employee_count = 7;
```

Expect: 0: the three accounts that exist were not changed either.

### Update many records without a catalog

Basics 2 of UpdateMultiple.

Many records in one call on a service created with only a connection string: the account table is read from the database once for the whole list, then every row is changed, all or nothing.

Good to know:

The extra round trip is paid once per call, not once per row; still, a service with a catalog never pays it.

Usings:

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

#### 1. A value per row

One entity per record, by the ids CreateMultiple returned.

In scope: `connectionString` (string).

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

var accounts = new List<ArxEntity>();
for (var i = 1; i <= 3; i++)
{
    var account = new ArxEntity("account");
    account["name"] = $"No catalog {i}";
    account["account_number"] = $"ACC-N03{i}";
    account["employee_count"] = 10;
    accounts.Add(account);
}

var ids = await service.CreateMultipleAsync(accounts);

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

int changed = await service.UpdateMultipleAsync(changes);

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

Check it in the database:

```sql
SELECT account_number, employee_count
FROM account
WHERE account_number IN ('ACC-N031', 'ACC-N032', 'ACC-N033')
ORDER BY account_number;
```

Expect: 3 rows, employee_count 20, 21, 22.

### Update every row a filter matches

Basics 3 of UpdateMultiple.

Every row a condition matches, changed in one call: the entity carries only the values to set, and a filter says which rows. The call returns how many rows it changed.

Good to know:

The filter takes the operators of a query, Or groups included, on the table's own columns; its values are parameters.

An empty filter is refused, so a missing condition never changes the whole table: pass new ArxFilterWriteOptions { AllRows = true } when every row is meant.

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

Usings:

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

#### 1. Every row the filter matches

Small accounts put on hold: one UPDATE for every row the filter matches, however many there are.

In scope: `connectionString` (string).

```csharp
var service = Service(connectionString);
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-F10{i}";
    account["employee_count"] = i * 5;
    accounts.Add(account);
}

await service.CreateMultipleAsync(accounts);

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

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

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

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

Defined in the sample, used above:

```csharp
private static ArxMsSqlDbService Service(string connectionString) => new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id"))
    .Build());
```

Check it in the database:

```sql
SELECT account_number, employee_count, status
FROM account
WHERE account_number LIKE 'ACC-F10%'
ORDER BY account_number;
```

Expect: 5 rows; ACC-F101 and ACC-F102 (5 and 10 employees) ON_HOLD, the other three ACTIVE.

#### 2. The values carry no id

The entity carries the values only: with an id as well as a filter, ARX can't tell which rows were meant.

In scope: `connectionString` (string).

```csharp
var service = Service(connectionString);

var account = new ArxEntity("account");
account["name"] = "Filter update with an id";
account["account_number"] = "ACC-F111";
var id = await service.CreateAsync(account);

var change = new ArxEntity("account");
change.RecordId = id;
change["status"] = "CLOSED";

var filter = new ArxFilterExpression();
filter.AddCondition("account_number", ArxConditionOperator.Equal, "ACC-F111");

try
{
    await service.UpdateMultipleAsync(change, filter);
}
catch (ArxValidationException error)
{
    Console.WriteLine($"Refused: {error.Code}. Nothing was changed.");
}
```

Check it in the database:

```sql
SELECT account_number, status
FROM account
WHERE account_number = 'ACC-F111';
```

Expect: 1 row, status ACTIVE.

### Put accounts on hold (selection set, by key)

Real case 1 of UpdateMultiple.

Credit control puts accounts on hold, in three scenarios: one account by its account number (the declared key); a value the status picklist doesn't allow is refused before the database; every account of a segment at once.

Usings:

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

Set up by the sample before its scenarios:

```csharp
foreach (var (number, segment) in new[] { ("ACC-U701", "smb"), ("ACC-U702", "smb"), ("ACC-U703", "smb") })
{
    var account = new ArxEntity("account");
    account["name"] = "On hold customer " + number;
    account["account_number"] = number;
    account["segments"] = segment;
    await service.CreateAsync(account);
}
```

#### 1. One account, by its account number

The credit team knows the account number, not the id: find the row by the declared key.

In scope: `service` (ArxMsSqlDbService).

```csharp
var hold = new ArxEntity("account");
hold["account_number"] = "ACC-U701";
hold["status"] = new ArxSelectionSetValue("AccountStatus", "ON_HOLD");

await service.UpdateAsync(hold, new ArxUpdateOptions { Match = ArxMatch.Key("account_number"), RequireSingleRow = true });
Console.WriteLine("ACC-U701 is ON_HOLD");
```

Check it in the database:

```sql
SELECT account_number, status
FROM account
WHERE account_number = 'ACC-U701';
```

Expect: 1 row, ON_HOLD.

#### 2. A value the picklist doesn't allow

"SUSPENDED" isn't an AccountStatus option: refused by the catalog, nothing sent.

In scope: `service` (ArxMsSqlDbService).

```csharp
var hold = new ArxEntity("account");
hold["account_number"] = "ACC-U702";
hold["status"] = new ArxSelectionSetValue("AccountStatus", "SUSPENDED");

try
{
    await service.UpdateAsync(hold, new ArxUpdateOptions { Match = ArxMatch.Key("account_number") });
}
catch (ArxValidationException error)
{
    Console.WriteLine(error.Message);
}
```

Check it in the database:

```sql
SELECT account_number, status
FROM account
WHERE account_number = 'ACC-U702';
```

Expect: 1 row, ACTIVE (the default): unchanged.

#### 3. Several accounts in one call

Several accounts by their numbers in one call: each found by its key, all or nothing.

In scope: `service` (ArxMsSqlDbService).

```csharp
var holds = new[] { "ACC-U701", "ACC-U703" }.Select(number =>
{
    var hold = new ArxEntity("account");
    hold["account_number"] = number;
    hold["status"] = new ArxSelectionSetValue("AccountStatus", "ON_HOLD");
    return hold;
}).ToList();

var changed = await service.UpdateMultipleAsync(holds, new ArxUpdateOptions { Match = ArxMatch.Key("account_number") });
Console.WriteLine($"{changed} accounts on hold");
```

Check it in the database:

```sql
SELECT account_number, status
FROM account
WHERE account_number IN ('ACC-U701', 'ACC-U702', 'ACC-U703')
ORDER BY account_number;
```

Expect: ACC-U701 ON_HOLD, ACC-U702 ACTIVE (refused in scenario 2), ACC-U703 ON_HOLD.

### Reprice 20,000 products from a feed

Real case 2 of UpdateMultiple.

A supplier's price feed reprices the catalogue, in two scenarios: 20,000 products by SKU in one call (one set-based statement), and a feed with a SKU the catalogue doesn't have, which changes nothing.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var products = Enumerable.Range(1, PRODUCTS).Select(i =>
{
    var product = new ArxEntity("product");
    product["sku"] = $"SKU-U8{i:000000}";
    product["name"] = $"Feed product {i}";
    product["unit_price"] = 10.00m;
    return product;
}).ToList();
await service.CreateMultipleAsync(products);
```

#### 1. The whole feed, by SKU

Every line of the feed is one entity, found by its SKU (the declared key). All of them go in one statement.

In scope: `service` (ArxMsSqlDbService).

```csharp
var feed = Enumerable.Range(1, PRODUCTS).Select(i => (Sku: $"SKU-U8{i:000000}", Price: 10.00m + i % 100 / 100m));

var prices = feed.Select(line =>
{
    var price = new ArxEntity("product");
    price["sku"] = line.Sku;
    price["unit_price"] = line.Price;
    return price;
}).ToList();

var started = DateTime.UtcNow;
var changed = await service.UpdateMultipleAsync(prices, new ArxUpdateOptions { Match = ArxMatch.Key("sku") });
Console.WriteLine($"{changed} prices changed in {(DateTime.UtcNow - started).TotalMilliseconds:0} ms");
```

Check it in the database:

```sql
SELECT count(*) AS products, min(unit_price), max(unit_price)
FROM product
WHERE sku LIKE 'SKU-U8%';
```

Expect: 20000, 10.00, 10.99.

#### 2. A feed with an unknown SKU

One SKU in the feed isn't in the catalogue: the call refuses the whole feed, so no price is half-applied.

In scope: `service` (ArxMsSqlDbService).

```csharp
var prices = new[] { ("SKU-U8000001", 99.00m), ("SKU-U8000002", 99.00m), ("SKU-NOT-IN-CATALOG", 99.00m) }.Select(line =>
{
    var price = new ArxEntity("product");
    price["sku"] = line.Item1;
    price["unit_price"] = line.Item2;
    return price;
}).ToList();

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

Check it in the database:

```sql
SELECT count(*)
FROM product
WHERE sku LIKE 'SKU-U8%' AND unit_price = 99.00;
```

Expect: 0: the two known SKUs weren't repriced either.

### Stock counts in batches: one transaction, a failed batch skipped

Advanced case 1 of UpdateMultiple.

A stock count file updates products in batches, in two scenarios: every batch in one transaction with automatic commit; and a batch that fails, skipped while the others are kept (savepoints, manual commit).

Usings:

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

Set up by the sample before its scenarios:

```csharp
foreach (var warehouse in new[] { "A", "B" })
{
    var products = Enumerable.Range(1, PRODUCTS).Select(i =>
    {
        var product = new ArxEntity("product");
        product["sku"] = $"SKU-U9{warehouse}{i:00000}";
        product["name"] = $"Counted product {warehouse}{i}";
        product["unit_price"] = 5.00m;
        return product;
    }).ToList();
    await service.CreateMultipleAsync(products);
}
```

#### 1. Four batches, one transaction

RunBatchAsync: four batches of 500, one commit when the code returns.

In scope: `service` (ArxMsSqlDbService).

```csharp
var options = new ArxUpdateOptions { Match = ArxMatch.Key("sku") };

await service.RunBatchAsync(async scope =>
{
    foreach (var batch in ReadStockFile("A", count: 12))
    {
        await scope.UpdateMultipleAsync(batch, options);
    }

    Console.WriteLine($"{scope.RowsWritten} products counted in {scope.BatchCount} batches");
});
```

Defined in the sample, used above:

```csharp
private static IEnumerable<List<ArxEntity>> ReadStockFile(string warehouse, int count, int? brokenBatch = null)
{
    for (var start = 1; start <= PRODUCTS; start += BATCH)
    {
        var batch = Enumerable.Range(start, BATCH).Select(i =>
        {
            var line = new ArxEntity("product");
            line["sku"] = $"SKU-U9{warehouse}{i:00000}";
            line["stock_quantity"] = count;
            return line;
        }).ToList();

        if ((start - 1) / BATCH == brokenBatch)
        {
            batch[3]["sku"] = "SKU-NOT-COUNTED";
        }

        yield return batch;
    }
}
```

Check it in the database:

```sql
SELECT count(*)
FROM product
WHERE sku LIKE 'SKU-U9A%' AND stock_quantity = 12;
```

Expect: 2000.

#### 2. A failed batch is skipped, the rest committed

Batch 3 has a SKU the catalogue doesn't know: that batch is undone (its savepoint), the others are kept.

In scope: `service` (ArxMsSqlDbService).

```csharp
var options = new ArxUpdateOptions { Match = ArxMatch.Key("sku") };

await using (var scope = await service.BeginBatchAsync(new ArxBatchOptions { Savepoints = true }))
{
    foreach (var batch in ReadStockFile("B", count: 30, brokenBatch: 2))
    {
        try
        {
            await scope.UpdateMultipleAsync(batch, options);
        }
        catch (ArxException error) when (error.Code == ArxErrorCode.RecordNotFound)
        {
            Console.WriteLine($"batch {error.BatchNumber} skipped: {error.Message}");
        }
    }

    await scope.CommitAsync();
    Console.WriteLine($"{scope.RowsWritten} products counted; failed batches: {string.Join(", ", scope.FailedBatches)}");
}
```

Check it in the database:

```sql
SELECT stock_quantity, count(*)
FROM product
WHERE sku LIKE 'SKU-U9B%'
GROUP BY stock_quantity
ORDER BY stock_quantity;
```

Expect: 0 for 500 products (batch 3, skipped: their stock stays at the default 0), 30 for 1500.

---

# Delete with ARX.SDK on SQL Server

## What a delete does

| | |
|---|---|
| **By id** | `DeleteAsync("account", id)`: the id CreateAsync returned, or a `long`, `Guid` or `string` of the declared id type. Returns `true` when a record went (an `INSTEAD OF` trigger that skips the row still counts it: SQL Server reports the rows the statement was for) |
| **By key or columns** | `DeleteAsync(entity, new ArxDeleteOptions { Match = ArxMatch.Key(...) })`: the entity carries the values to match, other attributes are ignored (so an entity you created or read can be passed as it is). Returns the count; by columns it may be many. Text is matched as SQL Server compares it (with the default collation, case-insensitively). Many entities go in one statement (`DELETE … FROM … JOIN` the values), 20 or more as one JSON parameter |
| **Nothing to delete** | Not an error: `false` or `0`. A retry, or two users deleting at once, is a normal outcome |
| **Strict** | `RequireSingleRow = true`: exactly one row per entity, else ARX2002 or ARX2003, and nothing is deleted |
| **A referenced row** | `ARX3002 ForeignKeyViolation`, naming the table that still references it. Cascading is the schema's choice (`ON DELETE CASCADE`), and the count covers only the table you deleted from |
| **Soft delete** | Not built in: it's an update of a status column (RealCase4) |
| **Without a catalog** | By the id, the table's primary key is read from the database first (one more round trip per call, so much slower than with a catalog: meant for simple CRUD and small applications); `ARX1019` when it has none ARX can use. By a key or columns, any table (Basic4) |
| **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`. |

## Reference

### Error codes a delete can raise

| Code | When |
|---|---|
| ARX1001 InvalidArgument | An id of the wrong type or one that can't exist (0, negative for a generated id, empty Guid or text, `ArxRecordId.None`), a NoIdColumn table deleted by id, a missing or NULL match value, a key the table doesn't declare, duplicates in a strict call |
| ARX1002, ARX1003 | Missing or invalid table or column name |
| ARX1017 SuspectedInjection | A table or match column name carrying SQL (a quote, `;`, a comment, `=`, an attack word after a space); nothing sent to the database (RealCase5) |
| ARX1016 IdNotDeclared | Deleting by id or key from a table nobody declared |
| ARX2002, ARX2003 | Strict calls only: an entity matched nothing, or several rows (nothing deleted) |
| ARX1008 InvalidConnection | A connection with an open transaction, given without that transaction |
| ARX3002 ForeignKeyViolation | The row is still referenced (`NO ACTION`; SQL Server has no deferred constraints, so it fails at the statement, never at commit) |
| ARX5001, ARX6001, ARX7003, ARX8001 | Missing table, no permission (or a row-level security block predicate), lock or command timeout, a view SQL Server can't delete from (several base tables, GROUP BY or aggregates) |

## The samples

Every Delete sample, in reading order. Each scenario shows the code (verified against SQL Server) and the SQL that checks what it did. `service` is the application's `ArxMsSqlDbService`; 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-sql-sample-application.md.

### Delete one record

Basics 1 of Delete.

The smallest delete: one record, in three scenarios. The usual way first (the id CreateAsync returned), then a text id, then the entity itself.

Usings:

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

#### 1. By the id Create returned

The usual way: the table and the id. True when a record was deleted.

In scope: `connectionString` (string).

```csharp
var service = Service(connectionString);

var account = new ArxEntity("account");
account["name"] = "Temporary account";
account["account_number"] = "ACC-D101";
ArxRecordId id = await service.CreateAsync(account);

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

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

Defined in the sample, used above:

```csharp
private static ArxMsSqlDbService Service(string connectionString) => new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id"))
    .Table("country", ArxIdColumn.Supplied<string>("code"))
    .Build());
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number = 'ACC-D101';
```

Expect: 0.

#### 2. A text id

A text id: delete a country by its code.

In scope: `connectionString` (string).

```csharp
var service = Service(connectionString);

var finland = new ArxEntity("country");
finland["code"] = "FI";
finland["name"] = "Finland";
await service.CreateAsync(finland);

bool deleted = await service.DeleteAsync("country", "FI");

Console.WriteLine($"country FI deleted = {deleted}");
```

Check it in the database:

```sql
SELECT count(*)
FROM country
WHERE code = 'FI';
```

Expect: 0.

#### 3. The entity itself

The entity itself: an entity you created (or read) carries its id, so you can pass it back to delete it. The row is found by the table's declared id only ("id" in the schema above), never by the other attributes.

In scope: `connectionString` (string).

```csharp
var service = Service(connectionString);

var account = new ArxEntity("account");
account["name"] = "Another temporary account";
account["account_number"] = "ACC-D103";
await service.CreateAsync(account); // sets account.RecordId to the new row's id

// Finds the row by account.RecordId: DELETE FROM account WHERE id = <that id>.
// "name" and "account_number" are not part of the match and are ignored: no other account is deleted because
// it has the same name or number, and account_number is not a key (it isn't declared as one in the schema).
// To delete by account_number instead, ask for it: new ArxDeleteOptions { Match = ArxMatch.Columns("account_number") }
// (see Basic3_HowToFindTheRows on this page).
int deleted = await service.DeleteAsync(account);

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

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number = 'ACC-D103';
```

Expect: 0 (the account created above is gone). account_number is used here only to find the row by hand, since you don't know its generated id; the delete itself matched on id.

### Nothing to delete is not an error

Basics 2 of Delete.

Deleting something that is already gone is a normal outcome, not an error (a retry, or two users at once). Two scenarios: one record (false), and a list (0).

Usings:

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

Set up by the sample before its scenarios:

```csharp
var service = new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id"))
    .Build());
```

#### 1. One record

Delete twice: the second call returns false, so a retry is safe.

In scope: `service` (ArxMsSqlDbService).

```csharp
var account = new ArxEntity("account");
account["name"] = "Deleted twice";
account["account_number"] = "ACC-D301";
var id = await service.CreateAsync(account);

var first = await service.DeleteAsync("account", id);
var second = await service.DeleteAsync("account", id);

Console.WriteLine($"first {first}, then {second}");
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number = 'ACC-D301';
```

Expect: 0.

#### 2. A list

A list of ids none of which exists: 0. An empty list is 0 too, without touching the database.

In scope: `service` (ArxMsSqlDbService).

```csharp
var none = await service.DeleteMultipleAsync("account", new List<long> { 999_999_994, 999_999_993 });
var empty = await service.DeleteMultipleAsync("account", new List<long>());

Console.WriteLine($"{none} deleted, empty list {empty}");
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE id IN (999999994, 999999993);
```

Expect: 0.

### Delete by key, by compound key, by columns

Basics 3 of Delete.

How a delete finds its rows when you don't have the id (ArxMatch), in four scenarios: by a declared key, by a compound key on a table with no id, by columns on a table with no id and no key, and strict: exactly one row or nothing.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var service = new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id")).Key("account", "account_number")
    .Table("country", ArxIdColumn.Supplied<string>("code"))
    .Table("price_list", ArxIdColumn.NoIdColumn).Key("price_list", "region", "sku")
    .Table("activity_log", ArxIdColumn.NoIdColumn)
    .Build());
```

#### 1. By a key

By a declared key: the account number.

In scope: `service` (ArxMsSqlDbService).

```csharp
var account = new ArxEntity("account");
account["name"] = "Found by number";
account["account_number"] = "ACC-D401";
await service.CreateAsync(account);

var byNumber = new ArxEntity("account");
byNumber["account_number"] = "ACC-D401";

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

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

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number = 'ACC-D401';
```

Expect: 0.

#### 2. By a compound key

A price has no id: region and SKU together find it.

In scope: `service` (ArxMsSqlDbService).

```csharp
var poland = new ArxEntity("country");
poland["code"] = "PL";
poland["name"] = "Poland";
await service.CreateAsync(poland);

foreach (var sku in new[] { "SKU-D401", "SKU-D402" })
{
    var price = new ArxEntity("price_list");
    price["region"] = "PL";
    price["sku"] = sku;
    price["unit_price"] = 3.00m;
    price["valid_from"] = new DateOnly(2026, 1, 1);
    await service.CreateAsync(price);
}

var one = new ArxEntity("price_list");
one["region"] = "PL";
one["sku"] = "SKU-D401";

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

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

Check it in the database:

```sql
SELECT sku
FROM price_list
WHERE region = 'PL';
```

Expect: 1 row, SKU-D402.

#### 3. By columns

The activity log has no id and no key: delete by any columns. Every matching row goes.

In scope: `service` (ArxMsSqlDbService).

```csharp
foreach (var action in new[] { "test login", "test login", "test logout" })
{
    var entry = new ArxEntity("activity_log");
    entry["action"] = action;
    await service.CreateAsync(entry);
}

var logins = new ArxEntity("activity_log");
logins["action"] = "test login";

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

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

Check it in the database:

```sql
SELECT action, count(*)
FROM activity_log
WHERE action LIKE 'test log%'
GROUP BY action;
```

Expect: 1 row, test logout, 1.

#### 4. Strict

Strict: RequireSingleRow refuses a delete that would remove more than one row, and deletes nothing.

In scope: `service` (ArxMsSqlDbService).

```csharp
foreach (var action in new[] { "strict delete", "strict delete" })
{
    var entry = new ArxEntity("activity_log");
    entry["action"] = action;
    await service.CreateAsync(entry);
}

var entries = new ArxEntity("activity_log");
entries["action"] = "strict delete";

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

Check it in the database:

```sql
SELECT count(*)
FROM activity_log
WHERE action = 'strict delete';
```

Expect: 2: nothing was deleted.

### Delete without a catalog

Basics 4 of Delete.

Deletes on a service created with only a connection string, in three scenarios: by the id, read from the table's primary key; a table with no primary key, by columns; and the same table by an id, refused with ARX1019 before anything is deleted.

Good to know:

Every call reads its table from the database first, which makes it much slower than a service with a catalog (Create Basic6 says when that is fine).

Usings:

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

Set up by the sample before its scenarios:

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

#### 1. By the id

The usual way: the table and the id Create returned.

In scope: `service` (ArxMsSqlDbService).

```csharp
var account = new ArxEntity("account");
account["name"] = "Woodgrove Bank";
account["account_number"] = "ACC-N041";
ArxRecordId id = await service.CreateAsync(account);

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

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

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number = 'ACC-N041';
```

Expect: 0.

#### 2. A table with no primary key, by columns

activity_log has no primary key: find the rows by a column, and every matching row goes.

In scope: `service` (ArxMsSqlDbService).

```csharp
for (var i = 0; i < 2; i++)
{
    var entry = new ArxEntity("activity_log") { IdColumn = ArxIdColumn.NoIdColumn };
    entry["action"] = "opened the trial";
    await service.CreateAsync(entry);
}

var match = new ArxEntity("activity_log");
match["action"] = "opened the trial";

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

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

Check it in the database:

```sql
SELECT count(*)
FROM activity_log
WHERE action = 'opened the trial';
```

Expect: 0.

#### 3. A table with no primary key, by an id

By an id on that table: there is no id column to find it by, so ARX1019, and nothing is deleted.

In scope: `service` (ArxMsSqlDbService).

```csharp
try
{
    await service.DeleteAsync("activity_log", 1L);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.TableNotResolvable)
{
    Console.WriteLine($"refused: {error.Message}");
}
```

Check it in the database:

```sql
SELECT count(*)
FROM activity_log;
```

Expect: the same count as before the call.

### Remove a contact by id or by email

Real case 1 of Delete.

Removing contacts, in two scenarios: from the contact screen (by id), and an "unsubscribe and forget me" request that only gives the email address (the declared key).

Usings:

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

Set up by the sample before its scenarios:

```csharp
var first = new ArxEntity("contact");
first["first_name"] = "Rosa";
first["last_name"] = "Delgado";
first["email"] = "rosa.delgado@example.com";
var second = new ArxEntity("contact");
second["first_name"] = "Omar";
second["last_name"] = "Haddad";
second["email"] = "omar.haddad@example.com";
var ids = await service.CreateMultipleAsync(new List<ArxEntity> { first, second });
```

#### 1. From the contact screen, by id

The screen has the contact's id.

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

```csharp
var deleted = await service.DeleteAsync("contact", id);
Console.WriteLine($"deleted: {deleted}");
```

Check it in the database:

```sql
SELECT count(*)
FROM contact
WHERE email = 'rosa.delgado@example.com';
```

Expect: 0.

#### 2. A forget-me request, by email

A "forget me" request has only the email: delete by the key, strictly one row.

In scope: `service` (ArxMsSqlDbService).

```csharp
var request = new ArxEntity("contact");
request["email"] = "omar.haddad@example.com";

var deleted = await service.DeleteAsync(request, new ArxDeleteOptions { Match = ArxMatch.Key("email"), RequireSingleRow = true });
Console.WriteLine($"{deleted} contact forgotten");
```

Check it in the database:

```sql
SELECT count(*)
FROM contact
WHERE email = 'omar.haddad@example.com';
```

Expect: 0.

### Undo an import of 5,000 contacts

Real case 2 of Delete.

A contact import went wrong and must be undone, in two scenarios: all 5,000 imported contacts by their lead source in one call; and the same call again, which finds nothing and says so with 0.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var imported = Enumerable.Range(1, 5_000).Select(i =>
{
    var contact = new ArxEntity("contact");
    contact["first_name"] = "Imported";
    contact["last_name"] = $"Row {i}";
    contact["email"] = $"bad-import-{i}@example.com";
    contact["lead_source"] = "Import 2026-11-08 (bad)";
    return contact;
}).ToList();
await service.CreateMultipleAsync(imported);
```

#### 1. Every contact of the bad import

The import tagged every row with its lead source: delete by that column. One statement.

In scope: `service` (ArxMsSqlDbService).

```csharp
var deleted = await service.DeleteAsync(TheBadImport(), new ArxDeleteOptions { Match = ArxMatch.Columns("lead_source") });
Console.WriteLine($"{deleted} contacts deleted");
```

Defined in the sample, used above:

```csharp
private static ArxEntity TheBadImport()
{
    var match = new ArxEntity("contact");
    match["lead_source"] = "Import 2026-11-08 (bad)";
    return match;
}
```

Check it in the database:

```sql
SELECT count(*)
FROM contact
WHERE lead_source = 'Import 2026-11-08 (bad)';
```

Expect: 0.

#### 2. Run the clean-up again

Running the clean-up again is safe: nothing matches, 0 deleted, no error.

In scope: `service` (ArxMsSqlDbService).

```csharp
var deleted = await service.DeleteAsync(TheBadImport(), new ArxDeleteOptions { Match = ArxMatch.Columns("lead_source") });
Console.WriteLine($"{deleted} deleted the second time");
```

Check it in the database:

```sql
SELECT count(*)
FROM contact
WHERE email LIKE 'bad-import-%';
```

Expect: 0.

### A referenced record: ARX3002, then delete in order

Real case 3 of Delete.

An account that contacts still point at can't be deleted, in two scenarios: the database refuses it (ARX3002, with the constraint's name); delete the contacts first, by their account, then the account.

Usings:

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

#### 1. Delete the account: refused

The account still has contacts: the foreign key refuses the delete, and names itself.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
try
{
    await service.DeleteAsync("account", accountId);
}
catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.ForeignKeyViolation)
{
    Console.WriteLine($"{error.Code}, constraint {error.ConstraintName}");
}
```

Check it in the database:

```sql
SELECT (SELECT count(*) FROM account WHERE account_number = 'ACC-D501') AS accounts,
       (SELECT count(*) FROM contact WHERE last_name = 'Kept') AS contacts;
```

Expect: 1, 2: still there.

#### 2. Contacts first, then the account

Delete the contacts by their account (a column match), then the account, in one batch scope: all or nothing.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
await service.RunBatchAsync(async scope =>
{
    var ofTheAccount = new ArxEntity("contact");
    ofTheAccount["account_id"] = new ArxEntityReference("account", accountId);

    var contacts = await scope.DeleteAsync(ofTheAccount, new ArxDeleteOptions { Match = ArxMatch.Columns("account_id") });
    var account = await scope.DeleteAsync("account", accountId);

    Console.WriteLine($"{contacts} contacts and {(account ? 1 : 0)} account deleted");
});
```

Check it in the database:

```sql
SELECT (SELECT count(*) FROM account WHERE account_number = 'ACC-D502') AS accounts,
       (SELECT count(*) FROM contact WHERE last_name = 'Closing') AS contacts;
```

Expect: 0, 0.

### Soft delete: close and restore an account

Real case 4 of Delete.

A soft delete keeps the row and marks it closed: it's an update of a status column, not a delete. Two scenarios: close an account; restore it.

Usings:

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

Set up by the sample before its scenarios:

```csharp
foreach (var number in new[] { "ACC-D601", "ACC-D602" })
{
    var account = new ArxEntity("account");
    account["name"] = "Soft deleted customer " + number;
    account["account_number"] = number;
    await service.CreateAsync(account);
}
```

#### 1. Close the account instead of deleting it

"Delete" in the app closes the account: an update of its status, by the account number.

In scope: `service` (ArxMsSqlDbService).

```csharp
await service.UpdateAsync(Status("ACC-D601", "CLOSED"), new ArxUpdateOptions { Match = ArxMatch.Key("account_number") });
Console.WriteLine("ACC-D601 closed; its history stays");
```

Defined in the sample, used above:

```csharp
private static ArxEntity Status(string number, string status)
{
    var change = new ArxEntity("account");
    change["account_number"] = number;
    change["status"] = new ArxSelectionSetValue("AccountStatus", status);
    return change;
}
```

Check it in the database:

```sql
SELECT account_number, status
FROM account
WHERE account_number = 'ACC-D601';
```

Expect: 1 row, CLOSED.

#### 2. Restore it

Undo is another update.

In scope: `service` (ArxMsSqlDbService).

```csharp
var byNumber = new ArxUpdateOptions { Match = ArxMatch.Key("account_number") };
await service.UpdateAsync(Status("ACC-D602", "CLOSED"), byNumber);
await service.UpdateAsync(Status("ACC-D602", "ACTIVE"), byNumber);
Console.WriteLine("ACC-D602 closed, then active again");
```

Check it in the database:

```sql
SELECT account_number, status
FROM account
WHERE account_number = 'ACC-D602';
```

Expect: 1 row, ACTIVE.

### Catch an SQL injection attempt on delete

Real case 5 of Delete.

Catching an SQL injection attempt on delete, in four scenarios. A table or match column that carries SQL is refused with ARX1017 SuspectedInjection before the database. SQL in the value to delete by, the classic `' OR '1'='1` that would empty a table when concatenated, is a parameter: it deletes nothing.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var accounts = new List<ArxEntity>();
foreach (var number in new[] { "ACC-I301", "ACC-I302", "ACC-I303" })
{
    var account = new ArxEntity("account");
    account["name"] = "Injection: keep me";
    account["account_number"] = number;
    accounts.Add(account);
}
var ids = await service.CreateMultipleAsync(accounts);
```

#### 1. A table name from the request carries SQL

`DELETE /api/{type}/{id}` with the type used as the table: the SQL in it is refused before the database.

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

```csharp
var type = "account WHERE 1=1; --";

try
{
    await service.DeleteAsync(type, id);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.SuspectedInjection)
{
    SecurityLog(error);
}
```

Defined in the sample, used above:

```csharp
private static void SecurityLog(ArxValidationException error) => Console.WriteLine($"SECURITY: {error.Message}");
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number IN ('ACC-I301', 'ACC-I302', 'ACC-I303');
```

Expect: 3: nothing was deleted.

#### 2. SQL in the value to delete by deletes nothing

The classic: "delete the account with this number", and the user types `ACC-I301' OR '1'='1`. Concatenated into SQL it would delete every account; as a parameter it is an account number nobody has.

In scope: `service` (ArxMsSqlDbService).

```csharp
var target = new ArxEntity("account");
target["account_number"] = "ACC-I301' OR '1'='1";

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

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

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number IN ('ACC-I301', 'ACC-I302', 'ACC-I303');
```

Expect: 3: the text was compared as a value, so it matched no account.

#### 3. The column to delete by, from the request, carries SQL

The column to delete by comes from the request: the SQL in it is refused; only allow columns from a fixed list.

In scope: `service` (ArxMsSqlDbService).

```csharp
var by = "name = name OR account_number";

var target = new ArxEntity("account");
target[by] = "x";

try
{
    await service.DeleteAsync(target, new ArxDeleteOptions { Match = ArxMatch.Columns(by) });
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.SuspectedInjection)
{
    SecurityLog(error);
}
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE name = 'Injection: keep me';
```

Expect: 3.

#### 4. DeleteMultiple with a table name carrying SQL

DeleteMultiple of the three ids with a table name carrying SQL: refused before the database, all three kept.

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

```csharp
try
{
    await service.DeleteMultipleAsync("account\"; TRUNCATE account; --", ids.ToList());
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.SuspectedInjection)
{
    SecurityLog(error);
}
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE name = 'Injection: keep me';
```

Expect: 3: every account is still there.

### Replace an order's lines: delete, create, update, all or nothing

Advanced case 1 of Delete.

A customer changes an order: its old lines are deleted, the new lines created and the total updated, in one batch scope. Two scenarios: the replacement succeeds; a new line is refused, and the order is left as it was.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var customer = new ArxEntity("account");
customer["name"] = "Replace order customer";
customer["account_number"] = "ACC-D801";
var customerId = await service.CreateAsync(customer);
var product = new ArxEntity("product");
product["sku"] = "SKU-D801";
product["name"] = "Replaceable widget";
product["unit_price"] = 2.00m;
var productId = await service.CreateAsync(product);
var order = new ArxEntity("sales_order");
order["order_number"] = "SO-D801";
order["account_id"] = new ArxEntityReference("account", customerId);
order["ordered_on"] = DateTime.UtcNow;
order["total"] = 4.00m;
var orderId = await service.CreateAsync(order);
var line = new ArxEntity("order_line");
line["order_id"] = new ArxEntityReference("sales_order", orderId);
line["product_id"] = new ArxEntityReference("product", productId);
line["quantity"] = 2;
line["unit_price"] = 2.00m;
await service.CreateAsync(line);
```

#### 1. Replace the lines

Old line out, two new lines in, total updated: committed together.

In scope: `service` (ArxMsSqlDbService), `orderId` (ArxRecordId), `productId` (ArxRecordId).

```csharp
await service.RunBatchAsync(scope => ReplaceAsync(scope, orderId, productId, 1, 4));
Console.WriteLine("SO-D801 now has two lines, total 10.00");
```

Defined in the sample, used above:

```csharp
private static async Task ReplaceAsync(IArxBatchScope scope, ArxRecordId orderId, ArxRecordId productId, params int[] quantities)
{
    var ofTheOrder = new ArxEntity("order_line");
    ofTheOrder["order_id"] = new ArxEntityReference("sales_order", orderId);
    await scope.DeleteAsync(ofTheOrder, new ArxDeleteOptions { Match = ArxMatch.Columns("order_id") });

    var lines = quantities.Select(quantity =>
    {
        var line = new ArxEntity("order_line");
        line["order_id"] = new ArxEntityReference("sales_order", orderId);
        line["product_id"] = new ArxEntityReference("product", productId);
        line["quantity"] = quantity;
        line["unit_price"] = 2.00m;
        return line;
    }).ToList();
    await scope.CreateMultipleAsync(lines);

    var total = new ArxEntity("sales_order");
    total.RecordId = orderId;
    total["total"] = quantities.Sum() * 2.00m;
    await scope.UpdateAsync(total);
}
```

Check it in the database:

```sql
SELECT o.total, count(l.id) AS lines, sum(l.quantity) AS quantity
FROM sales_order o
JOIN order_line l ON l.order_id = o.id
WHERE o.order_number = 'SO-D801'
GROUP BY o.total;
```

Expect: 1 row, total 10.00, 2 lines, quantity 5.

#### 2. A refused line: nothing changes

A quantity of 0 is refused by the database: the delete before it is undone too.

In scope: `service` (ArxMsSqlDbService), `orderId` (ArxRecordId), `productId` (ArxRecordId).

```csharp
try
{
    await service.RunBatchAsync(scope => ReplaceAsync(scope, orderId, productId, 3, 0));
}
catch (ArxDatabaseException error)
{
    Console.WriteLine(error.Message);
}
```

Check it in the database:

```sql
SELECT o.total, count(l.id) AS lines
FROM sales_order o
JOIN order_line l ON l.order_id = o.id
WHERE o.order_number = 'SO-D801'
GROUP BY o.total;
```

Expect: 1 row, total 10.00, 2 lines: as after scenario 1.

### 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` (ArxMsSqlDbService), `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(ArxMsSqlDbService 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
SELECT count(*)
FROM account
WHERE 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` (ArxMsSqlDbService), `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(ArxMsSqlDbService 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
SELECT count(*)
FROM account
WHERE 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` (ArxMsSqlDbService), `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(ArxMsSqlDbService 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(ArxMsSqlDbService 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
SELECT account_number
FROM account
WHERE account_number IN ('ACC-G303', 'ACC-G304');
```

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

### Delete on a preloaded schema: keys only, Strict

Advanced case 3 of Delete.

Delete on a schema preloaded at service setup, in three scenarios: the convenient setup (keys only) deleting by the key the preload found, with the table it added because it is referenced; Strict refusing a delete matched by a column that wasn't preloaded; and the same delete on a service that isn't Strict.

Good to know:

Under Strict, every column that finds the rows to delete must be preloaded, so an endpoint can only delete by the columns you chose. Create Advanced6 shows the preload itself and its JSON.

Usings:

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

Set up by the sample before its scenarios:

```csharp
foreach (var (first, email) in new[] { ("Dora", "dora.delete@example.com"), ("Dev", "dev.delete@example.com"), ("Dee", "dee.delete@example.com") })
{
    var contact = new ArxEntity("contact");
    contact["first_name"] = first;
    contact["last_name"] = "Deleteme";
    contact["email"] = email;
    await service.CreateAsync(contact);
}
```

#### 1. KeysOnly: delete by the key the preload found

Keys only: contact.email is unique, so it is a key; account comes too (contact.account_id references it).

In scope: `connectionString` (string).

```csharp
var tables = new ArxPreLoadTableRegister()
    .PreLoadTable("contact");

var schema = await new ArxPreLoadTable(tables, ArxPreLoadMode.KeysOnly).LoadAsync(connectionString);
var service = new ArxMsSqlDbService(connectionString, schema);

foreach (var table in schema.Tables)
{
    Console.WriteLine($"{table.Name,-8} {table.Reason}; keys {string.Join(" ", table.Keys)}");
}

var dora = new ArxEntity("contact");
dora["email"] = "dora.delete@example.com";
Console.WriteLine($"deleted {await service.DeleteAsync(dora, new ArxDeleteOptions { Match = ArxMatch.Key("email") })} row");
```

Check it in the database:

```sql
SELECT count(*)
FROM contact
WHERE email = 'dora.delete@example.com';
```

Expect: 0.

#### 2. Strict: a delete matched by a column that wasn't preloaded

Strict with only email preloaded: deleting every contact named "Deleteme" is refused before the database.

In scope: `connectionString` (string).

```csharp
var tables = new ArxPreLoadTableRegister()
    .PreLoadTable("contact", ["email"]);

var schema = await new ArxPreLoadTable(tables).LoadAsync(connectionString);
var strict = new ArxMsSqlDbService(connectionString, schema) { Strict = true };

var byLastName = new ArxEntity("contact");
byLastName["last_name"] = "Deleteme";

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

var byEmail = new ArxEntity("contact");
byEmail["email"] = "dev.delete@example.com";
Console.WriteLine($"by the preloaded email: deleted {await strict.DeleteAsync(byEmail, new ArxDeleteOptions { Match = ArxMatch.Key("email") })} row");
```

Check it in the database:

```sql
SELECT email
FROM contact
WHERE last_name = 'Deleteme';
```

Expect: 1 row, dee.delete@example.com: the delete by last_name was refused, dev's by email ran (until scenario 3 deletes it).

#### 3. Not Strict: the same delete works

The same schema, not Strict: last_name isn't preloaded, and the delete by it still works.

In scope: `connectionString` (string).

```csharp
var tables = new ArxPreLoadTableRegister()
    .PreLoadTable("contact", ["email"]);

var schema = await new ArxPreLoadTable(tables).LoadAsync(connectionString);
var service = new ArxMsSqlDbService(connectionString, schema);

var byLastName = new ArxEntity("contact");
byLastName["last_name"] = "Deleteme";
Console.WriteLine($"deleted {await service.DeleteAsync(byLastName, new ArxDeleteOptions { Match = ArxMatch.Columns("last_name") })} row");
```

Check it in the database:

```sql
SELECT count(*)
FROM contact
WHERE last_name = 'Deleteme';
```

Expect: 0.

---

# DeleteMultiple with ARX.SDK on SQL Server

## What a DeleteMultiple does

| | |
|---|---|
| **Many by id** | `DeleteMultipleAsync("account", ids)`: up to 2,000 ids as one parameter each in an `IN` list; more as one JSON parameter, `IN (SELECT v FROM OPENJSON(@p0) WITH (v bigint '$'))`, up to 1,000,000 a statement; never a concatenated list. Returns the count; duplicates count once |
| **Without a catalog** | By ids, the table is read once for the whole list (Basic2) |
| **By a filter** | `DeleteMultipleAsync("account", filter)`: one `DELETE … FROM … WHERE …` for every row the filter matches, returning the count, 0 when none (Basic3). An empty filter needs `AllRows`. From 0.1.0-preview.5 |

## The samples

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

### Delete many records in one call

Basics 1 of DeleteMultiple.

Many records in one call, in two scenarios: by the ids CreateMultiple returned; and a list with an id that doesn't exist and one given twice, both counted honestly.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var service = new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id"))
    .Build());
```

#### 1. By the ids Create returned

The usual way: the table and the ids. The call returns how many records went.

In scope: `service` (ArxMsSqlDbService).

```csharp
var ids = await CreateAccountsAsync(service, 1, 5);

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

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

Defined in the sample, used above:

```csharp
private static async Task<IReadOnlyList<ArxRecordId>> CreateAccountsAsync(ArxMsSqlDbService service, int first, int count)
{
    var accounts = Enumerable.Range(first, count).Select(i =>
    {
        var account = new ArxEntity("account");
        account["name"] = $"Delete many {i}";
        account["account_number"] = $"ACC-D2{i:00}";
        return account;
    }).ToList();

    return await service.CreateMultipleAsync(accounts);
}
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number BETWEEN 'ACC-D201' AND 'ACC-D205';
```

Expect: 0.

#### 2. Missing and repeated ids

A missing id isn't an error, and an id given twice goes once: the count says what really happened.

In scope: `service` (ArxMsSqlDbService).

```csharp
var ids = (await CreateAccountsAsync(service, 11, 3)).Select(x => x.AsInt64()).ToList();
var list = new List<long> { ids[0], ids[1], ids[2], ids[0], 999_999_995 };

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

Console.WriteLine($"asked for {list.Count}, {deleted} deleted");
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number BETWEEN 'ACC-D211' AND 'ACC-D213';
```

Expect: 0.

### Delete many records without a catalog

Basics 2 of DeleteMultiple.

Many records in one call on a service created with only a connection string: ARX reads the table's id column once, then deletes every id in the list and returns how many went.

Good to know:

The extra round trip is paid once per call, not once per id; still, a service with a catalog never pays it.

Usings:

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

#### 1. By the ids Create returned

The table and the ids CreateMultiple returned.

In scope: `connectionString` (string).

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

var accounts = new List<ArxEntity>();
for (var i = 1; i <= 3; i++)
{
    var account = new ArxEntity("account");
    account["name"] = $"To remove {i}";
    account["account_number"] = $"ACC-N05{i}";
    accounts.Add(account);
}

var ids = await service.CreateMultipleAsync(accounts);

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

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

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number IN ('ACC-N051', 'ACC-N052', 'ACC-N053');
```

Expect: 0.

### Delete every row a filter matches

Basics 3 of DeleteMultiple.

Every row a condition matches, deleted in one call: the table and a filter. The call returns how many rows went, and 0 when none matched, which isn't an error.

Good to know:

The filter takes the operators of a query on the table's own columns; an empty filter is refused unless new ArxFilterWriteOptions { AllRows = true } is passed.

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

Usings:

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

#### 1. Every row the filter matches

Closed accounts removed with one DELETE; the same filter again deletes nothing and returns 0.

In scope: `connectionString` (string).

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

for (var i = 1; i <= 4; i++)
{
    var account = new ArxEntity("account");
    account["name"] = $"Filter delete {i}";
    account["account_number"] = $"ACC-F20{i}";
    account["status"] = i % 2 == 0 ? "CLOSED" : "ACTIVE";
    accounts.Add(account);
}

await service.CreateMultipleAsync(accounts);

var filter = new ArxFilterExpression();
filter.AddCondition("account_number", ArxConditionOperator.BeginsWith, "ACC-F20");
filter.AddCondition("status", ArxConditionOperator.Equal, "CLOSED");

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

Console.WriteLine($"{deleted} closed accounts deleted; the same filter again deleted {again}");
```

Defined in the sample, used above:

```csharp
private static ArxMsSqlDbService Service(string connectionString) => new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id"))
    .Build());
```

Check it in the database:

```sql
SELECT account_number, status
FROM account
WHERE account_number LIKE 'ACC-F20%'
ORDER BY account_number;
```

Expect: 2 rows, ACC-F201 and ACC-F203, both ACTIVE.

### Remove the contacts a user selected

Real case 1 of DeleteMultiple.

Removing the contacts a user ticked, in two scenarios: a list screen that has their ids, and an unsubscribe file that has only email addresses (the contact table's declared key).

Good to know:

Both are one call, all or nothing: either every matching contact goes or, when the database refuses one (a reference to it, for example), none does.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var contacts = new List<ArxEntity>();
foreach (var (first, last) in new[] { ("Ines", "Moreau"), ("Kenji", "Sato"), ("Lena", "Fischer"), ("Marco", "Bianchi"), ("Nadia", "Rahman") })
{
    var contact = new ArxEntity("contact");
    contact["first_name"] = first;
    contact["last_name"] = last;
    contact["email"] = $"{first.ToLowerInvariant()}.{last.ToLowerInvariant()}@example.com";
    contacts.Add(contact);
}
var ids = await service.CreateMultipleAsync(contacts);
```

#### 1. The ticked rows of a list

The list screen sends the ids of the rows the user ticked.

In scope: `service` (ArxMsSqlDbService), `selected` (IReadOnlyList<ArxRecordId>).

```csharp
int deleted = await service.DeleteMultipleAsync("contact", selected);

Console.WriteLine($"{deleted} of {selected.Count} selected contacts removed");
```

Check it in the database:

```sql
SELECT count(*)
FROM contact
WHERE email IN ('ines.moreau@example.com', 'kenji.sato@example.com', 'lena.fischer@example.com');
```

Expect: 0.

#### 2. An unsubscribe file of emails

An unsubscribe file has only emails: one entity per address, found by the declared key.

In scope: `service` (ArxMsSqlDbService).

```csharp
var file = new[] { "marco.bianchi@example.com", "nadia.rahman@example.com", "nobody@example.com" };

var requests = file.Select(email =>
{
    var request = new ArxEntity("contact");
    request["email"] = email;
    return request;
}).ToList();

// An address with no contact isn't an error: the count says how many were really there.
int deleted = await service.DeleteMultipleAsync(requests, new ArxDeleteOptions { Match = ArxMatch.Key("email") });

Console.WriteLine($"{deleted} of {file.Length} addresses removed");
```

Check it in the database:

```sql
SELECT count(*)
FROM contact
WHERE email IN ('marco.bianchi@example.com', 'nadia.rahman@example.com');
```

Expect: 0.

### Deletes in one ARX transaction: roll back or commit

Advanced case 1 of DeleteMultiple.

Deletes inside an ARX transaction, in two scenarios: delete, check, roll back (nothing is lost); and delete several tables' rows and commit them together.

Usings:

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

#### 1. Delete, check, roll back

Delete three accounts, then your check says it was the wrong list: roll back and they're all back.

In scope: `service` (ArxMsSqlDbService), `connectionString` (string), `ids` (IReadOnlyList<ArxRecordId>).

```csharp
await using var connection = new ArxMsSqlConnection(connectionString);
await using var transaction = await connection.BeginTransactionAsync();

var deleted = await service.DeleteMultipleAsync("account", ids, transaction);

if (deleted != 2)
{
    await transaction.RollbackAsync();
    Console.WriteLine($"expected 2, deleted {deleted}: rolled back");
}
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number IN ('ACC-D701', 'ACC-D702', 'ACC-D703');
```

Expect: 3: all back.

#### 2. Delete and log, then commit

The right list this time, and a log entry for it: committed together.

In scope: `service` (ArxMsSqlDbService), `connectionString` (string), `ids` (IReadOnlyList<ArxRecordId>).

```csharp
await using var connection = new ArxMsSqlConnection(connectionString);
await using var transaction = await connection.BeginTransactionAsync();

var deleted = await service.DeleteMultipleAsync("account", new[] { ids[0], ids[1] }, transaction);

var log = new ArxEntity("activity_log");
log["action"] = $"deleted {deleted} test accounts";
await service.CreateAsync(log, transaction);

await transaction.CommitAsync();
Console.WriteLine($"{deleted} deleted and logged");
```

Check it in the database:

```sql
SELECT (SELECT count(*) FROM account WHERE account_number IN ('ACC-D711', 'ACC-D712', 'ACC-D713')) AS accounts,
       (SELECT count(*) FROM activity_log WHERE action = 'deleted 2 test accounts') AS logged;
```

Expect: 1, 1.

---

# Retrieve with ARX.SDK on SQL Server

## What a read does

| | |
|---|---|
| **Retrieve** | One record by id (`RetrieveAsync("account", id, columns)`), or by a key or columns (`RetrieveAsync(entity, new ArxRetrieveOptions { Match = ArxMatch.Key("email") })`). **Null when nothing matches**; ARX2003 when a key or columns match more than one row |
| **What it reads from** | A table, a view (in any schema: `reporting.vw_product_catalogue`), or a table-valued function (`ArxRowSource.Function("fn", value)`, arguments by position). Procedures and scalar functions are Execute's. The samples name views `vw_`, functions `fn_` and procedures `sp_` |
| **Columns** | The ones asked for, plus the declared id (so `RecordId` is set); none means all. `column._lk.table.c1,c2` joins the referenced table directly (columns `column.c1`); a link entity's columns come back as `alias.column` |
| **Values read back** | `date` → `DateOnly`, `time` → `TimeOnly`, `datetime2` → `DateTime` (Kind `Unspecified`), `datetimeoffset` → `DateTimeOffset` with its offset; NULL is a null attribute, `""` stays `""` |
| **Lookups** | A column declared with `.Lookup(...)` reads back as an `ArxEntityReference`, its `Name` filled from the referenced table's `.DisplayName(...)` |
| **Isolation** | Reads on connections ARX opens are `READ COMMITTED`, whatever another user of the pooled connection left set. Under it a row another transaction changed but hasn't committed makes the read wait (until the commit or the command timeout); a database with `READ_COMMITTED_SNAPSHOT` on reads the last committed version instead |
| **Security** | A name carrying SQL (a sort field, a filter field, a table from a request) is ARX1017, refused before the database; a name that breaks the name rules (a space, a hyphen) is ARX1003, and a well-formed name the table doesn't have (a misspelling) comes back from the database as ARX5002 |
| **Without a catalog** | By id or key, the table is read from the database first to find the row (one more round trip per call, so much slower than with a catalog: meant for simple CRUD and small applications). The values come back as they would with a catalog, so without one a read sets no `RecordId` (ask for the id column) and a lookup column is its plain value (Basic4) |

## Reference

### Value types read back

| SQL Server column | Reads back as | Notes |
|---|---|---|
| `nvarchar(n)`, `varchar(n)`, `nchar(n)`, `nvarchar(max)` | `string` | `nchar(n)` keeps its padding spaces; `""` stays `""` |
| `tinyint` / `smallint` / `int` / `bigint` | `byte` / `short` / `int` / `long` | |
| `decimal(p, s)`, `money` | `decimal` | More than 28 significant digits: `ARX4001 ValueOutOfRange` |
| `real` / `float` | `float` / `double` | |
| `bit` | `bool` | |
| `uniqueidentifier` | `Guid` | |
| `datetimeoffset` | `DateTimeOffset` | The clock time and the offset it was stored with |
| `datetime2`, `datetime`, `smalldatetime` | `DateTime`, Kind `Unspecified` | The clock time as stored: SQL Server keeps no zone |
| `date` / `time` | `DateOnly` / `TimeOnly` | |
| `varbinary(n)`, `varbinary(max)`, `rowversion` | `byte[]` | |
| JSON in `nvarchar(max)` | `string` | SQL Server 2022 has no JSON type: the text as stored |
| `sql_variant` | the value's own type | |
| An alias type (`CREATE TYPE … FROM nvarchar(320)`) | Its base type | |
| A declared lookup (`.Lookup(...)`) | `ArxEntityReference` | `Name` from the referenced table's `.DisplayName(...)` |
| A selection set column | Its stored code | `entity.GetSelectionSet(column, set, catalog)` gives the option |
| NULL | `null` | The attribute is present, with a null value |

`RealCase1_EveryDataTypeReadBack` writes one row of every type with Create and prints each value's .NET type as it
reads back.

### Aggregates

| Aggregate | On SQL Server | Reads back as |
|---|---|---|
| Count, CountBig, CountDistinct, CountBigDistinct | `COUNT_BIG(*)`, or of the attribute when one is named | `long` |
| Avg | `AVG(column * 1.0)`: SQL Server's own AVG of integers is an integer (2 and 3 give 2) | `decimal` |
| Sum, Min, Max | as they are (a Sum of `int` past 2³¹ is an overflow error, ARX4001, not a wrong value) | the column's type |

### Error codes a read can raise

| Code | When |
|---|---|
| ARX1001 InvalidArgument | An id of the wrong type or one that can't exist; a NoIdColumn table read by id; an operator without the values it needs (Between with one value, Equal with NULL, NULL in an In list); paging values below 1; Top with PageInfo; an alias used twice or reserved; a link asking for all columns |
| ARX1002, ARX1003, ARX1004 | A missing table name; an invalid name; a column asked for twice |
| ARX1008 InvalidConnection | A connection with an open transaction, given without that transaction |
| ARX1016 IdNotDeclared | Retrieve by id or key on an undeclared table; a `_lk` join on an undeclared table without `~column` |
| ARX1017 SuspectedInjection | A table, column, alias, condition, order, group-by, aggregate, function or argument name carrying SQL |
| ARX2003 MultipleRecordsMatched | Retrieve by a key or columns found more than one row |
| ARX4001 ValueOutOfRange | A `decimal` larger than a .NET `decimal` can hold; an aggregate that overflows |
| ARX4003 InvalidValueFormat | Text compared with a number column that doesn't convert (`'forty'` against an `int`) |
| ARX4004 DataTypeMismatch | A value of a type the column can't be compared with; a function given more arguments than it has |
| ARX5001, ARX5002 | A table, view or function that doesn't exist (a scalar function read as a table, too); a column that doesn't exist, or a selected column that isn't grouped |
| ARX6001 PermissionDenied | No SELECT permission |
| ARX7003 Timeout | `CommandTimeout` exceeded (a read waiting for another transaction's lock, too) |
| ARX8001 NotSupported | A removed column form (`._opt`, `ignoredefault.`); paging a source with no order and no declared id; a function with named arguments or declared result columns; full-text search on a column without a full-text index; a procedure |

## The samples

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

### Retrieve one record

Basics 1 of Retrieve.

The smallest read: one record, in three scenarios. The usual way first (the id CreateAsync returned, and the columns you want), then a text id, then an id nobody has: Retrieve returns null, not an error.

Usings:

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

#### 1. By the id Create returned

The usual way: the table, the id Create returned, and the columns to read.

In scope: `connectionString` (string).

```csharp
var service = Service(connectionString);

var account = new ArxEntity("account");
account["name"] = "Northwind Traders";
account["account_number"] = "ACC-R101";
account["employee_count"] = 120;
ArxRecordId id = await service.CreateAsync(account);

ArxEntity read = await service.RetrieveAsync("account", id, new ArxColumnSet("name", "employee_count")) ?? throw new InvalidOperationException("The account record isn't there.");

// The id is always read too, so read.RecordId can update or delete this record later.
Console.WriteLine($"{read["name"]}, {read["employee_count"]} employees, RecordId {read.RecordId}");
```

Defined in the sample, used above:

```csharp
private static ArxMsSqlDbService Service(string connectionString) => new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id"))
    .Table("country", ArxIdColumn.Supplied<string>("code"))
    .Build());
```

Check it in the database:

```sql
SELECT name, employee_count
FROM account
WHERE account_number = 'ACC-R101';
```

Expect: 1 row, Northwind Traders, 120: what Retrieve returned.

#### 2. A text id

A text id: a country by its code. Without columns, every column is read.

In scope: `connectionString` (string).

```csharp
var service = Service(connectionString);

var iceland = new ArxEntity("country");
iceland["code"] = "IS";
iceland["name"] = "Iceland";
await service.CreateAsync(iceland);

ArxEntity read = await service.RetrieveAsync("country", "IS") ?? throw new InvalidOperationException("The country record isn't there.");

Console.WriteLine($"{read["code"]} is {read["name"]} ({read.Attributes.Count} columns)");
```

Check it in the database:

```sql
SELECT code, name
FROM country
WHERE code = 'IS';
```

Expect: 1 row, IS, Iceland.

#### 3. An id nobody has

An id that doesn't exist: null, so "is it there?" needs no try/catch.

In scope: `connectionString` (string).

```csharp
var service = Service(connectionString);

ArxEntity? read = await service.RetrieveAsync("account", 999_999_999L);

Console.WriteLine($"{(read == null ? "null" : "found")}");
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE id = 999999999;
```

Expect: 0.

### Choose the columns to read

Basics 2 of Retrieve.

Which columns come back, in three scenarios: the ones you ask for (plus the id); all of them; and how an empty value differs from a NULL one.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var service = new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id"))
    .Build());
var account = new ArxEntity("account");
account["name"] = "Columns Ltd";
account["account_number"] = "ACC-R301";
account["website"] = "";
ArxRecordId id = await service.CreateAsync(account);
```

#### 1. Some columns

The columns asked for; the id comes too, so the entity can be updated or deleted later.

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

```csharp
var read = await service.RetrieveAsync("account", id, new ArxColumnSet("name", "account_number")) ?? throw new InvalidOperationException("The account record isn't there.");

Console.WriteLine($"{string.Join(", ", read.Attributes.Select(x => x.Key))}");
```

Check it in the database:

```sql
SELECT id, name, account_number
FROM account
WHERE account_number = 'ACC-R301';
```

Expect: 1 row, Columns Ltd: the three attributes Retrieve returned.

#### 2. All columns

No column set (or `ArxColumnSet.All`): every column of the table.

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

```csharp
var read = await service.RetrieveAsync("account", id) ?? throw new InvalidOperationException("The account record isn't there.");

Console.WriteLine($"{read.Attributes.Count} attributes, status {read["status"]}");
```

Check it in the database:

```sql
SELECT count(*)
FROM information_schema.columns
WHERE table_name = 'account';
```

Expect: 17, the number of attributes Retrieve returned.

#### 3. Empty and NULL

An empty website is "", a missing one is null, and both are attributes: nothing is dropped.

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

```csharp
var read = await service.RetrieveAsync("account", id, new ArxColumnSet("website", "annual_revenue")) ?? throw new InvalidOperationException("The account record isn't there.");

Console.WriteLine($"website \"{read["website"]}\", annual_revenue {(read["annual_revenue"] == null ? "null" : read["annual_revenue"])}, both present: {read.Contains("website") && read.Contains("annual_revenue")}");
```

Check it in the database:

```sql
SELECT CASE WHEN website = '' THEN 1 ELSE 0 END AS empty_website,
       CASE WHEN annual_revenue IS NULL THEN 1 ELSE 0 END AS no_revenue
FROM account
WHERE account_number = 'ACC-R301';
```

Expect: 1 row, 1, 1.

### Find a record by id, key or columns

Basics 3 of Retrieve.

The three ways to find one record, in four scenarios: by id, by a declared key, by any columns, and what happens when columns match more than one row.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var service = new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("contact", ArxIdColumn.Generated<long>("id")).Key("contact", "email")
    .Build());
var contacts = new List<ArxEntity>();
foreach (var (first, email) in new[] { ("Ines", "ines.r401@example.com"), ("Ivo", "ivo.r401@example.com") })
{
    var contact = new ArxEntity("contact");
    contact["first_name"] = first;
    contact["last_name"] = "Find-R401";
    contact["email"] = email;
    contacts.Add(contact);
}
var ids = await service.CreateMultipleAsync(contacts);
```

#### 1. By id

By id, through an entity: the same as RetrieveAsync("contact", id).

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

```csharp
var match = new ArxEntity("contact") { RecordId = id };

var read = await service.RetrieveAsync(match, new ArxRetrieveOptions { Columns = new ArxColumnSet("first_name") }) ?? throw new InvalidOperationException("The record isn't there.");

Console.WriteLine($"{read["first_name"]}");
```

Check it in the database:

```sql
SELECT first_name
FROM contact
WHERE email = 'ines.r401@example.com';
```

Expect: 1 row, Ines.

#### 2. By a declared key

By the declared key: the email is unique, so it finds one record.

In scope: `service` (ArxMsSqlDbService).

```csharp
var match = new ArxEntity("contact");
match["email"] = "ivo.r401@example.com";

var read = await service.RetrieveAsync(match, new ArxRetrieveOptions { Match = ArxMatch.Key("email"), Columns = new ArxColumnSet("first_name") }) ?? throw new InvalidOperationException("The record isn't there.");

Console.WriteLine($"{read["first_name"]}");
```

Check it in the database:

```sql
SELECT first_name
FROM contact
WHERE email = 'ivo.r401@example.com';
```

Expect: 1 row, Ivo.

#### 3. By any columns

By any columns: the first name and last name together, no declaration needed.

In scope: `service` (ArxMsSqlDbService).

```csharp
var match = new ArxEntity("contact");
match["first_name"] = "Ines";
match["last_name"] = "Find-R401";

var read = await service.RetrieveAsync(match, new ArxRetrieveOptions { Match = ArxMatch.Columns("first_name", "last_name"), Columns = new ArxColumnSet("email") }) ?? throw new InvalidOperationException("The record isn't there.");

Console.WriteLine($"{read["email"]}");
```

Check it in the database:

```sql
SELECT email
FROM contact
WHERE first_name = 'Ines' AND last_name = 'Find-R401';
```

Expect: 1 row, ines.r401@example.com.

#### 4. Several rows

A Retrieve answers one record: columns that match two rows are ARX2003. Use RetrieveMultiple for many.

In scope: `service` (ArxMsSqlDbService).

```csharp
var match = new ArxEntity("contact");
match["last_name"] = "Find-R401";

try
{
    await service.RetrieveAsync(match, new ArxRetrieveOptions { Match = ArxMatch.Columns("last_name") });
}
catch (ArxException error) when (error.Code == ArxErrorCode.MultipleRecordsMatched)
{
    Console.WriteLine($"{error.Code}");
}
```

Check it in the database:

```sql
SELECT count(*)
FROM contact
WHERE last_name = 'Find-R401';
```

Expect: 2: that's why one record couldn't be returned.

### Retrieve without a catalog

Basics 4 of Retrieve.

One record read on a service created with only a connection string, in two scenarios: by the id, which ARX finds from the table's primary key; and by a key, the unique account number.

Good to know:

A read by the id reads the table first, which makes it much slower than on a service with a catalog (Create Basic6 says when that is fine). What was read only finds the row: the values come back the same with or without a catalog. So, without one, a read sets no `RecordId` (ask for the id column, as scenario 2 does) and a lookup column comes back as its plain value, not an `ArxEntityReference`.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var service = new ArxMsSqlDbService(connectionString);
var account = new ArxEntity("account");
account["name"] = "Wingtip Toys";
account["account_number"] = "ACC-N061";
account["employee_count"] = 75;
ArxRecordId id = await service.CreateAsync(account);
```

#### 1. By the id

The table and the id Create returned.

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

```csharp
ArxEntity read = await service.RetrieveAsync("account", id, new ArxColumnSet("name", "employee_count")) ?? throw new InvalidOperationException("The account record isn't there.");

Console.WriteLine($"{read["name"]}, {read["employee_count"]} employees");
```

Check it in the database:

```sql
SELECT name, employee_count
FROM account
WHERE account_number = 'ACC-N061';
```

Expect: 1 row, Wingtip Toys, 75.

#### 2. By a key

By a key: the entity holds the value, the option names the column; ask for the id to keep it.

In scope: `service` (ArxMsSqlDbService).

```csharp
var match = new ArxEntity("account");
match["account_number"] = "ACC-N061";

ArxEntity read = await service.RetrieveAsync(match, new ArxRetrieveOptions
{
    Match = ArxMatch.Key("account_number"),
    Columns = new ArxColumnSet("id", "name")
}) ?? throw new InvalidOperationException("The record isn't there.");

Console.WriteLine($"{read["name"]}, id {read["id"]}");
```

Check it in the database:

```sql
SELECT id, name
FROM account
WHERE account_number = 'ACC-N061';
```

Expect: 1 row, Wingtip Toys.

### Every data type, written and read back

Real case 1 of Retrieve.

What each SQL Server type reads back as, in one scenario: a row of data_type_showcase is written with Create and read with Retrieve, and each attribute's .NET type is printed.

Usings:

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

#### 1. Written with Create, read with Retrieve

Write one value of each type, read the row, print each attribute's type and value.

In scope: `service` (ArxMsSqlDbService).

```csharp
var row = new ArxEntity("data_type_showcase");
row["text_value"] = "Read back R641";
row["char_value"] = "AB";
row["smallint_value"] = (short)7;
row["int_value"] = 42;
row["bigint_value"] = 9_000_000_000L;
row["decimal_value"] = 12345.678901m;
row["money_value"] = 19.99m;
row["real_value"] = 1.5f;
row["float_value"] = 2.25d;
row["bit_value"] = true;
row["uniqueidentifier_value"] = Guid.Parse("6c1b3a6e-7c2d-4c7e-9a51-2a9c6b0e1641");
row["datetimeoffset_value"] = new DateTimeOffset(2026, 4, 1, 11, 30, 0, TimeSpan.FromHours(2));
row["datetime2_value"] = new DateTime(2026, 4, 1, 9, 30, 0);
row["date_value"] = new DateOnly(2026, 4, 1);
row["time_value"] = new TimeOnly(9, 30);
row["varbinary_value"] = new byte[] { 1, 2, 3 };
row["json_value"] = new ArxJsonB { SerializedValue = "{\"tier\": \"gold\"}" };
row["mood"] = new ArxSelectionSetValue("Mood", "happy");
var id = await service.CreateAsync(row);

var read = await service.RetrieveAsync("data_type_showcase", id) ?? throw new InvalidOperationException("The data_type_showcase record isn't there.");

foreach (var column in new[] { "text_value", "char_value", "smallint_value", "int_value", "bigint_value", "decimal_value", "money_value", "real_value",
             "float_value", "bit_value", "uniqueidentifier_value", "datetimeoffset_value", "datetime2_value", "date_value", "time_value",
             "varbinary_value", "json_value", "mood" })
{
    var value = read[column];
    var text = value switch
    {
        ArxJsonB json => json.SerializedValue,
        DateTime dateTime => $"{dateTime:yyyy-MM-dd HH:mm} ({dateTime.Kind})",
        DateTimeOffset offset => $"{offset:yyyy-MM-dd HH:mm zzz}",
        Array array => "[" + string.Join(", ", array.Cast<object>()) + "]",
        _ => value?.ToString()
    };

    Console.WriteLine($"{column,-18} {value?.GetType().Name,-10} {text}");
}
```

Check it in the database:

```sql
SELECT char_value, date_value, time_value, datetimeoffset_value, json_value, mood
FROM data_type_showcase
WHERE text_value = 'Read back R641';
```

Expect: 1 row, "AB " (nchar(3) keeps its padding), 2026-04-01, 09:30:00.0000000, 2026-04-01 11:30:00.0000000 +02:00, {"tier": "gold"}, happy.

### Read your own writes in an ARX transaction

Advanced case 1 of Retrieve.

Reads inside an ARX transaction, in two scenarios: a read sees the transaction's own uncommitted writes, another connection doesn't; and a check that reads before it commits, rolling back when the check fails.

Usings:

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

#### 1. Inside the transaction, its writes are visible

Inside, the new account is there; outside, it isn't until the commit.

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

```csharp
await using var connection = new ArxMsSqlConnection(connectionString);
await using var transaction = await connection.BeginTransactionAsync();

var account = new ArxEntity("account");
account["name"] = "Transacted Textiles";
account["account_number"] = "ACC-R711";
var id = await service.CreateAsync(account, transaction);

var inside = await service.RetrieveAsync("account", id, transaction, new ArxColumnSet("name"));
Console.WriteLine($"inside: {inside?["name"] ?? "not there"}");

// Outside, SQL Server's default READ COMMITTED waits for the uncommitted row instead of skipping it (unless the
// database turns READ_COMMITTED_SNAPSHOT on): here a 1-second timeout ends the wait.
var query = new ArxQueryExpression("account");
query.Criteria.AddCondition("account_number", ArxConditionOperator.Equal, "ACC-R711");

try
{
    await service.RetrieveMultipleAsync(query, new ArxRetrieveOptions { CommandTimeout = 1 });
}
catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.Timeout)
{
    Console.WriteLine("outside before commit: the read waits for the transaction (timed out after 1 s)");
}

await transaction.CommitAsync();
Console.WriteLine($"outside after commit: {(await service.RetrieveAsync("account", id))?["name"]}");
```

Check it in the database:

```sql
SELECT name
FROM account
WHERE account_number = 'ACC-R711';
```

Expect: 1 row, Transacted Textiles (committed).

#### 2. Read to check, then commit or roll back

An import of contacts, checked before commit: one too many for the account's limit, so it's rolled back.

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

```csharp
await using var connection = new ArxMsSqlConnection(connectionString);
await using var transaction = await connection.BeginTransactionAsync();

var contacts = Enumerable.Range(1, 4).Select(i =>
{
    var contact = new ArxEntity("contact");
    contact["first_name"] = $"Import {i}";
    contact["last_name"] = "Checked-R712";
    return contact;
}).ToList();

await service.CreateMultipleAsync(contacts, transaction);

var query = new ArxQueryExpression("contact") { AggregateInfo = new ArxAggregateInfo() };
query.Criteria.AddCondition("last_name", ArxConditionOperator.Equal, "Checked-R712");
query.AggregateInfo.AddAggregateColumn("imported", "", ArxAggregateOperator.Count);
var imported = (long)(await service.RetrieveMultipleAsync(query, transaction)).Entities[0]["imported"]!;

if (imported > 3)
{
    await transaction.RollbackAsync();
    Console.WriteLine($"{imported} contacts, more than the limit of 3: rolled back");
}
else
{
    await transaction.CommitAsync();
}
```

Check it in the database:

```sql
SELECT count(*)
FROM contact
WHERE last_name = 'Checked-R712';
```

Expect: 0: the check saw 4 and rolled the import back.

---

# RetrieveMultiple with ARX.SDK on SQL Server

## What a RetrieveMultiple does

| | |
|---|---|
| **RetrieveMultiple** | A query (`ArxQueryExpression`): columns, criteria (nested AND/OR), link entities, order, paging with the total in the same round trip, distinct, aggregates and group by |
| **Paging** | `PageInfo` (page size and number), sent as `OFFSET … ROWS FETCH NEXT … ROWS ONLY`. SQL Server needs an order to page: ARX adds the declared id to break ties, and a source with neither an order nor a declared id is ARX8001. `HasNextPage` is always right (ARX reads one row more than the page). The total is **opt-in** (`ReturnTotalRecordCount = true`): a second statement in the same round trip that counts every matching row, so leave it off when the screen doesn't show it, or ask on the first page only |
| **Values** | Every value in a condition is a parameter (an In list of more than 1,000 values is one JSON parameter). Contains, BeginsWith and EndsWith use `LIKE … ESCAPE`, so `%`, `_` and `[` are found literally; whether they ignore case is the column's collation's choice (the default ignores it) |
| **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 |
| **Without a catalog** | Nothing is read first: a query costs the same with or without a catalog. Without one, rows have no `RecordId` (ask for the id column) and a lookup column is its plain value (Basic3) |

## The samples

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

### Retrieve many records with a query

Basics 1 of RetrieveMultiple.

Many records with a query, in three scenarios: a condition and an order; several conditions together; and how many rows came back. Every value in a condition is sent as a parameter, never written into the SQL.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var service = new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id"))
    .Build());
var accounts = new List<ArxEntity>();
foreach (var (number, name, employees) in new[] { ("ACC-R201", "Basic Bikes", 12), ("ACC-R202", "Basic Boats", 340), ("ACC-R203", "Basic Books", 45) })
{
    var account = new ArxEntity("account");
    account["name"] = name;
    account["account_number"] = number;
    account["employee_count"] = employees;
    accounts.Add(account);
}
await service.CreateMultipleAsync(accounts);
```

#### 1. A condition and an order

The accounts whose name begins with "Basic B", by name.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = new ArxQueryExpression("account");
query.ColumnSet.AddColumns("name", "employee_count");
query.Criteria.AddCondition("name", ArxConditionOperator.BeginsWith, "Basic B");
query.AddOrder("name", ArxOrderType.Ascending);

ArxEntityCollection result = await service.RetrieveMultipleAsync(query);

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

Check it in the database:

```sql
SELECT name
FROM account
WHERE name LIKE 'Basic B%'
ORDER BY name;
```

Expect: 3 rows, Basic Bikes, Basic Boats, Basic Books.

#### 2. Several conditions

Conditions in the criteria are joined with AND: "Basic" accounts with 40 employees or more.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = new ArxQueryExpression("account");
query.ColumnSet.AddColumns("name", "employee_count");
query.Criteria.AddCondition("name", ArxConditionOperator.BeginsWith, "Basic B");
query.Criteria.AddCondition("employee_count", ArxConditionOperator.GreaterEqual, 40);
query.AddOrder("employee_count", ArxOrderType.Descending);

var result = await service.RetrieveMultipleAsync(query);

Console.WriteLine($"{string.Join(", ", result.Entities.Select(x => $"{x["name"]} ({x["employee_count"]})"))}");
```

Check it in the database:

```sql
SELECT name, employee_count
FROM account
WHERE name LIKE 'Basic B%' AND employee_count >= 40
ORDER BY employee_count DESC;
```

Expect: 2 rows, Basic Boats 340, Basic Books 45.

#### 3. How many came back

Without paging, TotalRecordCount is the number of rows that came back.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = new ArxQueryExpression("account");
query.ColumnSet.AddColumn("name");
query.Criteria.AddCondition("account_number", ArxConditionOperator.In, "ACC-R201", "ACC-R203", "ACC-R999");

var result = await service.RetrieveMultipleAsync(query);

Console.WriteLine($"{result.TotalRecordCount} of the 3 numbers asked for");
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number IN ('ACC-R201', 'ACC-R203', 'ACC-R999');
```

Expect: 2 (nobody has ACC-R999).

### Page through results

Basics 2 of RetrieveMultiple.

Paging, in three scenarios: one page with the total (in the same round trip); every page in a loop on HasNextPage; and a page past the end, which still knows the total.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var service = new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("product", ArxIdColumn.Generated<long>("id"))
    .Build());
var products = Enumerable.Range(1, 7).Select(i =>
{
    var product = new ArxEntity("product");
    product["sku"] = $"PAGE-R50{i}";
    product["name"] = $"Paged product {i}";
    product["unit_price"] = 10m + i;
    return product;
}).ToList();
await service.CreateMultipleAsync(products);
```

#### 1. One page

Page 1 of 3 rows, with the total: one round trip.

In scope: `service` (ArxMsSqlDbService).

```csharp
var page = await service.RetrieveMultipleAsync(Products(1, total: true));

Console.WriteLine($"{string.Join(", ", page.Entities.Select(x => x["sku"]))}; {page.TotalRecordCount} in all, {page.TotalPageCount} pages, next page: {page.HasNextPage}");
```

Defined in the sample, used above:

```csharp
private static ArxQueryExpression Products(int pageNumber, bool total)
{
    var query = new ArxQueryExpression("product");
    query.ColumnSet.AddColumns("sku", "name");
    query.Criteria.AddCondition("sku", ArxConditionOperator.BeginsWith, "PAGE-R50");
    query.AddOrder("sku", ArxOrderType.Ascending);
    query.PageInfo = new ArxPagingInfo { Count = 3, PageNumber = pageNumber, ReturnTotalRecordCount = total };
    return query;
}
```

Check it in the database:

```sql
SELECT sku
FROM product
WHERE sku LIKE 'PAGE-R50%'
ORDER BY sku
OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY;
```

Expect: 3 rows, PAGE-R501, PAGE-R502, PAGE-R503 (and 7 rows without the FETCH).

#### 2. Every page

Every page: HasNextPage is right without the total (ARX reads one row more than the page).

In scope: `service` (ArxMsSqlDbService).

```csharp
var read = new List<string>();
var pageNumber = 1;
ArxEntityCollection page;

do
{
    page = await service.RetrieveMultipleAsync(Products(pageNumber++, total: false));
    read.AddRange(page.Entities.Select(x => (string)x["sku"]!));
}
while (page.HasNextPage);

Console.WriteLine($"{read.Count} products in {pageNumber - 1} pages");
```

Check it in the database:

```sql
SELECT count(*)
FROM product
WHERE sku LIKE 'PAGE-R50%';
```

Expect: 7, the products the loop read.

#### 3. Past the end

A page past the end: no rows, and the total is still there.

In scope: `service` (ArxMsSqlDbService).

```csharp
var page = await service.RetrieveMultipleAsync(Products(9, total: true));

Console.WriteLine($"{page.Entities.Count} rows, total {page.TotalRecordCount}");
```

Check it in the database:

```sql
SELECT count(*)
FROM product
WHERE sku LIKE 'PAGE-R50%';
```

Expect: 7, the total page 9 reported.

### Query without a catalog

Basics 3 of RetrieveMultiple.

A query on a service created with only a connection string. A query names its columns and conditions itself, so ARX reads nothing from the database first: it runs as it would with a catalog.

Good to know:

Without a catalog the values come back as the database returns them: no `RecordId` on the rows (ask for the id column to keep it), and a lookup column is its plain value, not an `ArxEntityReference`. Declare the table in a catalog for both.

Usings:

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

#### 1. A query

Columns, a condition and an order: nothing to resolve.

In scope: `connectionString` (string).

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

var accounts = new List<ArxEntity>();
foreach (var (name, number, employees) in new[] { ("Coho Winery", "ACC-N071", 30), ("Fourth Coffee", "ACC-N072", 18) })
{
    var account = new ArxEntity("account");
    account["name"] = name;
    account["account_number"] = number;
    account["employee_count"] = employees;
    accounts.Add(account);
}

await service.CreateMultipleAsync(accounts);

var query = new ArxQueryExpression("account");
query.ColumnSet.AddColumns("name", "employee_count");
query.Criteria.AddCondition("account_number", ArxConditionOperator.BeginsWith, "ACC-N07");
query.AddOrder("name", ArxOrderType.Ascending);

ArxEntityCollection rows = await service.RetrieveMultipleAsync(query);

foreach (var row in rows.Entities)
{
    Console.WriteLine($"{row["name"]}, {row["employee_count"]} employees");
}
```

Check it in the database:

```sql
SELECT name, employee_count
FROM account
WHERE account_number LIKE 'ACC-N07%'
ORDER BY name;
```

Expect: 2 rows, Coho Winery 30, Fourth Coffee 18.

### A contact search screen

Real case 1 of RetrieveMultiple.

A contact search screen, in three scenarios: a free-text box (Contains, case-insensitive, on two columns with OR); filters the user ticked (an In list); and text with wildcards in it, searched literally.

Usings:

```csharp
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, title, channel) in new[]
{
    ("Maria", "Search-R601", "Sales Manager", "E"),
    ("Mario", "Search-R601", "Engineer", "P"),
    ("Anna", "Marino-R601", "100% remote engineer", "E"),
    ("Tom", "Search-R601", "Buyer", "S"),
})
{
    var contact = new ArxEntity("contact");
    contact["first_name"] = first;
    contact["last_name"] = last;
    contact["job_title"] = title;
    contact["preferred_channel"] = new ArxSelectionSetValue("ContactChannel", channel[0]);
    contacts.Add(contact);
}
await service.CreateMultipleAsync(contacts);
```

#### 1. A free-text box: first or last name contains "mari"

The user types "mari": first name or last name contains it, in any case.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = Contacts();
var text = query.Criteria.AddFilter(ArxLogicalOperator.Or);
text.AddCondition("first_name", ArxConditionOperator.Contains, "mari");
text.AddCondition("last_name", ArxConditionOperator.Contains, "mari");

var found = await service.RetrieveMultipleAsync(query);
Console.WriteLine(string.Join(", ", found.Entities.Select(x => $"{x["first_name"]} {x["last_name"]}")));
```

Defined in the sample, used above:

```csharp
private static ArxQueryExpression Contacts()
{
    var query = new ArxQueryExpression("contact");
    query.ColumnSet.AddColumns("first_name", "last_name", "job_title");
    var ours = query.Criteria.AddFilter(ArxLogicalOperator.Or);
    ours.AddCondition("last_name", ArxConditionOperator.EndsWith, "-R601");
    query.AddOrder("first_name", ArxOrderType.Ascending);
    return query;
}
```

Check it in the database:

```sql
SELECT first_name, last_name
FROM contact
WHERE last_name LIKE '%-R601' AND (first_name LIKE '%mari%' OR last_name LIKE '%mari%')
ORDER BY first_name;
```

Expect: 3 rows, Anna Marino-R601, Maria Search-R601, Mario Search-R601.

#### 2. Ticked filters: preferred channel e-mail or phone

The user ticked "e-mail" and "phone" as preferred channels: one In list, one parameter a value.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = Contacts();
query.Criteria.AddCondition("preferred_channel", ArxConditionOperator.In, "E", "P");

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

Check it in the database:

```sql
SELECT first_name
FROM contact
WHERE last_name LIKE '%-R601' AND preferred_channel IN ('E', 'P')
ORDER BY first_name;
```

Expect: 3 rows, Anna, Maria, Mario.

#### 3. "100%" is searched as text, not as a pattern

"100%" typed into the box: the % is a character to find, not a wildcard.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = Contacts();
query.Criteria.AddCondition("job_title", ArxConditionOperator.Contains, "100%");

var found = await service.RetrieveMultipleAsync(query);
Console.WriteLine(string.Join(", ", found.Entities.Select(x => $"{x["first_name"]}: {x["job_title"]}")));
```

Check it in the database:

```sql
SELECT first_name, job_title
FROM contact
WHERE last_name LIKE '%-R601' AND CHARINDEX('100%', job_title) > 0;
```

Expect: 1 row, Anna, 100% remote engineer.

### Accounts and contacts in one query

Real case 2 of RetrieveMultiple.

Reading across tables, in three scenarios: a link entity (a join with its own alias, columns and criteria); a LEFT join that keeps rows without a match; and the short form, a direct join written in the column set (`account_id._lk.account.name,account_number`).

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "Linked Logistics";
account["account_number"] = "ACC-R602";
account["status"] = new ArxSelectionSetValue("AccountStatus", "ACTIVE");
var accountId = await service.CreateAsync(account);
var contacts = new List<ArxEntity>();
foreach (var (first, withAccount) in new[] { ("Lena", true), ("Lars", true), ("Lone", false) })
{
    var contact = new ArxEntity("contact");
    contact["first_name"] = first;
    contact["last_name"] = "Link-R602";
    contact["account_id"] = withAccount ? new ArxEntityReference("account", accountId) : null;
    contacts.Add(contact);
}
await service.CreateMultipleAsync(contacts);
```

#### 1. A link entity: each contact with its account

Contacts with their account's name and number: an inner join; the columns come back as `acc.name`.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = Contacts();
var account = query.AddLink("account", "account_id", "id", ArxJoinOperator.Inner);
account.EntityAlias = "acc";
account.ColumnSet.AddColumns("name", "account_number");
account.LinkCriteria.AddCondition("status", ArxConditionOperator.Equal, "ACTIVE");

var rows = await service.RetrieveMultipleAsync(query);

foreach (var row in rows.Entities)
{
    Console.WriteLine($"{row["first_name"]} works at {row["acc.name"]} ({row["acc.account_number"]})");
}
```

Defined in the sample, used above:

```csharp
private static ArxQueryExpression Contacts()
{
    var query = new ArxQueryExpression("contact");
    query.ColumnSet.AddColumn("first_name");
    query.Criteria.AddCondition("last_name", ArxConditionOperator.Equal, "Link-R602");
    query.AddOrder("first_name", ArxOrderType.Ascending);
    return query;
}
```

Check it in the database:

```sql
SELECT c.first_name, a.name, a.account_number
FROM contact c
JOIN account a ON a.id = c.account_id AND a.status = 'ACTIVE'
WHERE c.last_name = 'Link-R602'
ORDER BY c.first_name;
```

Expect: 2 rows, Lars and Lena at Linked Logistics (ACC-R602).

#### 2. A LEFT join keeps the contact without an account

A LEFT join: Lone has no account and is kept, with a null account name.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = Contacts();
var account = query.AddLink("account", "account_id", "id", ArxJoinOperator.Left);
account.EntityAlias = "acc";
account.ColumnSet.AddColumn("name");

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

Check it in the database:

```sql
SELECT c.first_name, a.name
FROM contact c
LEFT JOIN account a ON a.id = c.account_id
WHERE c.last_name = 'Link-R602'
ORDER BY c.first_name;
```

Expect: 3 rows, Lars and Lena with Linked Logistics, Lone with NULL.

#### 3. A direct join written in the column set

The short form: `account_id._lk.account.name,account_number` in the column set joins the account on its declared id and reads its columns as `account_id.name` and `account_id.account_number`.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = Contacts();
query.ColumnSet.AddColumn("account_id._lk.account.name,account_number");

var rows = await service.RetrieveMultipleAsync(query);
Console.WriteLine(string.Join(", ", rows.Entities.Select(x => $"{x["first_name"]}: {x["account_id.account_number"] ?? "-"}")));
```

Check it in the database:

```sql
SELECT c.first_name, a.account_number
FROM contact c
LEFT JOIN account a ON a.id = c.account_id
WHERE c.last_name = 'Link-R602'
ORDER BY c.first_name;
```

Expect: 3 rows, Lars ACC-R602, Lena ACC-R602, Lone NULL (a direct join is a LEFT join).

### A report: accounts by industry

Real case 3 of RetrieveMultiple.

A report, in two scenarios: accounts per industry with their total and average revenue (aggregates and group by, ordered by an aggregate); and one number for a dashboard tile (a count with no grouping).

Usings:

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

Set up by the sample before its scenarios:

```csharp
var accounts = new List<ArxEntity>();
var rows = new (string Number, Industry Industry, decimal Revenue)[]
{
    ("ACC-R631", Industry.Retail, 1_000_000m),
    ("ACC-R632", Industry.Retail, 3_000_000m),
    ("ACC-R633", Industry.Finance, 9_000_000m),
    ("ACC-R634", Industry.Technology, 2_500_000m),
    ("ACC-R635", Industry.Technology, 1_500_000m),
    ("ACC-R636", Industry.Technology, 500_000m),
};
foreach (var (number, industry, revenue) in rows)
{
    var account = new ArxEntity("account");
    account["name"] = "Report " + number;
    account["account_number"] = number;
    account["industry"] = new ArxSelectionSetValue("Industry", industry);
    account["annual_revenue"] = revenue;
    accounts.Add(account);
}
await service.CreateMultipleAsync(accounts);
```

#### 1. Accounts, total and average revenue per industry

Count, sum and average per industry, the industries with most accounts first.

In scope: `service` (ArxMsSqlDbService), `catalog` (ArxSchemaCatalog).

```csharp
var query = new ArxQueryExpression("account") { AggregateInfo = new ArxAggregateInfo() };
query.ColumnSet.AddColumn("industry");
query.Criteria.AddCondition("account_number", ArxConditionOperator.BeginsWith, "ACC-R63");
query.AggregateInfo.AddAggregateColumn("accounts", "", ArxAggregateOperator.Count);
query.AggregateInfo.AddAggregateColumn("total_revenue", "annual_revenue", ArxAggregateOperator.Sum);
query.AggregateInfo.AddAggregateColumn("average_revenue", "annual_revenue", ArxAggregateOperator.Avg);
query.AggregateInfo.AddGroupByColumn("industry");
query.AddOrder("accounts", ArxOrderType.Descending);

var report = await service.RetrieveMultipleAsync(query);

foreach (var row in report.Entities)
{
    // The raw code read back; its option from the catalog.
    var industry = row.GetSelectionSet("industry", "Industry", catalog)?.DisplayText;
    Console.WriteLine($"{industry,-12} {row["accounts"]} accounts, total {row["total_revenue"]:N0}, average {(decimal)row["average_revenue"]!:N0}");
}
```

Check it in the database:

```sql
SELECT industry, count(*), sum(annual_revenue), CAST(AVG(annual_revenue) AS decimal(15, 0))
FROM account
WHERE account_number LIKE 'ACC-R63%'
GROUP BY industry
ORDER BY count(*) DESC;
```

Expect: 3 rows, 5 (Technology) 3 4500000.00 1500000, 1 (Retail) 2 4000000.00 2000000, 4 (Finance) 1 9000000.00 9000000.

#### 2. One number for a dashboard tile

One number for a dashboard: how many of these accounts earn a million or more.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = new ArxQueryExpression("account") { AggregateInfo = new ArxAggregateInfo() };
query.Criteria.AddCondition("account_number", ArxConditionOperator.BeginsWith, "ACC-R63");
query.Criteria.AddCondition("annual_revenue", ArxConditionOperator.GreaterEqual, 1_000_000m);
query.AggregateInfo.AddAggregateColumn("big_accounts", "", ArxAggregateOperator.Count);

var tile = (await service.RetrieveMultipleAsync(query)).Entities[0];
Console.WriteLine($"{tile["big_accounts"]} accounts with a revenue of a million or more");
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number LIKE 'ACC-R63%' AND annual_revenue >= 1000000;
```

Expect: 5.

### Dates and times in conditions

Real case 4 of RetrieveMultiple.

Dates in conditions, in three scenarios: a date column against a `DateOnly`; a `datetimeoffset` column against a UTC moment; and "on this day" for a moment, comparing only its date part. No hidden offset is added.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var accounts = new List<ArxEntity>();
foreach (var (number, founded) in new[] { ("ACC-R651", new DateOnly(1999, 12, 31)), ("ACC-R652", new DateOnly(2000, 1, 1)), ("ACC-R653", new DateOnly(2010, 6, 15)) })
{
    var account = new ArxEntity("account");
    account["name"] = "Dated " + number;
    account["account_number"] = number;
    account["founded_on"] = founded;
    account["modified_on"] = new DateTime(2026, 2, 1, 23, 30, 0, DateTimeKind.Utc).AddDays(int.Parse(number[^1..]) - 1);
    accounts.Add(account);
}
await service.CreateMultipleAsync(accounts);
```

#### 1. Founded on or after 1 January 2000

Founded in 2000 or later: a date column and a DateOnly value.

In scope: `service` (ArxMsSqlDbService).

```csharp
Console.WriteLine(await NumbersAsync(service, "founded_on", ArxConditionOperator.GreaterEqual, new DateOnly(2000, 1, 1)));
```

Defined in the sample, used above:

```csharp
private static async Task<string> NumbersAsync(ArxMsSqlDbService service, string attribute, ArxConditionOperator op, object value)
{
    var query = new ArxQueryExpression("account");
    query.ColumnSet.AddColumn("account_number");
    query.Criteria.AddCondition("account_number", ArxConditionOperator.BeginsWith, "ACC-R65");
    query.Criteria.AddCondition(attribute, op, value);
    query.AddOrder("account_number", ArxOrderType.Ascending);
    var rows = await service.RetrieveMultipleAsync(query);
    return string.Join(", ", rows.Entities.Select(x => x["account_number"]));
}
```

Check it in the database:

```sql
SELECT account_number
FROM account
WHERE account_number LIKE 'ACC-R65%' AND founded_on >= '2000-01-01'
ORDER BY account_number;
```

Expect: 2 rows, ACC-R652, ACC-R653.

#### 2. Changed after 2 February 2026, 23:00 UTC

Changed after a UTC moment: a datetimeoffset column and a UTC DateTime, compared to the second.

In scope: `service` (ArxMsSqlDbService).

```csharp
Console.WriteLine(await NumbersAsync(service, "modified_on", ArxConditionOperator.GreaterThan, new DateTime(2026, 2, 2, 23, 0, 0, DateTimeKind.Utc)));
```

Check it in the database:

```sql
SELECT account_number
FROM account
WHERE account_number LIKE 'ACC-R65%' AND modified_on > CAST('2026-02-02 23:00:00 +00:00' AS datetimeoffset)
ORDER BY account_number;
```

Expect: 2 rows, ACC-R652 (23:30 on the 2nd), ACC-R653.

#### 3. Changed on 1 February 2026 (any time that day)

Changed on 1 February: only the date part of the moment is compared.

In scope: `service` (ArxMsSqlDbService).

```csharp
var onTheDay = new ArxDateTimeConditionOption { Value = new DateTime(2026, 2, 1), ComparingType = ArxDateTimeConditionOptionType.DateOnly };
Console.WriteLine(await NumbersAsync(service, "modified_on", ArxConditionOperator.Equal, onTheDay));
```

Check it in the database:

```sql
SELECT account_number
FROM account
WHERE account_number LIKE 'ACC-R65%' AND CAST(modified_on AS date) = '2026-02-01'
ORDER BY account_number;
```

Expect: 1 row, ACC-R651 (the date in the stored offset: ARX writes a UTC DateTime as +00:00).

### Read a view

Real case 5 of RetrieveMultiple.

A view, read like a table, in two scenarios: the view as it is (no declaration needed: no RecordId); and the view declared with its id, so its rows carry a RecordId and Retrieve by id works.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "Viewed Ventures";
account["account_number"] = "ACC-R661";
var accountId = await service.CreateAsync(account);
foreach (var first in new[] { "Vera", "Vik" })
{
    var contact = new ArxEntity("contact");
    contact["first_name"] = first;
    contact["last_name"] = "View-R661";
    contact["account_id"] = new ArxEntityReference("account", accountId);
    await service.CreateAsync(contact);
}
```

#### 1. The view as it is

vw_account_overview (an account and its number of contacts), filtered like a table.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = new ArxQueryExpression("vw_account_overview");
query.ColumnSet.AddColumns("name", "contacts");
query.Criteria.AddCondition("account_number", ArxConditionOperator.Equal, "ACC-R661");

var row = (await service.RetrieveMultipleAsync(query)).Entities[0];
Console.WriteLine($"{row["name"]}: {row["contacts"]} contacts, RecordId {(row.RecordId == null ? "none (the view isn't declared)" : row.RecordId)}");
```

Check it in the database:

```sql
SELECT name, contacts
FROM vw_account_overview
WHERE account_number = 'ACC-R661';
```

Expect: 1 row, Viewed Ventures, 2.

#### 2. The view declared with its id

Declared with its id (a view has no primary key, so the id is supplied, not generated): RecordId and Retrieve by id.

In scope: `connectionString` (string), `accountId` (ArxRecordId).

```csharp
var service = new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("vw_account_overview", ArxIdColumn.Supplied<long>("id"))
    .Build());

var row = await service.RetrieveAsync("vw_account_overview", accountId.AsInt64(), new ArxColumnSet("name", "contacts")) ?? throw new InvalidOperationException("The vw_account_overview record isn't there.");
Console.WriteLine($"Retrieve by id: {row["name"]}, RecordId {row.RecordId}");
```

Check it in the database:

```sql
SELECT o.name, o.contacts
FROM vw_account_overview o
JOIN account a ON a.id = o.id
WHERE a.account_number = 'ACC-R661';
```

Expect: 1 row, Viewed Ventures, 2.

### Read a function that returns a table

Real case 6 of RetrieveMultiple.

A table-valued function, read like a table, in three scenarios: its argument by position, sent as a typed parameter; filtered, ordered and paged on top; and a named argument, which SQL Server's functions don't take (ARX8001, before anything is sent). SQL Server functions can't change data, so a function read is only a read.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "Function Foods";
account["account_number"] = "ACC-R671";
var accountId = await service.CreateAsync(account);
foreach (var first in new[] { "Fay", "Finn", "Flo", "Fred" })
{
    var contact = new ArxEntity("contact");
    contact["first_name"] = first;
    contact["last_name"] = "Func-R671";
    contact["email"] = $"{first.ToLowerInvariant()}.r671@example.com";
    contact["account_id"] = new ArxEntityReference("account", accountId);
    await service.CreateAsync(contact);
}
```

#### 1. An argument by position

fn_contacts_of_account(@p0): the argument, by position, is a typed parameter.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
var query = new ArxQueryExpression(ArxRowSource.Function("fn_contacts_of_account", accountId.AsInt64()));
query.AddOrder("first_name", ArxOrderType.Ascending);

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

Check it in the database:

```sql
SELECT f.first_name
FROM account a CROSS APPLY dbo.fn_contacts_of_account(a.id) f
WHERE a.account_number = 'ACC-R671'
ORDER BY f.first_name;
```

Expect: 4 rows, Fay, Finn, Flo, Fred.

#### 2. Filtered, ordered and paged

Criteria, order and paging on top of the function, as on a table.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
var query = new ArxQueryExpression(ArxRowSource.Function("fn_contacts_of_account", accountId.AsInt64()));
query.Criteria.AddCondition("first_name", ArxConditionOperator.BeginsWith, "F");
query.Criteria.AddCondition("first_name", ArxConditionOperator.NotEqual, "Flo");
query.AddOrder("first_name", ArxOrderType.Descending);
query.PageInfo = new ArxPagingInfo { Count = 2, ReturnTotalRecordCount = true };

var page = await service.RetrieveMultipleAsync(query);
Console.WriteLine($"{string.Join(", ", page.Entities.Select(x => x["first_name"]))} (page 1 of {page.TotalPageCount}, {page.TotalRecordCount} in all)");
```

Check it in the database:

```sql
SELECT f.first_name
FROM account a CROSS APPLY dbo.fn_contacts_of_account(a.id) f
WHERE a.account_number = 'ACC-R671' AND f.first_name <> 'Flo'
ORDER BY f.first_name DESC
OFFSET 0 ROWS FETCH NEXT 2 ROWS ONLY;
```

Expect: 2 rows, Fred, Finn (3 in all).

#### 3. A named argument is refused

The same function with its argument by name: SQL Server's functions take their arguments by position only, so ARX refuses it before anything is sent instead of guessing the order. Pass the arguments in the function's order.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
try
{
    await service.RetrieveMultipleAsync(new ArxQueryExpression(ArxRowSource.Function("fn_contacts_of_account", ("p_account_id", (object?)accountId.AsInt64()))));
}
catch (ArxNotSupportedException error) when (error.Code == ArxErrorCode.NotSupported)
{
    Console.WriteLine(error.Message);
}
```

Check it in the database:

```sql
SELECT name, type_desc
FROM sys.objects
WHERE name = 'fn_contacts_of_account';
```

Expect: 1 row, SQL_INLINE_TABLE_VALUED_FUNCTION: the function is there; only the named call was refused.

### Lookups and display names

Real case 7 of RetrieveMultiple.

Lookups read back as references with a display name, in three scenarios: a contact's account, named by the account's name (declared in SampleCatalog.cs); an account without one; and the names turned off when a screen doesn't need them (one join fewer).

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "Named Nurseries";
account["account_number"] = "ACC-R681";
var accountId = await service.CreateAsync(account);
var withAccount = new ArxEntity("contact");
withAccount["first_name"] = "Nora";
withAccount["last_name"] = "Lookup-R681";
withAccount["account_id"] = new ArxEntityReference("account", accountId);
var noraId = await service.CreateAsync(withAccount);
var withoutAccount = new ArxEntity("contact");
withoutAccount["first_name"] = "Ned";
withoutAccount["last_name"] = "Lookup-R681";
await service.CreateAsync(withoutAccount);
```

#### 1. A contact's account, with its name

account_id reads back as an ArxEntityReference: the table, the id, and the account's name.

In scope: `service` (ArxMsSqlDbService), `contactId` (ArxRecordId).

```csharp
var contact = await service.RetrieveAsync("contact", contactId, new ArxColumnSet("first_name", "account_id")) ?? throw new InvalidOperationException("The contact record isn't there.");
var account = (ArxEntityReference)contact["account_id"]!;
Console.WriteLine($"{contact["first_name"]} → {account.EntityName} {account.RecordId}: \"{account.Name}\"");
```

Check it in the database:

```sql
SELECT c.first_name, a.name
FROM contact c
JOIN account a ON a.id = c.account_id
WHERE c.first_name = 'Nora' AND c.last_name = 'Lookup-R681';
```

Expect: 1 row, Nora, Named Nurseries.

#### 2. A contact without an account

A contact without an account: the attribute is there, and null.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = new ArxQueryExpression("contact");
query.ColumnSet.AddColumns("first_name", "account_id");
query.Criteria.AddCondition("first_name", ArxConditionOperator.Equal, "Ned");
query.Criteria.AddCondition("last_name", ArxConditionOperator.Equal, "Lookup-R681");

var ned = (await service.RetrieveMultipleAsync(query)).Entities[0];
Console.WriteLine($"{ned["first_name"]} → {(ned["account_id"] == null ? "no account" : "an account")}");
```

Check it in the database:

```sql
SELECT CASE WHEN account_id IS NULL THEN 1 ELSE 0 END AS no_account
FROM contact
WHERE first_name = 'Ned' AND last_name = 'Lookup-R681';
```

Expect: 1 row, 1.

#### 3. Names off: the id only, one join fewer

LookupNames = false: the references keep their id, without the join that reads the names.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = new ArxQueryExpression("contact");
query.ColumnSet.AddColumns("first_name", "account_id");
query.Criteria.AddCondition("last_name", ArxConditionOperator.Equal, "Lookup-R681");
query.Criteria.AddCondition("account_id", ArxConditionOperator.NotNull);

var nora = (await service.RetrieveMultipleAsync(query, new ArxRetrieveOptions { LookupNames = false })).Entities[0];
var account = (ArxEntityReference)nora["account_id"]!;
Console.WriteLine($"{nora["first_name"]} → {account.EntityName} {account.RecordId}, name {(account.Name == null ? "not read" : account.Name)}");
```

Check it in the database:

```sql
SELECT CASE WHEN account_id IS NOT NULL THEN 1 ELSE 0 END AS has_account
FROM contact
WHERE first_name = 'Nora' AND last_name = 'Lookup-R681';
```

Expect: 1 row, 1.

### Turn read errors into messages

Real case 8 of RetrieveMultiple.

Read errors turned into messages, in two scenarios: what the database refuses (a table or column that doesn't exist, a value of the wrong type for its column); and what ARX refuses before the database (an id of the wrong type, an operator without the values it needs, a removed column form).

Usings:

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

#### 1. What the database refuses

The database refuses: each error has a code to choose the message by.

In scope: `service` (ArxMsSqlDbService).

```csharp
var noTable = new ArxQueryExpression("accounts_typo");

var noColumn = new ArxQueryExpression("account");
noColumn.ColumnSet.AddColumn("fax_number");

var textForNumber = new ArxQueryExpression("account");
textForNumber.Criteria.AddCondition("employee_count", ArxConditionOperator.Equal, "forty");

foreach (var (what, query) in new[] { ("a table that doesn't exist", noTable), ("a column that doesn't exist", noColumn), ("text for a number", textForNumber) })
{
    try
    {
        await service.RetrieveMultipleAsync(query);
    }
    catch (ArxDatabaseException error)
    {
        Console.WriteLine($"{what}: {error.Code}, the user sees \"{ForTheUser(error)}\"");
    }
}
```

Defined in the sample, used above:

```csharp
private static string ForTheUser(ArxException error) => error.Code switch
{
    ArxErrorCode.UndefinedEntity => "That list doesn't exist.",
    ArxErrorCode.UndefinedAttribute => "One of the fields doesn't exist.",
    ArxErrorCode.DataTypeMismatch => "One of the filter values has the wrong type.",
    _ => "The search couldn't be run."
};
```

Check it in the database:

```sql
SELECT CASE WHEN OBJECT_ID('dbo.accounts_typo') IS NULL THEN 1 ELSE 0 END AS no_such_table,
       CASE WHEN COL_LENGTH('dbo.account', 'fax_number') IS NULL THEN 1 ELSE 0 END AS no_such_column;
```

Expect: 1, 1: nothing was created or changed by the failed reads.

#### 2. What ARX refuses before the database

ARX refuses before the database: these never reach it.

In scope: `service` (ArxMsSqlDbService).

```csharp
var between = new ArxQueryExpression("account");
between.Criteria.AddCondition("employee_count", ArxConditionOperator.Between, 10);

var removed = new ArxQueryExpression("account");
removed.ColumnSet.AddColumn("status._opt");

var attempts = new (string What, Func<Task> Call)[]
{
    ("a text id for a number id", () => service.RetrieveAsync("account", "ACC-R691")),
    ("Between with one value", () => service.RetrieveMultipleAsync(between)),
    ("a removed column form", () => service.RetrieveMultipleAsync(removed)),
};

foreach (var (what, call) in attempts)
{
    try
    {
        await call();
    }
    catch (ArxException error) when (error is ArxValidationException or ArxNotSupportedException)
    {
        Console.WriteLine($"{what}: {error.Code}");
    }
}
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number = 'ACC-R691';
```

Expect: 0: the refused reads were about records that don't exist, and changed nothing.

### Catch an SQL injection attempt on a search

Real case 9 of RetrieveMultiple.

Catching an SQL injection attempt on a search, in four scenarios. The sort field and the filter field of a search screen come from the request: SQL in them is ARX1017 SuspectedInjection, refused before the database. A badly formed name is ARX1003, a well-formed misspelling ARX5002. SQL in a search value, the classic `' OR '1'='1`, is a parameter: it matches nothing.

Usings:

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

Set up by the sample before its scenarios:

```csharp
foreach (var number in new[] { "ACC-R6A1", "ACC-R6A2" })
{
    var account = new ArxEntity("account");
    account["name"] = "Injection search " + number;
    account["account_number"] = number;
    await service.CreateAsync(account);
}
```

#### 1. A sort field from the request carries SQL

`?sort=name; DROP TABLE account; --` used as the order column.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = new ArxQueryExpression("account");
query.ColumnSet.AddColumn("name");
query.AddOrder("name; DROP TABLE account; --", ArxOrderType.Ascending);

try
{
    await service.RetrieveMultipleAsync(query);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.SuspectedInjection)
{
    Report(error);
}
```

Defined in the sample, used above:

```csharp
private static void Report(ArxValidationException error)
    => Console.WriteLine(error.Code == ArxErrorCode.SuspectedInjection ? $"SECURITY: {error.Message}" : $"bug, not an attack: {error.Message}");
```

Check it in the database:

```sql
SELECT CASE WHEN OBJECT_ID('dbo.account') IS NOT NULL THEN 1 ELSE 0 END AS account_exists;
```

Expect: 1.

#### 2. A filter field from the request carries SQL

`?field=name) OR (1=1` used as the condition's column.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = new ArxQueryExpression("account");
query.Criteria.AddCondition("name) OR (1=1", ArxConditionOperator.Equal, "x");

try
{
    await service.RetrieveMultipleAsync(query);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.SuspectedInjection)
{
    Report(error);
}

// The fix: the fields a search may filter or sort by, from a fixed list.
var searchable = new HashSet<string> { "name", "account_number", "status" };
Console.WriteLine(searchable.Contains("name) OR (1=1") ? "would search" : "the allow-list refused the field");
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number LIKE 'ACC-R6A%';
```

Expect: 2: the accounts are there; the refused search read none of them.

#### 3. SQL in the search value matches nothing

The classic in a value: concatenated it would return every account; as a parameter it is a name nobody has.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = new ArxQueryExpression("account");
query.ColumnSet.AddColumn("name");
query.Criteria.AddCondition("name", ArxConditionOperator.Equal, "x' OR '1'='1");

var rows = await service.RetrieveMultipleAsync(query);
Console.WriteLine($"{rows.Entities.Count} accounts found");
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE name = 'x'' OR ''1''=''1';
```

Expect: 0.

#### 4. A badly formed name is ARX1003, not ARX1017

A badly formed field name ("account number", with a space) is ARX1003: a bug to fix, not an attack. A well-formed misspelling reaches the database and is ARX5002.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = new ArxQueryExpression("account");
query.AddOrder("account number", ArxOrderType.Ascending);

try
{
    await service.RetrieveMultipleAsync(query);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.InvalidIdentifier)
{
    Report(error);
}
```

Check it in the database:

```sql
SELECT count(*)
FROM information_schema.columns
WHERE table_name = 'account' AND column_name = 'account_number';
```

Expect: 1: the column is account_number, with an underscore.

### Read in a batch scope

Advanced case 1 of RetrieveMultiple.

Reads in a batch scope, in two scenarios: an order created in the scope, read back to total it before the order header is updated, all in one transaction; and a read that fails in the scope, undone alone by its savepoint, so the scope carries on.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "Scoped Supplies";
account["account_number"] = "ACC-R721";
var accountId = await service.CreateAsync(account);
var product = new ArxEntity("product");
product["sku"] = "SKU-R721";
product["name"] = "Scoped widget";
product["unit_price"] = 10.00m;
var productId = await service.CreateAsync(product);
```

#### 1. Create, read back, update: one transaction

Create an order and its lines, read the lines back to total them, write the total, commit.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId), `productId` (ArxRecordId).

```csharp
await service.RunBatchAsync(async scope =>
{
    var order = new ArxEntity("sales_order");
    order["order_number"] = "SO-R721";
    order["account_id"] = new ArxEntityReference("account", accountId);
    order["ordered_on"] = new DateTime(2026, 11, 8, 9, 0, 0, DateTimeKind.Utc);
    var orderId = await scope.CreateAsync(order);

    var lines = new List<ArxEntity>();

    foreach (var (quantity, price) in new[] { (2, 10.00m), (1, 25.50m), (3, 4.25m) })
    {
        var line = new ArxEntity("order_line");
        line["order_id"] = new ArxEntityReference("sales_order", orderId);
        line["product_id"] = new ArxEntityReference("product", productId);
        line["quantity"] = quantity;
        line["unit_price"] = price;
        lines.Add(line);
    }

    await scope.CreateMultipleAsync(lines);

    // The lines aren't committed yet: only a read in this scope sees them.
    var query = new ArxQueryExpression("order_line");
    query.ColumnSet.AddColumns("quantity", "unit_price");
    query.Criteria.AddCondition("order_id", ArxConditionOperator.Equal, orderId.AsInt64());
    var read = await scope.RetrieveMultipleAsync(query);
    var total = read.Entities.Sum(x => (int)x["quantity"]! * (decimal)x["unit_price"]!);

    var header = new ArxEntity("sales_order") { RecordId = orderId };
    header["total"] = total;
    await scope.UpdateAsync(header);

    Console.WriteLine($"{read.Entities.Count} lines read in the scope, total {total:N2} written");
});
```

Check it in the database:

```sql
SELECT total
FROM sales_order
WHERE order_number = 'SO-R721';
```

Expect: 1 row, 58.25.

#### 2. A failed read in the scope is undone alone

A read of a table that doesn't exist fails; its savepoint undoes it alone, and the scope commits the rest.

In scope: `service` (ArxMsSqlDbService).

```csharp
await using var scope = await service.BeginBatchAsync(new ArxBatchOptions { Savepoints = true });

var before = new ArxEntity("contact");
before["first_name"] = "Before";
before["last_name"] = "Scope-R722";
await scope.CreateAsync(before);

try
{
    await scope.RetrieveMultipleAsync(new ArxQueryExpression("no_such_table"));
}
catch (ArxDatabaseException error)
{
    Console.WriteLine($"call {error.BatchNumber} failed: {error.Code}; the scope is still {scope.State}");
}

var after = new ArxEntity("contact");
after["first_name"] = "After";
after["last_name"] = "Scope-R722";
await scope.CreateAsync(after);

await scope.CommitAsync();
```

Check it in the database:

```sql
SELECT first_name
FROM contact
WHERE last_name = 'Scope-R722'
ORDER BY first_name;
```

Expect: 2 rows, After, Before.

### Export a large table in pages

Advanced case 2 of RetrieveMultiple.

Exporting a large table, in two scenarios: every row in pages of 500 (each page one round trip, the memory of one page at a time); and why paging without an order is still safe with ARX (the declared id breaks ties).

Usings:

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

Set up by the sample before its scenarios:

```csharp
var products = Enumerable.Range(1, PRODUCTS).Select(i =>
{
    var product = new ArxEntity("product");
    product["sku"] = $"EXP-R731-{i:D5}";
    product["name"] = $"Exported product {i}";
    product["unit_price"] = 1m + i % 100;
    return product;
}).ToList();
await service.CreateMultipleAsync(products);
```

#### 1. Every row, 500 at a time

Read page after page until HasNextPage is false; write each page out before reading the next.

In scope: `service` (ArxMsSqlDbService).

```csharp
var watch = Stopwatch.StartNew();
var exported = 0;
var pages = 0;
var total = 0m;
ArxEntityCollection page;

do
{
    page = await service.RetrieveMultipleAsync(Page(++pages, ordered: true));
    exported += page.Entities.Count;
    total += page.Entities.Sum(x => (decimal)x["unit_price"]!);
}
while (page.HasNextPage);

Console.WriteLine($"{exported} products in {pages} pages, prices summing to {total:N0} ({watch.ElapsedMilliseconds} ms on this machine; not a benchmark)");
```

Defined in the sample, used above:

```csharp
private static ArxQueryExpression Page(int number, bool ordered)
{
    var query = new ArxQueryExpression("product");
    query.ColumnSet.AddColumns("sku", "unit_price");
    query.Criteria.AddCondition("sku", ArxConditionOperator.BeginsWith, "EXP-R731-");

    if (ordered)
    {
        query.AddOrder("sku", ArxOrderType.Ascending);
    }

    query.PageInfo = new ArxPagingInfo { Count = 500, PageNumber = number };
    return query;
}
```

Check it in the database:

```sql
SELECT count(*), sum(unit_price)
FROM product
WHERE sku LIKE 'EXP-R731-%';
```

Expect: 2000, 101000.00: what the export read.

#### 2. Pages without an order: still no row twice

No order given: ARX adds the declared id as the order, so no row is read twice or missed.

In scope: `service` (ArxMsSqlDbService).

```csharp
var seen = new HashSet<string>();
var read = 0;

for (var number = 1; number <= PRODUCTS / 500; number++)
{
    foreach (var row in (await service.RetrieveMultipleAsync(Page(number, ordered: false))).Entities)
    {
        read++;
        seen.Add((string)row["sku"]!);
    }
}

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

Check it in the database:

```sql
SELECT count(DISTINCT sku)
FROM product
WHERE sku LIKE 'EXP-R731-%';
```

Expect: 2000.

### A security check around a search endpoint

Advanced case 3 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` (ArxMsSqlDbService), `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(ArxMsSqlDbService 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
SELECT account_number
FROM account
WHERE account_number LIKE 'ACC-R74%'
ORDER BY account_number;
```

Expect: 2 rows, 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` (ArxMsSqlDbService), `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
SELECT CASE WHEN OBJECT_ID('dbo.account') IS NOT NULL THEN 1 ELSE 0 END AS account_exists;
```

Expect: 1.

#### 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` (ArxMsSqlDbService), `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(ArxMsSqlDbService 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
SELECT count(*)
FROM information_schema.columns
WHERE table_name = 'account' AND column_name = 'password';
```

Expect: 0: there is no such column, 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` (ArxMsSqlDbService), `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
SELECT count(*)
FROM account
WHERE account_number LIKE 'ACC-R74%';
```

Expect: 2, what the mapped search read.

### Filter expressions in depth: nesting, recursion, links, And and Or

Advanced case 4 of RetrieveMultiple.

Filter expressions in depth, in five scenarios. An `ArxFilterExpression` holds conditions and other filter expressions, each with its own operator (And, Or): a tree, as deep as you need. Each child filter becomes one parenthesised group in the SQL, so the tree is the precedence.

- A tree four levels deep, built by hand and printed by walking it recursively.
- A saved search (a nested rule from a query builder screen) turned into filters by a recursive function.
- A link entity with its own nested criteria, applied in the join.
- Two filters built apart (the user's search and a security rule) joined under And, then under Or.
- The same three conditions grouped two ways: And and Or change the result.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var accountIds = new Dictionary<string, ArxRecordId>();
foreach (var (number, name, industry, revenue, status) in new[]
{
    ("ACC-R751", "Filter Retail", Industry.Retail, 500_000m, "ACTIVE"),
    ("ACC-R752", "Filter Bank", Industry.Finance, 5_000_000m, "ACTIVE"),
    ("ACC-R753", "Filter Closed", Industry.Technology, 2_000_000m, "CLOSED"),
})
{
    var account = new ArxEntity("account");
    account["name"] = name;
    account["account_number"] = number;
    account["industry"] = new ArxSelectionSetValue("Industry", industry);
    account["annual_revenue"] = revenue;
    account["status"] = new ArxSelectionSetValue("AccountStatus", status);
    accountIds[number] = await service.CreateAsync(account);
}
var contacts = new List<ArxEntity>();
foreach (var (first, title, channel, doNotEmail, account) in new (string, string, char, bool, string?)[]
{
    ("Ana", "Sales Manager", 'E', false, "ACC-R751"),
    ("Ben", "Engineer", 'P', false, "ACC-R752"),
    ("Cleo", "Account Manager", 'S', true, "ACC-R752"),
    ("Dan", "Buyer", 'E', true, "ACC-R753"),
    ("Zoe", "Intern", 'P', false, null),
    ("Zack", "Store Manager", 'E', false, "ACC-R753"),
})
{
    var contact = new ArxEntity("contact");
    contact["first_name"] = first;
    contact["last_name"] = LAST_NAME;
    contact["job_title"] = title;
    contact["preferred_channel"] = new ArxSelectionSetValue("ContactChannel", channel);
    contact["do_not_email"] = doNotEmail;
    contact["account_id"] = account == null ? null : new ArxEntityReference("account", accountIds[account]);
    contacts.Add(contact);
}
await service.CreateMultipleAsync(contacts);
```

#### 1. A tree four levels deep

last_name AND ( (title contains "manager" AND (channel = E OR do_not_email = false)) OR (first name begins with "Z" AND (title = "Intern" OR title contains "store")) ).

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = Contacts();

var either = query.Criteria.AddFilter(ArxLogicalOperator.Or);               // level 2

var managers = either.AddFilter(ArxLogicalOperator.And);                     // level 3
managers.AddCondition("job_title", ArxConditionOperator.Contains, "manager");
var reachable = managers.AddFilter(ArxLogicalOperator.Or);                   // level 4
reachable.AddCondition("preferred_channel", ArxConditionOperator.Equal, "E");
reachable.AddCondition("do_not_email", ArxConditionOperator.Equal, false);

var zNames = either.AddFilter(ArxLogicalOperator.And);                       // level 3
zNames.AddCondition("first_name", ArxConditionOperator.BeginsWith, "Z");
var juniorOrStore = zNames.AddFilter(ArxLogicalOperator.Or);                 // level 4
juniorOrStore.AddCondition("job_title", ArxConditionOperator.Equal, "Intern");
juniorOrStore.AddCondition("job_title", ArxConditionOperator.Contains, "store");

Print(query.Criteria);
Console.WriteLine($"found: {await FirstNamesAsync(service, query)}");
```

Defined in the sample, used above:

```csharp
private static ArxQueryExpression Contacts()
{
    var query = new ArxQueryExpression("contact");
    query.ColumnSet.AddColumns("first_name", "job_title");
    query.Criteria.AddCondition("last_name", ArxConditionOperator.Equal, LAST_NAME);
    query.AddOrder("first_name", ArxOrderType.Ascending);
    return query;
}

private static async Task<string> FirstNamesAsync(ArxMsSqlDbService service, ArxQueryExpression query)
    => string.Join(", ", (await service.RetrieveMultipleAsync(query)).Entities.Select(x => x["first_name"]));

private static void Print(ArxFilterExpression filter, int depth = 0)
{
    var indent = new string(' ', depth * 4);
    Console.WriteLine($"{indent}{filter.FilterOperator.ToString().ToUpperInvariant()} (level {depth + 1})");

    foreach (var condition in filter.Conditions)
    {
        Console.WriteLine($"{indent}    {condition.AttributeName} {condition.Operator} {string.Join(", ", condition.Values.Select(v => v is ArxSelectionSetValue choice ? $"{choice.SetName} {choice.Value}" : v))}");
    }

    foreach (var child in filter.Filters)
    {
        Print(child, depth + 1);
    }
}
```

Check it in the database:

```sql
SELECT first_name
FROM contact
WHERE last_name = 'Filter-R751'
  AND ( (job_title LIKE '%manager%' AND (preferred_channel = 'E' OR do_not_email = 0))
     OR (first_name LIKE 'Z%' AND (job_title = 'Intern' OR job_title LIKE '%store%')) )
ORDER BY first_name;
```

Expect: 3 rows, Ana, Zack, Zoe (Cleo is a manager, but neither by e-mail nor willing to get one).

#### 2. A saved search, built recursively

A saved search five levels deep: (manager AND (e-mail OR (do-not-email AND (first name B… OR C…)))) OR first name is Dan. The recursive ToFilter turns any depth into filters.

In scope: `service` (ArxMsSqlDbService).

```csharp
var none = Array.Empty<Rule>();
var saved = new Rule(ArxLogicalOperator.Or,
    new[] { ("first_name", ArxConditionOperator.Equal, (object)"Dan") },
    new[]
    {
        new Rule(ArxLogicalOperator.And,
            new[] { ("job_title", ArxConditionOperator.Contains, (object)"manager") },
            new[]
            {
                new Rule(ArxLogicalOperator.Or,
                    new[] { ("preferred_channel", ArxConditionOperator.Equal, (object)"E") },
                    new[]
                    {
                        new Rule(ArxLogicalOperator.And,
                            new[] { ("do_not_email", ArxConditionOperator.Equal, (object)true) },
                            new[]
                            {
                                new Rule(ArxLogicalOperator.Or,
                                    new[] { ("first_name", ArxConditionOperator.BeginsWith, (object)"B"), ("first_name", ArxConditionOperator.BeginsWith, (object)"C") },
                                    none),
                            }),
                    }),
            }),
    });

var query = Contacts();
query.Criteria.AddFilter(ToFilter(saved));

Print(query.Criteria);
Console.WriteLine($"found: {await FirstNamesAsync(service, query)}");
```

Defined in the sample, used above:

```csharp
private static ArxQueryExpression Contacts()
{
    var query = new ArxQueryExpression("contact");
    query.ColumnSet.AddColumns("first_name", "job_title");
    query.Criteria.AddCondition("last_name", ArxConditionOperator.Equal, LAST_NAME);
    query.AddOrder("first_name", ArxOrderType.Ascending);
    return query;
}

private static async Task<string> FirstNamesAsync(ArxMsSqlDbService service, ArxQueryExpression query)
    => string.Join(", ", (await service.RetrieveMultipleAsync(query)).Entities.Select(x => x["first_name"]));

private static void Print(ArxFilterExpression filter, int depth = 0)
{
    var indent = new string(' ', depth * 4);
    Console.WriteLine($"{indent}{filter.FilterOperator.ToString().ToUpperInvariant()} (level {depth + 1})");

    foreach (var condition in filter.Conditions)
    {
        Console.WriteLine($"{indent}    {condition.AttributeName} {condition.Operator} {string.Join(", ", condition.Values.Select(v => v is ArxSelectionSetValue choice ? $"{choice.SetName} {choice.Value}" : v))}");
    }

    foreach (var child in filter.Filters)
    {
        Print(child, depth + 1);
    }
}

private sealed record Rule(ArxLogicalOperator Operator, (string Attribute, ArxConditionOperator Operator, object Value)[] Conditions, Rule[] Groups);

private static ArxFilterExpression ToFilter(Rule rule)
{
    var filter = new ArxFilterExpression(rule.Operator);

    foreach (var (attribute, op, value) in rule.Conditions)
    {
        filter.AddCondition(attribute, op, value);
    }

    foreach (var group in rule.Groups)
    {
        filter.AddFilter(ToFilter(group));
    }

    return filter;
}
```

Check it in the database:

```sql
SELECT first_name
FROM contact
WHERE last_name = 'Filter-R751'
  AND ( first_name = 'Dan'
     OR (job_title LIKE '%manager%'
         AND (preferred_channel = 'E'
              OR (do_not_email = 1 AND (first_name LIKE 'B%' OR first_name LIKE 'C%')))) )
ORDER BY first_name;
```

Expect: 4 rows, Ana, Cleo, Dan, Zack (Cleo through the deepest group).

#### 3. A link entity with its own filter

A link entity with its own filter tree: contacts of accounts that are Retail, OR (a million or more AND active). The link's criteria go in the join (`ON … AND (…)`); on an INNER join they also drop the contacts whose account doesn't match.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = Contacts();
var account = query.AddLink("account", "account_id", "id", ArxJoinOperator.Inner);
account.EntityAlias = "acc";
account.ColumnSet.AddColumn("name");

// The link's filter is an ArxFilterExpression too: its conditions refer to the account's columns.
account.LinkCriteria = new ArxFilterExpression(ArxLogicalOperator.Or);
account.LinkCriteria.AddCondition("industry", ArxConditionOperator.Equal, new ArxSelectionSetValue("Industry", Industry.Retail));
var bigAndActive = account.LinkCriteria.AddFilter(ArxLogicalOperator.And);
bigAndActive.AddCondition("annual_revenue", ArxConditionOperator.GreaterEqual, 1_000_000m);
bigAndActive.AddCondition("status", ArxConditionOperator.Equal, "ACTIVE");

Print(account.LinkCriteria);
var rows = await service.RetrieveMultipleAsync(query);
Console.WriteLine($"found: {string.Join(", ", rows.Entities.Select(x => $"{x["first_name"]} ({x["acc.name"]})"))}");
```

Defined in the sample, used above:

```csharp
private static ArxQueryExpression Contacts()
{
    var query = new ArxQueryExpression("contact");
    query.ColumnSet.AddColumns("first_name", "job_title");
    query.Criteria.AddCondition("last_name", ArxConditionOperator.Equal, LAST_NAME);
    query.AddOrder("first_name", ArxOrderType.Ascending);
    return query;
}

private static void Print(ArxFilterExpression filter, int depth = 0)
{
    var indent = new string(' ', depth * 4);
    Console.WriteLine($"{indent}{filter.FilterOperator.ToString().ToUpperInvariant()} (level {depth + 1})");

    foreach (var condition in filter.Conditions)
    {
        Console.WriteLine($"{indent}    {condition.AttributeName} {condition.Operator} {string.Join(", ", condition.Values.Select(v => v is ArxSelectionSetValue choice ? $"{choice.SetName} {choice.Value}" : v))}");
    }

    foreach (var child in filter.Filters)
    {
        Print(child, depth + 1);
    }
}
```

Check it in the database:

```sql
SELECT c.first_name, a.name
FROM contact c
JOIN account a ON a.id = c.account_id
 AND (a.industry = 1 OR (a.annual_revenue >= 1000000 AND a.status = 'ACTIVE'))
WHERE c.last_name = 'Filter-R751'
ORDER BY c.first_name;
```

Expect: 3 rows, Ana (Filter Retail), Ben (Filter Bank), Cleo (Filter Bank).

#### 4. Two filters joined: under And, then under Or

Two filters built apart and joined: the user's search (first name A… OR Z…) and a security rule (the account isn't closed, OR there is no account). The rule refers to the joined account as `acc.status`. Joined under the criteria (And): both must hold. Joined in an Or group: either is enough.

In scope: `service` (ArxMsSqlDbService).

```csharp
ArxFilterExpression UserSearch()
{
    var search = new ArxFilterExpression(ArxLogicalOperator.Or);
    search.AddCondition("first_name", ArxConditionOperator.BeginsWith, "A");
    search.AddCondition("first_name", ArxConditionOperator.BeginsWith, "Z");
    return search;
}

ArxFilterExpression SecurityRule()
{
    // NotEqual skips NULL (SQL's rule), so "no account" is asked for on its own.
    var rule = new ArxFilterExpression(ArxLogicalOperator.Or);
    rule.AddCondition("acc.status", ArxConditionOperator.NotEqual, "CLOSED");
    rule.AddCondition("acc.id", ArxConditionOperator.Null);
    return rule;
}

ArxQueryExpression WithAccount()
{
    var query = Contacts();
    query.AddLink("account", "account_id", "id", ArxJoinOperator.Left).EntityAlias = "acc";
    return query;
}

var both = WithAccount();
both.Criteria.AddFilter(UserSearch());
both.Criteria.AddFilter(SecurityRule());
Console.WriteLine($"search AND rule: {await FirstNamesAsync(service, both)}");

var either = WithAccount();
var orGroup = either.Criteria.AddFilter(ArxLogicalOperator.Or);
orGroup.AddFilter(UserSearch());
orGroup.AddFilter(SecurityRule());
Console.WriteLine($"search OR rule:  {await FirstNamesAsync(service, either)}");
```

Defined in the sample, used above:

```csharp
private static ArxQueryExpression Contacts()
{
    var query = new ArxQueryExpression("contact");
    query.ColumnSet.AddColumns("first_name", "job_title");
    query.Criteria.AddCondition("last_name", ArxConditionOperator.Equal, LAST_NAME);
    query.AddOrder("first_name", ArxOrderType.Ascending);
    return query;
}

private static async Task<string> FirstNamesAsync(ArxMsSqlDbService service, ArxQueryExpression query)
    => string.Join(", ", (await service.RetrieveMultipleAsync(query)).Entities.Select(x => x["first_name"]));
```

Check it in the database:

```sql
SELECT c.first_name,
       CASE WHEN c.first_name LIKE 'A%' OR c.first_name LIKE 'Z%' THEN 1 ELSE 0 END AS search,
       CASE WHEN a.status <> 'CLOSED' OR a.id IS NULL THEN 1 ELSE 0 END AS [rule]
FROM contact c
LEFT JOIN account a ON a.id = c.account_id
WHERE c.last_name = 'Filter-R751'
ORDER BY c.first_name;
```

Expect: 6 rows; search and rule both 1 for Ana, Zoe; either 1 for Ana, Ben, Cleo, Zack, Zoe (Dan: neither).

#### 5. And and Or: the grouping is the meaning

The same three conditions, two groupings: (e-mail AND willing) OR Z…, against e-mail AND (willing OR Z…).

In scope: `service` (ArxMsSqlDbService).

```csharp
var first = Contacts();
var any = first.Criteria.AddFilter(ArxLogicalOperator.Or);
var emailAndWilling = any.AddFilter(ArxLogicalOperator.And);
emailAndWilling.AddCondition("preferred_channel", ArxConditionOperator.Equal, "E");
emailAndWilling.AddCondition("do_not_email", ArxConditionOperator.Equal, false);
any.AddCondition("first_name", ArxConditionOperator.BeginsWith, "Z");
Console.WriteLine($"(e-mail AND willing) OR Z…: {await FirstNamesAsync(service, first)}");

var second = Contacts();
second.Criteria.AddCondition("preferred_channel", ArxConditionOperator.Equal, "E");
var willingOrZ = second.Criteria.AddFilter(ArxLogicalOperator.Or);
willingOrZ.AddCondition("do_not_email", ArxConditionOperator.Equal, false);
willingOrZ.AddCondition("first_name", ArxConditionOperator.BeginsWith, "Z");
Console.WriteLine($"e-mail AND (willing OR Z…): {await FirstNamesAsync(service, second)}");
```

Check it in the database:

```sql
SELECT first_name,
       CASE WHEN (preferred_channel = 'E' AND do_not_email = 0) OR first_name LIKE 'Z%' THEN 1 ELSE 0 END AS first_grouping,
       CASE WHEN preferred_channel = 'E' AND (do_not_email = 0 OR first_name LIKE 'Z%') THEN 1 ELSE 0 END AS second_grouping
FROM contact
WHERE last_name = 'Filter-R751'
ORDER BY first_name;
```

Expect: 6 rows; first_grouping 1 for Ana, Zack, Zoe; second_grouping 1 for Ana, Zack (Zoe prefers the phone).

### Link entities in depth: chains, branches, criteria per level, recursion

Advanced case 5 of RetrieveMultiple.

Link entities in depth, in five scenarios. An `ArxLinkEntity` joins one table and holds its own columns, criteria, alias, join type, and its own `LinkEntities`: a tree of joins, as deep as needed.

- A chain four joins deep (order line → order → account → parent → grandparent), plus a second branch (product → category), printed by walking the tree.
- Each level with its own join type and criteria.
- Conditions and order on aliases deep in the tree, in the main criteria.
- A self-join chain of ancestors, built by a recursive function.
- A one-to-many link multiplies rows: aggregated per account.

Usings:

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

Set up by the sample before its scenarios:

```csharp
ArxRecordId? parent = null;
var accounts = new Dictionary<string, ArxRecordId>();
foreach (var (number, name) in new[] { ("ACC-R761", "Holding R76"), ("ACC-R762", "Group R76"), ("ACC-R763", "Shop R76") })
{
    var account = new ArxEntity("account");
    account["name"] = name;
    account["account_number"] = number;
    account["parent_account_id"] = parent == null ? null : new ArxEntityReference("account", parent);
    parent = await service.CreateAsync(account);
    accounts[name] = parent;
}
var solo = new ArxEntity("account");
solo["name"] = "Solo R76";
solo["account_number"] = "ACC-R764";
accounts["Solo R76"] = await service.CreateAsync(solo);
foreach (var (first, account) in new[] { ("Sam", "Shop R76"), ("Sue", "Shop R76"), ("Sol", "Solo R76") })
{
    var contact = new ArxEntity("contact");
    contact["first_name"] = first;
    contact["last_name"] = "Links-R76";
    contact["account_id"] = new ArxEntityReference("account", accounts[account]);
    await service.CreateAsync(contact);
}
var categories = new Dictionary<string, ArxRecordId>();
foreach (var name in new[] { "Bikes R76", "Helmets R76" })
{
    var category = new ArxEntity("product_category");
    category["name"] = name;
    categories[name] = await service.CreateAsync(category);
}
var products = new Dictionary<string, ArxRecordId>();
foreach (var (sku, name, category, price) in new[] { ("SKU-R761", "Road bike", "Bikes R76", 900m), ("SKU-R762", "Kids helmet", "Helmets R76", 35m) })
{
    var product = new ArxEntity("product");
    product["sku"] = sku;
    product["name"] = name;
    product["category_id"] = new ArxEntityReference("product_category", categories[category]);
    product["unit_price"] = price;
    products[sku] = await service.CreateAsync(product);
}
foreach (var (orderNumber, account, lines) in new[]
{
    ("SO-R761", "Shop R76", new[] { ("SKU-R761", 2), ("SKU-R762", 1) }),
    ("SO-R762", "Solo R76", new[] { ("SKU-R762", 3) }),
})
{
    var order = new ArxEntity("sales_order");
    order["order_number"] = orderNumber;
    order["account_id"] = new ArxEntityReference("account", accounts[account]);
    order["ordered_on"] = new DateTime(2026, 11, 8, 10, 0, 0, DateTimeKind.Utc);
    var orderId = await service.CreateAsync(order);

    foreach (var (sku, quantity) in lines)
    {
        var line = new ArxEntity("order_line");
        line["order_id"] = new ArxEntityReference("sales_order", orderId);
        line["product_id"] = new ArxEntityReference("product", products[sku]);
        line["quantity"] = quantity;
        line["unit_price"] = sku == "SKU-R761" ? 900m : 35m;
        await service.CreateAsync(line);
    }
}
```

#### 1. A chain four joins deep, and a second branch

Every line with its order, account, parent and grandparent, and its product and category.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = OrderLines(out _, out _, out _, out _);
Print(query.LinkEntities);

foreach (var row in (await service.RetrieveMultipleAsync(query)).Entities)
{
    Console.WriteLine(Line(row));
}
```

Defined in the sample, used above:

```csharp
private static ArxLinkEntity Link(string table, string from, string to, ArxJoinOperator join, string alias, params string[] columns)
{
    var link = new ArxLinkEntity(table, from, to, join) { EntityAlias = alias };
    link.ColumnSet.AddColumns(columns);
    return link;
}

private static ArxQueryExpression OrderLines(out ArxLinkEntity order, out ArxLinkEntity account, out ArxLinkEntity parent, out ArxLinkEntity category)
{
    var query = new ArxQueryExpression("order_line");
    query.ColumnSet.AddColumn("quantity");

    // Branch 1: order → account → parent → grandparent (each link nested in the one before).
    order = Link("sales_order", "order_id", "id", ArxJoinOperator.Inner, "ord", "order_number");
    order.LinkCriteria.AddCondition("order_number", ArxConditionOperator.BeginsWith, "SO-R76");
    account = Link("account", "account_id", "id", ArxJoinOperator.Inner, "acc", "name");
    parent = Link("account", "parent_account_id", "id", ArxJoinOperator.Left, "parent", "name");
    var grandparent = Link("account", "parent_account_id", "id", ArxJoinOperator.Left, "grand", "name");
    parent.LinkEntities.Add(grandparent);
    account.LinkEntities.Add(parent);
    order.LinkEntities.Add(account);
    query.LinkEntities.Add(order);

    // Branch 2: product → category.
    var product = Link("product", "product_id", "id", ArxJoinOperator.Inner, "prod", "sku", "name");
    category = Link("product_category", "category_id", "id", ArxJoinOperator.Inner, "cat", "name");
    product.LinkEntities.Add(category);
    query.LinkEntities.Add(product);

    query.AddOrder("ord", "order_number", ArxOrderType.Ascending);
    query.AddOrder("prod", "sku", ArxOrderType.Ascending);
    return query;
}

private static void Print(IEnumerable<ArxLinkEntity> links, string parentAlias = "main", int depth = 1)
{
    foreach (var link in links)
    {
        Console.WriteLine($"{new string(' ', depth * 4)}{link.JoinOperator} JOIN {link.LinkToEntityName} AS {link.EntityAlias} ON {link.EntityAlias}.{link.LinkToAttributeName} = {parentAlias}.{link.LinkFromAttributeName}"
            + (link.LinkCriteria.Conditions.Count + link.LinkCriteria.Filters.Count > 0 ? "  (+ its own criteria)" : string.Empty));
        Print(link.LinkEntities, link.EntityAlias, depth + 1);
    }
}

private static string Line(ArxEntity x)
    => $"{x["ord.order_number"]} {x["prod.sku"]} x{x["quantity"]} ({x["cat.name"]}) for {x["acc.name"]} < {x["parent.name"] ?? "-"} < {x["grand.name"] ?? "-"}";
```

Check it in the database:

```sql
SELECT o.order_number, p.sku, l.quantity, c.name, a.name, pa.name, ga.name
FROM order_line l
JOIN sales_order o ON o.id = l.order_id AND o.order_number LIKE 'SO-R76%'
JOIN account a ON a.id = o.account_id
LEFT JOIN account pa ON pa.id = a.parent_account_id
LEFT JOIN account ga ON ga.id = pa.parent_account_id
JOIN product p ON p.id = l.product_id
JOIN product_category c ON c.id = p.category_id
ORDER BY o.order_number, p.sku;
```

Expect: 3 rows: SO-R761 SKU-R761 2 Bikes R76 Shop R76 Group R76 Holding R76; SO-R761 SKU-R762 1 Helmets R76 Shop R76 Group R76 Holding R76; SO-R762 SKU-R762 3 Helmets R76 Solo R76 NULL NULL.

#### 2. Each level with its own criteria: INNER filters, LEFT keeps

Criteria at two depths: the category (an INNER join, so it filters the lines) must be helmets; the parent (a LEFT join, so it filters nothing) must begin with "Group", or it reads as empty.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = OrderLines(out _, out _, out var parent, out var category);
category.LinkCriteria.AddCondition("name", ArxConditionOperator.Equal, "Helmets R76");
parent.LinkCriteria.AddCondition("name", ArxConditionOperator.BeginsWith, "Group");

foreach (var row in (await service.RetrieveMultipleAsync(query)).Entities)
{
    Console.WriteLine(Line(row));
}
```

Defined in the sample, used above:

```csharp
private static ArxLinkEntity Link(string table, string from, string to, ArxJoinOperator join, string alias, params string[] columns)
{
    var link = new ArxLinkEntity(table, from, to, join) { EntityAlias = alias };
    link.ColumnSet.AddColumns(columns);
    return link;
}

private static ArxQueryExpression OrderLines(out ArxLinkEntity order, out ArxLinkEntity account, out ArxLinkEntity parent, out ArxLinkEntity category)
{
    var query = new ArxQueryExpression("order_line");
    query.ColumnSet.AddColumn("quantity");

    // Branch 1: order → account → parent → grandparent (each link nested in the one before).
    order = Link("sales_order", "order_id", "id", ArxJoinOperator.Inner, "ord", "order_number");
    order.LinkCriteria.AddCondition("order_number", ArxConditionOperator.BeginsWith, "SO-R76");
    account = Link("account", "account_id", "id", ArxJoinOperator.Inner, "acc", "name");
    parent = Link("account", "parent_account_id", "id", ArxJoinOperator.Left, "parent", "name");
    var grandparent = Link("account", "parent_account_id", "id", ArxJoinOperator.Left, "grand", "name");
    parent.LinkEntities.Add(grandparent);
    account.LinkEntities.Add(parent);
    order.LinkEntities.Add(account);
    query.LinkEntities.Add(order);

    // Branch 2: product → category.
    var product = Link("product", "product_id", "id", ArxJoinOperator.Inner, "prod", "sku", "name");
    category = Link("product_category", "category_id", "id", ArxJoinOperator.Inner, "cat", "name");
    product.LinkEntities.Add(category);
    query.LinkEntities.Add(product);

    query.AddOrder("ord", "order_number", ArxOrderType.Ascending);
    query.AddOrder("prod", "sku", ArxOrderType.Ascending);
    return query;
}

private static string Line(ArxEntity x)
    => $"{x["ord.order_number"]} {x["prod.sku"]} x{x["quantity"]} ({x["cat.name"]}) for {x["acc.name"]} < {x["parent.name"] ?? "-"} < {x["grand.name"] ?? "-"}";
```

Check it in the database:

```sql
SELECT o.order_number, p.sku, l.quantity, a.name, pa.name
FROM order_line l
JOIN sales_order o ON o.id = l.order_id AND o.order_number LIKE 'SO-R76%'
JOIN account a ON a.id = o.account_id
LEFT JOIN account pa ON pa.id = a.parent_account_id AND pa.name LIKE 'Group%'
JOIN product p ON p.id = l.product_id
JOIN product_category c ON c.id = p.category_id AND c.name = 'Helmets R76'
ORDER BY o.order_number;
```

Expect: 2 rows: SO-R761 SKU-R762 1 Shop R76 Group R76; SO-R762 SKU-R762 3 Solo R76 NULL.

#### 3. Conditions and order on aliases deep in the tree

The main criteria and order can name any alias in the tree: the lines of accounts whose grandparent is "Holding R76", the biggest quantity first. A condition on a LEFT-joined alias in the main criteria drops the rows where it is NULL (WHERE runs after the joins); in the link's own criteria it wouldn't.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = OrderLines(out _, out _, out _, out _);
query.Criteria.AddCondition("grand.name", ArxConditionOperator.Equal, "Holding R76");
query.Orders.Clear();
query.AddOrder("quantity", ArxOrderType.Descending);

foreach (var row in (await service.RetrieveMultipleAsync(query)).Entities)
{
    Console.WriteLine(Line(row));
}
```

Check it in the database:

```sql
SELECT p.sku, l.quantity
FROM order_line l
JOIN sales_order o ON o.id = l.order_id
JOIN account a ON a.id = o.account_id
LEFT JOIN account pa ON pa.id = a.parent_account_id
LEFT JOIN account ga ON ga.id = pa.parent_account_id
JOIN product p ON p.id = l.product_id
WHERE o.order_number LIKE 'SO-R76%' AND ga.name = 'Holding R76'
ORDER BY l.quantity DESC;
```

Expect: 2 rows: SKU-R761 2, SKU-R762 1 (Solo has no grandparent).

#### 4. Ancestors, built by a recursive function

A self-join chain built recursively: Shop's parent, grandparent and great-grandparent (there is none).

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = new ArxQueryExpression("account");
query.ColumnSet.AddColumn("name");
query.Criteria.AddCondition("account_number", ArxConditionOperator.Equal, "ACC-R763");
query.LinkEntities.Add(Ancestors(1, 3));
Print(query.LinkEntities);

var shop = (await service.RetrieveMultipleAsync(query)).Entities[0];
Console.WriteLine($"{shop["name"]} < {shop["up1.name"]} < {shop["up2.name"]} < {shop["up3.name"] ?? "(none)"}");
```

Defined in the sample, used above:

```csharp
private static ArxLinkEntity Link(string table, string from, string to, ArxJoinOperator join, string alias, params string[] columns)
{
    var link = new ArxLinkEntity(table, from, to, join) { EntityAlias = alias };
    link.ColumnSet.AddColumns(columns);
    return link;
}

private static void Print(IEnumerable<ArxLinkEntity> links, string parentAlias = "main", int depth = 1)
{
    foreach (var link in links)
    {
        Console.WriteLine($"{new string(' ', depth * 4)}{link.JoinOperator} JOIN {link.LinkToEntityName} AS {link.EntityAlias} ON {link.EntityAlias}.{link.LinkToAttributeName} = {parentAlias}.{link.LinkFromAttributeName}"
            + (link.LinkCriteria.Conditions.Count + link.LinkCriteria.Filters.Count > 0 ? "  (+ its own criteria)" : string.Empty));
        Print(link.LinkEntities, link.EntityAlias, depth + 1);
    }
}

private static ArxLinkEntity Ancestors(int level, int levels)
{
    var link = Link("account", "parent_account_id", "id", ArxJoinOperator.Left, $"up{level}", "name");

    if (level < levels)
    {
        link.LinkEntities.Add(Ancestors(level + 1, levels));
    }

    return link;
}
```

Check it in the database:

```sql
SELECT a.name, u1.name, u2.name, u3.name
FROM account a
LEFT JOIN account u1 ON u1.id = a.parent_account_id
LEFT JOIN account u2 ON u2.id = u1.parent_account_id
LEFT JOIN account u3 ON u3.id = u2.parent_account_id
WHERE a.account_number = 'ACC-R763';
```

Expect: 1 row: Shop R76, Group R76, Holding R76, NULL.

#### 5. A one-to-many link, aggregated per account

A link to the many side (account → its contacts) gives one row per contact. Aggregated: contacts per account, counting the linked ids (accounts without contacts count 0, kept by the LEFT join).

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = new ArxQueryExpression("account") { AggregateInfo = new ArxAggregateInfo() };
query.ColumnSet.AddColumn("name");
query.Criteria.AddCondition("account_number", ArxConditionOperator.BeginsWith, "ACC-R76");
query.LinkEntities.Add(Link("contact", "id", "account_id", ArxJoinOperator.Left, "c"));
query.AggregateInfo.AddAggregateColumn("contacts", "c.id", ArxAggregateOperator.Count);
query.AggregateInfo.AddGroupByColumn("name");
query.AddOrder("contacts", ArxOrderType.Descending);
query.AddOrder("name", ArxOrderType.Ascending);

foreach (var row in (await service.RetrieveMultipleAsync(query)).Entities)
{
    Console.WriteLine($"{row["name"],-12} {row["contacts"]} contacts");
}
```

Defined in the sample, used above:

```csharp
private static ArxLinkEntity Link(string table, string from, string to, ArxJoinOperator join, string alias, params string[] columns)
{
    var link = new ArxLinkEntity(table, from, to, join) { EntityAlias = alias };
    link.ColumnSet.AddColumns(columns);
    return link;
}
```

Check it in the database:

```sql
SELECT a.name, count(c.id) AS contacts
FROM account a
LEFT JOIN contact c ON c.account_id = a.id
WHERE a.account_number LIKE 'ACC-R76%'
GROUP BY a.name
ORDER BY contacts DESC, a.name;
```

Expect: 4 rows: Shop R76 2, Solo R76 1, Group R76 0, Holding R76 0.

### Paging in depth: the total is opt-in, ask once, load more, Top, seek paging

Advanced case 6 of RetrieveMultiple.

Paging in depth, and how to keep it cheap, in six scenarios. The main choice is the total: it is opt-in (`ReturnTotalRecordCount`, false by default). Asked for, ARX sends a second statement in the same round trip, `SELECT count(*) FROM (the query without its order and paging)`, and the database reads every matching row to count them, not only the page. Not asked for, the page still knows whether a next one exists (`HasNextPage`: ARX reads one row more than the page).

- The same page with and without the total: what each gives back.
- What the total costs, timed on this machine (not a benchmark; `RetrievePagingBenchmarks` measures it).
- "Page 1 of 40": ask for the total on the first page only, then page without it.
- "Load more": never ask for the total; HasNextPage is enough.
- One page only: Top (no extra row, no offset); Top together with PageInfo is ARX1001.
- Deep pages: seek past the last row read (keyset paging) instead of an ever larger OFFSET.
The plans below assume the table's statistics are current. SQL Server updates them on its own when enough rows change and a query next needs them; until then it may plan from stale numbers, so the checks start with `UPDATE STATISTICS product`.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var products = Enumerable.Range(1, PRODUCTS).Select(i =>
{
    var product = new ArxEntity("product");
    product["sku"] = $"{PREFIX}{i:D5}";
    product["name"] = $"Paged product {i}";
    product["unit_price"] = 1m + i % 50;
    return product;
}).ToList();
await service.CreateMultipleAsync(products);
```

#### 1. The same page with and without the total

Page 2, twice. With the total: 20000 rows in 40 pages. Without it: TotalRecordCount and TotalPageCount stay 0 (not counted, not "no rows"), and HasNextPage is still right.

In scope: `service` (ArxMsSqlDbService).

```csharp
Console.WriteLine("with:    " + Describe(await service.RetrieveMultipleAsync(Page(2, total: true))));
Console.WriteLine("without: " + Describe(await service.RetrieveMultipleAsync(Page(2, total: false))));
```

Defined in the sample, used above:

```csharp
private static ArxQueryExpression Page(int pageNumber, bool total, int pageSize = PAGE_SIZE)
{
    var query = new ArxQueryExpression("product");
    query.ColumnSet.AddColumns("sku", "unit_price");
    query.Criteria.AddCondition("sku", ArxConditionOperator.BeginsWith, PREFIX);
    query.AddOrder("sku", ArxOrderType.Ascending);
    query.PageInfo = new ArxPagingInfo { Count = pageSize, PageNumber = pageNumber, ReturnTotalRecordCount = total };
    return query;
}

private static string Describe(ArxEntityCollection page)
    => $"{page.Entities.Count} rows ({page.Entities[0]["sku"]} to {page.Entities[^1]["sku"]}), page {page.CurrentPage}, "
       + $"total {page.TotalRecordCount}, pages {page.TotalPageCount}, next page: {page.HasNextPage}";
```

Check it in the database:

```sql
SELECT min(sku), max(sku), count(*)
FROM (SELECT sku FROM product WHERE sku LIKE 'PGN-R77-%' ORDER BY sku OFFSET 500 ROWS FETCH NEXT 500 ROWS ONLY) AS page;
```

Expect: PGN-R77-00501, PGN-R77-01000, 500: page 2 both times. SELECT count(*) FROM product WHERE sku LIKE 'PGN-R77-%'; 20000, the total the first read reported (the statement ARX adds when asked).

#### 2. What the total costs (timed on this machine; not a benchmark)

The total's cost: the page reads 501 rows, the count reads all 20000 that match. Timed here over 20 reads of page 1 each way; the numbers are this machine's and only show the direction (the benchmark is `RetrievePagingBenchmarks`, categories Page and PageAndTotal).

In scope: `service` (ArxMsSqlDbService).

```csharp
const int READS = 20;

async Task<double> TimeAsync(bool total)
{
    await service.RetrieveMultipleAsync(Page(1, total)); // warm up
    var watch = Stopwatch.StartNew();

    for (var i = 0; i < READS; i++)
    {
        await service.RetrieveMultipleAsync(Page(1, total));
    }

    return watch.Elapsed.TotalMilliseconds / READS;
}

var without = await TimeAsync(total: false);
var with = await TimeAsync(total: true);
Console.WriteLine($"page 1 without the total: {without:N2} ms a read; with it: {with:N2} ms a read");
```

Defined in the sample, used above:

```csharp
private static ArxQueryExpression Page(int pageNumber, bool total, int pageSize = PAGE_SIZE)
{
    var query = new ArxQueryExpression("product");
    query.ColumnSet.AddColumns("sku", "unit_price");
    query.Criteria.AddCondition("sku", ArxConditionOperator.BeginsWith, PREFIX);
    query.AddOrder("sku", ArxOrderType.Ascending);
    query.PageInfo = new ArxPagingInfo { Count = pageSize, PageNumber = pageNumber, ReturnTotalRecordCount = total };
    return query;
}
```

Check it in the database:

```sql
UPDATE STATISTICS product;
SET STATISTICS IO ON;
SELECT sku, unit_price FROM product WHERE sku LIKE 'PGN-R77-%' ORDER BY sku OFFSET 0 ROWS FETCH NEXT 501 ROWS ONLY;
SELECT count(*) FROM (SELECT sku, unit_price FROM product WHERE sku LIKE 'PGN-R77-%') AS arx_count;
-- Expect (Messages tab, with the actual execution plan on): the page is a seek on product_sku_key that stops
-- after 501 rows, a few logical reads; the count reads all 20000 rows.
```

#### 3. "Page 1 of 40": the total on the first page only

A grid that shows "page 1 of 40": the total is asked for on page 1 and kept; pages 2 to 40 don't ask again (the count would be the same 20000 rows read 39 more times). Rows added meanwhile show up on later pages, and the page count shown is the one from page 1: refresh it when the user goes back to page 1.

In scope: `service` (ArxMsSqlDbService).

```csharp
var first = await service.RetrieveMultipleAsync(Page(1, total: true));
var pageCount = first.TotalPageCount;
var read = first.Entities.Count;
var pageNumber = 1;
var page = first;

while (page.HasNextPage)
{
    page = await service.RetrieveMultipleAsync(Page(++pageNumber, total: false));
    read += page.Entities.Count;
}

Console.WriteLine($"page 1 of {pageCount} ({first.TotalRecordCount} products); read {read} rows in {pageNumber} pages, counted once");
```

Check it in the database:

```sql
SELECT count(*), CEILING(count(*) / 500.0)
FROM product
WHERE sku LIKE 'PGN-R77-%';
```

Expect: 20000, 40: the total and the page count page 1 reported, and what the loop read.

#### 4. "Load more": no total at all

A "load more" list or an infinite scroll shows no page count, so it never asks for one: each page is one statement, and HasNextPage decides whether to show the button. Here: the first three loads.

In scope: `service` (ArxMsSqlDbService).

```csharp
for (var load = 1; load <= 3; load++)
{
    var page = await service.RetrieveMultipleAsync(Page(load, total: false, pageSize: 50));
    Console.WriteLine($"load {load}: {page.Entities[0]["sku"]} to {page.Entities[^1]["sku"]}; show \"Load more\": {page.HasNextPage}");
}
```

Check it in the database:

```sql
SELECT min(sku), max(sku)
FROM (SELECT sku FROM product WHERE sku LIKE 'PGN-R77-%' ORDER BY sku OFFSET 0 ROWS FETCH NEXT 150 ROWS ONLY) AS loaded;
```

Expect: PGN-R77-00001, PGN-R77-00150: the three loads of 50.

#### 5. One page only: Top

"The five cheapest" is one page and never a next one: Top reads exactly five rows (PageInfo would read six, to know about a next page). Top and PageInfo together are ARX1001: one says how many rows, the other which page.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = new ArxQueryExpression("product") { Top = 5 };
query.ColumnSet.AddColumns("sku", "unit_price");
query.Criteria.AddCondition("sku", ArxConditionOperator.BeginsWith, PREFIX);
query.AddOrder("unit_price", ArxOrderType.Ascending);
query.AddOrder("sku", ArxOrderType.Ascending);

var cheapest = await service.RetrieveMultipleAsync(query);
Console.WriteLine(string.Join(", ", cheapest.Entities.Select(x => $"{x["sku"]} {x["unit_price"]:N2}")));

try
{
    query.PageInfo = new ArxPagingInfo { Count = 5 };
    await service.RetrieveMultipleAsync(query);
}
catch (ArxException error) when (error.Code == ArxErrorCode.InvalidArgument)
{
    Console.WriteLine(error.Message);
}
```

Check it in the database:

```sql
SELECT sku, unit_price
FROM product
WHERE sku LIKE 'PGN-R77-%'
ORDER BY unit_price, sku
OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY;
```

Expect: PGN-R77-00050, PGN-R77-00100, PGN-R77-00150, PGN-R77-00200, PGN-R77-00250, all 1.00.

#### 6. Deep pages: seek past the last row instead of OFFSET

OFFSET makes the database read and throw away every row before the page: page 40 reads 20000 rows to return 500. Seek paging remembers the last SKU read and asks for the rows after it, always as page 1, so each page reads only its own rows through the index on the order column (here the unique index on sku). The order must be unique (add the id after a column with repeats, and seek on both), and the page can't be jumped to by number: it suits exports and "load more", not "go to page 37". Rows added meanwhile aren't read twice or skipped.

In scope: `service` (ArxMsSqlDbService).

```csharp
var byOffset = await service.RetrieveMultipleAsync(Page(40, total: false));

string? last = null;
ArxEntityCollection page;
var pages = 0;

do
{
    var query = Page(1, total: false);

    if (last != null)
    {
        query.Criteria.AddCondition("sku", ArxConditionOperator.GreaterThan, last);
    }

    page = await service.RetrieveMultipleAsync(query);
    last = (string)page.Entities[^1]["sku"]!;
    pages++;
}
while (page.HasNextPage);

Console.WriteLine($"page 40 by OFFSET: {byOffset.Entities[0]["sku"]} to {byOffset.Entities[^1]["sku"]}");
Console.WriteLine($"page {pages} by seek:   {page.Entities[0]["sku"]} to {page.Entities[^1]["sku"]}, the same rows");
```

Check it in the database:

```sql
UPDATE STATISTICS product;
SET STATISTICS IO ON;
SELECT sku, unit_price FROM product WHERE sku LIKE 'PGN-R77-%' ORDER BY sku OFFSET 19500 ROWS FETCH NEXT 501 ROWS ONLY;
SELECT sku, unit_price FROM product WHERE sku LIKE 'PGN-R77-%' AND sku > 'PGN-R77-19500' ORDER BY sku OFFSET 0 ROWS FETCH NEXT 501 ROWS ONLY;
```

Expect: both return PGN-R77-19501 to PGN-R77-20000 (500 rows); the OFFSET plan reads the index through all 20000 rows (more logical reads), the seek plan starts at sku > 'PGN-R77-19500' and reads 500.

### Read other row sources: a view in a schema, a summary view, a table with no id

Advanced case 7 of RetrieveMultiple.

RetrieveMultiple reads more than tables: whatever name the query is given, SQL Server resolves it, so a view, a view in another schema or a table without an id is read with the same `ArxQueryExpression`: criteria, links, order, paging, aggregates. Three scenarios (functions are `Advanced8`):

- A view in another schema (`reporting.vw_product_catalogue`), linked to a table and aggregated.
- A summary view: it runs its query at each read, so a new order shows at once.
- A table with no id (`web_visit`), read with a date range and a link.
SQL Server has no materialized views (an indexed view is the nearest, and reads the same way) and no inheritance; a partitioned table is read like any table: partitions are invisible to statements.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var accounts = new Dictionary<string, ArxRecordId>();
foreach (var (number, name) in new[] { ("ACC-R781", "Summit Supplies"), ("ACC-R782", "Harbor Hardware") })
{
    var account = new ArxEntity("account");
    account["name"] = name;
    account["account_number"] = number;
    accounts[number] = await service.CreateAsync(account);
}
var category = new ArxEntity("product_category");
category["name"] = "Tools R78";
var categoryId = await service.CreateAsync(category);
var products = new Dictionary<string, ArxRecordId>();
foreach (var (sku, name, price, active) in new[] { ("SKU-R781", "Hammer", 25m, true), ("SKU-R782", "Saw", 40m, true), ("SKU-R783", "Old drill", 90m, false) })
{
    var product = new ArxEntity("product");
    product["sku"] = sku;
    product["name"] = name;
    product["category_id"] = new ArxEntityReference("product_category", categoryId);
    product["unit_price"] = price;
    product["is_active"] = active;
    products[sku] = await service.CreateAsync(product);
}
foreach (var (orderNumber, account, total, lines) in new[]
{
    ("SO-R781", "ACC-R781", 180m, new[] { ("SKU-R781", 4, 25m), ("SKU-R782", 2, 40m) }),
    ("SO-R782", "ACC-R781", 200m, Array.Empty<(string, int, decimal)>()),
    ("SO-R783", "ACC-R782", 115m, new[] { ("SKU-R781", 1, 25m), ("SKU-R783", 1, 90m) }),
})
{
    var order = new ArxEntity("sales_order");
    order["order_number"] = orderNumber;
    order["account_id"] = new ArxEntityReference("account", accounts[account]);
    order["ordered_on"] = new DateTime(2026, 11, 8, 9, 0, 0, DateTimeKind.Utc);
    order["total"] = total;
    var orderId = await service.CreateAsync(order);

    foreach (var (sku, quantity, price) in lines)
    {
        var line = new ArxEntity("order_line");
        line["order_id"] = new ArxEntityReference("sales_order", orderId);
        line["product_id"] = new ArxEntityReference("product", products[sku]);
        line["quantity"] = quantity;
        line["unit_price"] = price;
        await service.CreateAsync(line);
    }
}
```

#### 1. A view in another schema, linked to a table and aggregated

`reporting.vw_product_catalogue` (active products with their category's name), named with its schema, then used like any table: a criterion, a LEFT link to `order_line` (a table), units sold per product, the best seller first. The inactive Old drill isn't in the view, so it isn't in the result.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = new ArxQueryExpression("reporting.vw_product_catalogue") { AggregateInfo = new ArxAggregateInfo() };
query.ColumnSet.AddColumns("sku", "name");
query.Criteria.AddCondition("category", ArxConditionOperator.Equal, "Tools R78");
query.LinkEntities.Add(new ArxLinkEntity("order_line", "id", "product_id", ArxJoinOperator.Left) { EntityAlias = "l" });
query.AggregateInfo.AddAggregateColumn("units", "l.quantity", ArxAggregateOperator.Sum);
query.AggregateInfo.AddGroupByColumns("sku", "name");
query.AddOrder("units", ArxOrderType.Descending);

var rows = await service.RetrieveMultipleAsync(query);

foreach (var row in rows.Entities)
{
    Console.WriteLine($"{row["sku"]} {row["name"],-6} {row["units"]} sold   (entity name: {row.EntityName})");
}
```

Check it in the database:

```sql
SELECT v.sku, v.name, sum(l.quantity) AS units
FROM reporting.vw_product_catalogue v
LEFT JOIN order_line l ON l.product_id = v.id
WHERE v.category = 'Tools R78'
GROUP BY v.sku, v.name
ORDER BY units DESC;
```

Expect: 2 rows: SKU-R781 Hammer 5, SKU-R782 Saw 2 (no Old drill: inactive).

#### 2. A summary view: always up to date

`vw_account_sales_summary` (orders and revenue per account) runs its query at each read: an order created afterwards shows in the next read at once.

In scope: `service` (ArxMsSqlDbService), `harbor` (ArxRecordId).

```csharp
foreach (var row in (await service.RetrieveMultipleAsync(SalesSummary())).Entities)
{
    Console.WriteLine($"{row["name"],-15} orders {row["orders"]}, revenue {row["revenue"]:N2}");
}

var order = new ArxEntity("sales_order");
order["order_number"] = "SO-R784";
order["account_id"] = new ArxEntityReference("account", harbor);
order["ordered_on"] = new DateTime(2026, 11, 8, 15, 0, 0, DateTimeKind.Utc);
order["total"] = 500m;
await service.CreateAsync(order);

var snapshot = (await service.RetrieveMultipleAsync(SalesSummary())).Entities.Single(x => (string)x["name"]! == "Harbor Hardware");

var live = new ArxQueryExpression("sales_order") { AggregateInfo = new ArxAggregateInfo() };
live.Criteria.AddCondition("account_id", ArxConditionOperator.Equal, harbor.AsInt64());
live.AggregateInfo.AddAggregateColumn("orders", "id", ArxAggregateOperator.Count);
live.AggregateInfo.AddAggregateColumn("revenue", "total", ArxAggregateOperator.Sum);
var now = (await service.RetrieveMultipleAsync(live)).Entities[0];

Console.WriteLine($"a new order for Harbor: the view says {snapshot["orders"]} orders, {snapshot["revenue"]:N2}; the tables say {now["orders"]} orders, {now["revenue"]:N2}");
```

Defined in the sample, used above:

```csharp
private static ArxQueryExpression SalesSummary()
{
    var query = new ArxQueryExpression("vw_account_sales_summary");
    query.ColumnSet.AddColumns("name", "orders", "revenue");
    query.Criteria.AddCondition("account_number", ArxConditionOperator.BeginsWith, "ACC-R78");
    query.AddOrder("revenue", ArxOrderType.Descending);
    return query;
}
```

Check it in the database:

```sql
SELECT name, orders, revenue
FROM vw_account_sales_summary
WHERE account_number LIKE 'ACC-R78%'
ORDER BY revenue DESC;
```

Expect: Harbor Hardware 2 615.00, Summit Supplies 2 380.00 (the view is always current). SELECT COUNT(*), SUM(total) FROM sales_order WHERE order_number IN ('SO-R783', 'SO-R784'); 2, 615.00: Harbor's orders now.

#### 3. A table with no id, read with a date range and a link

`web_visit` has no id (declared NoIdColumn): its rows are created together and read with a date range and a link to the account.

In scope: `service` (ArxMsSqlDbService), `accounts` (Dictionary<string, ArxRecordId>).

```csharp
var visits = new[]
{
    ("ACC-R781", "/pricing", new DateOnly(2026, 3, 10)),
    ("ACC-R781", "/pricing", new DateOnly(2026, 8, 2)),
    ("ACC-R781", "/contact", new DateOnly(2026, 9, 15)),
    ("ACC-R782", "/pricing", new DateOnly(2026, 10, 1)),
}.Select(x =>
{
    var visit = new ArxEntity("web_visit");
    visit["account_id"] = new ArxEntityReference("account", accounts[x.Item1]);
    visit["page"] = x.Item2;
    visit["visited_on"] = x.Item3;
    return visit;
}).ToList();

await service.CreateMultipleAsync(visits);

var query = new ArxQueryExpression("web_visit");
query.ColumnSet.AddColumns("page", "visited_on");
query.Criteria.AddCondition("visited_on", ArxConditionOperator.GreaterEqual, new DateOnly(2026, 7, 1));
query.Criteria.AddCondition("visited_on", ArxConditionOperator.LessThan, new DateOnly(2026, 10, 1));
var account = query.AddLink("account", "account_id", "id", ArxJoinOperator.Inner);
account.EntityAlias = "acc";
account.ColumnSet.AddColumn("name");
account.LinkCriteria.AddCondition("account_number", ArxConditionOperator.BeginsWith, "ACC-R78");
query.AddOrder("visited_on", ArxOrderType.Ascending);

foreach (var row in (await service.RetrieveMultipleAsync(query)).Entities)
{
    Console.WriteLine($"July to September: {row["visited_on"]:yyyy-MM-dd} {row["page"],-9} {row["acc.name"]}");
}
```

Check it in the database:

```sql
SELECT v.visited_on, v.page, a.name
FROM web_visit v
JOIN account a ON a.id = v.account_id AND a.account_number LIKE 'ACC-R78%'
WHERE v.visited_on >= '2026-07-01' AND v.visited_on < '2026-10-01'
ORDER BY v.visited_on;
```

Expect: 2 rows: 2026-08-02 /pricing, 2026-09-15 /contact (both Summit Supplies).

### Read functions in depth: table rows, one row, scalar values, what functions don't take, procedures

Advanced case 8 of RetrieveMultiple.

Functions as row sources on SQL Server, in five scenarios. A table-valued function is named with `Function` (arguments by position), and the rest of the query works on its rows as on a table's. Every argument is a typed parameter. RealCase6 shows an inline table-valued function; this sample shows the other shapes and what SQL Server's functions don't do:

- An inline function returning a table's rows, with a criterion, a link entity and an order.
- A multi-statement function returning one row of totals.
- A scalar function: not a row source; its value comes from Execute.
- Named arguments and declared result columns: refused (ARX8001).
- A procedure: not a row source; procedures are Execute's.
SQL Server functions can't write, so there is no AllowWrites case here.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var active = new ArxEntity("account");
active["name"] = "Fn Active";
active["account_number"] = "ACC-R791";
var activeId = await service.CreateAsync(active);
var held = new ArxEntity("account");
held["name"] = "Fn Held";
held["account_number"] = "ACC-R792";
held["status"] = new ArxSelectionSetValue("AccountStatus", "ON_HOLD");
held["parent_account_id"] = new ArxEntityReference("account", activeId);
await service.CreateAsync(held);
foreach (var number in new[] { "SO-R791", "SO-R792", "SO-R793" })
{
    var order = new ArxEntity("sales_order");
    order["order_number"] = number;
    order["account_id"] = new ArxEntityReference("account", activeId);
    order["ordered_on"] = new DateTime(2026, 11, 8, 10, 0, 0, DateTimeKind.Utc);
    await service.CreateAsync(order);
}
```

#### 1. A function returning a table's rows, linked

`fn_accounts_with_status('ON_HOLD')` returns whole account rows. Its rows take a criterion and an order like a table's, and a link entity: here the parent account's name.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = new ArxQueryExpression(ArxRowSource.Function("fn_accounts_with_status", "ON_HOLD"));
query.ColumnSet.AddColumns("name", "account_number", "status");
query.Criteria.AddCondition("account_number", ArxConditionOperator.BeginsWith, "ACC-R79");
var parent = query.AddLink("account", "parent_account_id", "id", ArxJoinOperator.Left);
parent.EntityAlias = "parent";
parent.ColumnSet.AddColumn("name");
query.AddOrder("name", ArxOrderType.Ascending);

foreach (var row in (await service.RetrieveMultipleAsync(query)).Entities)
{
    Console.WriteLine($"{row["name"]} ({row["account_number"]}, {row["status"]}), parent: {row["parent.name"] ?? "-"}");
}
```

Check it in the database:

```sql
SELECT f.name, f.account_number, f.status, p.name AS parent
FROM fn_accounts_with_status('ON_HOLD') AS f
LEFT JOIN account p ON p.id = f.parent_account_id
WHERE f.account_number LIKE 'ACC-R79%';
```

Expect: 1 row: Fn Held, ACC-R792, ON_HOLD, Fn Active.

#### 2. A multi-statement function: one row

`fn_account_totals()`, a multi-statement function: one row, an attribute per column.

In scope: `service` (ArxMsSqlDbService).

```csharp
var totals = (await service.RetrieveMultipleAsync(new ArxQueryExpression(ArxRowSource.Function("fn_account_totals")))).Entities.Single();
Console.WriteLine($"accounts: {totals["accounts"]}, contacts: {totals["contacts"]}");
```

Check it in the database:

```sql
SELECT * FROM fn_account_totals();
```

Expect: the two numbers the sample printed (they count every sample's rows since the last reset).

#### 3. A scalar function: not a row source; its value through Execute

`fn_order_count` is a scalar function: SQL Server can't select rows from it (UndefinedEntity). A value comes from Execute instead, `result.Value<T>()`.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
try
{
    await service.RetrieveMultipleAsync(new ArxQueryExpression(ArxRowSource.Function("fn_order_count", accountId.AsInt64())));
}
catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.UndefinedEntity)
{
    Console.WriteLine(error.Message);
}

var count = new ArxProcedureExpression("fn_order_count") { ProcedureType = ArxProcedureType.Function };
count.AddParameter(accountId.AsInt64());
Console.WriteLine($"through Execute: fn_order_count = {(await service.ExecuteAsync(count)).Value<long>()}");
```

Check it in the database:

```sql
SELECT dbo.fn_order_count(a.id)
FROM account a
WHERE a.account_number = 'ACC-R791';
```

Expect: 3.

#### 4. Named arguments and declared columns are refused

SQL Server functions take their arguments by position, and declare their own columns: named arguments and `Returns(...)` are refused before the database (ARX8001), never silently ignored.

In scope: `service` (ArxMsSqlDbService).

```csharp
try
{
    await service.RetrieveMultipleAsync(new ArxQueryExpression(ArxRowSource.Function("fn_accounts_with_status", ("p_status", "ON_HOLD"))));
}
catch (ArxNotSupportedException error)
{
    Console.WriteLine(error.Message);
}

try
{
    await service.RetrieveMultipleAsync(new ArxQueryExpression(ArxRowSource.Function("fn_account_totals").Returns(("accounts", typeof(long)))));
}
catch (ArxNotSupportedException error)
{
    Console.WriteLine(error.Message);
}
```

Check it in the database:

```sql
SELECT COUNT(*) FROM account WHERE account_number LIKE 'ACC-R79%';
```

Expect: 2: nothing was read or written by the refused calls.

#### 5. A procedure is not a row source

A procedure returns no rows to select from: UndefinedEntity; procedures are Execute's.

In scope: `service` (ArxMsSqlDbService).

```csharp
try
{
    await service.RetrieveMultipleAsync(new ArxQueryExpression(ArxRowSource.Function("sp_recalculate_order_totals")));
}
catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.UndefinedEntity)
{
    Console.WriteLine(error.Message);
    Console.WriteLine("call it with ExecuteAsync(new ArxProcedureExpression(\"sp_recalculate_order_totals\"))");
}
```

Check it in the database:

```sql
SELECT type_desc FROM sys.objects WHERE name = 'sp_recalculate_order_totals';
```

Expect: SQL_STORED_PROCEDURE.

### Views in depth: computed criteria, links both ways, declared ids and lookups, column boundaries

Advanced case 9 of RetrieveMultiple.

Views in depth, in five scenarios. A view is a saved query with a name: ARX reads it by that name like a table, and SQL Server runs the view's query inside the one ARX sends, so criteria, links, order and paging all apply. The sample schema names views `vw_` (materialized views `mv_`, functions `fn_`, procedures `sp_`).

- A computed column as a criterion: what a view gives that a table can't (here, accounts with two or more contacts), ordered and paged with the total.
- Links both ways: from a view to a table, and from a table to a view.
- A view declared with its id and a lookup: Retrieve by id, a RecordId, the lookup read back with its display name, and the `_lk` direct join from a view's column.
- A view as a column boundary: a directory without the contacts' private columns; asking it for one is ARX5002.
- A view on a view: read like any other; SQL Server expands both into one query.
RealCase5 is the short version (a view as it is, and declared with its id); Advanced7 reads a view in another schema, and a materialized view.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var accounts = new Dictionary<string, ArxRecordId>();
foreach (var (number, name, contacts) in new[]
{
    ("ACC-R801", "Alpha Views", new[] { "Ann", "Al", "Ash" }),
    ("ACC-R802", "Beta Views", new[] { "Bea", "Bo" }),
    ("ACC-R803", "Gamma Views", new[] { "Gus" }),
})
{
    var account = new ArxEntity("account");
    account["name"] = name;
    account["account_number"] = number;
    accounts[number] = await service.CreateAsync(account);

    foreach (var first in contacts)
    {
        var contact = new ArxEntity("contact");
        contact["first_name"] = first;
        contact["last_name"] = "Views-R80";
        contact["job_title"] = first == contacts[0] ? "Buyer" : "Engineer";
        contact["email"] = $"{first.ToLowerInvariant()}.r80@example.com";
        contact["account_id"] = new ArxEntityReference("account", accounts[number]);
        await service.CreateAsync(contact);
    }
}
foreach (var (orderNumber, account) in new[] { ("SO-R801", "ACC-R801"), ("SO-R802", "ACC-R803") })
{
    var order = new ArxEntity("sales_order");
    order["order_number"] = orderNumber;
    order["account_id"] = new ArxEntityReference("account", accounts[account]);
    order["ordered_on"] = new DateTime(2026, 11, 8, 12, 0, 0, DateTimeKind.Utc);
    await service.CreateAsync(order);
}
```

#### 1. A computed column as a criterion, ordered and paged

`vw_account_overview.contacts` is a count the view computes. A table has no such column; on the view it's a criterion like any other (the HAVING of the view's GROUP BY, written as a plain condition), and an order. One account per page, with the total.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = new ArxQueryExpression("vw_account_overview");
query.ColumnSet.AddColumns("name", "contacts");
query.Criteria.AddCondition("account_number", ArxConditionOperator.BeginsWith, "ACC-R80");
query.Criteria.AddCondition("contacts", ArxConditionOperator.GreaterEqual, 2);
query.AddOrder("contacts", ArxOrderType.Descending);

for (var pageNumber = 1; ; pageNumber++)
{
    query.PageInfo = new ArxPagingInfo { Count = 1, PageNumber = pageNumber, ReturnTotalRecordCount = pageNumber == 1 };
    var page = await service.RetrieveMultipleAsync(query);
    var row = page.Entities.Single();
    Console.WriteLine($"page {pageNumber}: {row["name"]}, {row["contacts"]} contacts" + (pageNumber == 1 ? $" ({page.TotalRecordCount} accounts in {page.TotalPageCount} pages)" : string.Empty));

    if (!page.HasNextPage)
    {
        break;
    }
}
```

Check it in the database:

```sql
SELECT name, contacts
FROM vw_account_overview
WHERE account_number LIKE 'ACC-R80%' AND contacts >= 2
ORDER BY contacts DESC;
```

Expect: 2 rows: Alpha Views 3, Beta Views 2 (Gamma has 1).

#### 2. Links both ways: view to table, table to view

From the view to a table: each key account's buyers (a LEFT link to contact with its own criterion). From a table to the view: each order with its account's number of contacts, which the orders table doesn't have.

In scope: `service` (ArxMsSqlDbService).

```csharp
var fromView = new ArxQueryExpression("vw_account_overview");
fromView.ColumnSet.AddColumn("name");
fromView.Criteria.AddCondition("account_number", ArxConditionOperator.BeginsWith, "ACC-R80");
var buyers = fromView.AddLink("contact", "id", "account_id", ArxJoinOperator.Left);
buyers.EntityAlias = "buyer";
buyers.ColumnSet.AddColumn("first_name");
buyers.LinkCriteria.AddCondition("job_title", ArxConditionOperator.Equal, "Buyer");
fromView.AddOrder("name", ArxOrderType.Ascending);

foreach (var row in (await service.RetrieveMultipleAsync(fromView)).Entities)
{
    Console.WriteLine($"view → table: {row["name"],-12} buyer {row["buyer.first_name"]}");
}

var toView = new ArxQueryExpression("sales_order");
toView.ColumnSet.AddColumn("order_number");
toView.Criteria.AddCondition("order_number", ArxConditionOperator.BeginsWith, "SO-R80");
var overview = toView.AddLink("vw_account_overview", "account_id", "id", ArxJoinOperator.Inner);
overview.EntityAlias = "acc";
overview.ColumnSet.AddColumns("name", "contacts");
toView.AddOrder("order_number", ArxOrderType.Ascending);

foreach (var row in (await service.RetrieveMultipleAsync(toView)).Entities)
{
    Console.WriteLine($"table → view: {row["order_number"]} for {row["acc.name"]}, contacts: {row["acc.contacts"]}");
}
```

Check it in the database:

```sql
SELECT v.name, c.first_name
FROM vw_account_overview v
LEFT JOIN contact c ON c.account_id = v.id AND c.job_title = 'Buyer'
WHERE v.account_number LIKE 'ACC-R80%'
ORDER BY v.name;
```

Expect: 3 rows: Alpha Views Ann, Beta Views Bea, Gamma Views Gus. SELECT o.order_number, v.name, v.contacts FROM sales_order o JOIN vw_account_overview v ON v.id = o.account_id WHERE o.order_number LIKE 'SO-R80%' ORDER BY o.order_number; 2 rows: SO-R801 Alpha Views 3, SO-R802 Gamma Views 1.

#### 3. A view declared with its id and a lookup

A view has no primary key SQL Server could report, so ARX knows its id only when the developer declares it (as for any table). Declared here with its id and its account_id lookup: Retrieve by id works, the row has a RecordId, and account_id reads back as a reference with the account's display name. The `_lk` direct join works from a view's column too.

In scope: `connectionString` (string).

```csharp
var service = new ArxMsSqlDbService(connectionString, ArxSchemaCatalog.Create()
    .Table("account", ArxIdColumn.Generated<long>("id")).DisplayName("account", "name")
    .Table("vw_contact_directory", ArxIdColumn.Supplied<long>("id")).Lookup("vw_contact_directory", "account_id", "account")
    .Build());

var query = new ArxQueryExpression("vw_contact_directory");
query.ColumnSet.AddColumns("first_name", "account_id", "account_id._lk.account.account_number");
query.Criteria.AddCondition("first_name", ArxConditionOperator.Equal, "Bea");
var bea = (await service.RetrieveMultipleAsync(query)).Entities.Single();
var account = (ArxEntityReference)bea["account_id"]!;
Console.WriteLine($"{bea["first_name"]}: RecordId on column {bea.RecordId!.Column}, account_id → {account.EntityName} '{account.Name}', number {bea["account_id.account_number"]}");

var again = await service.RetrieveAsync("vw_contact_directory", bea.RecordId, new ArxColumnSet("first_name", "job_title"));
Console.WriteLine($"Retrieve by that id: {again!["first_name"]}, {again["job_title"]}");
```

Check it in the database:

```sql
SELECT d.first_name, d.job_title, a.name, a.account_number
FROM vw_contact_directory d
JOIN account a ON a.id = d.account_id
WHERE d.first_name = 'Bea' AND d.last_name = 'Views-R80';
```

Expect: 1 row: Bea, Buyer, Beta Views, ACC-R802.

#### 4. A view as a column boundary

`vw_contact_directory` leaves out email, phone, birthday and notes. Asking it for email is ARX5002 (no such column), from the database itself: code that reads the directory can't get them, whatever its query says. Give a role SELECT on the view and not on the table, and the boundary holds outside ARX too.

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = new ArxQueryExpression("vw_contact_directory");
query.ColumnSet.AddColumns("first_name", "email");
query.Criteria.AddCondition("last_name", ArxConditionOperator.Equal, "Views-R80");

try
{
    await service.RetrieveMultipleAsync(query);
}
catch (ArxException error) when (error.Code == ArxErrorCode.UndefinedAttribute)
{
    Console.WriteLine(error.Message);
}

query.ColumnSet = new ArxColumnSet("first_name", "job_title");
Console.WriteLine($"the directory's own columns: {(await service.RetrieveMultipleAsync(query)).Entities.Count} contacts, first name and job title");
```

Check it in the database:

```sql
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'vw_contact_directory'
ORDER BY ordinal_position;
```

Expect: id, first_name, last_name, job_title, account_id (no email).

#### 5. A view on a view

`vw_key_accounts` is a view on `vw_account_overview` (accounts with two or more contacts). ARX reads it like the others; SQL Server expands both views into one query over the tables (the grouped one as an aggregate step over contact).

In scope: `service` (ArxMsSqlDbService).

```csharp
var query = new ArxQueryExpression("vw_key_accounts");
query.ColumnSet.AddColumns("name", "contacts");
query.Criteria.AddCondition("account_number", ArxConditionOperator.BeginsWith, "ACC-R80");
query.AddOrder("name", ArxOrderType.Ascending);

Console.WriteLine("key accounts: " + string.Join(", ", (await service.RetrieveMultipleAsync(query)).Entities.Select(x => $"{x["name"]} ({x["contacts"]})")));
```

Check it in the database:

```sql
SELECT name, contacts
FROM vw_key_accounts
WHERE account_number LIKE 'ACC-R80%'
ORDER BY name;
```

Expect: 2 rows: Alpha Views 3, Beta Views 2. GO SET SHOWPLAN_TEXT ON; GO SELECT name FROM vw_key_accounts WHERE account_number LIKE 'ACC-R80%'; GO SET SHOWPLAN_TEXT OFF; one plan over account and contact, with no view in it: SQL Server expands both views into the query (the grouped one as an aggregate step over contact).

### Retrieve on a preloaded schema: lookups found, column types, Strict, saved JSON

Advanced case 10 of RetrieveMultiple.

Retrieve on a schema preloaded at service setup, in four scenarios: a lookup the preload found (account.primary_contact_id, which added contact) read back as a reference with the contact's name; a column of fixed text values (a CHECK lists them: SQL Server has no enum types) and its preloaded type; Strict, where reading all columns means the preloaded ones and any other column is refused; and a preload saved as JSON, so the next startup needs no catalog queries.

Good to know:

Display names and selection sets are the developer's: a preload doesn't guess them. Add them in the preload's `configure` callback, as below.

Usings:

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

#### 1. A lookup the preload found, read back with its display name

Preload account (contact comes with it), name contacts, and read the primary contact back as a reference.

In scope: `connectionString` (string).

```csharp
var tables = new ArxPreLoadTableRegister()
    .PreLoadTable("account");

var schema = await new ArxPreLoadTable(tables).LoadAsync(
    connectionString,
    configure => configure.DisplayName("contact", ["first_name", "last_name"], " "));
var service = new ArxMsSqlDbService(connectionString, schema);

var contact = new ArxEntity("contact");
contact["first_name"] = "Rita";
contact["last_name"] = "Retrieve";
contact["email"] = "rita.retrieve@example.com";
var contactId = await service.CreateAsync(contact);

var account = new ArxEntity("account");
account["name"] = "Lookup Lighting";
account["account_number"] = "ACC-PRE111";
account["primary_contact_id"] = new ArxEntityReference("contact", contactId);
await service.CreateAsync(account);

var query = new ArxQueryExpression("account");
query.ColumnSet.AddColumns("name", "primary_contact_id");
query.Criteria.AddCondition("account_number", ArxConditionOperator.Equal, "ACC-PRE111");
var read = (await service.RetrieveMultipleAsync(query)).Entities.Single();
var primary = (ArxEntityReference)read["primary_contact_id"]!;
Console.WriteLine($"{read["name"]}: primary contact {primary.EntityName} {primary.RecordId}, \"{primary.Name}\"");

return schema;
```

Check it in the database:

```sql
SELECT a.name, c.first_name + ' ' + c.last_name AS primary_contact
FROM account a JOIN contact c ON c.id = a.primary_contact_id
WHERE a.account_number = 'ACC-PRE111';
```

Expect: Lookup Lighting, Rita Retrieve.

#### 2. A column of fixed values: its type, a string in and out

The showcase table's mood: fixed text values a CHECK guards; its preloaded type, a string in and out.

In scope: `connectionString` (string).

```csharp
var tables = new ArxPreLoadTableRegister()
    .PreLoadTable("data_type_showcase", ["text_value", "mood"]);

var schema = await new ArxPreLoadTable(tables).LoadAsync(connectionString);
var service = new ArxMsSqlDbService(connectionString, schema);

schema.TryGetTable("data_type_showcase", out var table);
Console.WriteLine($"mood: {table!.Columns["mood"]}, {table.Columns["mood"].Traits}");

var row = new ArxEntity("data_type_showcase");
row["text_value"] = "preloaded mood";
row["mood"] = "happy";
await service.CreateAsync(row);

var query = new ArxQueryExpression("data_type_showcase");
query.ColumnSet.AddColumns("text_value", "mood");
query.Criteria.AddCondition("mood", ArxConditionOperator.Equal, "happy");
query.Criteria.AddCondition("text_value", ArxConditionOperator.Equal, "preloaded mood");
var read = (await service.RetrieveMultipleAsync(query)).Entities.Single();
Console.WriteLine($"read back: mood \"{read["mood"]}\" ({read["mood"]!.GetType().Name})");
```

Check it in the database:

```sql
SELECT text_value, mood, SQL_VARIANT_PROPERTY(mood, 'BaseType') AS stored_as
FROM data_type_showcase
WHERE text_value = 'preloaded mood';
```

Expect: 1 row, happy, nvarchar: text the column's CHECK allows (SQL Server has no enum types).

#### 3. Strict: reading all columns reads the preloaded ones

Strict: all columns means the three preloaded; any other column is refused before the database.

In scope: `connectionString` (string).

```csharp
// A public directory endpoint: name, number and website, never the credit limit.
var tables = new ArxPreLoadTableRegister()
    .PreLoadTable("account", ["name", "account_number", "website"]);

var schema = await new ArxPreLoadTable(tables).LoadAsync(connectionString);
var directory = new ArxMsSqlDbService(connectionString, schema) { Strict = true };

var all = new ArxQueryExpression("account") { ColumnSet = ArxColumnSet.All };
all.Criteria.AddCondition("account_number", ArxConditionOperator.Equal, "ACC-PRE111");
var read = (await directory.RetrieveMultipleAsync(all)).Entities.Single();
Console.WriteLine($"all columns: {string.Join(", ", read.Attributes.Keys)}");

var probe = new ArxQueryExpression("account");
probe.ColumnSet.AddColumns("name", "credit_limit");

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

Check it in the database:

```sql
SELECT id, name, account_number, website
FROM account
WHERE account_number = 'ACC-PRE111';
```

Expect: 1 row: the four columns the Strict read returned (the id is always loaded).

#### 4. Save the preload as JSON and start from it

Save the preload as JSON; the next startup builds the catalog from it, without a catalog query.

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

```csharp
// ARX doesn't write files: the application keeps the JSON where it likes (a file, a settings store).
var json = preloaded.ToJson();
var schema = ArxSchemaCatalog.Create().AddJson(json).Build();
var service = new ArxMsSqlDbService(connectionString, schema);

// The JSON doesn't drift by itself: check it against the database at startup, as with any declared catalog.
await service.ValidateSchemaAsync();

var query = new ArxQueryExpression("account");
query.ColumnSet.AddColumns("name", "primary_contact_id");
query.Criteria.AddCondition("account_number", ArxConditionOperator.Equal, "ACC-PRE111");
var primary = (ArxEntityReference)(await service.RetrieveMultipleAsync(query)).Entities.Single()["primary_contact_id"]!;
Console.WriteLine($"from the JSON ({json.Length:N0} characters): primary contact \"{primary.Name}\"");
```

Note: Nothing new is stored: the catalog was rebuilt in memory from the JSON.

---

# Execute with ARX.SDK on SQL Server

## What an Execute does

| | |
|---|---|
| **The call** | `new ArxProcedureExpression("sp_place_order")`, in the pattern of `ArxQueryExpression`: `AddParameter`, `AddOutParameter<T>`, `AddInOutParameter<T>`; `ProcedureType = ArxProcedureType.Function` for a function. Never SQL text. `AddCursorParameter`, `AddVariadicParameter` and `ReturnsCursors` are ARX8001: SQL Server has no cursor parameters and no VARIADIC |
| **Procedure or function** | The caller says which: a procedure runs as `EXEC @arx_return = sp @p_name = @p0, …`, a function as `SELECT * FROM fn(…)` (table-valued) or `SELECT fn(…)` (scalar: ARX finds out on the first call and remembers it). A table-valued function called as a procedure is ARX8001 (SQL Server's error 2809), naming the right `ProcedureType`; a scalar one runs, because SQL Server's `EXEC` accepts it, and its value comes back as the RETURN value. A procedure called as a function is ARX5001 (there is no function by that name) |
| **Arguments** | A procedure's by name (with or without the `@`) or by position (positional first); a function's by position only. Each is a typed parameter through the same value path as Create (dates, JSON, selection sets, lookups). A null whose type matters needs it: `AddParameter(name, type, null)` |
| **What comes back** | `ArxExecuteResult`: `Outputs` (OUTPUT values as their declared types, and the procedure's `RETURN` value as `Outputs[ArxMsSqlDbService.RETURN_VALUE]`), `ResultSets` (each SELECT the procedure runs, by position, or a function's rows as `ResultSets[0]`), `Messages` (`PRINT` and `RAISERROR` of severity 10 or less, in order). `Value<T>()` reads a function's single value |
| **OUT and INOUT** | SQL Server has one kind, `OUTPUT`, which is both: an OUT starts as NULL. ARX sizes each to hold any value of its declared type (`nvarchar(max)`, `decimal(38, 10)`, …), so nothing is cut short |
| **Tables from a procedure** | Every SELECT the procedure runs is a result set, read by position (`ResultSets[0]`, `[1]` …) in one round trip. OUTPUT values arrive after the last one |
| **Paging** | `PageInfo`, as on a query, three ways: the procedure pages itself (`PagingParameters`: ARX fills its page arguments and reads its total), ARX pages a function's rows (`OFFSET … FETCH NEXT`, the total in one round trip), ARX pages a procedure's result set (on the client: earlier rows read and skipped, the rest counted; every row crosses the network). The total is opt-in |
| **Transactions** | ARX's own call runs outside any transaction, so a procedure may begin and commit its own; in an `ArxTransaction` or a batch scope, a procedure that commits or rolls back the caller's transaction is ARX8001 (SQL Server's error 266: the transaction count changed), and the transaction is aborted |
| **Errors the procedure raises** | `THROW` and `RAISERROR` of severity 11 or more are ARX3005 RaisedByDatabase, with the procedure's message and its error number as `NativeCode` (`50101` for `THROW 50101, …`), so the application can branch on its own numbers |
| **Security** | A procedure, function or parameter name carrying SQL is ARX1017, refused before the database; values are parameters |
| **Names in the samples** | Procedures `sp_`, functions `fn_` (views `vw_`) |
| **Without a catalog** | Needs no catalog and reads nothing first: a service created with only the connection string calls procedures and functions as any other |

## Reference

### Error codes an Execute can raise

| Code | When |
|---|---|
| ARX1001 InvalidArgument | A positional argument after a named one; OUT or INOUT parameters on a function; an OUT without a type; `AddOrder` on a procedure; `PageInfo` with nothing to page or below 1; `PagingParameters` without `PageInfo`; `PagedResultSet` naming no result set |
| ARX1002, ARX1003, ARX1004 | No routine name; an invalid name (three parts, a typo); a parameter given twice |
| ARX1007, ARX1013 | A value type ARX can't write; `default(DateTime)` |
| ARX1008 InvalidConnection | A connection with an open transaction, given without that transaction |
| ARX1017 SuspectedInjection | A procedure, function, parameter or paging name carrying SQL |
| ARX2003 MultipleRecordsMatched | `Value<T>()` on several rows |
| ARX3001–3004 | A constraint the procedure broke (kept from the database) |
| ARX3005 RaisedByDatabase | The procedure refused: `THROW`, or `RAISERROR` of severity 11 to 19 (error numbers 50000 and up) |
| ARX4003 InvalidValueFormat | An argument that doesn't convert to the parameter's type (`'abc'` for an `int`) |
| ARX4004 DataTypeMismatch | Arguments of a type the parameter can't take; too many arguments (8144); a named argument the procedure doesn't have (8145); a required parameter left out (201) |
| ARX5001 UndefinedEntity | No procedure or function by that name (2812, 208) |
| ARX5003 SchemaMismatch | An OUT value of another type than declared, that can't be widened |
| ARX6001 PermissionDenied | No `EXECUTE` permission |
| ARX7003 Timeout | `CommandTimeout` exceeded (SQL Server has no statement timeout of its own: the client's applies) |
| ARX8001 NotSupported | A table-valued function called as a procedure (2809); a procedure that commits or rolls back inside an ARX transaction or a scope (266); cursor parameters, VARIADIC, `ReturnsCursors` (before the database); named arguments or result columns on a function |

## The samples

Every Execute sample, in reading order. Each scenario shows the code (verified against SQL Server) and the SQL that checks what it did. `service` is the application's `ArxMsSqlDbService`; 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-sql-sample-application.md.

### Call a stored procedure

Basics 1 of Execute.

Calling a stored procedure, in three scenarios: named arguments; positional arguments; a parameter left out, so the procedure's default is used. The call is described with `ArxProcedureExpression`, the way a query is described with `ArxQueryExpression`; ARX sends `EXEC` with every value as a parameter.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var service = new ArxMsSqlDbService(connectionString);
var account = new ArxEntity("account");
account["name"] = "Old Name Ltd";
account["account_number"] = "ACC-E101";
var accountId = await service.CreateAsync(account);
```

#### 1. Named arguments

`sp_rename_account(p_account_id, p_name, p_reason)` with every argument named. Nothing comes back.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
var procedure = new ArxProcedureExpression("sp_rename_account");
procedure.AddParameter("p_account_id", accountId.AsInt64());
procedure.AddParameter("p_name", "New Name Ltd");
procedure.AddParameter("p_reason", "rebranding");

var result = await service.ExecuteAsync(procedure);
Console.WriteLine($"renamed; outputs: {result.Outputs.Attributes.Count()}, result sets: {result.ResultSets.Count}");
```

Check it in the database:

```sql
SELECT name
FROM account
WHERE account_number = 'ACC-E101';
```

Expect: New Name Ltd (until scenario 2 renames it again).

#### 2. Positional arguments

The same procedure with positional arguments, in the procedure's order.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
var procedure = new ArxProcedureExpression("sp_rename_account");
procedure.AddParameter(accountId.AsInt64());
procedure.AddParameter("Positional Ltd");
procedure.AddParameter("a positional call");

await service.ExecuteAsync(procedure);
Console.WriteLine("renamed to Positional Ltd");
```

Check it in the database:

```sql
SELECT name
FROM account
WHERE account_number = 'ACC-E101';
```

Expect: Positional Ltd (until scenario 3).

#### 3. A parameter left out: the default is used

`p_reason` left out: the procedure's default (`'not given'`) is used.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
var procedure = new ArxProcedureExpression("sp_rename_account");
procedure.AddParameter("p_account_id", accountId.AsInt64());
procedure.AddParameter("p_name", "Final Name Ltd");

await service.ExecuteAsync(procedure);
Console.WriteLine("renamed to Final Name Ltd, without a reason");
```

Check it in the database:

```sql
SELECT name, CASE WHEN modified_on IS NOT NULL THEN 1 ELSE 0 END AS modified
FROM account
WHERE account_number = 'ACC-E101';
```

Expect: Final Name Ltd, 1.

### Read OUT and INOUT values

Basics 2 of Execute.

OUT and INOUT values, in two scenarios: an INOUT goes in and comes back (here filled in by the procedure when it arrives as NULL), and OUT values come back typed in `Outputs`. SQL Server has one kind, `OUTPUT`, which is both: ARX sends an OUT as an OUTPUT parameter that starts as NULL. Each is declared with its .NET type, and ARX sizes it to hold any value of that type.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "Totals Trading";
account["account_number"] = "ACC-E201";
var accountId = await service.CreateAsync(account);
foreach (var (number, year, total) in new[] { ("SO-E201", 2025, 100m), ("SO-E202", 2026, 250m), ("SO-E203", 2026, 50m) })
{
    var order = new ArxEntity("sales_order");
    order["order_number"] = number;
    order["account_id"] = new ArxEntityReference("account", accountId);
    order["ordered_on"] = new DateTime(year, 6, 1, 12, 0, 0, DateTimeKind.Utc);
    order["total"] = total;
    await service.CreateAsync(order);
}
```

#### 1. A year in, the totals out

`p_year` = 2026 in; the number of orders and the revenue of that year out.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
var outputs = (await service.ExecuteAsync(Totals(accountId, 2026))).Outputs;
Console.WriteLine($"year {outputs["p_year"]}: {outputs["p_orders"]} orders ({outputs["p_orders"]!.GetType().Name}), revenue {outputs["p_revenue"]:N2} ({outputs["p_revenue"]!.GetType().Name})");
```

Defined in the sample, used above:

```csharp
private static ArxProcedureExpression Totals(ArxRecordId accountId, int? year)
{
    var procedure = new ArxProcedureExpression("sp_account_totals");
    procedure.AddParameter("p_account_id", accountId.AsInt64());
    procedure.AddInOutParameter("p_year", year);
    procedure.AddOutParameter<long>("p_orders");
    procedure.AddOutParameter<decimal>("p_revenue");
    return procedure;
}
```

Check it in the database:

```sql
SELECT count(*), sum(total)
FROM sales_order
WHERE order_number LIKE 'SO-E20%' AND YEAR(ordered_on) = 2026;
```

Expect: 2, 300.00.

#### 2. An INOUT the procedure fills in

`p_year` sent as NULL: the procedure fills in the current year and returns it.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
var outputs = (await service.ExecuteAsync(Totals(accountId, null))).Outputs;
Console.WriteLine($"no year given: the procedure used {outputs["p_year"]}, {outputs["p_orders"]} orders");
```

Check it in the database:

```sql
SELECT YEAR(SYSDATETIMEOFFSET()) AS this_year, count(*)
FROM sales_order
WHERE order_number LIKE 'SO-E20%' AND YEAR(ordered_on) = YEAR(SYSDATETIMEOFFSET());
```

Expect: the year and the count the sample printed.

### Call a function: a value, rows, an effect

Basics 3 of Execute.

Calling a function, in three scenarios: one value (`Value<T>()`); rows (`ResultSets[0]`); and an effect, which on SQL Server is a procedure's job. A function is the same `ArxProcedureExpression` with `Function`: ARX sends `SELECT * FROM fn(…)` for a table-valued function and `SELECT fn(…)` for a scalar one, instead of `EXEC`. To filter, order or page a function's rows like a table, Retrieve reads it as a row source (RetrieveMultiple's RealCase6, Advanced8).

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "Function Fabrics";
account["account_number"] = "ACC-E301";
var accountId = await service.CreateAsync(account);
foreach (var (first, title) in new[] { ("Fran", "Buyer"), ("Finn", "Engineer"), ("Faye", "Director") })
{
    var contact = new ArxEntity("contact");
    contact["first_name"] = first;
    contact["last_name"] = "Fn-E301";
    contact["job_title"] = title;
    contact["account_id"] = new ArxEntityReference("account", accountId);
    await service.CreateAsync(contact);
}
```

#### 1. One value

`fn_contact_count(p_account_id)` returns a bigint.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
var function = new ArxProcedureExpression("fn_contact_count") { ProcedureType = ArxProcedureType.Function };
function.AddParameter(accountId.AsInt64());

var count = (await service.ExecuteAsync(function)).Value<long>();
Console.WriteLine($"{count} contacts");
```

Check it in the database:

```sql
SELECT dbo.fn_contact_count(a.id)
FROM account a
WHERE a.account_number = 'ACC-E301';
```

Expect: 3.

#### 2. Rows

`fn_contacts_named(p_last_name)` returns a table: its rows are the first result set.

In scope: `service` (ArxMsSqlDbService).

```csharp
var function = new ArxProcedureExpression("fn_contacts_named") { ProcedureType = ArxProcedureType.Function };
function.AddParameter("Fn-E301");

var rows = (await service.ExecuteAsync(function)).ResultSets[0];
Console.WriteLine(string.Join(", ", rows.Entities.Select(x => $"{x["first_name"]} ({x["job_title"]})")));
```

Check it in the database:

```sql
SELECT first_name, job_title
FROM fn_contacts_named('Fn-E301')
ORDER BY first_name;
```

Expect: 3 rows: Faye Director, Finn Engineer, Fran Buyer.

#### 3. An effect: a procedure, not a function

A SQL Server function can't change data: an effect is a procedure's job. `sp_rename_account` renames the account and sets modified_on; it returns no rows, and its RETURN value is 0.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
var procedure = new ArxProcedureExpression("sp_rename_account");
procedure.AddParameter("p_account_id", accountId.AsInt64());
procedure.AddParameter("p_name", "Fabrikam Functions");

var result = await service.ExecuteAsync(procedure);
Console.WriteLine($"result sets: {result.ResultSets.Count}, RETURN value {result.Outputs[ArxMsSqlDbService.RETURN_VALUE]}");
```

Check it in the database:

```sql
SELECT name, CASE WHEN modified_on IS NOT NULL THEN 1 ELSE 0 END AS touched
FROM account
WHERE account_number = 'ACC-E301';
```

Expect: Fabrikam Functions, 1.

### A procedure that returns tables

Basics 4 of Execute.

A procedure that returns tables, in two scenarios. A SQL Server procedure returns a table by selecting it: each SELECT it runs is a result set. ARX runs the call in one round trip and returns the result sets by position (`ResultSets[0]`, `ResultSets[1]` …), in the order the procedure sends them.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "Tables Inc";
account["account_number"] = "ACC-E401";
var accountId = await service.CreateAsync(account);
foreach (var (sku, price) in new[] { ("SKU-E401", 10m), ("SKU-E402", 4.5m) })
{
    var product = new ArxEntity("product");
    product["sku"] = sku;
    product["name"] = $"Product {sku}";
    product["unit_price"] = price;
    await service.CreateAsync(product);
}
var place = new ArxProcedureExpression("sp_place_order");
place.AddParameter("p_account_id", accountId.AsInt64());
place.AddParameter("p_order_number", "SO-E401");
place.AddParameter("p_skus", "SKU-E401,SKU-E402");
place.AddParameter("p_quantity", 2);
place.AddOutParameter<long>("p_order_id");
place.AddOutParameter<decimal>("p_total");
var orderId = (long)(await service.ExecuteAsync(place)).Outputs["p_order_id"]!;
```

#### 1. One table

`sp_order_with_lines`'s second table, the lines: `ResultSets[1]`.

In scope: `service` (ArxMsSqlDbService), `orderId` (long).

```csharp
var procedure = new ArxProcedureExpression("sp_order_with_lines");
procedure.AddParameter("p_order_id", orderId);

var lines = (await service.ExecuteAsync(procedure)).ResultSets[1];
Console.WriteLine(string.Join(", ", lines.Entities.Select(x => $"{x["sku"]} x{x["quantity"]} at {x["unit_price"]:N2}")));
```

Check it in the database:

```sql
SELECT p.sku, l.quantity, l.unit_price
FROM order_line l
JOIN sales_order o ON o.id = l.order_id
JOIN product p ON p.id = l.product_id
WHERE o.order_number = 'SO-E401'
ORDER BY p.sku;
```

Expect: 2 rows: SKU-E401 2 10.00, SKU-E402 2 4.50.

#### 2. Two tables, by position

Both tables: the order (`ResultSets[0]`) and its lines (`ResultSets[1]`).

In scope: `service` (ArxMsSqlDbService), `orderId` (long).

```csharp
var procedure = new ArxProcedureExpression("sp_order_with_lines");
procedure.AddParameter("p_order_id", orderId);

var result = await service.ExecuteAsync(procedure);
var order = result.ResultSets[0].Entities.Single();
Console.WriteLine($"{order["order_number"]} for {order["account"]}, total {order["total"]:N2}; {result.ResultSets[1].Entities.Count} lines (ResultSets[1] is \"{result.ResultSets[1].EntityName}\")");
```

Check it in the database:

```sql
SELECT o.order_number, a.name, o.total, (SELECT count(*) FROM order_line l WHERE l.order_id = o.id) AS lines
FROM sales_order o
JOIN account a ON a.id = o.account_id
WHERE o.order_number = 'SO-E401';
```

Expect: SO-E401, Tables Inc, 29.00, 2.

### Place an order through a procedure

Real case 1 of Execute.

Placing an order through a procedure, in two scenarios: `sp_place_order` writes the order and its lines (from the products' current prices) in one call and returns the new id and the total; the id is then used like any other (Retrieve reads the order back). The application doesn't repeat the pricing rule the procedure owns.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "Order Oasis";
account["account_number"] = "ACC-E501";
var accountId = await service.CreateAsync(account);
foreach (var (sku, price) in new[] { ("SKU-E501", 12m), ("SKU-E502", 3.25m), ("SKU-E503", 99m) })
{
    var product = new ArxEntity("product");
    product["sku"] = sku;
    product["name"] = $"Product {sku}";
    product["unit_price"] = price;
    await service.CreateAsync(product);
}
```

#### 1. Place the order

Two products, three of each: the procedure prices them and returns the order's id and total.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
var place = new ArxProcedureExpression("sp_place_order");
place.AddParameter("p_account_id", accountId.AsInt64());
place.AddParameter("p_order_number", "SO-E501");
place.AddParameter("p_skus", "SKU-E501,SKU-E502");
place.AddParameter("p_quantity", 3);
place.AddOutParameter<long>("p_order_id");
place.AddOutParameter<decimal>("p_total");

var outputs = (await service.ExecuteAsync(place)).Outputs;
Console.WriteLine($"order {outputs["p_order_id"]} placed, total {outputs["p_total"]:N2}");

return (long)outputs["p_order_id"]!;
```

Check it in the database:

```sql
SELECT o.total, count(l.id) AS lines, sum(l.quantity) AS units
FROM sales_order o
JOIN order_line l ON l.order_id = o.id
WHERE o.order_number = 'SO-E501'
GROUP BY o.total;
```

Expect: 45.75, 2, 6.

#### 2. Read it back by the returned id

The returned id is the order's id: Retrieve reads the order by it.

In scope: `service` (ArxMsSqlDbService), `orderId` (long).

```csharp
var order = await service.RetrieveAsync("sales_order", orderId, new ArxColumnSet("order_number", "total"));
Console.WriteLine($"{order!["order_number"]}: {order["total"]:N2}");
```

Check it in the database:

```sql
SELECT order_number, total
FROM sales_order
WHERE order_number = 'SO-E501';
```

Expect: SO-E501, 45.75.

### A report with several result sets

Real case 2 of Execute.

An account page built from one call: `sp_account_report` returns three tables (the contacts, the orders, a summary row), read together in one round trip, each by its position: the order of the procedure's SELECTs.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "Report Rangers";
account["account_number"] = "ACC-E601";
var accountId = await service.CreateAsync(account);
foreach (var first in new[] { "Rae", "Rob" })
{
    var contact = new ArxEntity("contact");
    contact["first_name"] = first;
    contact["last_name"] = "Report-E601";
    contact["account_id"] = new ArxEntityReference("account", accountId);
    await service.CreateAsync(contact);
}
foreach (var (number, total) in new[] { ("SO-E601", 120m), ("SO-E602", 80m) })
{
    var order = new ArxEntity("sales_order");
    order["order_number"] = number;
    order["account_id"] = new ArxEntityReference("account", accountId);
    order["ordered_on"] = new DateTime(2026, 11, 8, 9, 0, 0, DateTimeKind.Utc);
    order["total"] = total;
    await service.CreateAsync(order);
}
```

#### 1. Three tables in one call

Contacts, orders and a summary: ResultSets[0], [1] and [2].

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
var report = new ArxProcedureExpression("sp_account_report");
report.AddParameter("p_account_id", accountId.AsInt64());

var sets = (await service.ExecuteAsync(report)).ResultSets;
var summary = sets[2].Entities.Single();

Console.WriteLine($"contacts: {string.Join(", ", sets[0].Entities.Select(x => x["first_name"]))}");
Console.WriteLine($"orders: {string.Join(", ", sets[1].Entities.Select(x => $"{x["order_number"]} {x["total"]:N2}"))}");
Console.WriteLine($"summary: {summary["contacts"]} contacts, {summary["orders"]} orders, revenue {summary["revenue"]:N2}");
```

Check it in the database:

```sql
SELECT (SELECT count(*) FROM contact c WHERE c.account_id = a.id) AS contacts,
       (SELECT count(*) FROM sales_order o WHERE o.account_id = a.id) AS orders,
       (SELECT sum(total) FROM sales_order o WHERE o.account_id = a.id) AS revenue
FROM account a
WHERE a.account_number = 'ACC-E601';
```

Expect: 2, 2, 200.00.

### Page through a procedure's result: the procedure pages itself, ARX pages a function, ARX pages a result set

Real case 3 of Execute.

Paging what a procedure or function returns, the three ways, in four scenarios. Each sets `PageInfo`, as a query does, and gets back a result set with `CurrentPage`, `HasNextPage` and, when asked, the total. The total is opt-in: ask for it once (page 1), as for RetrieveMultiple.

- **The routine pages itself** (best for large data: the database produces only the page). ARX fills its page arguments from `PageInfo` and reads its total from an OUT parameter.
- **ARX pages a function's rows**: `OFFSET`/`FETCH NEXT` and the total in one round trip. Cheap for an inline table-valued function (SQL Server expands it, so the order and the limit reach the index); a multi-statement function fills its whole table first.
- **ARX pages a table the procedure returns** (a result set): SQL Server has no cursor to move, so ARX reads past the earlier rows, keeps one page and one row more, and counts the rest. Every row crosses the network.
- Which to choose.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var products = Enumerable.Range(1, 120).Select(i =>
{
    var product = new ArxEntity("product");
    product["sku"] = $"{PREFIX}{i:D3}";
    product["name"] = $"Paged product {i}";
    product["unit_price"] = 1m + i % 10;
    return product;
}).ToList();
await service.CreateMultipleAsync(products);
```

#### 1. The procedure pages itself

`sp_products_page(p_prefix, p_page, p_page_size, INOUT p_rows, INOUT p_total)` pages itself. ARX fills `p_page` and `p_page_size`, reads the table from `p_rows` and the total from `p_total`.

In scope: `service` (ArxMsSqlDbService).

```csharp
foreach (var pageNumber in new[] { 1, 3 })
{
    var procedure = new ArxProcedureExpression("sp_products_page");
    procedure.AddParameter("p_prefix", PREFIX);
    procedure.PageInfo = new ArxPagingInfo { Count = 50, PageNumber = pageNumber, ReturnTotalRecordCount = pageNumber == 1 };
    procedure.PagingParameters = ArxPagingParameters.PageNumber("p_page", "p_page_size", totalRecordCount: "p_total");

    Console.WriteLine(Describe((await service.ExecuteAsync(procedure)).ResultSets[0]));
}
```

Defined in the sample, used above:

```csharp
private static string Describe(ArxEntityCollection page)
    => $"page {page.CurrentPage}: {page.Entities.Count} rows ({page.Entities.FirstOrDefault()?["sku"]} to {page.Entities.LastOrDefault()?["sku"]}), next page: {page.HasNextPage}"
       + (page.TotalRecordCount > 0 ? $", {page.TotalRecordCount} in all, {page.TotalPageCount} pages" : string.Empty);
```

Check it in the database:

```sql
SELECT count(*), min(sku), max(sku)
FROM (SELECT sku FROM product WHERE sku LIKE 'PG-E7%' ORDER BY sku OFFSET 100 ROWS FETCH NEXT 50 ROWS ONLY) AS page_3;
```

Expect: 20, PG-E7101, PG-E7120 (page 1 is PG-E7001 to PG-E7050; 120 in all, 3 pages).

#### 2. ARX pages a function's rows

`fn_products_like(p_prefix)` returns every matching product; ARX orders and pages its rows, with the total in the same round trip.

In scope: `service` (ArxMsSqlDbService).

```csharp
var function = new ArxProcedureExpression("fn_products_like") { ProcedureType = ArxProcedureType.Function };
function.AddParameter(PREFIX);
function.AddOrder("unit_price", ArxOrderType.Descending);
function.AddOrder("sku", ArxOrderType.Ascending);
function.PageInfo = new ArxPagingInfo { Count = 25, PageNumber = 2, ReturnTotalRecordCount = true };

var page = (await service.ExecuteAsync(function)).ResultSets[0];
Console.WriteLine(Describe(page) + $"; prices {page.Entities[0]["unit_price"]:N2} to {page.Entities[^1]["unit_price"]:N2}");
```

Check it in the database:

```sql
SELECT sku, unit_price
FROM fn_products_like('PG-E7')
ORDER BY unit_price DESC, sku OFFSET 25 ROWS FETCH NEXT 25 ROWS ONLY;
```

Expect: 25 rows, PG-E7017 (8.00) first and PG-E7015 (6.00) last (120 in all, 5 pages).

#### 3. ARX pages a table the procedure returns

`sp_products` returns every matching product as a result set; ARX pages it.

In scope: `service` (ArxMsSqlDbService).

```csharp
var procedure = new ArxProcedureExpression("sp_products");
procedure.AddParameter("p_prefix", PREFIX);
procedure.PageInfo = new ArxPagingInfo { Count = 40, PageNumber = 3, ReturnTotalRecordCount = true };

Console.WriteLine(Describe((await service.ExecuteAsync(procedure)).ResultSets[0]));
```

Check it in the database:

```sql
SELECT count(*), min(sku), max(sku)
FROM (SELECT sku FROM product WHERE sku LIKE 'PG-E7%' ORDER BY sku OFFSET 80 ROWS FETCH NEXT 40 ROWS ONLY) AS page_3;
```

Expect: 40, PG-E7081, PG-E7120: the last page (no next page; 120 in all, 3 pages).

#### 4. Which to choose

The choice, with the numbers of `ExecutePagingBenchmarks`.

```csharp
Console.WriteLine("large data, deep pages: let the routine page itself (1); the database produces only the page.");
Console.WriteLine("an inline table-valued function: let ARX page it (2); SQL Server expands it, so the limit reaches the index.");
Console.WriteLine("a multi-statement function or a procedure's result set: ARX pages it (2, 3), but every skipped row is still produced (and, for 3, sent).");
Console.WriteLine("the total costs a count of every row: ask for it on page 1 only, or not at all for \"load more\".");
```

### Business errors raised by a procedure

Real case 4 of Execute.

Business rules the database enforces, in three scenarios. `sp_reserve_credit` refuses an account on hold (`THROW 50101`) and an amount over the credit limit (`THROW 50102`). Both arrive as `ARX3005 RaisedByDatabase` with the procedure's message and its error number as `NativeCode`, so the application can tell "the database said no" from "something broke", and branch on the procedure's own number.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var open = new ArxEntity("account");
open["name"] = "Credit Cove";
open["account_number"] = "ACC-E801";
open["credit_limit"] = 100m;
var openId = await service.CreateAsync(open);
var held = new ArxEntity("account");
held["name"] = "Held Harbour";
held["account_number"] = "ACC-E802";
held["credit_limit"] = 500m;
held["status"] = new ArxSelectionSetValue("AccountStatus", "ON_HOLD");
var heldId = await service.CreateAsync(held);
```

#### 1. Within the limit

40 of a limit of 100: reserved.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
await service.ExecuteAsync(Reserve(accountId, 40m));
Console.WriteLine("40.00 reserved");
```

Defined in the sample, used above:

```csharp
private static ArxProcedureExpression Reserve(ArxRecordId accountId, decimal amount)
{
    var procedure = new ArxProcedureExpression("sp_reserve_credit");
    procedure.AddParameter("p_account_id", accountId.AsInt64());
    procedure.AddParameter("p_amount", amount);
    return procedure;
}
```

Check it in the database:

```sql
SELECT credit_limit
FROM account
WHERE account_number = 'ACC-E801';
```

Expect: 60.00.

#### 2. Over the limit

250 more: refused, with the procedure's message and number; nothing changed.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
try
{
    await service.ExecuteAsync(Reserve(accountId, 250m));
}
catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.RaisedByDatabase)
{
    Console.WriteLine(error.Message);
    Console.WriteLine($"number {error.NativeCode}; for the user: \"{ForTheUser(error)}\"");
}
```

Defined in the sample, used above:

```csharp
private static ArxProcedureExpression Reserve(ArxRecordId accountId, decimal amount)
{
    var procedure = new ArxProcedureExpression("sp_reserve_credit");
    procedure.AddParameter("p_account_id", accountId.AsInt64());
    procedure.AddParameter("p_amount", amount);
    return procedure;
}

private static string ForTheUser(ArxException error) => error.NativeCode switch
{
    "50101" => "This account is on hold. Please contact accounts.",
    "50102" => "That's over the credit limit.",
    _ => "The request couldn't be completed."
};
```

Check it in the database:

```sql
SELECT credit_limit
FROM account
WHERE account_number = 'ACC-E801';
```

Expect: 60.00: the refused call changed nothing.

#### 3. An account on hold: the procedure's own number

An account on hold: the procedure's own number, `50101`, so the application shows another message.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
try
{
    await service.ExecuteAsync(Reserve(accountId, 10m));
}
catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.RaisedByDatabase)
{
    Console.WriteLine($"{error.NativeCode}: {error.Message}; for the user: \"{ForTheUser(error)}\"");
}
```

Check it in the database:

```sql
SELECT status, credit_limit
FROM account
WHERE account_number = 'ACC-E802';
```

Expect: ON_HOLD, 500.00.

### A nightly job that commits in batches

Real case 5 of Execute.

A nightly job that commits as it goes, in two scenarios. `sp_recalculate_totals` recalculates order totals in batches and commits after each, so a failure late in the run keeps the batches already done, and reports its progress. ARX's own call runs outside any transaction, which a procedure that commits needs (inside one, SQL Server ends the caller's transaction at the procedure's COMMIT and reports error 266, a changed transaction count); inside an ARX transaction or a batch scope the same call is refused with ARX8001 and the reason.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "Nightly Nets";
account["account_number"] = "ACC-E901";
var accountId = await service.CreateAsync(account);
var product = new ArxEntity("product");
product["sku"] = "SKU-E901";
product["name"] = "Recalculated product";
product["unit_price"] = 5m;
var productId = await service.CreateAsync(product);
for (var i = 1; i <= 7; i++)
{
    var order = new ArxEntity("sales_order");
    order["order_number"] = $"SO-E9{i:D2}";
    order["account_id"] = new ArxEntityReference("account", accountId);
    order["ordered_on"] = new DateTime(2026, 11, 8, 1, 0, 0, DateTimeKind.Utc);
    order["total"] = -1m;
    var orderId = await service.CreateAsync(order);

    var line = new ArxEntity("order_line");
    line["order_id"] = new ArxEntityReference("sales_order", orderId);
    line["product_id"] = new ArxEntityReference("product", productId);
    line["quantity"] = 2;
    line["unit_price"] = 5m;
    await service.CreateAsync(line);
}
```

#### 1. Not inside an ARX transaction

The job in an ARX transaction: SQL Server reports the changed transaction count (266), ARX8001 says why, and the transaction rolls back.

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

```csharp
await using var connection = new ARX.SDK.SQL.Services.ArxMsSqlConnection(connectionString);
await using var transaction = await connection.BeginTransactionAsync();

try
{
    await service.ExecuteAsync(Job(), transaction);
}
catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.NotSupported)
{
    Console.WriteLine(error.Message);
}
```

Defined in the sample, used above:

```csharp
private static ArxProcedureExpression Job()
{
    var procedure = new ArxProcedureExpression("sp_recalculate_totals");
    procedure.AddParameter("p_prefix", "SO-E9");
    procedure.AddParameter("p_batch", 3);
    return procedure;
}
```

Check it in the database:

```sql
SELECT count(*)
FROM sales_order
WHERE order_number LIKE 'SO-E9%' AND total < 0;
```

Expect: 7 right after this scenario (the refused call changed nothing); 0 once scenario 2 has run.

#### 2. Run the job alone: a commit per batch

Batches of three, each committed; the progress arrives as messages. A long job sets a longer timeout.

In scope: `service` (ArxMsSqlDbService).

```csharp
var result = await service.ExecuteAsync(Job(), new ArxExecuteOptions { CommandTimeout = 600 });

foreach (var message in result.Messages)
{
    Console.WriteLine($"{message.Severity}: {message.Text}");
}
```

Check it in the database:

```sql
SELECT SUM(CASE WHEN total = 10 THEN 1 ELSE 0 END) AS recalculated, SUM(CASE WHEN total < 0 THEN 1 ELSE 0 END) AS [left]
FROM sales_order
WHERE order_number LIKE 'SO-E9%';
```

Expect: 7, 0.

### Progress messages from a procedure

Real case 6 of Execute.

The messages a procedure sends while it runs, in two scenarios: `PRINT` and `RAISERROR` of severity 10 or less (information, not errors; SQL Server reports their severity as 0) come back in `Messages`, in order, for the application to show or log as it chooses (ARX never writes them to a log); `CollectMessages = false` leaves them out.

Usings:

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

#### 1. The messages, in order

An import of 1,000 rows: a progress message every 250 rows, then one because it's large.

In scope: `service` (ArxMsSqlDbService).

```csharp
var result = await service.ExecuteAsync(Import(1_000));

foreach (var message in result.Messages)
{
    Console.WriteLine($"{message.Severity,-7} {message.Text}");
}
```

Defined in the sample, used above:

```csharp
private static ArxProcedureExpression Import(int rows)
{
    var procedure = new ArxProcedureExpression("sp_import_progress");
    procedure.AddParameter("p_rows", rows);
    return procedure;
}
```

Check it in the database:

```sql
SELECT name, type_desc
FROM sys.objects
WHERE name = 'sp_import_progress';
```

Expect: sp_import_progress, SQL_STORED_PROCEDURE (the messages themselves are only seen by the session that ran the call).

#### 2. Not collected

The same call without collecting them.

In scope: `service` (ArxMsSqlDbService).

```csharp
var result = await service.ExecuteAsync(Import(1_000), new ArxExecuteOptions { CollectMessages = false });
Console.WriteLine($"{result.Messages.Count} messages");
```

Check it in the database:

```sql
SELECT COUNT(*)
FROM sys.procedures
WHERE name = 'sp_import_progress';
```

Expect: 1.

### Handle Execute errors

Real case 7 of Execute.

The errors an Execute call can raise, turned into messages, in two scenarios: what the database refuses (a procedure that doesn't exist, arguments of the wrong type, a function called as a procedure, a timeout) and what ARX refuses before it (a positional argument after a named one, an OUT parameter without a type, a cursor parameter).

Usings:

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

#### 1. What the database refuses

Mapped from SQL Server's errors.

In scope: `service` (ArxMsSqlDbService).

```csharp
await TryAsync("a procedure that doesn't exist", () => service.ExecuteAsync(new ArxProcedureExpression("sp_no_such_procedure")));

var wrongTypes = new ArxProcedureExpression("sp_rename_account");
wrongTypes.AddParameter("p_account_id", Guid.NewGuid());
wrongTypes.AddParameter("p_name", "x");
await TryAsync("the wrong argument types", () => service.ExecuteAsync(wrongTypes));

// A table-valued function called as a procedure. (A scalar one would run: SQL Server's EXEC accepts it and returns
// its value as the RETURN value. Call functions as functions either way.)
var function = new ArxProcedureExpression("fn_contacts_named");
function.AddParameter("Report-E601");
await TryAsync("a function called as a procedure", () => service.ExecuteAsync(function));

var slow = new ArxProcedureExpression("sp_slow");
slow.AddParameter("p_seconds", 3.0);
await TryAsync("a timeout of 1 second", () => service.ExecuteAsync(slow, new ArxExecuteOptions { CommandTimeout = 1 }));
```

Defined in the sample, used above:

```csharp
private static async Task TryAsync(string label, Func<Task> call)
{
    try
    {
        await call();
        Console.WriteLine($"{label}: no error");
    }
    catch (ArxException error)
    {
        Console.WriteLine($"{label}: {error.Code} ({(int)error.Code}), {error.Category}");
    }
}
```

Check it in the database:

```sql
SELECT name, type
FROM sys.objects
WHERE name IN ('sp_no_such_procedure', 'sp_rename_account', 'fn_contact_count', 'sp_slow')
ORDER BY name;
```

Expect: 3 rows: fn_contact_count FN, sp_rename_account P, sp_slow P (no sp_no_such_procedure).

#### 2. What ARX refuses first

Refused before any connection opens.

In scope: `service` (ArxMsSqlDbService).

```csharp
var mixed = new ArxProcedureExpression("sp_rename_account");
mixed.AddParameter("p_account_id", 1L);
mixed.AddParameter("positional after a named one");
await TryAsync("a positional argument after a named one", () => service.ExecuteAsync(mixed));

var untyped = new ArxProcedureExpression("sp_account_totals");
untyped.Parameters.Add(new ArxProcedureParameter("p_orders", null, null, ArxProcedureParameterDirection.Out));
await TryAsync("an OUT parameter without a type", () => service.ExecuteAsync(untyped));

var cursor = new ArxProcedureExpression("sp_order_with_lines");
cursor.AddCursorParameter("p_lines");
await TryAsync("a cursor parameter (SQL Server has none)", () => service.ExecuteAsync(cursor));
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE id = 1 AND name = 'positional after a named one';
```

Expect: 0: nothing reached the database.

### Catch an SQL injection attempt on Execute

Real case 8 of Execute.

Catching an SQL injection attempt on Execute, in three scenarios. A routine name or a parameter name that carries SQL is refused with ARX1017 SuspectedInjection before the database. SQL in a value is a parameter: it's stored as text and runs nothing. Execute never takes SQL text.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "Injection: keep me";
account["account_number"] = "ACC-I801";
var accountId = await service.CreateAsync(account);
```

#### 1. A routine name from the request carries SQL

`POST /api/run/{report}` with the report used as the procedure name.

In scope: `service` (ArxMsSqlDbService).

```csharp
try
{
    await service.ExecuteAsync(new ArxProcedureExpression("sp_account_report(1); DROP TABLE account; --"));
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.SuspectedInjection)
{
    SecurityLog(error);
}
```

Defined in the sample, used above:

```csharp
private static void SecurityLog(ArxValidationException error) => Console.WriteLine($"SECURITY: {error.Message}");
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number = 'ACC-I801';
```

Expect: 1: the table is there.

#### 2. A parameter name from the request carries SQL

A form whose field names become parameter names.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
var procedure = new ArxProcedureExpression("sp_rename_account");
procedure.AddParameter("p_account_id", accountId.AsInt64());
procedure.AddParameter("p_name => 'x'); DELETE FROM account; --", "Pwned");

try
{
    await service.ExecuteAsync(procedure);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.SuspectedInjection)
{
    SecurityLog(error);
}
```

Check it in the database:

```sql
SELECT name
FROM account
WHERE account_number = 'ACC-I801';
```

Expect: Injection: keep me (until scenario 3).

#### 3. SQL in a value is a value

SQL typed into a value: it's the account's new name, nothing more.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
var procedure = new ArxProcedureExpression("sp_rename_account");
procedure.AddParameter("p_account_id", accountId.AsInt64());
procedure.AddParameter("p_name", "x'); DROP TABLE account; --");

await service.ExecuteAsync(procedure);
Console.WriteLine("renamed, literally");
```

Check it in the database:

```sql
SELECT name
FROM account
WHERE account_number = 'ACC-I801';
```

Expect: x'); DROP TABLE account; --

### Execute in an ARX transaction

Advanced case 1 of Execute.

Execute inside an ARX transaction, in two scenarios: a Create and a procedure in one transaction, the procedure's writes read back inside it, and everything gone after a rollback; and a business error from the procedure aborting the transaction, so the next call is refused (ARX1015) until the application rolls back.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var product = new ArxEntity("product");
product["sku"] = "SKU-EA11";
product["name"] = "Transactional product";
product["unit_price"] = 20m;
await service.CreateAsync(product);
```

#### 1. Create, call, read inside, roll back

Create an account, place its order through the procedure, read the order inside the transaction, roll back.

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

```csharp
await using var connection = new ArxMsSqlConnection(connectionString);
await using var transaction = await connection.BeginTransactionAsync();

var account = new ArxEntity("account");
account["name"] = "Rolled Back Retail";
account["account_number"] = "ACC-EA11";
var accountId = await service.CreateAsync(account, transaction);

var place = new ArxProcedureExpression("sp_place_order");
place.AddParameter("p_account_id", accountId.AsInt64());
place.AddParameter("p_order_number", "SO-EA11");
place.AddParameter("p_skus", "SKU-EA11");
place.AddParameter("p_quantity", 4);
place.AddOutParameter<long>("p_order_id");
place.AddOutParameter<decimal>("p_total");
var outputs = (await service.ExecuteAsync(place, transaction)).Outputs;

var inside = await service.RetrieveAsync("sales_order", (long)outputs["p_order_id"]!, transaction, new ArxColumnSet("total"));
Console.WriteLine($"inside the transaction: order total {inside!["total"]:N2}");

await transaction.RollbackAsync();
Console.WriteLine("rolled back");
```

Check it in the database:

```sql
SELECT (SELECT count(*) FROM account WHERE account_number = 'ACC-EA11') AS accounts,
       (SELECT count(*) FROM sales_order WHERE order_number = 'SO-EA11') AS orders;
```

Expect: 0, 0: the account and the procedure's order went with the rollback.

#### 2. A raised error aborts the transaction

The procedure refuses (ARX3005); the transaction is aborted, so the next call is ARX1015.

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

```csharp
await using var connection = new ArxMsSqlConnection(connectionString);
await using var transaction = await connection.BeginTransactionAsync();

var account = new ArxEntity("account");
account["name"] = "Aborted Atelier";
account["account_number"] = "ACC-EA12";
account["credit_limit"] = 10m;
var accountId = await service.CreateAsync(account, transaction);

var reserve = new ArxProcedureExpression("sp_reserve_credit");
reserve.AddParameter("p_account_id", accountId.AsInt64());
reserve.AddParameter("p_amount", 50m);

try
{
    await service.ExecuteAsync(reserve, transaction);
}
catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.RaisedByDatabase)
{
    Console.WriteLine($"refused: {error.Message}; aborted: {transaction.IsAborted}");
}

try
{
    await service.RetrieveAsync("account", accountId, transaction);
}
catch (ArxValidationException error) when (error.Code == ArxErrorCode.TransactionAborted)
{
    Console.WriteLine(error.Message);
}
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number = 'ACC-EA12';
```

Expect: 0: the aborted transaction was rolled back when it was disposed.

### Execute in a batch scope: a refused call undone alone

Advanced case 2 of Execute.

Execute in a batch scope, in one scenario: an import of three orders through the procedure, one transaction. Each call has its own savepoint (asked for with Savepoints = true), so the order the procedure refuses (a credit limit) is undone alone and the import continues; the others are committed together at the end.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var product = new ArxEntity("product");
product["sku"] = "SKU-EA21";
product["name"] = "Scoped product";
product["unit_price"] = 10m;
await service.CreateAsync(product);
var account = new ArxEntity("account");
account["name"] = "Scoped Supplies";
account["account_number"] = "ACC-EA21";
account["credit_limit"] = 100m;
var accountId = await service.CreateAsync(account);
```

#### 1. Import three orders in one scope

Three orders: each is a credit reservation then the order. The second is over the limit: its reservation is refused and the order skipped; the scope stays active.

In scope: `service` (ArxMsSqlDbService), `accountId` (ArxRecordId).

```csharp
await service.RunBatchAsync(async scope =>
{
    foreach (var (number, amount, quantity) in new[] { ("SO-EA21", 30m, 3), ("SO-EA22", 500m, 50), ("SO-EA23", 20m, 2) })
    {
        var reserve = new ArxProcedureExpression("sp_reserve_credit");
        reserve.AddParameter("p_account_id", accountId.AsInt64());
        reserve.AddParameter("p_amount", amount);

        try
        {
            await scope.ExecuteAsync(reserve);
        }
        catch (ArxDatabaseException error) when (error.Code == ArxErrorCode.RaisedByDatabase)
        {
            Console.WriteLine($"{number}: refused in call {error.BatchNumber} ({error.Message}); the scope is {scope.State}");
            continue;
        }

        var place = new ArxProcedureExpression("sp_place_order");
        place.AddParameter("p_account_id", accountId.AsInt64());
        place.AddParameter("p_order_number", number);
        place.AddParameter("p_skus", "SKU-EA21");
        place.AddParameter("p_quantity", quantity);
        place.AddOutParameter<long>("p_order_id");
        place.AddOutParameter<decimal>("p_total");
        var total = (await scope.ExecuteAsync(place)).Outputs["p_total"];
        Console.WriteLine($"{number}: placed, total {total:N2}");
    }
}, new ArxBatchOptions { Savepoints = true });
```

Check it in the database:

```sql
SELECT (SELECT STRING_AGG(CONCAT(order_number, ' ', total), ', ') WITHIN GROUP (ORDER BY order_number) FROM sales_order WHERE order_number LIKE 'SO-EA2%') AS orders,
       (SELECT credit_limit FROM account WHERE account_number = 'ACC-EA21') AS credit_left;
```

Expect: SO-EA21 30.00, SO-EA23 20.00 | 50.00.

### Export what a procedure returns, page by page

Advanced case 3 of Execute.

Exporting everything a procedure returns, page by page, in one scenario: `sp_products_page` pages itself (the first way of paging), the export asks for the total on the first page only, and reads pages until `HasNextPage` is false. Each page is one call, so the memory is one page's.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var products = Enumerable.Range(1, 1_050).Select(i =>
{
    var product = new ArxEntity("product");
    product["sku"] = $"{PREFIX}-{i:D4}";
    product["name"] = $"Exported product {i}";
    product["unit_price"] = 1m + i % 7;
    return product;
}).ToList();
await service.CreateMultipleAsync(products);
```

#### 1. Every page, the total asked once

Pages of 200: the first says how many pages there are; the loop ends on HasNextPage.

In scope: `service` (ArxMsSqlDbService).

```csharp
var exported = 0;
var sum = 0m;
var pageCount = 0;
ArxEntityCollection page;
var number = 0;

do
{
    var procedure = new ArxProcedureExpression("sp_products_page");
    procedure.AddParameter("p_prefix", PREFIX);
    procedure.PageInfo = new ArxPagingInfo { Count = 200, PageNumber = ++number, ReturnTotalRecordCount = number == 1 };
    procedure.PagingParameters = ArxPagingParameters.PageNumber("p_page", "p_page_size", totalRecordCount: "p_total");

    page = (await service.ExecuteAsync(procedure)).ResultSets[0];

    if (number == 1)
    {
        pageCount = page.TotalPageCount;
    }

    exported += page.Entities.Count;
    sum += page.Entities.Sum(x => (decimal)x["unit_price"]!);
}
while (page.HasNextPage);

Console.WriteLine($"{exported} products in {number} pages (page 1 said {pageCount}), prices summing to {sum:N2}");
```

Check it in the database:

```sql
SELECT count(*), sum(unit_price), CEILING(count(*) / 200.0)
FROM product
WHERE sku LIKE 'EXP-EA3%';
```

Expect: 1050, the sum the sample printed, 6.

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

Advanced case 4 of Execute.

A security check around Execute (`SecurityGuard`, in the project root), in three scenarios: a report run through an allowed list of procedures passes; an endpoint that runs whatever procedure the request names is probed, and the attacker is blocked after three attempts; and a false positive (an old client sending "account report (v1)") is triaged: support unblocks the user and the endpoint maps the old name.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var guard = new SecurityGuard(blockAfter: 3);
var account = new ArxEntity("account");
account["name"] = "Guarded Goods";
account["account_number"] = "ACC-EA41";
var accountId = await service.CreateAsync(account);
```

#### 1. A report through the allowed list

`POST /api/reports/account-report`: run.

In scope: `service` (ArxMsSqlDbService), `guard` (SecurityGuard), `accountId` (ArxRecordId).

```csharp
var result = await RunThroughTheListAsync(service, guard, "ana", "account-report", accountId);
var unknown = await RunThroughTheListAsync(service, guard, "ana", "sp_account_report; DROP TABLE account", accountId);

Console.WriteLine($"ana: {result.Outcome}, {result.Value!.ResultSets[2].Entities[0]["contacts"]} contacts; an unknown report: \"{unknown.ForTheUser}\" (never reaches ARX)");
```

Defined in the sample, used above:

```csharp
private static readonly Dictionary<string, string> Reports = new(StringComparer.OrdinalIgnoreCase)
{
    ["account-report"] = "sp_account_report",
};

private static ArxProcedureExpression Report(string procedureName, ArxRecordId accountId)
{
    var report = new ArxProcedureExpression(procedureName);
    report.AddParameter("p_account_id", accountId.AsInt64());
    return report;
}

private static Task<GuardResult<ArxExecuteResult>> RunThroughTheListAsync(ArxMsSqlDbService service, SecurityGuard guard, string user, string report, ArxRecordId accountId)
    => Reports.TryGetValue(report, out var procedure)
        ? guard.RunAsync(user, $"report {report}", NameSource.Code, () => service.ExecuteAsync(Report(procedure, accountId)))
        : Task.FromResult(new GuardResult<ArxExecuteResult>(GuardOutcome.Refused, null, $"404 Not Found: no report '{report}'."));
```

Check it in the database:

```sql
SELECT count(*)
FROM contact c
JOIN account a ON a.id = c.account_id
WHERE a.account_number = 'ACC-EA41';
```

Expect: 0, the contacts the report counted.

#### 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` (ArxMsSqlDbService), `guard` (SecurityGuard), `accountId` (ArxRecordId).

```csharp
foreach (var probe in new[] { "sp_account_report; DROP TABLE account; --", "sp_account_report\"(1); --", "sp_account_report WAITFOR DELAY '0:0:5'" })
{
    await RunDirectlyAsync(service, guard, "mallory", probe, accountId);
}

var valid = await RunDirectlyAsync(service, guard, "mallory", "sp_account_report", accountId);

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

Defined in the sample, used above:

```csharp
private static ArxProcedureExpression Report(string procedureName, ArxRecordId accountId)
{
    var report = new ArxProcedureExpression(procedureName);
    report.AddParameter("p_account_id", accountId.AsInt64());
    return report;
}

private static Task<GuardResult<ArxExecuteResult>> RunDirectlyAsync(ArxMsSqlDbService service, SecurityGuard guard, string user, string procedure, ArxRecordId accountId)
    => guard.RunAsync(user, $"report {procedure}", NameSource.UserInput, () => service.ExecuteAsync(Report(procedure, accountId)));
```

Check it in the database:

```sql
SELECT count(*)
FROM account
WHERE account_number = 'ACC-EA41';
```

Expect: 1: the table is there.

#### 3. A false positive, triaged

A false positive: an old client sends "account report (v1)". 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 the procedure.

In scope: `service` (ArxMsSqlDbService), `guard` (SecurityGuard), `accountId` (ArxRecordId).

```csharp
for (var attempt = 0; attempt < 3; attempt++)
{
    await RunDirectlyAsync(service, guard, "olga", "account report (v1)", accountId);
}

Console.WriteLine($"olga blocked: {guard.IsBlocked("olga")}; logged: {guard.Events.Last(x => x.User == "olga").Detail}");

guard.Unblock("olga");
Reports["account report (v1)"] = "sp_account_report";
var mapped = await RunThroughTheListAsync(service, guard, "olga", "account report (v1)", accountId);

Console.WriteLine($"after triage: olga blocked: {guard.IsBlocked("olga")}, the old name runs: {mapped.Outcome}");
```

Defined in the sample, used above:

```csharp
private static readonly Dictionary<string, string> Reports = new(StringComparer.OrdinalIgnoreCase)
{
    ["account-report"] = "sp_account_report",
};

private static ArxProcedureExpression Report(string procedureName, ArxRecordId accountId)
{
    var report = new ArxProcedureExpression(procedureName);
    report.AddParameter("p_account_id", accountId.AsInt64());
    return report;
}

private static Task<GuardResult<ArxExecuteResult>> RunThroughTheListAsync(ArxMsSqlDbService service, SecurityGuard guard, string user, string report, ArxRecordId accountId)
    => Reports.TryGetValue(report, out var procedure)
        ? guard.RunAsync(user, $"report {report}", NameSource.Code, () => service.ExecuteAsync(Report(procedure, accountId)))
        : Task.FromResult(new GuardResult<ArxExecuteResult>(GuardOutcome.Refused, null, $"404 Not Found: no report '{report}'."));

private static Task<GuardResult<ArxExecuteResult>> RunDirectlyAsync(ArxMsSqlDbService service, SecurityGuard guard, string user, string procedure, ArxRecordId accountId)
    => guard.RunAsync(user, $"report {procedure}", NameSource.UserInput, () => service.ExecuteAsync(Report(procedure, accountId)));
```

Check it in the database:

```sql
SELECT COUNT(*)
FROM sys.procedures
WHERE name = 'sp_account_report';
```

Expect: 1: the procedure the old name now maps to.

### Execute on a preloaded, Strict schema: routines aren't covered, tables are

Advanced case 5 of Execute.

Execute on a service whose schema is preloaded and Strict, in three scenarios: a procedure and a function called as usual, because Strict covers tables and views, not routines; a function read as a row source, with the routine's own columns; and the same service still refusing a table column that wasn't preloaded.

Good to know:

A routine's parameters and result columns are the routine's, not a table's, so a preload doesn't list them and Strict doesn't check them. What a routine may touch is the database's business: grant EXECUTE only on the routines an endpoint may call.

Usings:

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

Set up by the sample before its scenarios:

```csharp
var account = new ArxEntity("account");
account["name"] = "Routine Roasters";
account["account_number"] = "ACC-PRE-EX5";
account["credit_limit"] = 2_500m;
var accountId = await service.CreateAsync(account);
var tables = new ArxPreLoadTableRegister()
    .PreLoadTable("account", ["name", "account_number", "status"]);
var schema = await new ArxPreLoadTable(tables).LoadAsync(connectionString);
var service = new ArxMsSqlDbService(connectionString, schema) { Strict = true };
```

#### 1. A procedure and a function on a Strict service

A procedure renames the account; a function counts its contacts. Neither is in the preload.

In scope: `service` (ArxMsSqlDbService), `accountId` (long).

```csharp
var rename = new ArxProcedureExpression("sp_rename_account");
rename.AddParameter("p_account_id", accountId);
rename.AddParameter("p_name", "Routine Roasters & Co");
await service.ExecuteAsync(rename);

var count = new ArxProcedureExpression("fn_contact_count") { ProcedureType = ArxProcedureType.Function };
count.AddParameter(accountId);
Console.WriteLine($"renamed through sp_rename_account; fn_contact_count says {(await service.ExecuteAsync(count)).Value<long>()} contacts");

var renamed = new ArxQueryExpression("account");
renamed.ColumnSet.AddColumns("name");
renamed.Criteria.AddCondition("account_number", ArxConditionOperator.Equal, "ACC-PRE-EX5");
Console.WriteLine($"read back through the preloaded columns: {(await service.RetrieveMultipleAsync(renamed)).Entities.Single()["name"]}");
```

Check it in the database:

```sql
SELECT name, CASE WHEN modified_on IS NOT NULL THEN 1 ELSE 0 END AS renamed
FROM account
WHERE account_number = 'ACC-PRE-EX5';
```

Expect: Routine Roasters & Co, 1.

#### 2. A function as a row source: the routine's columns

fn_accounts_with_status returns account rows: as a row source, every column it returns comes back.

In scope: `service` (ArxMsSqlDbService).

```csharp
var active = new ArxQueryExpression(ArxRowSource.Function("fn_accounts_with_status", "ACTIVE")) { ColumnSet = ArxColumnSet.All };
active.Criteria.AddCondition("account_number", ArxConditionOperator.Equal, "ACC-PRE-EX5");
var row = (await service.RetrieveMultipleAsync(active)).Entities.Single();
Console.WriteLine($"{row.Attributes.Count} columns, credit_limit {row["credit_limit"]:N2} among them");
```

Check it in the database:

```sql
SELECT account_number, credit_limit
FROM fn_accounts_with_status('ACTIVE')
WHERE account_number = 'ACC-PRE-EX5';
```

Expect: 1 row, 2500.00.

#### 3. The table is still an allow list

The table itself stays an allow list: credit_limit wasn't preloaded.

In scope: `service` (ArxMsSqlDbService).

```csharp
var probe = new ArxQueryExpression("account");
probe.ColumnSet.AddColumns("name", "credit_limit");

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

Note: Nothing to check: the read was refused before the database (ARX1018).

---

# ARX.SDK on SQL Server: benchmarks

A living summary of the SQL Server benchmarks: how ARX compares with raw Microsoft.Data.SqlClient, Dapper and EF Core.
**Update it whenever a benchmark is rerun or an operation is added.**

| | |
|---|---|
| Last updated | 2026-11-09: first full run of every operation (108 benchmarks), then Delete rerun after the JSON-list change |
| Operations measured | Create, CreateMultiple, Update, UpdateMultiple, Delete, DeleteMultiple, Retrieve, RetrieveMultiple (rows, a page with the total), Execute (a procedure with result rows and OUTPUT values, a scalar function) |
| Machine | Intel Core i7-8750H, Windows 11, .NET 10.0.14, SQL Server 2022 (16.0.4225.2, `mcr.microsoft.com/mssql/server:2022-latest`) in Docker Desktop, Microsoft.Data.SqlClient 7.1.1, BenchmarkDotNet 0.15.8, job Standard (8 iterations, 3 warm-ups) |
| Compared with | Raw SqlClient (hand-written, typed parameters), Dapper 2.1.89, EF Core 10 (`Microsoft.EntityFrameworkCore.SqlServer` 10.0.0), `SqlBulkCopy` |
| Reports | The full run: results/2026-11-09-full-run/. Delete after the change: [results/2026-11-09-delete-after-json-list.md](#report-2026-11-09-delete-after-json-list). The IN-list probe: [results/probes/in-list-vs-openjson.md](#report-probes-in-list-vs-openjson). After READ COMMITTED on writes (E16): [Update](#report-2026-11-09-update-after-read-committed), [Delete](#report-2026-11-09-delete-after-read-committed) (ARX and raw only). |

**In one line:** ARX is level with hand-written SqlClient for single-row work, level with the best hand-written form for
many-row writes (and far ahead of the usual one and of EF Core), level with Dapper and EF Core for reads, and ahead of
everyone for deleting lists of 100 to 1,000 ids. Raw typed readers are faster than ARX's dictionaries for large reads.

Read the ratios, not the absolute times, which depend on the machine. Differences under about 10% on a single run are
inside the noise here. One run of each benchmark so far: treat close calls as level.

## Every operation at a glance (2026-11-09)

Mean times. "Usual" is the hand-written form most code uses; "best" is the fastest hand-written form found.

| Operation | **ARX** | Raw SqlClient | Dapper | EF Core | ARX vs raw |
|---|---|---|---|---|---|
| Create, one row | **2.65 ms** | 2.57 ms | 2.58 ms | 3.83 ms | level (1.03×) |
| CreateMultiple, 100 rows, ids back | **6.28 ms** | usual 24.5 ms, best 8.18 ms | – | 24.0 ms | 3.9× faster than usual, 0.77× best |
| CreateMultiple, 1,000 rows, ids back | **35.5 ms** | usual 437 ms, best 31.8 ms | – | 286 ms | 12× faster than usual, 1.12× best |
| CreateMultiple, 10,000 rows, ids back | **339 ms** | usual 4.43 s, best 345 ms | – | 1.85 s | level with best |
| CreateMultiple, 10,000 rows, no ids needed | **365 ms** (still returns ids) | `SqlBulkCopy` 111 ms | one statement a row: 27.2 s | – | 3.3× `SqlBulkCopy` |
| Update, one row | **2.67–2.86 ms** | 2.47–2.78 ms | 2.76 ms | 3.72 ms | 0.96–1.16× (two runs) |
| UpdateMultiple, 100 rows | **6.23 ms** | 5.73 ms | 273 ms | 25.0 ms | 1.09× |
| UpdateMultiple, 1,000 rows | **27.8 ms** | 23.2 ms | 2.52 s | 262 ms | 1.20× |
| UpdateMultiple, 10,000 rows | **269–279 ms** | 219–261 ms | 25.7 s | 1.96 s | 1.07–1.23× (two runs) |
| Delete, one row | **4.38–4.74 ms** | 4.20–4.66 ms | 4.14 ms | 12.4 ms | 0.96–1.12× (three runs) |
| DeleteMultiple, 100 ids | **4.87 ms** | IN list 5.89 ms | 6.12 ms | 15.1 ms | 0.83× |
| DeleteMultiple, 1,000 ids | **10.5–11.5 ms** | IN list 180 ms | 174 ms | 264 ms | 16× faster |
| DeleteMultiple, 10,000 ids | **52.3 ms** | OPENJSON 48.5 ms | 2.27 s | 61.4 ms | level |
| Retrieve, one row by id | **1.60 ms** | 1.47 ms | 1.44 ms | 2.12 ms | 1.09× |
| RetrieveMultiple, 100 rows | **1.88 ms** | 1.97 ms | 2.76 ms | 2.83 ms | level |
| RetrieveMultiple, 1,000 rows | **5.82 ms** | 4.01 ms | 4.97 ms | 5.15 ms | 1.45× (1.17× Dapper) |
| RetrieveMultiple, 10,000 rows | **62.8 ms** | typed 34.3 ms | 64.4 ms | 61.0 ms | 1.83× typed; level with Dapper and EF Core |
| A page of 50 with the total | **1.76–2.88 ms** | 2.68–2.73 ms | – | – | level |
| Execute: a procedure, 1 row + OUTPUT values | **3.24 ms** | 3.25 ms | 2.96 ms | no way to read OUTPUT values | level |
| Execute: a procedure, 1,000 rows | **4.78 ms** | 4.06 ms | 4.04 ms | – | 1.18× |
| Execute: a scalar function | **1.47–1.81 ms** | 1.43–1.51 ms | – | – | 1.03–1.20× |

"Usual" CreateMultiple is a multi-row `INSERT … OUTPUT` of up to 2,000 parameters a statement (ids unordered); "best"
is a hand-written `MERGE` from one `OPENJSON` parameter returning ids in list order, which is what ARX sends for groups
of 20 rows or more. The Dapper CreateMultiple and UpdateMultiple columns are `ExecuteAsync(sql, list)`, one statement
per row: what Dapper does with a list.

## What the numbers say

- **Single-row calls are level** with raw SqlClient and Dapper (0.96–1.16× across the runs), and 1.3–2.6× faster than EF Core. Getting
  Create there needed the plain `OUTPUT` change: the table variable cost about 2 ms a call.
- **Many-row writes are level with the best hand-written SQL** (0.77–1.20×): the JSON row source sends one parameter for
  any number of rows, so SQL Server compiles one plan and there is no 2,100-parameter limit to split around. Against
  the usual hand-written SQL (multi-row VALUES) ARX is 4–13× faster, and 4–9× faster than EF Core.
- **`SqlBulkCopy` is 3.3× faster for 10,000 rows when ids aren't needed.** ARX always returns ids; a bulk path without
  them would be a new option.
- **Lists of ids:** SQL Server's cost for an IN list climbs faster than the list (a SELECT by 1,000 ids is 28 ms as an
  IN list, 5 ms as one OPENJSON parameter, with a cached plan). ARX sends lists from 50 values as OPENJSON, so it beats
  every IN-list approach from 100 ids up (16× at 1,000).
- **Reads are level with Dapper and EF Core.** A raw typed reader is faster for large results (1.8× at 10,000 rows):
  ARX builds a dictionary per row, as on PostgreSQL, and allocates 2.3× what the raw reader does.
