MongoDB · Work with AI
AI Instructions
Copyable rules for configuring AI coding assistants to use ARX.SDK accurately and consistently.
An instruction file tells a coding assistant, in every conversation, how the project works. These are for a project that uses ARX.SDK 1.0.0-rc.1 with MongoDB (ARX.SDK.MongoDb): they name the provider, send the assistant to the knowledge files before it writes code, allow only the API those files list, and ask it to build and check its work. Each is generated from the same rules as the knowledge base and the skill; copy the one for your tool into the file named above it.
What the instructions require
- The right provider: the package, service and knowledge files of MongoDB, not another database's.
- Read before writing: the provider's guide and types file, then the operation's file and its closest verified sample.
- No invented API: only types, constructors, properties and methods the types file lists; anything else is reported as not offered.
- Verify: build, run the tests, check every ARX call against the types file, and say what could not be checked.
- Keep them current: when the ARX.SDK version changes, replace the knowledge files or the skill and update the version in the instructions.
Claude Code
CLAUDE.md: Add to the project's CLAUDE.md. With the arx-sdk skill in .claude/skills/arx-sdk, Claude Code loads the skill when it works on ARX.SDK code; this section makes the project's provider and the verification steps explicit.
## Data access: ARX.SDK
This project uses ARX.SDK 1.0.0-rc.1 with MongoDB (package `ARX.SDK.MongoDb`, service `ArxMongoDbService`).
- Use the `arx-sdk` skill for any code that reads or writes the database. Its references for this provider are in
`.claude/skills/arx-sdk/references/mongodb/`: read `arx-mongodb-guide.md` and `arx-mongodb-types.md` first.
- Use only the ARX types and members listed in `arx-mongodb-types.md`. If something isn't there, say ARX doesn't
offer it; don't invent a name.
- Start from the closest verified sample in the operation's file (`arx-mongodb-create.md` and the others).
- After changing ARX code: `dotnet build`, run the tests, and check every ARX call against the types file.
- When the ARX.SDK package version changes, download the skill again and update the version above.
GitHub Copilot
.github/copilot-instructions.md: Copilot's repository-wide instructions, read with every request in the repository.
## Data access: ARX.SDK
This project uses ARX.SDK 1.0.0-rc.1 with MongoDB (package `ARX.SDK.MongoDb`, service `ArxMongoDbService`). Its knowledge
files, generated from the library and its verified samples, are in `docs/arx-sdk/references/mongodb/`; the files named
below are there. Read `arx-mongodb-guide.md` and `arx-mongodb-types.md` before writing data access code,
and the operation's file (`arx-mongodb-create.md`, `-create-multiple.md`, `-update.md`, `-update-multiple.md`,
`-delete.md`, `-delete-multiple.md`, `-retrieve.md`, `-retrieve-multiple.md`, `-execute.md`) for its samples.
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.
Before finishing: `dotnet build`, run the tests, and check every ARX call against `arx-mongodb-types.md`. When the
ARX.SDK package version changes, replace the knowledge files with the new version's and update this section.
.github/instructions/arx-sdk.instructions.md: Applied to C# files only (applyTo). On GitHub.com, path-specific instructions are used by Copilot's cloud agent and code review; editors such as VS Code use them in chat.
---
applyTo: "**/*.cs"
---
# ARX.SDK data access
This project uses ARX.SDK 1.0.0-rc.1 with MongoDB (package `ARX.SDK.MongoDb`, service `ArxMongoDbService`). Its knowledge
files are in `docs/arx-sdk/references/mongodb/`; the files named below are there.
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.
Check every ARX call against `arx-mongodb-types.md`, and build before finishing.
Cursor
.cursor/rules/arx-sdk.mdc: A Cursor project rule (.mdc, in .cursor/rules), attached automatically when a C# file is in context. Cursor also reads AGENTS.md.
---
description: Writing or changing data access code that uses ARX.SDK (ARX.SDK.MongoDb)
globs: **/*.cs
alwaysApply: false
---
# ARX.SDK data access
This project uses ARX.SDK 1.0.0-rc.1 with MongoDB (package `ARX.SDK.MongoDb`, service `ArxMongoDbService`). Its knowledge
files are in `docs/arx-sdk/references/mongodb/`; the files named below are there. Read `arx-mongodb-guide.md` and
`arx-mongodb-types.md` before writing data access code.
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.
Check every ARX call against `arx-mongodb-types.md`, and build before finishing.
General AI coding assistants
AGENTS.md: For agents that read AGENTS.md (Codex, Cursor, GitHub Copilot's coding agent, Gemini CLI and others; Claude Code too when the project has no CLAUDE.md). Keep the arx-sdk folder from the skill package in the repository where the agent can open it.
## Data access: ARX.SDK
This project uses ARX.SDK 1.0.0-rc.1 with MongoDB (package `ARX.SDK.MongoDb`, service `ArxMongoDbService`). Its knowledge
files, generated from the library and its verified samples, are in `docs/arx-sdk/references/mongodb/`; the files named
below are there. Read `arx-mongodb-guide.md` and `arx-mongodb-types.md` before writing data access code,
and the operation's file (`arx-mongodb-create.md`, `-create-multiple.md`, `-update.md`, `-update-multiple.md`,
`-delete.md`, `-delete-multiple.md`, `-retrieve.md`, `-retrieve-multiple.md`, `-execute.md`) for its samples.
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.
Before finishing: `dotnet build`, run the tests, and check every ARX call against `arx-mongodb-types.md`. When the
ARX.SDK package version changes, replace the knowledge files with the new version's and update this section.
An assistant's own custom instructions (a chat assistant's project or profile instructions): paste the rules alone.
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.Where the knowledge files go
- Claude Code: install the arx-sdk skill in
.claude/skills/arx-sdk/; theCLAUDE.mdsection points at its references. - AGENTS.md, GitHub Copilot and Cursor: the templates expect the skill's folder in
docs/arx-sdk/(itsreferences/hold the knowledge files). Put it elsewhere if you like and change the path in the instructions. - Chat assistants: add the knowledge base files to the project or conversation, with the rules above as its instructions.
Keep them current
The instructions name ARX.SDK 1.0.0-rc.1. When the project moves to a newer version, copy them again from this page (they are regenerated with every release of these pages), replace the knowledge files or the skill, and keep any rules of your own in a section of your own so the next update is a plain replacement.