# Coming from EF Core or Dapper

ARX.SDK isn't an ORM and doesn't replace EF Core or Dapper. It is a data-access layer with one entity model run
against five databases. This page maps what you know to how ARX does it, and says plainly what it doesn't do.

## The main differences

| You're used to | In ARX |
|---|---|
| A class per table (EF Core), or mapping rows to your classes (Dapper) | `ArxEntity`: a table name and a dictionary of attributes. You map to and from your own classes, if you want them |
| LINQ (EF Core), or SQL strings (Dapper) | `ArxQueryExpression`: conditions, columns, order, links (joins), paging and aggregates as objects, translated per database |
| `DbContext` with change tracking: `SaveChanges` writes what changed | No tracking. `UpdateAsync` writes exactly the attributes the entity carries; one left out keeps its value |
| Migrations create and change the tables | You own the tables and their DDL. The schema catalog declares what ARX needs to know (ids, keys, lookups), and `ValidateSchemaAsync()` checks it against the database |
| Keys found by convention (`Id`) | Declared, never guessed: `.Table("account", ArxIdColumn.Generated<long>("id"))` |
| Navigation properties, `Include` | A lookup reads back as an `ArxEntityReference` with the referenced record's display name (not on DynamoDB, which makes no joins: `Name` is null there); a link in the query joins. Read through a link, a lookup column is its plain id |
| `SaveChanges` in a transaction, `TransactionScope` | `service.BeginTransactionAsync()` (from 0.1.0-preview.5, on every provider), `BeginTransactionAsync()` on the provider's connection (`ArxPgConnection`, …), or a batch scope (`RunBatchAsync`), with the transaction passed to each call |
| Exceptions of the database driver | `ArxException` with a stable code, the same on every database; the driver's exception is the `InnerException` |
| `SaveChanges` returning 0, or Dapper's rows affected, when nothing matched | `UpdateAsync` throws ARX2002 RecordNotFound; `DeleteAsync` returns `false` (or 0); `RetrieveAsync` returns `null` (Errors in practice, "When the record isn't there") |
| A concurrency token (`[ConcurrencyCheck]`, `rowversion`) | A version column declared in the catalog, `.Version("account", "row_version")`: a stale update or delete is ARX2004 RecordChanged (every provider from 0.1.0-preview.4) |
| `ToQueryString()`, or the SQL you wrote | The driver's own logging, or the database's statement views: Logging and tracing, "Seeing the SQL ARX runs" |
| Raw SQL when you need it | Procedures and functions through `ExecuteAsync`. For any other SQL, use the driver (or Dapper) beside ARX, in the same transaction when it must be (below) |

## Mapping to your own classes

ARX hands you entities; keep the mapping in one place:

```csharp
public sealed record Account(long Id, string Name, int? Employees);

static Account ToAccount(ArxEntity entity) => new(
    entity.RecordId!.AsInt64(),   // set on every read with a catalog; null only without one
    Required<string>(entity, "name"),
    entity.GetAttributeValue<int?>("employee_count"));

// A misspelt name reads as null through entity["nmae"] or GetAttributeValue (a column read as NULL is null too).
// Where a column must be there, check that it was read, so a typo fails at once instead of mapping null.
// On MongoDB and DynamoDB a name in the column set that no record has reads back as null (there are no columns to
// check it against), so Contains is true there: declare the fields and use Strict, or keep the names in constants.
static T Required<T>(ArxEntity entity, string column)
    => entity.Contains(column) ? entity.GetAttributeValue<T>(column)! : throw new InvalidOperationException($"'{column}' wasn't read.");
```

`GetAttributeValue<T>` converts numbers when nothing is lost (a Count, always a `long`, read as `int`): ARX4001
ValueOutOfRange when the number doesn't fit (1.5 read as `int`), ARX4004 DataTypeMismatch for a value of another kind, so it's the safer read where a value can come back as more than one .NET type (see Switching providers). A
misspelt name in a write, a filter or an order is caught: ARX5002 on the SQL databases.

### From a JSON request body

An entity takes .NET values, not `JsonElement` (ARX1007 UnsupportedValueType). Convert each field to its column's type,
and accept only the fields the caller may write: the list is also what keeps a request from setting columns it
shouldn't.

```csharp
static readonly Dictionary<string, Type> Writable = new()
{
    ["name"] = typeof(string),
    ["email"] = typeof(string),
    ["credit_limit"] = typeof(decimal),
    ["birthday"] = typeof(DateOnly)
};

static ArxEntity FromBody(string table, JsonElement body)
{
    var entity = new ArxEntity(table);

    foreach (var field in body.EnumerateObject())
    {
        if (!Writable.TryGetValue(field.Name, out var type))
        {
            throw new BadHttpRequestException($"'{field.Name}' can't be written.");
        }

        entity[field.Name] = field.Value.ValueKind == JsonValueKind.Null ? null : field.Value.Deserialize(type);
    }

    return entity;
}
```

A field left out of the body is left out of the entity, so `UpdateAsync` keeps its value, and an explicit `null`
clears it: a PATCH works without more code. With a request class per endpoint, map its properties to attributes
instead.

## When nothing matched

