# ARX.SDK for MongoDB: a guide for AI assistants

These files teach an AI assistant (Claude, ChatGPT, GitHub Copilot, Cursor and the like) to write correct ARX.SDK code
for MongoDB. They are generated from the same verified samples, READMEs and doc comments as the documentation site:
every code example in them was compiled and run against MongoDB.

ARX.SDK is an entity-based data access library for .NET: one dictionary-like entity model (`ArxEntity`,
`ArxQueryExpression`) for every database, with a provider package per database. This knowledge base covers the
MongoDB provider, `ARX.SDK.MongoDb`, built on the MongoDB C# driver.

## Instructions for the assistant

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

1. **Use ARX's types for database work.** Create, update, delete and read documents, and run transactions through
   `ArxMongoDbService`, `ArxMongoConnection` and `ArxTransaction`. Don't write driver code (`IMongoCollection`,
   `BsonDocument`, `FilterDefinition`) for work ARX does.
2. **Don't invent API.** Use only the types, methods, properties and parameters listed in `arx-mongodb-types.md`. If
   something isn't there, say ARX doesn't offer it rather than guessing a name.
3. **A document is an `ArxEntity`.** `var account = new ArxEntity("account");` then values by field name:
   `account["name"] = "Northwind Traders";`. The entity's name is the collection. Field names keep their case.
4. **Declare every collection's id once, at startup,** in an `ArxSchemaCatalog`, and give it to the service:
   `ArxSchemaCatalog.Create().Table("account", ArxIdColumn.Generated<string>("account_id")).Build()`, then
   `new ArxMongoDbService(connectionString, schema)`; the connection string names the database
   (`mongodb://host:27017/crm`). The declared id is the document's `_id`: a generated string id is an ObjectId ARX makes
   (returned as its 24-character text), a generated Guid one ARX makes, `ArxIdColumn.Supplied<string>("code")` an id the
   application gives. A generated number id is ARX8001 (no sequences) and `NoIdColumn` is ARX1001 (every document has
   an `_id`). Add business keys with `.Key("account", "account_number")` and back them with unique indexes.
5. **Lookups are declared**, since MongoDB has no foreign keys: `.Lookup("contact", "account_id", "account")`. Write one
   with `new ArxEntityReference("account", id)`; it reads back as an `ArxEntityReference` with the account's display
   name (`.DisplayName("account", "name")`).
6. **Every call is asynchronous** (`CreateAsync`, `CreateMultipleAsync`, `UpdateAsync`, `UpdateMultipleAsync`,
   `DeleteAsync`, `DeleteMultipleAsync`, `RetrieveAsync`, `RetrieveMultipleAsync`); await it, and pass the caller's
   `CancellationToken`. Make one service for the application: it shares one MongoDB client per connection string.
7. **Writes of several documents are all or nothing** (CreateMultiple, UpdateMultiple, DeleteMultiple with checks), in
   a transaction: MongoDB needs a replica set or a sharded cluster for that (one replica set node is enough). On a
   standalone server they are ARX8001.
8. **Update and Delete find documents by the id, a declared key, or fields you name:**
   `new ArxUpdateOptions { Match = ArxMatch.Key("account_number") }`, `ArxMatch.Columns(...)`. An update writes only the
   attributes that are set (`$set`); null stores null. A document's `_id` can't change (ARX8001).
   **To change or delete every document a condition matches** (from 0.1.0-preview.5): an entity with only the values to set
   and an `ArxFilterExpression`, `int changed = await service.UpdateMultipleAsync(values, filter);`, or
   `await service.DeleteMultipleAsync("account", filter)`; one `updateMany` or `deleteMany` in a transaction (a replica set), all or nothing, returning the count. The
   entity carries no id (ARX1001 with a filter); an empty filter is ARX1001 unless
   `new ArxFilterWriteOptions { AllRows = true }`. For a large insert whose ids aren't needed:
   `CreateMultipleAsync(entities, new ArxCreateMultipleOptions { ReturnIds = false })` (every id `ArxRecordId.None`).
