# 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.

