SQL Server
Create
24 samples, from the least code that works to complete workflows.
What a create does
| Names | The entity name is the table; each attribute name is a column. Only ASCII letters, digits and underscores (at most 128 characters): ARX1017 SuspectedInjection when a name carries SQL, ARX1003 InvalidIdentifier for a plain bad name, both before anything is sent. With the default, case-insensitive collation SQL Server treats Account and account as one table; the schema catalog matches names exactly |
| Values | Always parameters: Robert'); DROP TABLE contact;--, O'Brien, emoji and backslashes are stored exactly as typed |
| The id column | Every table declares its one id column, or ArxIdColumn.NoIdColumn: in the schema catalog given to the service (the usual way), or on the entity (entity.IdColumn) when the service has no schema; never both. Undeclared: ARX1016 IdNotDeclared. Generated means the database fills the id in when you leave it out (an IDENTITY column refuses one you set, ARX5002; a uniqueidentifier with a default takes it); Supplied means you always give it (missing or null: ARX1001) |
| What comes back | CreateAsync returns the new id as an ArxRecordId (AsInt64(), AsInt32(), AsString(), AsGuid(), or Value); the same id is set on entity.RecordId. A table with no id column gives ArxRecordId.None (HasValue is false) |
| Keys | Optional (.Key(table, columns…)). Create doesn't use them: the database's unique constraints enforce them. Update and Delete find rows by them |
| Defaults | Columns you don't set get their database defaults. new ArxIgnore() leaves an attribute out |
| Errors | The class says where the error was found, the code what happened: ArxValidationException for a request ARX refuses before the database (1xxx, and the 4xxx value checks ARX makes itself); ArxDatabaseException for an error the database reports (3xxx, 7xxx, and the 4xxx and 5xxx it reports); ArxNotSupportedException for ARX8001. The outcome codes (2xxx: nothing matched, several matched, a changed record) and a value read back as a type the schema doesn't expect (ARX5003) can come as the base ArxException. All of them are ArxException, with Code, Category, EntityName and AttributeName: catch ArxException and branch on the code or category, never on the class or the message. ARX's messages name tables, columns and constraints, never values, so they are safe to log. SQL Server's own message (the SqlException in InnerException) can quote the value: log ARX's |
| Without a catalog | A service created with only the connection string reads each table's primary key and column types from the database before the call: the id is generated when the key has a default or identity, supplied otherwise; one more round trip per call, so much slower than with a catalog: meant for simple CRUD and small applications. RememberResolvedTables = true reads each table once per service. No primary key, a compound one or one of an unusable type is ARX1019 TableNotResolvable (declare it in a catalog or with entity.IdColumn); a service with a catalog never resolves (Basic6, Advanced10) |
Reference
Where a table is declared
| The service has | The call passes a schema | The entity declares | Result |
|---|---|---|---|
| A schema | No | Nothing | The service's schema |
| A schema | Yes | Nothing | The call's schema for the tables it declares; the service's for the others (selection sets the same way, by name) |
| No schema | Yes | Nothing | The call's schema |
| No schema | No | IdColumn (and Keys) |
The entity's declaration |
| A schema, or the call passes one | IdColumn or Keys |
Refused: ARX1001, before anything is sent |
|
| No schema | No | Nothing | Read from the database before the call (a service without a catalog): the table's primary key is its id; ARX1019 TableNotResolvable when the call needs an id the table doesn't give (no primary key, a compound one, a type ARX can't use) |
| A schema that doesn't declare the table | No | Nothing | Refused: ARX1016 IdNotDeclared, naming the line to add |
Entities with the same declaration share one statement, even when each gets its own ArxIdColumn object.
Ids by kind of table
| The table has | Declared as | What you do |
|---|---|---|
A number the database generates (IDENTITY) |
Generated<long>("id") |
Leave it out; read the returned id (or entity.RecordId) after Create |
A uuid the database generates (DEFAULT NEWID()) |
Generated<Guid>("id") |
Leave it out, or set it to write your own (device["id"] = Guid.NewGuid()) |
| A natural id (code, SKU) | Supplied<string>("code") |
Always set it; missing or null is ARX1001 |
| No id column (a log, or rows identified by several columns) | NoIdColumn, plus .Key(table, columns…) if columns identify a row |
Set the columns; Create returns ArxRecordId.None, and the database enforces the key |
Id types: short, int, long, decimal, string, Guid, DateOnly, DateTime. ARX reads the new ids back with
OUTPUT, in list order; on a table with a trigger (where SQL Server allows only OUTPUT INTO) it switches to a table
variable on its own and remembers the table.
Lookups
new ArxEntityReference(table, id, name?): the referenced table first, then the id (of any id type, or the
ArxRecordId Create returned), then an optional display name.
| Id type of the referenced table | Constructor |
|---|---|
short, int, long |
new ArxEntityReference("account", 42L) |
string |
new ArxEntityReference("country", "PT") |
Guid |
new ArxEntityReference("device", deviceId) |
decimal, DateOnly, DateTime |
new ArxEntityReference("t", value) |
| Any: the id Create returned | new ArxEntityReference("account", accountId) |
- Checked before anything is sent (
ARX1005 InvalidLookup, naming the attribute): the reference has an id (not missing, zero, negative, an empty Guid or empty text); with a schema, the table is declared, has an id column, and the id has its type (integer widths are interchangeable). - Checked by the database: the referenced record exists, through the foreign key (
ARX3002 ForeignKeyViolation). Without a foreign key, an id that matches nothing is stored as given.
Value types
| .NET value | Column | Stored as |
|---|---|---|
string |
nvarchar(n), varchar(n), nchar(n), nvarchar(max) |
as written (nchar(n) pads with spaces) |
byte / short / int / long |
tinyint / smallint / int / bigint |
200 / 32000 / 2000000000 / 9000000000000 |
decimal |
decimal(p, s), money |
12345.678901 |
float / double |
real / float |
3.25 / 2.718281828459045 |
bool |
bit |
1 |
Guid |
uniqueidentifier |
6F9619FF-8B86-D011-B42D-00C04FC964FF |
DateTimeOffset |
datetimeoffset |
the moment, in UTC: 22:30 +08:00 is stored as 2026-11-07 14:30:00 +00:00. The same with a hand-written catalog, a preload or none: like PostgreSQL and MySQL, ARX keeps the moment, not the offset (store the offset in a column of its own if the application needs it) |
DateTimeOffset |
datetime2, datetime |
the moment in UTC (2026-11-07 14:30:00), with or without a catalog (SQL Server alone would drop the offset and keep 22:30, eight hours off) |
DateTime |
datetime2, datetime |
as written: 2026-11-07 09:00:00 (datetime rounds to 1/300 of a second) |
DateOnly / TimeOnly |
date / time |
2026-11-07 / 17:45:30 |
byte[], ArxBytea |
varbinary(n), varbinary(max) |
0xCAFE |
ArxJsonB |
nvarchar(max), with CHECK (ISJSON(column) = 1) |
the JSON text (invalid JSON fails the check: ARX3004) |
ArxEntityReference |
the lookup column | the referenced id |
ArxSelectionSetValue |
the set's column | the option's code |
null |
any | NULL |
sbyte / ushort / uint / ulong |
converted to smallint / int / bigint / decimal |
-100 / 65000 / 4000000000 / 18446744073709551615 |
char |
text | X |
a plain enum |
refused (ARX1007) | write (int)value or value.ToString(), or use a selection set, which stores the code its set declares |
DateTime with Kind Utc |
datetimeoffset |
the moment, offset +00:00 |
DateTime with Kind Local |
datetime2, datetimeoffset |
converted to UTC first |
Refused before anything is sent: any other type (ARX1007 UnsupportedValueType: write uri.ToString() rather than a
Uri); arrays (ARX8001: SQL Server has no array columns, so store a list as delimited text, JSON or rows of its own);
NaN and the infinities, and a TimeSpan below zero or of 24 hours or more (ARX4001: time holds a time of day, so
store a duration as a number).
With the column types preloaded (ArxPreLoadMode.DataTypes), a number with a fraction written to a whole-number column is refused before SQL Server (ARX4001 ValueOutOfRange) on a create, an update and a write by a filter, where SQL Server would round or truncate it; a condition in a read (qty < 7.5) isn't refused. A NaN or an infinity written to a declared decimal column is refused the same way.
SQL Server converts some values silently that other databases refuse: "42" into an int, a number into a bit, a
decimal into an int by cutting off the fraction. A preload with column types (ArxPreLoadMode.DataTypes) refuses
those before the database. RealCase2_EveryDataType writes every row of this table and prints what SQL Server stored.
Dates and times
| You have | Use | Column |
|---|---|---|
| A moment (an order was placed) | DateTimeOffset |
datetimeoffset |
| A moment, in a column without an offset | DateTime with Kind Utc |
datetime2 (store UTC, read it back as UTC) |
| A wall-clock time with no zone | DateTime with Kind Unspecified |
datetime2 |
| A calendar date | DateOnly |
date |
| A time of day | TimeOnly |
time |
| A duration | a number (seconds, minutes) | int, bigint |
SQL Server keeps no time zone: datetime2 stores the clock time it is given, and datetimeoffset the clock time and
its offset. ARX writes a DateTimeOffset as its UTC instant and converts a local DateTime to UTC first, so a moment is
never stored hours off. Refused: default(DateTime) (ARX1013: it means "not set",
and SQL Server would store 0001-01-01; use null). ARX never parses a string as a date, but SQL Server converts
one it can read, by the session's date format (01/07/1992 is January 7 under us_english), and refuses one it
can't (ARX4003); with a schema preloaded with data types, ARX refuses every date string first (ARX4004). Parse
dates yourself. Full rules: Data types.
Selection sets
A selection set (picklist, option set) is a column that accepts only a fixed list of codes, each with a display text.
The catalog lists them once, in code (or loaded from JSON, a table or configuration at startup), and the service
checks every ArxSelectionSetValue against it in memory, before any SQL is sent: no lookup table, no extra query.
| Part | What it says | Example |
|---|---|---|
| Name | What an ArxSelectionSetValue refers to |
"AccountStatus" |
| Kind | What the codes are, matching the column | String, Integer, Character, Boolean, Decimal, Guid (DatabaseEnum is ARX8001: SQL Server has no enum types; a CHECK on a text column does that job) |
| Multiplicity | One code, or several | Single, or several stored as Flags, Delimited or Json (Array is ARX8001) |
| Options | Each code with its display text and order | ("ON_HOLD", "On hold") |
| Active | A retired option stays readable in old rows, but can't be written | IsActive: false |
Refused before the database: a code that isn't in the set, a retired option, several values for a single-choice set
(ARX1011), a set the catalog doesn't have (ARX1012). A plain value (not an ArxSelectionSetValue) isn't checked.
catalog.TryGetSet(name, out var set) gives a set's options, for example for a drop-down list.
Database errors as user messages
| What the user did | Code | Details on the exception |
|---|---|---|
| Reused an account number | ARX3001 UniqueViolation |
ConstraintName |
| Left the name empty | ARX3003 NotNullViolation |
AttributeName |
| Entered a negative employee count | ARX3004 CheckViolation |
ConstraintName |
Pasted a 220-character website into nvarchar(200) |
ARX4002 ValueTooLong |
AttributeName |
Entered 3,000,000,000 employees into an int |
ARX4001 ValueOutOfRange |
– |
| Typed "forty" into a number | ARX4003 InvalidValueFormat |
– (SQL Server doesn't say which column) |
| A field the table doesn't have | ARX5002 UndefinedAttribute |
the column, in the message |
| A table name with SQL in it | ARX1017 SuspectedInjection, before the database |
the name, in the message |
ArxDatabaseException keeps SQL Server's error number as NativeCode (2627) and the SqlException as
InnerException; IsTransient is true for errors worth retrying (deadlocks, timeouts, dropped connections). All
codes: the error codes.
Transactions, batch scopes and services
One service per database, a singleton. It holds no connection; every call takes one from the Microsoft.Data.SqlClient pool. Inject
IArxBatchDataServicewhere code only needs batch scopes.An ARX transaction (
new ArxMsSqlConnection(cs), thenBeginTransactionAsync()) is for code that decides, step by step, whether the work is kept. A failed database call aborts it:After IsAbortedNext call CommitAsync()A database error (constraint, type, connection, cancellation) trueARX1015 TransactionAbortedrolls back, throws ARX1015A validation error (before the database) falseworks commits A batch scope gives each call its own savepoint (
SAVE TRANSACTION), so a failed batch is undone alone (BeginBatchAsyncto decide yourself,RunBatchAsyncto commit when the body returns). WithSavepoints = false, any failure rolls back everything. Nothing a scope writes is visible to other connections until it commits; under SQL Server's defaultREAD COMMITTED, another connection that reads those rows waits for the commit.Two databases can't share a transaction. Write the source of truth first and commit; then write the copy, made safe to repeat (a unique key there).
Interop only:
CreateAsync(entity, IDbConnection, IDbTransaction)accepts aSqlConnectionand itsSqlTransaction, to share them with another library; ARX never commits, rolls back or closes them. A connection with an open transaction must be given that transaction (ARX1008).
Checking declarations at startup
ValidateSchemaAsync compares each declaration with the database and reports every difference at once
(ARX5003 SchemaMismatch): the table exists; the id column exists with a compatible type, is generated (IDENTITY or
a default) when declared so, and identifies one row on its own; every key's columns have a primary key or unique index
on exactly those columns (a unique index filtered WHERE column IS NOT NULL counts: it is SQL Server's way to make a
nullable column unique). A drifted declaration that reaches a write is ARX5003 too, never a raw .NET exception:
without a transaction the row is already committed, in an ARX transaction nothing is kept, in a batch scope that call
is rolled back to its savepoint.
Preloading the schema at service setup
| Choose | Mode | What you get | Trade-off |
|---|---|---|---|
| Convenience | ArxPreLoadMode.KeysOnly |
The table keys, as you would declare them: the id, business keys (unique constraints and indexes), lookups (foreign keys) | Values go to the database as they are, and SQL Server converts what it can ("42" into an int) |
| Typed (the default) | ArxPreLoadMode.DataTypes |
The keys plus every loaded column's type: a wrong type refused before the database (ARX4004) | A little more to load at startup |
| An allow list | DataTypes + Strict = true |
Only the preloaded tables and columns can be used; anything else is ARX1018 NotPreloaded, before the database |
Every column a call uses must be listed; functions and Execute aren't covered |
A table a loaded column references is added with its reason ("added: referenced by account.primary_contact_id").
Display names and selection sets aren't discovered: add them in LoadAsync(cs, configure => …).
The samples
Basics
The least code that works.
1Create one record and get its idStandaloneThe smallest create: one record, in four scenarios. Read them in order: the usual way first (the id column declared in the schema), then the other way (declared on the entity), then two tables with no id column: one identified by a compound key, one with no key at all.
A record that points at another one (a lookup), in four scenarios: by the referenced entity's RecordId, by the id CreateAsync returned, to a record whose id is text, and what happens when the lookup is wrong.
Catching errors, in three scenarios: a database error by its code, several database errors by their category, and a mistake ARX refuses before anything is sent to the database.
Ids that aren't numbers, in three scenarios: a text id you supply (a country code), a uuid the database generates, and a uuid your code generates.
Where a table's id column and keys are declared, in three scenarios: on the entity (for a service with no schema), in a schema passed to one call (which overrides the service's schema for that call), and the rule that a table is declared in one place only.
A service created with only a connection string, in four scenarios: ARX reads the table's id column and column types from the database before the create, for a generated id and for a supplied text id; then the two tables it can't resolve an id for, one with no primary key and one with a compound primary key, refused with ARX1019, and the one line that fixes each.
Real cases
What an application does: a small CRM and product catalogue.
1A sign-up form creates an accountProgressiveA sign-up form creates a customer account, in three scenarios: the account from the form (columns left out get their database defaults), a subsidiary that points at its parent, and form fields the table doesn't have (ArxIgnore).
Every kind of value the SQL Server provider writes, into data_type_showcase, and what SQL Server stored, in four scenarios: every supported type, types converted to one that holds every value, null, and a type that's refused. The comment on each line is the stored value as SQL Server prints it.
Dates and times, in four scenarios: an order placed in Manila written three ways (the same instant, stored in UTC), calendar values (a birthday, an opening time, a delivery window, a local event time), and what is refused: default(DateTime), and a date given as a string SQL Server can't read.
Selection sets (picklists, option sets), in seven scenarios: the catalog, a catalog without the sets, an account's industry, status and segments, a contact's preferred channel and a survey mood, sets loaded from JSON, values the catalog refuses, and a plain string that skips the catalog.
An "add account" screen that turns ARX errors into messages for the user, in four scenarios: constraints the database enforces, values the column can't hold, requests that don't fit the table, and the native details kept on a database error. Every error has a stable code (the same on every database), a category, and the entity, attribute or constraint involved. Messages name things, never values, so they are safe to log.
Saving what users typed and what files contain, in three scenarios: a public "contact us" form full of SQL-looking text, a CSV header used directly as attribute names, and the better way, mapping the file's columns. Values are always sent as parameters, so any text is stored exactly as typed. Names are validated and quoted, so a name can't change the SQL.
Ids that aren't a generated number, and a table with no id column, in five scenarios: countries by their ISO code, devices with uuid ids, a regional price list identified by region + SKU, and the two mistakes SQL Server catches: the same region and SKU again, and one part of the key missing. All declared in SampleCatalog.cs.
Catching an SQL injection attempt on create, in four scenarios. ARX refuses a name (table or attribute) that carries SQL with its own code, ARX1017 SuspectedInjection, before anything is sent to the database, so you can log it as a security event instead of a bug. Values are never refused: they are parameters, and SQL in a value is just text.
Advanced cases
Dependency injection, transactions, batch scopes and setting the service up.
1Register ARX in a DI container and inject itAdvancedRegistering ARX in a dependency injection container (Microsoft.Extensions.DependencyInjection, as in ASP.NET Core and worker services) and using it from an application service, in two scenarios: register, then use.
Several ARX calls in one transaction, with ARX types only, in three scenarios: commit, roll back, and a failed call that aborts the transaction. Open an ArxMsSqlConnection, begin a transaction on it, pass the transaction to each call, then commit or roll back. No SqlClient type appears in application code.
Preloading the schema catalog, in three scenarios: each table's id column and keys come from JSON, as an application might keep them in a file or a settings store; they are checked against the database at startup with ValidateSchemaAsync; and a declaration that drifted is found before any write.
Two databases in one application, in three scenarios: one service per database, an order placed in the CRM (the source of truth) and recorded in a reporting database (an event log), and a retry of the copy. One ArxMsSqlDbService per database, each with its own connection string and schema catalog, registered as keyed services and injected by key.
A security check around every create (SecurityGuard, in the project root), in four scenarios: a normal request passes through; an attack is logged and the user sees a plain message; repeated attacks block the user; and a false positive, a harmless file header flagged as SQL, is triaged and fixed without blocking anyone.
The schema preloaded when the service is set up, in four scenarios: what a preload of "account" finds, including the contact table it added because account.primary_contact_id references it (with that reason, in the JSON); the convenient setup, keys only; the typed setup, data types for the columns you name; and Strict, where the preloaded columns are an allow list.
Set the service up for production, in three scenarios: depend on IArxDataService, check the server at startup, and trace and measure every call.
Use a table whose columns are named outside ARX's name rule, in three scenarios: declare an alias for each such column and create a record through them, read and query by the aliases, and see that the database's own name is still refused when a call uses it.
Log every ARX call through ILogger, in three scenarios: one line per call with its table and how long it took, a failed call with its ARX code (and the exception logged where it is caught), and only the calls worth keeping in production.
The three ways to set up a service, from the least code to the fastest, in three scenarios: without a catalog, reading the tables on every call; without a catalog, remembering what was read (RememberResolvedTables); and the tables preloaded into a catalog once, at startup.