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 IArxBatchDataService where code only needs batch scopes.

  • An ARX transaction (new ArxMsSqlConnection(cs), then BeginTransactionAsync()) is for code that decides, step by step, whether the work is kept. A failed database call aborts it:

    After IsAborted Next call CommitAsync()
    A database error (constraint, type, connection, cancellation) true ARX1015 TransactionAborted rolls back, throws ARX1015
    A validation error (before the database) false works commits
  • A batch scope gives each call its own savepoint (SAVE TRANSACTION), so a failed batch is undone alone (BeginBatchAsync to decide yourself, RunBatchAsync to commit when the body returns). With Savepoints = false, any failure rolls back everything. Nothing a scope writes is visible to other connections until it commits; under SQL Server's default READ 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 a SqlConnection and its SqlTransaction, 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 idStandalone

The 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.

2Create a record that points at another oneStandalone

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.

3Catch an error by its codeStandalone

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.

4Ids that aren't numbers: text and uuidStandalone

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.

5Where to declare: on the entity, or a schema for one call, never bothStandalone

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.

6Create without a catalogStandalone

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 accountProgressive

A 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).

2Every supported .NET type, and what SQL Server storesProgressive

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.

3Instants, local times, dates and times of dayProgressive

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.

4Picklist values checked against the application's catalogProgressive

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.

5Turn ARX error codes into messages for the userProgressive

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.

6Values and names that come from users or filesProgressive

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.

7Natural ids, uuid ids and a table with a compound keyProgressive

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.

8Catch an SQL injection attempt on createProgressive

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 itAdvanced

Registering 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.

2Several calls in one ARX transaction: commit or roll backAdvanced

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.

3Load ids and keys from JSON and validate them at startupAdvanced

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.

4Write to two databases, each with its own serviceAdvanced

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.

5A security check around creates: log, block, handle false positivesAdvanced

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.

6Preload the schema at service setup: keys only, data types, StrictAdvanced

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.

7Set the service up for productionAdvanced

Set the service up for production, in three scenarios: depend on IArxDataService, check the server at startup, and trace and measure every call.

8Use columns named outside the name rule, through aliasesAdvanced

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.

9Log every call through ILoggerAdvanced

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.

10Remember resolved tables, or preload themAdvanced

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.