9. **Reads.** `RetrieveAsync` returns `null` when nothing matches. `RetrieveMultipleAsync` takes an
   `ArxQueryExpression` (criteria, columns, order, paging, joins as link entities, aggregates) and returns the
   documents in `.Entities`. `Null` matches a stored null and a missing field; NotEqual never matches either. Page by
   `PageInfo.PageNumber`, or pass each page's `PagingCookie` to the next query. Execute is ARX8001: MongoDB has no
   stored procedures.
   **Query cheat sheet** (the exact names; there are no others, such as `AddGroupBy` or `MoreRecords`):
   `var query = new ArxQueryExpression("book");` · columns: `query.ColumnSet = new ArxColumnSet("title", "year");`
   (empty: every column) · conditions: `query.Criteria.AddCondition("year", ArxConditionOperator.Between, 2000, 2010);`
   (Equal, NotEqual, GreaterThan, GreaterEqual, LessThan, LessEqual, Between, In, NotIn, Like, Contains, BeginsWith,
   EndsWith, Null, NotNull and their Not/DoesNot forms) · an OR group: `var either = query.Criteria.AddFilter(ArxLogicalOperator.Or);`
   then `either.AddCondition(…)` · order: `query.AddOrder("title", ArxOrderType.Ascending);`
   · a join: `var author = query.AddLink("author", "author_id", "id", ArxJoinOperator.Inner); author.EntityAlias = "a";
   author.ColumnSet = new ArxColumnSet("name");`, read as `row["a.name"]` · paging:
   `query.PageInfo = new ArxPagingInfo { Count = 20, PageNumber = 1, ReturnTotalRecordCount = true };`, then
   `result.Entities`, `result.HasNextPage`, `result.TotalRecordCount` (or page by `result.PagingCookie`) · grouped counts:
   `query.AggregateInfo = new ArxAggregateInfo(); query.AggregateInfo.AddAggregateColumn("books", "id", ArxAggregateOperator.Count);
   query.AggregateInfo.AddGroupByColumn("author_id");` (Count reads back as a `long`). A foreign key column reads back as
   an `ArxEntityReference` (with the referenced row's name) only when the catalog declares it:
   `.Lookup("book", "author_id", "author")` and `.DisplayName("author", "name")`; otherwise it is the plain id.
10. **Values are never run as queries:** pass them as .NET values. A name with `$` or SQL syntax is ARX1017, so names
    that carry operators are refused, but a valid name is used as given and reaches any collection or field the database
    user can read: map a name from a user to one the application allows (an allow list), or preload the schema and set
    `Strict = true`. Search text is escaped.
11. **Dates:** a BSON date holds milliseconds, in UTC: a `DateTime` is stored as UTC (Local converted, Unspecified taken
    as UTC), a `DateTimeOffset` as its instant (the offset isn't kept), a `DateOnly` as midnight UTC. Never
    `default(DateTime)` (ARX1013: use `null`).
12. **Errors** are `ArxException`s with a stable `Code`: catch by code
    (`catch (ArxException error) when (error.Code == ArxErrorCode.UniqueViolation)`: catch the base class, since the class tells where an error was found and the code what happened) and retry only when
    `error.IsTransient` is true. `arx-mongodb-error-codes.md` lists every code. Log `error.Message`, not the driver's:
    MongoDB's duplicate key message quotes the value.
13. **Transactions:** `await using var connection = new ArxMongoConnection(service);`,
    `await using var transaction = await connection.BeginTransactionAsync();`, pass `transaction` to each call, then
    `await transaction.CommitAsync();`. Disposing without committing rolls back. Code written against `IArxDataService` opens one without the provider type: `await using var transaction = await service.BeginTransactionAsync();` (from 0.1.0-preview.5; the transaction owns its connection). MongoDB has no savepoints: a call that
    fails at the database aborts the transaction, and batch scopes have no savepoints: leave `Savepoints` unset (`true` is ARX8001), and a failed call rolls the scope back.
14. **Base answers on the samples** in the operation files: they are verified code. Say which sample a pattern comes
    from when that helps the developer find more.
15. **Reading values and namespaces.** `entity["col"]` is the value or null; a misspelt or unread attribute
   reads null too, so take column names from the table. `GetAttributeValue<T>` of NULL is `default(T)` (0 for `int`:
   ask for `int?` to tell NULL from 0); it converts numbers when nothing is lost (a Count is always a `long`, Sum and
   Avg are decimal or double by provider: read them as `long` and `decimal`), and reads a declared lookup (an
   `ArxEntityReference`, also in a grouped query) as its id, and a UTC or Local `DateTime` as a `DateTimeOffset` (and
   back, as UTC); a number that doesn't fit is ARX4001 (1.5 as `int`), any other mismatch is ARX4004, an `ArxException`.
   Through a link a lookup column is its plain id, not an `ArxEntityReference`.
   Namespaces: `ArxEntity`, `ArxEntityReference`, `ArxColumnSet`, `ArxSchemaCatalog` and `ArxMatch` are in
   `ARX.SDK.Services`; `ArxQueryExpression`, `ArxConditionOperator`, `ArxJoinOperator`, `ArxAggregateOperator` and
   `ArxOrderType` in `ARX.SDK.Query`; `ArxException` and `ArxErrorCode` in `ARX.SDK`; `ArxMongoDbService` and `ArxMongoConnection` in `ARX.SDK.MongoDb.Services`. Write every `using` a file needs.
   Names that differ from the Dynamics XRM SDK: the join operators are `ArxJoinOperator.Inner` and `ArxJoinOperator.Left` (not XRM's `LeftOuter`);
   a link's columns are `link.ColumnSet`, its alias `link.EntityAlias`, its conditions `link.LinkCriteria`; the referenced
   table of an `ArxEntityReference` is `EntityName` and its id `RecordId`. `UpdateAsync` throws ARX2002 RecordNotFound
   when no record matches (`DeleteAsync` returns false or 0 instead). A page's `TotalRecordCount` is an `int`; an aggregate
   Count is a `long`.
16. **Ids and linked columns are plain values.** `entity.RecordId` is an `ArxRecordId`: read it with
    `AsInt64()`, `AsGuid()` or `AsString()`, pass it where an id goes (`RetrieveAsync("account", id)`,
    `new ArxEntityReference("account", entity.RecordId!)`), or pass the plain value instead. `ArxRecordId` is a class, not
    a nullable struct: `RecordId` is null when no id is known, and `.Value` is the id as an `object` (pass the
    `ArxRecordId` itself, or `AsInt64()`, where a typed id goes, never `.Value`). A linked table's column reads back as `row["alias.column"]`, the plain value (no `AliasedValue`); without an alias the prefix is the table's name (`row["account.name"]`). Table names
    are matched exactly (`"Account"` isn't `account`: ARX1016); column names ignoring case.

## How to use these files

- **One file:** `arx-mongodb-all.md` holds everything; add it to your assistant's knowledge (a project's files, a custom
  assistant's knowledge, or a file in your repository that your coding assistant reads).
- **Some files:** start with this guide and the types file, then add the operations you use.
- **As instructions:** paste "Instructions for the assistant" above into your assistant's custom instructions or your
  repository's instruction file (for example `CLAUDE.md`, `AGENTS.md` or `.github/copilot-instructions.md`).
- **As a skill:** coding agents that load skills (Claude Code and others) can use the `arx-sdk` skill, which holds
  these files with a workflow for writing and checking ARX.SDK code; ready-made instruction files for Claude Code,
  GitHub Copilot, Cursor and `AGENTS.md` are on the AI Instructions page.

## Packages and setup

- .NET: .NET SDK 10.0.100 or later 10.0.x (target `net10.0`); database: MongoDB 7.0, a replica set (tested with 7.0.45, one node); driver: MongoDB.Driver 3.13.0 or later (the lowest version, which the provider is built on; Versions and compatibility lists the versions it is tested on).
- `ARX.SDK.MongoDb` 1.0.0-rc.1: The MongoDB provider. It brings ARX.SDK and MongoDB.Driver 3.13.0 with it. Install: `dotnet add package ARX.SDK.MongoDb --version 1.0.0-rc.1`.
- The samples read the connection string from the `ARX_SAMPLES_MONGODB` environment variable, for example `mongodb://localhost:27017/arx_samples?replicaSet=rs0`. Never put real credentials in code.

## Quick reference

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

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

In scope: `connectionString` (string).

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

var service = new ArxMongoDbService(connectionString, schema);

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

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

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

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

In scope: `service` (ArxMongoDbService).

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

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

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

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

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

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

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

int changed = await service.UpdateAsync(change);

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

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

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

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

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

int changed = await service.UpdateMultipleAsync(changes);

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

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

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

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

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

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

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

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

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

### Retrieve: Read one record, By id

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

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

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

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

In scope: `service` (ArxMongoDbService).

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

var page = await service.RetrieveMultipleAsync(query);

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

Defined in the sample, used above:

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

## The other files

- `arx-mongodb-types.md`: Every public type: its declaration, constructors, properties, methods and their parameters, with what each does.
- `arx-mongodb-sample-application.md`: The files the samples build on (the schema catalog, dbContext and its Service, the output helpers): what the samples' code refers to that isn't in the packages.
- `arx-mongodb-data-types.md`: What each .NET type is stored as in MongoDB, the date and time rules, and what each MongoDB type reads back as.
- `arx-mongodb-error-codes.md`: Every ARX error code: what it means, whether it is safe to retry, and the MongoDB errors it comes from.
- `arx-mongodb-create.md`: What a create does, its rules, and every Create sample: each scenario's code and the mongosh query that checks it.
- `arx-mongodb-create-multiple.md`: What a CreateMultiple does, its rules, and every CreateMultiple sample: each scenario's code and the mongosh query that checks it.
- `arx-mongodb-update.md`: What an update does, its rules, and every Update sample: each scenario's code and the mongosh query that checks it.
- `arx-mongodb-update-multiple.md`: What an UpdateMultiple does, its rules, and every UpdateMultiple sample: each scenario's code and the mongosh query that checks it.
- `arx-mongodb-delete.md`: What a delete does, its rules, and every Delete sample: each scenario's code and the mongosh query that checks it.
- `arx-mongodb-delete-multiple.md`: What a DeleteMultiple does, its rules, and every DeleteMultiple sample: each scenario's code and the mongosh query that checks it.
- `arx-mongodb-retrieve.md`: What a retrieve does, its rules, and every Retrieve sample: each scenario's code and the mongosh query that checks it.
- `arx-mongodb-retrieve-multiple.md`: What a RetrieveMultiple does, its rules, and every RetrieveMultiple sample: each scenario's code and the mongosh query that checks it.
- `arx-mongodb-benchmarks.md`: How ARX compares with the MongoDB driver used directly, measured: the only source for performance claims.
- `arx-mongodb-all.md`: everything in one file.