EF Core's `SaveChanges` and Dapper's `Execute` return 0 when an update finds no row. `UpdateAsync` throws ARX2002
instead, so the change isn't lost without a word. Where "not there" is an expected answer (a PUT to an id that may have
been deleted), catch that code:

```csharp
async Task<bool> TryUpdateAsync(ArxEntity change)
{
    try
    {
        await service.UpdateAsync(change);
        return true;
    }
    catch (ArxException error) when (error.Code == ArxErrorCode.RecordNotFound)
    {
        return false;   // 404 in a web API
    }
}
```

`DeleteAsync` already returns `false` (by id) or 0 (by columns), and `RetrieveAsync` returns `null`. A write by a filter
(next section) returns 0 when nothing matched, as EF Core's `ExecuteUpdateAsync` does.

## Updating or deleting by a condition

EF Core's `ExecuteUpdateAsync` and `ExecuteDeleteAsync`, or a hand-written `UPDATE … WHERE` in Dapper, change every row
a condition matches in one statement. In ARX the entity carries the values to set and an `ArxFilterExpression` says
which rows (from 0.1.0-preview.5):

```csharp
var change = new ArxEntity("customer");
change["optin_marketingemail"] = false;

var filter = new ArxFilterExpression();
filter.AddCondition("country", ArxConditionOperator.Equal, "PH");
filter.AddCondition("last_login", ArxConditionOperator.LessThan, cutoff);

int changed = await service.UpdateMultipleAsync(change, filter);
int removed = await service.DeleteMultipleAsync("session", expiredFilter);
```

The filter takes the operators of a query, Or groups included, on the table's own columns; values are parameters. The
entity must not carry the id (with a filter as well, ARX can't tell which rows you meant: ARX1001), and an empty filter
is refused unless you pass `new ArxFilterWriteOptions { AllRows = true }`. A version column goes up by one on every
changed row. What ARX doesn't express is a value computed from the row (`SET n = n + 1`): use Dapper on the same
transaction for that (below).

For a large insert where you don't need the new ids, `CreateMultipleAsync(entities, new ArxCreateMultipleOptions {
ReturnIds = false })` reads none back, so a large load takes PostgreSQL's binary `COPY` (5,000 rows or more of one shape) or SQL Server's
`SqlBulkCopy` (1,000 or more), the
paths EFCore.BulkExtensions or a hand-written importer would take, still all or nothing.

## ARX and Dapper in one transaction

For SQL ARX doesn't express (an `UPDATE … SET n = n + 1`, an upsert, a CTE), write it with Dapper or the driver on the
same connection and transaction. The operations have an overload that takes the driver's connection and transaction for
this:

```csharp
await using var connection = new NpgsqlConnection(connectionString);
await connection.OpenAsync();
await using var transaction = await connection.BeginTransactionAsync();

var account = new ArxEntity("account");
account["name"] = "Northwind Traders";
ArxRecordId id = await service.CreateAsync(account, connection, transaction);

await connection.ExecuteAsync(
    "UPDATE account SET employee_count = COALESCE(employee_count, 0) + 10 WHERE id = @id",
    new { id = id.AsInt64() },
    transaction);

await transaction.CommitAsync();   // both, or (on RollbackAsync or an error) neither
```

Application code that only uses ARX passes an `ArxTransaction` instead (from `ArxPgConnection`, …); the driver's types
are for this kind of mixing.

## Text matching

`BeginsWith`, `Contains`, `EndsWith` and `Like` ignore case on PostgreSQL (`ILIKE`) and MongoDB (a case-insensitive
regular expression); on SQL Server and MySQL the column's collation decides. EF Core's `StartsWith` is case-sensitive on
PostgreSQL, so code moved from it can return more rows. An ordinary index doesn't serve `ILIKE` or a case-insensitive
regex: on a large PostgreSQL table, add a trigram index (`pg_trgm`) or a `lower(column)` index if those searches matter.

## The driver and the session

ARX.SDK.PostgreSql works with Npgsql 8.0.3 or later and is tested on 8.0.3 and 10.0.3. In an application that also uses
EF Core 10, NuGet picks Npgsql 10 for both (Versions and compatibility, "Supported driver versions"). The connections
ARX opens itself run in a UTC session (`Timezone=UTC` is added unless your connection string names a zone), so stored
instants don't depend on the server's zone. A connection you pass in keeps its own session.

## What ARX doesn't do

- No change tracking, lazy loading or identity map: each call reads or writes what you ask, when you ask.
- No migrations or table creation.
- No LINQ provider.
- No update from an expression of the row (`SET n = n + 1`); a write by a filter sets values.
- No caching.
- No logging of its own: from 0.1.0-preview.2 every call is an activity and a metric that OpenTelemetry reads
  (`AddSource("ARX.SDK")`, `AddMeter("ARX.SDK")`); log ARX's exceptions, which carry stable codes.

## When it fits

ARX is worth it when the same data code has to run on more than one database, or when you want the same error codes,
the same refusals of what a database can't do, and the same injection checks on every one. On one database with a team
fluent in EF Core or Dapper, those are the better-known tools.

How ARX compares with the raw drivers, Dapper and EF Core on each database is measured on each provider's Benchmarks
page. Read the numbers there rather than any general claim.
