MySQL

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 64 characters): ARX1017 SuspectedInjection when a name carries SQL (a backtick included), ARX1003 InvalidIdentifier for a plain bad name, both before anything is sent. Column names are case-insensitive in MySQL; table names follow the server's lower_case_table_names (case-sensitive on Linux, not on Windows). The schema catalog matches names exactly
Values Always parameters: Robert'); DROP TABLE contact;--, O'Brien, emoji and backslashes are stored exactly as typed. MySqlConnector writes each value into the command text, escaped for the session; ARX keeps that escaping in step with the session's SQL mode (NO_BACKSLASH_ESCAPES)
The session On the connections ARX opens, the first command of each call sets the time zone to UTC and adds a strict SQL mode (STRICT_TRANS_TABLES, NO_ZERO_DATE, NO_ZERO_IN_DATE, ERROR_FOR_DIVISION_BY_ZERO) to the server's own, so a value that doesn't fit is an error, never truncated. A connection you pass keeps its own session
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 AUTO_INCREMENT column; MySQL also takes a value you set, and the id is that value); Supplied means you always give it (missing or null: ARX1001). MySQL hands back only generated integer ids, so a generated Guid or text id is ARX8001: declare it Supplied and set it
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. MySQL's own message (the MySqlException 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 (AUTO_INCREMENT) Generated<long>("id") Leave it out; read the returned id (or entity.RecordId) after Create
A uuid (CHAR(36), or BINARY(16) with the schema preloaded) Supplied<Guid>("id") Set it (device["id"] = Guid.NewGuid()); MySQL can't hand back a uuid its default made
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. A single row's generated id comes from MySQL's reply to the insert (no extra query). A statement whose rows write the id column, some with a value and some with 0 or NULL, makes MySQL generate ids row by row, so ARX sends those rows one INSERT each.

Generated ids of many records, with other inserts running

MySQL has no RETURNING. For each multi-row INSERT, ARX reads in the same round trip the first generated id (LAST_INSERT_ID()), the increment (@@auto_increment_increment, 1 unless the server says otherwise) and the number of rows inserted, and gives the i-th row the id first + i × increment. That is each row's own id when the statement's rows got one contiguous block of ids.

