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
IArxBatchDataServicewhere code only needs batch scopes.An ARX transaction (
new ArxMySqlConnection(cs), thenBeginTransactionAsync()) 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 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 (
SAVEPOINT), so a failed batch is undone alone (BeginBatchAsyncto decide yourself,RunBatchAsyncto commit when the body returns). WithSavepoints = 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
CREATEorDROP(or a procedure that runs one) commits the open transaction first, in MySQL.Interop only:
CreateAsync(entity, IDbConnection, IDbTransaction)accepts aMySqlConnectionand itsMySqlTransaction, 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 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 would generate (MySQL can't hand one back, so ARX refuses it), 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 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.
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.
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 MySQL 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 ArxMySqlConnection, begin a transaction on it, pass the transaction to each call, then commit or roll back. No MySqlConnector 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 ArxMySqlDbService 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.