# Delete with ARX.SDK on MongoDB

## What a delete does

| Rule | On MongoDB |
|---|---|
| By id | `DeleteAsync(collection, id)` returns whether a document went; nothing to delete is false, not an error |
| By an entity | By the declared id, a key or any fields; returns how many went (0 is a normal outcome) |
| RequireSingleRow | None is ARX2002, several ARX2003, and nothing is deleted |
| Match values | A document or array given as a match value is compared as data, never run as an operator |
| Rows by a condition | Delete finds rows by equal values: the id, a declared key, or columns you give values for (`ArxMatch.Columns`). To delete every row that meets a condition (`amount > 100`), call `DeleteMultipleAsync(table, filter)`: one call, all or nothing, from 0.1.0-preview.5 (DeleteMultiple, Basics). Before that version: read the rows with `RetrieveMultipleAsync` and pass them to `DeleteMultipleAsync`. |

## The samples

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

### Delete one record by id

Basics 1 of Delete.

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

Good to know:

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

Usings:

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

Set up by the sample before its scenarios:

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

#### 1. One document

Delete the account by the id Create returned.

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

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

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

Check it in the database:

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

Expect: 0.

#### 2. Already gone

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

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

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

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

Check it in the database:

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

Expect: 0.

### Choose how the documents are found

Basics 2 of Delete.

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

Good to know:

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

Usings:

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

Set up by the sample before its scenarios:

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

#### 1. By a compound key

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

In scope: `service` (ArxMongoDbService).

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

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

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

Check it in the database:

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

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

#### 2. Require exactly one

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

In scope: `service` (ArxMongoDbService).

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

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

Check it in the database:

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

Expect: 2.

#### 3. By any fields

Every price of the batch.

In scope: `service` (ArxMongoDbService).

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

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

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

Check it in the database:

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

Expect: 0.

### Catch an injection attempt

Real case 1 of Delete.

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

Good to know:

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

Usings:

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

Set up by the sample before its scenarios:

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

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

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

In scope: `service` (ArxMongoDbService).

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

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

Check it in the database:

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

Expect: 1.

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

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

In scope: `service` (ArxMongoDbService).

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

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

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

Check it in the database:

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

Expect: 1: the victim is still there.

### Replace a record in a transaction

Advanced case 1 of Delete.

Replace an order in one ARX transaction: delete the old one, create the new one, commit, in two scenarios: the replacement, and a replacement that fails half way and leaves the old order in place.

Good to know:

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

Usings:

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

Set up by the sample before its scenarios:

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

#### 1. Replace

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

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

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

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

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

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

Check it in the database:

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

Expect: 1 document, SO-M932.

#### 2. A failed replacement

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

In scope: `service` (ArxMongoDbService).

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

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

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

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

Check it in the database:

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

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

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

Advanced case 2 of Delete.

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

Usings:

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

Set up by the sample before its scenarios:

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

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

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

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

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

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

Defined in the sample, used above:

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

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

Check it in the database:

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

Expect: 0: deleted.

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

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

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

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

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

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

Defined in the sample, used above:

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

Check it in the database:

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

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

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

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

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

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

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

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

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

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

Defined in the sample, used above:

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

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

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

Check it in the database:

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

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

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

Advanced case 3 of Delete.

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

Good to know:

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

Usings:

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

Set up by the sample before its scenarios:

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

#### 1. Preload at startup

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

In scope: `connectionString` (string).

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

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

return service;
```

Check it in the database:

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

Expect: _id_ and the unique email_1 index.

#### 2. Delete by the preloaded key

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

In scope: `service` (ArxMongoDbService).

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

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

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

Check it in the database:

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

Expect: 0.