Question Answer
Can another connection's insert change the ids I get back? No. LAST_INSERT_ID() belongs to the connection, and InnoDB reserves an INSERT … VALUES statement's whole block at once, so another statement's ids fall before or after the block, never inside it
What does MySQL promise? Consecutive ids per statement in innodb_autoinc_lock_mode 0 and 1, and in mode 2 (the default) when only INSERT … VALUES statements run. Beside a concurrent INSERT … SELECT, LOAD DATA or an insert that gives some ids itself, mode 2's manual says only that a statement's ids "may not be consecutive". ARX relies on InnoDB reserving the block at once there; it was tested: 8,000 of 8,000 statements got one block while bulk and partly-given inserts ran on the same table
What does ARX check? The row count: a statement that inserted another number of rows is refused (ARX9001), never answered with wrong ids. MySQL gives no way to read back which ids a statement got, so the contiguity itself can't be checked
In a transaction? The ids are given when the statement runs. If the transaction or batch scope rolls back, the rows are gone and MySQL doesn't reuse their ids; if it commits, they are the rows' ids
Gaps in the table? Normal: a rolled-back insert, or a bulk insert's unused reservation, leaves gaps between statements. They don't affect the ids ARX returns
I need the manual's promise in every case Set innodb_autoinc_lock_mode = 1 on the server (a server setting; bulk inserts then lock the table's counter while they run), or let the application supply the ids (ArxIdColumn.Supplied)

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 VARCHAR(n), CHAR(n), TEXT as written (CHAR(n) pads with spaces when it stores and drops them when it reads)
byte / short / int / long TINYINT / SMALLINT / INT / BIGINT 100 / 32000 / 2000000000 / 9000000000000
uint INT UNSIGNED 4000000000
decimal DECIMAL(p, s) 12345.678901
float / double FLOAT / DOUBLE 3.25 / 2.718281828459045 (a FLOAT keeps about six significant digits)
bool BOOLEAN (TINYINT(1)) 1
Guid CHAR(36) 6f9619ff-8b86-d011-b42d-00c04fc964ff (lower case); to a BINARY(16) column as its 16 bytes when the schema is preloaded
DateTimeOffset TIMESTAMP(6) the moment, stored as UTC: 22:30 +08:00 is 14:30:00.000000 (the offset isn't kept)
DateTime DATETIME(6), DATETIME as written: 2026-11-07 09:00:00.000000 (a plain DATETIME rounds to the second)
DateOnly / TimeOnly DATE / TIME(6) 2026-11-07 / 17:45:30.000000
byte[], ArxBytea BLOB, LONGBLOB, BINARY(n) 0xCAFE
ArxJsonB JSON MySQL's own form of the document (invalid JSON: ARX4003)
ArxEntityReference the lookup column the referenced id
ArxSelectionSetValue the set's column (an ENUM or SET column included) 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 TIMESTAMP(6) the moment
DateTime with Kind Local DATETIME(6), TIMESTAMP(6) converted to UTC first

Refused before anything is sent: any other type (ARX1007 UnsupportedValueType: write uri.ToString() rather than a Uri); arrays (ARX8001: MySQL has no array columns, so store a list as delimited text, JSON or rows of its own); NaN and the infinities (ARX4001). A TIME column also holds durations, up to ±838 hours (a TimeSpan).

With the column types preloaded (ArxPreLoadMode.DataTypes), a number with a fraction written to a whole-number column is refused before MySQL (ARX4001 ValueOutOfRange) on a create, an update and a write by a filter, where MySQL 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.

MySQL converts some values itself: "42" into an INT, 2.5 into an INT by rounding (half away from zero), a year-first string such as '2026-11-07' into a DATE. Text that isn't a number, and a date string in any other order, are errors (ARX4003). A preload with column types (ArxPreLoadMode.DataTypes) refuses a value of the wrong type before the database. RealCase2_EveryDataType writes every row of this table and prints what MySQL stored.

Dates and times

You have Use Column
A moment (an order was placed) DateTimeOffset, or DateTime with Kind Utc TIMESTAMP(6) (1970 to 2038)
A moment, past 2038 DateTime with Kind Utc DATETIME(6) (store UTC, read it back as UTC)
A wall-clock time with no zone DateTime with Kind Unspecified DATETIME(6)
A calendar date DateOnly DATE
A time of day TimeOnly TIME(6)
A duration a number (seconds, minutes), or a TimeSpan up to 838 hours INT, BIGINT, TIME

A TIMESTAMP is converted from the session's time zone to UTC when stored and back when read: ARX's sessions are UTC, so the instant is stored and read as it is. A DATETIME stores the clock time it is given: a DateTime with Kind Unspecified or Utc as it is, and one with Kind Local (DateTime.Now) converted to UTC first, as every ARX write does with a local time. It reads back with no zone, so use Kind Unspecified for a wall-clock time and Utc for a moment, never Local. Fractional seconds are kept only when the column declares them ((6)); MySQL rounds to the column's precision. Refused: default(DateTime) (ARX1013: it means "not set"; use null), and a date string MySQL can't read (ARX4003: 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 (an ENUM column, written as its label)
Multiplicity One code, or several Single, or several stored as Flags, Delimited (a SET column takes comma-delimited codes) 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 VARCHAR(200) ARX4002 ValueTooLong AttributeName
Entered 3,000,000,000 employees into an INT ARX4001 ValueOutOfRange AttributeName
Typed "forty" into a number ARX4003 InvalidValueFormat AttributeName
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 MySQL's error number as NativeCode (1062) and the MySqlException 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 MySqlConnector pool. Inject IArxBatchDataService where code only needs batch scopes.

  • An ARX transaction (new ArxMySqlConnection(cs), then BeginTransactionAsync()) is for code that decides, step by step, whether the work is kept. A failed database call aborts it (MySQL itself keeps a transaction open after a failed statement; ARX doesn't, so a call never half-commits):

    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 (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. A deadlock rolls back the whole transaction in MySQL, savepoints included: the scope can't go on (ARX7001, retry the whole scope). Nothing a scope writes is visible to other connections until it commits; InnoDB's consistent reads don't wait for it, they see the rows as last committed.

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

  • DDL commits. A CREATE or DROP (or a procedure that runs one) commits the open transaction first, in MySQL.

  • Interop only: CreateAsync(entity, IDbConnection, IDbTransaction) accepts a MySqlConnection and its MySqlTransaction, 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 (information_schema) and reports every difference at once (ARX5003 SchemaMismatch): the table exists; the id column exists with a compatible type, is generated (AUTO_INCREMENT) 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 key allows many NULLs in MySQL). 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), lookups (foreign keys) Values go to the database as they are, and MySQL converts what it can ("42" into an INT)
Typed (the default) ArxPreLoadMode.DataTypes The keys plus every loaded column's type (COLUMN_TYPE: unsigned, ENUM labels, BINARY(16)): 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 would generate (MySQL can't hand one back, so ARX refuses it), 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 MySQL storesProgressive

Every kind of value the MySQL provider writes, into data_type_showcase, and what MySQL 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 MySQL 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 and MySQL refuse: default(DateTime), and a date string MySQL 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 MySQL 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 ArxMySqlConnection, begin a transaction on it, pass the transaction to each call, then commit or roll back. No MySqlConnector 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 ArxMySqlDbService 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.