PostgreSQL

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 63 characters): ARX1017 SuspectedInjection when a name carries SQL, ARX1003 InvalidIdentifier for a plain bad name, both before anything is sent
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 can fill the id in (an id you set is written as given); 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. Messages name tables, columns and constraints, never values, so they are safe to log
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 Generated<long>("id") Leave it out; read the returned id (or entity.RecordId) after Create
A uuid the database generates 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.

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 text, varchar(n), char(n) as written (char(n) pads with spaces)
short / int / long smallint / int / bigint 32000 / 2000000000 / 9000000000000
decimal numeric(p, s) 12345.678901
float / double real / double precision 3.25 / 2.718281828459045
bool boolean true
Guid uuid 6f9619ff-8b86-d011-b42d-00c04fc964ff
DateTime (Kind Utc), DateTimeOffset timestamptz the UTC instant: 2026-11-07 14:30:00+00
DateTime (Kind Unspecified) timestamp as written: 2026-11-07 09:00:00
DateOnly / TimeOnly / TimeSpan date / time / interval 2026-11-07 / 17:45:30 / 1 day 12:30:00
byte[], ArxBytea bytea \xcafe
ArxJsonB jsonb the JSON
string[], int[], decimal[], Guid[], DateOnly[], DateTime[], … text[], int[], … {red,green}, {1,2,3}, …
ArxEntityReference the lookup column the referenced id
ArxSelectionSetValue the set's column the option's code
null any NULL
byte, sbyte / ushort / uint / ulong converted to smallint / int / bigint / numeric 200 / 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 Local timestamptz converted to UTC

Any other type is refused before anything is sent (ARX1007 UnsupportedValueType): write uri.ToString() rather than a Uri. RealCase2_EveryDataType writes every row of this table and prints what PostgreSQL stored.

With the column types preloaded (ArxPreLoadMode.DataTypes), a number with a fraction written to a whole-number column is refused before PostgreSQL (ARX4001 ValueOutOfRange) on a create, an update and a write by a filter, where PostgreSQL would round it (7.9 stored as 8); 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. Without declared types, PostgreSQL stores a double NaN in a numeric column, and it reads back as a double.

Dates and times

You have Use Column
A moment (an order was placed) DateTimeOffset, or DateTime with Kind Utc timestamptz
A wall-clock time with no zone DateTime with Kind Unspecified timestamp
A calendar date DateOnly date
A time of day TimeOnly time
A duration TimeSpan interval

The same moment written as a DateTimeOffset (+08:00), a UTC DateTime or a local DateTime is stored as the same instant. A DateTime with Kind Unspecified written to a timestamptz column is read in the session's time zone: ARX's own connections are UTC (Timezone=UTC), so 09:00 is stored as 09:00+00; a connection string that names another zone (Timezone=Asia/Manila) makes it 01:00+00. Give a moment Kind Utc, or a DateTimeOffset, so the session can't change it. Refused: default(DateTime) (ARX1013: it means "not set", and PostgreSQL would store -infinity; use null), and a date given as a string (ARX4004: ARX never parses strings as dates). 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 (a PostgreSQL enum type)
Multiplicity One code, or several Single, or several stored as Array, Flags, Delimited or Json
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 varchar(200) ARX4002 ValueTooLong – (PostgreSQL doesn't say which column)
Entered 3,000,000,000 employees into an int ARX4001 ValueOutOfRange –
Typed "forty" into a number ARX4004 DataTypeMismatch the column, in the message
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 SqlState (23505) and the PostgresException 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 Npgsql pool. Inject IArxBatchDataService where code only needs batch scopes.

  • An ARX transaction (new ArxPgConnection(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, 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.

  • 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 an Npgsql connection and transaction, to share one with another library; ARX never commits, rolls back or closes them.

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 (a default or an identity) 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 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 the whole scope is rolled back.

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), lookups (foreign keys) Values go to the database as they are: a string for an enum or jsonb column fails there
Typed (the default) ArxPreLoadMode.DataTypes The keys plus every loaded column's type: a wrong type refused before the database (ARX4004), a string cast for an enum or jsonb column, enum columns read as text from the first read 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 PostgreSQL storesProgressive

Every kind of value the PostgreSQL provider writes, into data_type_showcase, and what PostgreSQL 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 PostgreSQL prints it.

3Instants, local times, dates and times of dayProgressive

Dates and times, in four scenarios: an order placed in Manila written three ways (instants are stored as UTC), calendar values (a birthday, an opening time, a delivery window, a local event time), and what ARX refuses: default(DateTime) and a date given as a string.

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 PostgreSQL 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 ArxPgConnection, begin a transaction on it, pass the transaction to each call, then commit or roll back. No Npgsql 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 ArxPgDbService 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.