# Update with ARX.SDK on MongoDB

## What an update does

| Rule | On MongoDB |
|---|---|
| What is written | Only the attributes present (`$set`); a field left out keeps its value; null is stored as null |
| Which documents | By the declared id (default), a declared key (`ArxMatch.Key`) or any fields (`ArxMatch.Columns`); match fields are never written |
| Nothing matched | ARX2002 RecordNotFound |
| RequireSingleRow | More than one match is ARX2003 and nothing changes |
| The id | Can't change: MongoDB's `_id` is immutable (ARX8001) |
| Match values | A document or array given as a match value is compared as data (`$eq`), never run as an operator |
| Concurrency | Optional. Declare a version column (`.Version(table, column)` in the schema catalog) and every update checks it in the same statement (a read returns the version with each record, so read, change and update carries it): an entity that carries an old version is ARX2004 RecordChanged and nothing is written, and each update adds one to the version. `.Version(table, column, required: true)` refuses an update that doesn't carry it (ARX1001). Without a version column the last writer wins. |
| Rows by a condition | Update finds rows by equal values: the id, a declared key, or columns you give values for (`ArxMatch.Columns`). To change every row that meets a condition (`amount > 100`), pass the values and an `ArxFilterExpression` to `UpdateMultipleAsync(values, filter)`: one call, all or nothing, from 0.1.0-preview.5 (UpdateMultiple, Basics). Before that version: read the rows with `RetrieveMultipleAsync` and pass them to `UpdateMultipleAsync`. |

## The samples

Every Update sample, in reading order. Each scenario shows the code (verified against MongoDB) and the mongosh query that checks what it did. `service` is the application's `ArxMongoDbService`; values marked "In scope" come from earlier in the sample. What the samples' code refers to beyond the packages (SampleCatalog.cs, `dbContext`) is in the sample application file, arx-mongodb-sample-application.md.

### Update one record

Basics 1 of Update.

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

Good to know:

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

Usings:

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

Set up by the sample before its scenarios:

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

#### 1. By the id Create returned

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

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

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

int changed = await service.UpdateAsync(change);

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

Check it in the database:

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

Expect: 1 document, Fourth Coffee, employee_count 55.

#### 2. Clear a value

Null stores null: the field stays, empty.

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

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

await service.UpdateAsync(change);

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

Check it in the database:

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

Expect: 1 document, website null.

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

An id no document has: ARX2002.

In scope: `service` (ArxMongoDbService).

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

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

Check it in the database:

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

Expect: 0.

### Choose how the documents are found

Basics 2 of Update.

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

Good to know:

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

Usings:

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

Set up by the sample before its scenarios:

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

#### 1. By a declared key

By the account number, a declared key.

In scope: `service` (ArxMongoDbService).

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

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

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

Check it in the database:

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

Expect: 1 document, employee_count 120.

#### 2. By any fields

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

In scope: `service` (ArxMongoDbService).

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

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

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

Check it in the database:

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

Expect: 3.

#### 3. Require exactly one

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

In scope: `service` (ArxMongoDbService).

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

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

Check it in the database:

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

Expect: 0.

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

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

In scope: `service` (ArxMongoDbService).

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

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

Check it in the database:

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

Expect: 0.

### Catch an injection attempt

Real case 1 of Update.

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

Good to know:

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

Usings:

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

Set up by the sample before its scenarios:

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

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

A posted field named "$set": ARX1017.

In scope: `service` (ArxMongoDbService).

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

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

Check it in the database:

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

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

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

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

In scope: `service` (ArxMongoDbService).

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

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

Check it in the database:

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

Expect: 0.

### Update in a batch scope

Advanced case 1 of Update.

Creates and updates in one batch scope, in two scenarios: committed when the body returns, and rolled back when the body throws.

Good to know:

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

Usings:

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

#### 1. Committed when the body returns

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

In scope: `service` (ArxMongoDbService).

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

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

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

Check it in the database:

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

Expect: 1 document, status "ON_HOLD".

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

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

In scope: `service` (ArxMongoDbService).

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

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

Check it in the database:

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

Expect: 0.

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

Advanced case 2 of Update.

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

Usings:

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

Set up by the sample before its scenarios:

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

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

A batch of two changes goes through the guard untouched.

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

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

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

Defined in the sample, used above:

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

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

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

        changes.Add(change);
    }

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

Check it in the database:

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

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

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

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

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

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

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

Check it in the database:

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

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

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

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

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

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

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

Defined in the sample, used above:

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

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

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

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

        changes.Add(change);
    }

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

Check it in the database:

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

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

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

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

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

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

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

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

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

Check it in the database:

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

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

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

Advanced case 3 of Update.

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

Good to know:

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

Usings:

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

Set up by the sample before its scenarios:

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

#### 1. Preload at startup

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

In scope: `connectionString` (string).

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

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

return service;
```

Check it in the database:

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

Expect: _id_ and the unique email_1 index.

#### 2. Update by the preloaded key

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

In scope: `service` (ArxMongoDbService).

```csharp
var typo = new ArxEntity("lead");
typo["email"] = "lead-m841@example.com";
typo["scroe"] = 90;

try
{
    await service.UpdateAsync(typo, new ArxUpdateOptions { Match = ArxMatch.Key("email") });
}
catch (ArxValidationException exception) when (exception.Code == ArxErrorCode.NotPreloaded)
{
    Console.WriteLine(exception.Message);
}

var change = new ArxEntity("lead");
change["email"] = "lead-m841@example.com";
change["score"] = 90;
await service.UpdateAsync(change, new ArxUpdateOptions { Match = ArxMatch.Key("email") });
Console.WriteLine("score set to 90");
```

Check it in the database:

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

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

