CodeLogic.Data
5.0.114-preview
See the version list below for details.
dotnet add package CodeLogic.Data --version 5.0.114-preview
NuGet\Install-Package CodeLogic.Data -Version 5.0.114-preview
<PackageReference Include="CodeLogic.Data" Version="5.0.114-preview" />
<PackageVersion Include="CodeLogic.Data" Version="5.0.114-preview" />
<PackageReference Include="CodeLogic.Data" />
paket add CodeLogic.Data --version 5.0.114-preview
#r "nuget: CodeLogic.Data, 5.0.114-preview"
#:package CodeLogic.Data@5.0.114-preview
#addin nuget:?package=CodeLogic.Data&version=5.0.114-preview&prerelease
#tool nuget:?package=CodeLogic.Data&version=5.0.114-preview&prerelease
CodeLogic.Data
Status: 5.0 preview.
One database framework for MySQL, PostgreSQL, SQL Server and SQLite. It's the CodeLogic 5 replacement for CL.MySQL2, CL.MSSQL, CL.PostgreSQL and CL.SQLite: one core package plus one small package per database, with the same API and the same behaviour everywhere.
| Package | Database | Driver |
|---|---|---|
CodeLogic.Data |
core (no driver) | — |
CodeLogic.Data.MySql |
MySQL / MariaDB | MySqlConnector |
CodeLogic.Data.PostgreSql |
PostgreSQL | Npgsql |
CodeLogic.Data.SqlServer |
SQL Server / Azure SQL | Microsoft.Data.SqlClient |
CodeLogic.Data.Sqlite |
SQLite | Microsoft.Data.Sqlite |
dotnet add package CodeLogic.Data.PostgreSql # brings CodeLogic.Data
await using var db = Database.Create("main", PostgreSqlDataProvider.Instance, new DataConnectionConfig
{
Provider = "postgresql", Host = "localhost", Database = "app", Username = "app", Password = "secret",
}).ValueOrThrow();
await db.Schema.SyncAsync<Product>();
await db.Repository<Product>().InsertAsync(new Product { Sku = "A-1", Price = 9.95m });
var cheap = await db.Query<Product>().WithCache().Where(p => p.Price < 20m).OrderBy(p => p.Price).ToListAsync();
Features:
- Queries: typed LINQ (strictly translated, never evaluated on the client), projections, joins over 2–5 tables, grouping with HAVING, cursor paging and streaming, and compiled queries.
- Writes: repositories with upserts, counters and changed-column updates; set-based updates and deletes; transactions, savepoints, row locks, advisory locks and batches.
- Schema: table sync with safe levels and drift detection, migrations, seeding, admin, backup and retention.
- Caching: query and entity caching with exact tag invalidation, warm queries and cross-node invalidation.
- Replicas and resilience: read replicas with lag probes, sticky reads and failover; retries and per-node circuit breakers.
- Entity behaviour: auditing, optimistic concurrency, multi-tenancy, global filters and interceptors.
- Telemetry: OpenTelemetry spans and metrics, slow-query plans and N+1 detection.
- Bulk copy: native on every provider.
- Mapping: a source generator (shipped in this package) writes it at build time, with no reflection, and CL.Data is Native AOT compatible. Custom types (typed IDs, value objects) use value converters; see Entities.
Measured: within a few percent of raw ADO.NET and Dapper. Against CL.MySQL2 4.9 it uses 20–45% less memory, and cached reads take microseconds.
Documentation: docs/libs/data.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net11.0 is compatible. |
-
net11.0
- CodeLogic (>= 5.0.0-preview && < 6.0.0)
- CodeLogic.Common (>= 5.0.114-preview && < 6.0.0)
NuGet packages (5)
Showing the top 5 NuGet packages that depend on CodeLogic.Data:
| Package | Downloads |
|---|---|
|
CodeLogic.Data.MySql
MySQL / MariaDB provider for CodeLogic.Data (MySqlConnector). |
|
|
CodeLogic.Data.SqlServer
SQL Server / Azure SQL provider for CodeLogic.Data (Microsoft.Data.SqlClient). |
|
|
CodeLogic.Data.PostgreSql
PostgreSQL provider for CodeLogic.Data (Npgsql). |
|
|
CodeLogic.Data.Sqlite
SQLite provider for CodeLogic.Data (Microsoft.Data.Sqlite). |
|
|
CodeLogic.WebLogic.Data
Database stores for CodeLogic.WebLogic on CodeLogic.Data (MySQL, PostgreSQL, SQL Server or SQLite): users, sessions, API keys, recovery codes, one-time use, login throttle, rate limits, audit, widget settings, dashboards, passkeys and the DataProtection key ring — with schema sync and expiry sweeps. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 5.0.116-preview | 0 | 11/1/2026 |
| 5.0.115-preview | 38 | 11/1/2026 |
| 5.0.114-preview | 31 | 10/31/2026 |
| 5.0.113-preview | 33 | 10/31/2026 |
# Changelog
## 5.0 (unreleased)
### Fixed
- **Tenant isolation on writes (security):**
- **No tenant:** updating, deleting (soft or hard, by entity or by key) or adjusting a counter on a `[TenantKey]` entity without a tenant now fails with `data.tenant_missing`, exactly like reads. Before, such writes ran unscoped and could change another tenant's row by key.
- **Upserts:** an upsert can no longer update another tenant's row that has the same conflict key. Every dialect guards the conflict update by the tenant column (PostgreSQL/SQLite `DO UPDATE ... WHERE`, MySQL `IF(...)` per assignment, SQL Server join condition), and a pre-check reports `data.conflict` before anything is written.
- **Inserts, upserts and bulk copy (breaking):** these also need a tenant now. Before, an entity whose tenant column was already set could be inserted for any tenant through an unscoped database. Write for a specific tenant through `db.ForTenant(id)`.
- **Retention:** a purge from a tenant view deletes only that tenant's expired rows. Without a tenant, the purge stays the system-wide job for every tenant's rows.
- **MySQL: saving an unchanged entity:** `UpdateAsync` of an entity whose values were already stored failed with `data.not_found` on MySQL. CL.Data turns on `UseAffectedRows`, which insert-ignore needs, and with it MySQL counts only rows that changed. An update reporting no rows is now confirmed with a `SELECT 1` using the same key, tenant and token conditions. This only happens on providers flagged `ProviderCapabilities.AffectedRowsCountChangesOnly`, and only in that case.
- **Mixed identity upserts:** an `UpsertManyAsync` call mixing explicit and server-generated identities is split into two statements. Before, the identity column was dropped for every row and explicit identities were replaced.
- **Streaming:** `AsAsyncEnumerable` runs through the command pipeline: interceptors, telemetry, read routing (replicas, `ReadFrom`) and retries of the open. Before, streams always opened a primary connection directly.
- **Captured `SqlFn` calls:** a `SqlFn` call whose arguments are all captured values (for example `SqlFn.BucketUtc(at, 60)` in a projection) is translated to SQL. Before, it was evaluated on the client and failed.
- **Cache hits** no longer walk the statement to find the tables it reads; the tags are worked out only when a result is stored.
- **Mixed identity inserts:** when explicit and server-generated identities are interleaved in one `InsertManyAsync` call, generated identities are written back to the right entities. Before, a generated identity could overwrite an explicit entity's identity.
- Float-to-decimal materialization preserves the float's shortest round-trip representation instead
of promoting it to double first (for example, `0.1f` now reads as `0.1m`).
- Converter registry version increments are atomic under concurrent registration/removal. Removing a
nullable model type now removes the underlying registration, matching converter lookup.
- Query plan variants use volatile publication so concurrent readers see fully initialized snapshots.
### Changed
- N+1 diagnostics use a dedicated `System.Threading.Lock` for each counting window.
### Added
- New package, part of the CL.Data database framework that replaces CL.MySQL2, CL.MSSQL, CL.PostgreSQL and
CL.SQLite in the CodeLogic 5 line. Targets .NET 11 and CodeLogic 5.
- **Errors:** `DataErrors` holds every `data.*` error code, each always paired with the same error kind
(NotFound, Conflict, Timeout, Unavailable, Unauthorized, Validation, Internal). Messages never contain
parameter values or secrets.
- **Events:** `DataEvent` is a closed event hierarchy (query executed, slow query, N+1, cache hit/miss, schema
synced and drift, migration, seed, retention, bulk copy, node health, circuit state). Each event carries the
connection, provider and node.
- **Localization:** `DataStrings`, the localizable log messages (section `data`).
- **Configuration:**
- `data` section (`DataConfig`): named connections (`DataConnectionConfig`) with provider, server, failover hosts,
TLS, pooling, naming policy, sync mode, performance, diagnostics, resilience and replica settings, plus typed
per-provider options (`MySql`, `PostgreSql`, `SqlServer`, `Sqlite`).
- `data.cache` section (`DataCacheConfig`).
- Validation reports field paths. Connection names stay case-insensitive after loading from JSON.
- **Mapping attributes:**
- Entity and column attributes: `[Table]`, `[DataRow]`, `[Column]` (composite keys via `KeyOrder`), `[Ignore]`.
- Relationship and index attributes: `[ForeignKey]` and `[ForeignKey<TEntity>]`, `[Index]` (multi-column, direction,
included columns), `[CompositeIndex]`.
- Behavior attributes: `[SoftDelete]`, `[RetainDays]`, `[ConcurrencyToken]`, `[CreatedAt]`/`[UpdatedAt]`/`[CreatedBy]`/
`[UpdatedBy]` and `[TenantKey]`.
- Types: a provider-neutral `DataType` enum, plus provider hint attributes (`[MySqlTable]`/`[MySqlColumn]`,
`[PostgresTable]`/`[PostgresColumn]`, `[SqlServerTable]`/`[SqlServerColumn]`, `[SqliteTable]`/`[SqliteColumn]`).
- **Metadata model:**
- `EntityMap` / `ColumnMap` / `KeyMap`: composite keys are first-class. Column roles: identity, soft delete,
concurrency token, audit, tenant, retention. Also indexes and foreign keys.
- Names are resolved per `NamingPolicy` (AsIs, SnakeCase, CamelCase) and cached.
- Invalid models report every problem at once.
- **Entity keys:** `EntityKey` holds single and composite key values, normalized so `5` equals `5L`.
- **Accessors:** `IEntityAccessor<T>` with an `EntityAccessorRegistry` for generated accessors.
- Types without generated mapping fall back to reflection (compiled delegates where dynamic code is available).
- The fallback can be switched off with `CLDataReflectionMapping=false`; Native AOT publishes switch it off by default.
- **Value conversion:** `ValueReader<T>` / `ValueConversion` handle driver differences: integers as booleans or enums,
bytes or text as GUIDs, `DateTime` as `DateTimeOffset` (UTC), and double → decimal through round-trip text.
- **Provider contract:**
- `DataProvider` with `ProviderCapabilities`, `ITypeMapper`, `IErrorClassifier`, connection-string building per
`NodeEndpoint` (the primary or a replica) and a `DbDataSource` per node.
- `DataProviderRegistry` checks the contract version of each provider.
- `SqlDialect` base with ANSI quoting, parameter names and LIKE escaping.
- **Database and raw SQL:**
- `Database` (one per named connection) owns a pooled `DbDataSource` for the primary and each replica.
- Raw SQL with `SqlQueryAsync<T>`, `ExecuteSqlAsync`, `SqlScalarAsync<T>` and `PingAsync`. Raw SQL is written as
interpolated strings (`SqlText`): every hole becomes a typed parameter, and `Sql.Identifier(...)` holes become
quoted identifiers. Values never reach the SQL text.
- Failures and cancellation come back as `Result` errors, never exceptions.
- **Data sources:** `InitializingDataSource` is a base for data sources that need per-connection setup.
- **Command pipeline:** every command runs through one middleware chain (`ICommandMiddleware`, `CommandContext`,
allocation-free `CommandNext<T>`). Application `IDataInterceptor`s run outermost: they can rewrite the SQL,
refuse a command or observe its result. Caching, routing, resilience and telemetry plug into this chain.
- **Value converters:** one conversion layer for every boundary (reading rows, entity writes, raw SQL parameters,
scalars).
- `ValueConverter<TModel, TStore>` with a global `ValueConverters` registry and per-column `[Column(Converter = ...)]`.
- Typed ids implement `IDataValue<TSelf, TStore>` (static abstract members, no reflection).
- `EnumToStringConverter<TEnum>` stores enums by name.
- `JsonValueConverter<T>` stores JSON columns through System.Text.Json source-gen metadata.
- **Background jobs:** `DataScheduler` runs CL.Data's background work on CL.Common's `JobScheduler`, with one
clock (`TimeProvider`), no overlapping runs and failures reported per run. CL.Data now depends on CodeLogic.Common.
- **SQL tree:** statements are closed record trees (`SqlExpr`, `SqlStatement`: select with joins, grouping,
HAVING, paging and locks; insert with RETURNING; update; limited delete; upsert).
- `SqlWriter` renders them through dialect hooks with exhaustive switches, adding only the parentheses that
precedence needs.
- Limits and offsets are parameters, so the SQL text does not change with their values.
- Literals accept only numbers and booleans.
- **LINQ translation** (`SqlTranslator`, strict). Anything that touches a column must translate or fail with
`data.translation_failed`; values independent of the row are evaluated once and bound as parameters.
- **Null handling:** comparisons with null (including captured nulls) become `IS NULL`.
- **Booleans:** boolean columns used as conditions become `= TRUE`.
- **Strings:** `Contains`/`StartsWith`/`EndsWith` become escaped LIKE patterns.
- **Collections:** collection `Contains` becomes `IN`. This covers C# 14+ span-bound array calls, including the
three-argument `MemoryExtensions.Contains` that nullable and enum arrays bind to. Long lists are split, and a
null entry in the list matches NULL rows.
- **Converters:** compared values go through the column's converter (enums stored as text bind their names).
- **Functions:** arithmetic, string, math and date/time functions, plus `??` and `?:`.
- **SQL functions:** `SqlFn` holds query functions that work in `Where` too, not only in projections and updates.
- Date parts, `BucketUtc`, `UtcNow`, `Coalesce`/`IfNull`, `Least`/`Greatest`, string functions, `Like`, and generic-math
`Round`/`Floor`/`Ceiling`/`Abs`.
- `SqlFn.Json.Value/Exists/Contains` and `SqlFn.FullText.Match` translate to neutral function nodes that each
provider renders.
- **Dialect hooks:** conditions and boolean literals are rendered through dialect hooks (`RenderConditionLiteral`),
`SqlPredicates.IsPredicate` is public, and dialects declare when an upsert needs a transaction
(`UpsertRequiresTransaction`, `UpsertParameterMultiplier`).
- **Unified write path:** `WritePlanner` plans every entity write (insert, update, soft/hard delete, delete by key,
upsert with overwrite, increments and ignore) in one place. It covers:
- audit columns, tenant stamping and checks, and optimistic concurrency (integer tokens bumped after success;
rowversion tokens returned);
- server-generated identity (`RETURNING`/`OUTPUT`, or last-insert-id);
- batching by the provider's parameter limit;
- cache tags (`table:` and `entity:`).
- **Write context:** `IDataUserAccessor` and `IDataTenantAccessor` supply the current user and tenant.
- **Repositories:** `db.Repository<T>()`.
- Writes: `InsertAsync`/`InsertManyAsync` write generated identities back, `UpdateAsync` (also for selected
properties), `DeleteAsync` (soft), `HardDeleteAsync`, `DeleteByIdAsync`/`DeleteByKeyAsync`.
- Reads: `GetByIdAsync`/`GetByKeyAsync` (composite keys), `GetAllAsync`, `FindAsync`, `CountAsync`, and
`GetPagedAsync`, which counts only when asked and always sets `HasMore`.
- Reads hide soft-deleted rows and other tenants' rows. A stale update fails with `data.concurrency`; a duplicate
unique value fails with `data.duplicate_key`.
- **Write execution:** multi-statement upserts run in a serializable transaction. Generated values come from
RETURNING/OUTPUT, or from the provider's last-insert-id. When the provider's RETURNING does not keep insert order,
rows with generated keys are inserted one per statement.
- **Upserts and counters:** `UpsertAsync`/`UpsertManyAsync` (conflict on the key, a unique index or chosen properties),
`UpsertWithIncrementsAsync`, `InsertIgnoreAsync` (`InsertOutcome.Inserted`/`Duplicate`) and `InsertManyIgnoreAsync`,
plus atomic `IncrementAsync`/`DecrementAsync`/`AdjustAsync` (generic math). Each update also writes the audit
columns and bumps the concurrency token.
- **Query builder:** `db.Query<T>()` is immutable and translates its lambdas at the terminal call.
- Shaping: `Where`, `OrderBy`/`ThenBy` (and descending), `Take`/`Skip`, `Exclude`, `IncludeDeleted`.
- Subqueries: correlated `WhereExists`/`WhereNotExists`, and `WhereIn`/`WhereNotIn` (the inner entity's filters apply).
- Reads: `ToListAsync`, `FirstOrDefaultAsync`, `CountAsync`, `AnyAsync`, `Max`/`Min`/`Sum`/`AverageAsync`.
Aggregates over no rows return null, and sums return zero.
- Set-based writes: `UpdateAsync(x => new T { ... })` (values may use the row's columns; audit columns are written and
the token bumped) and `DeleteAsync` (soft unless `hard`).
- **Paging and streaming:** `ToPagedListAsync` (offset paging, count only on request), `ToCursorPageAsync` and
`AsAsyncEnumerable`.
- Cursor paging is keyset paging: the order is completed with the key, so ties are broken and the cost does not grow
with depth. Cursors from a differently ordered query fail with `data.invalid_cursor`.
- Streams hold one connection while you enumerate. A failure mid-stream throws `DataQueryException`, the one place
CL.Data throws.
- **Projections:** `Select` projects into scalars, anonymous types, records and DTOs.
- Projected values can be computed: arithmetic, string concatenation, comparisons, `?:` and `SqlFn` functions.
- `Distinct()` removes duplicate results.
- A projected column keeps its column converter, so enums stored as text come back as enums.
- Rows are built with the constructor and setters named in the expression (no extra reflection; Native AOT safe).
- **Joins:** typed `Join`/`LeftJoin` over 2 to 5 tables, including self-joins and key-selector joins (composite keys
through anonymous types).
- Lambdas take one parameter per table.
- `Where`/`OrderBy`/`ThenBy`/`Take`/`Skip`, `Select` projections, `CountAsync`, and `ToListAsync` returning
entity tuples (null for an unmatched LEFT JOIN).
- Joined entities' soft-delete and tenant filters go into their ON clause.
- **Grouping:** `GroupBy` takes a value, an anonymous type of columns, or a computed key.
- `Having`, `OrderBy`/`ThenBy` over aggregates, `Take`/`Skip`, and `Select` into any projection.
- Group lambdas receive `IDataGroup<TKey, T>`: `Key`, `Count`, `Count(predicate)`, `LongCount`, `CountDistinct`,
`Sum`, `Average`, `Min`, `Max`, `Any`.
- Sums of NULLs are zero, and integer averages are not truncated (SQL Server).
- **Transactions:** `BeginTransactionAsync` returns a `DataTransaction` whose `Db`, `Repository` and `Query` run
inside it; disposing without commit rolls back. Also savepoints.
- `InTransactionAsync` commits on success, rolls back on failure or exception, and retries after deadlocks and lock
timeouts. Nested calls join the outer transaction.
- **Locking reads:** `ForUpdate`/`ForShare` with `LockWait.NoWait`/`SkipLocked` need a transaction.
- **`UpdateAndGetAsync`:** locks the matching keys, updates them and returns the updated rows (RETURNING where
available). With `OrderBy`/`Take`/`SkipLocked` it is a safe job-queue claim.
- **Batches:** `db.Batch()` sends several queries in one round trip: `Add(query)`, `Count(query)`, `Sql<T>(...)`, then
`ExecuteAsync` and each result's `Value`.
- It uses `DbBatch` where the driver supports it, otherwise one multi-statement command with globally numbered
parameters.
- A planning error (such as a translation failure) runs nothing.
- **Fixes found on MySQL:**
- Upserts qualify target columns (shared `QualifyColumns`), because the incoming-row alias has the same column names.
- Savepoints fall back to standard `SAVEPOINT` SQL when a driver has no savepoint API.
- **Upsert fix (found on PostgreSQL):** `ON CONFLICT DO UPDATE` qualifies target columns, because PostgreSQL treats a
bare name as ambiguous with `EXCLUDED`.
- **Grouping fix (found on SQL Server):** a condition used as a group key is grouped as a CASE value, because SQL
Server cannot group by a predicate.
- **Breaking:** `SqlServerAuthentication.EntraPassword` was removed (deprecated by Microsoft).
- **Schema model:** `TableSchema` (columns, keys, indexes, foreign keys, options), `SchemaBuilder.FromMap` (the
desired table per provider and naming policy, generated index and key names cut to the provider's identifier
limit with a hash) and `SchemaBuilder.Fingerprint`.
- `SchemaDiff.Compare` plans closed `SchemaChange`s by sync level. Safe: create, add, rename through
`PreviousName`, widen, add indexes and foreign keys. Additive: also drop indexes and foreign keys. Full: also drop
columns and narrow.
- Indexes and foreign keys are matched by shape; indexes that back foreign keys are kept.
- **Provider hooks:** `NormalizeStoreType` and `TableOptions`; PostgreSQL normalizes its type aliases.
- **DDL generation:** `DdlGenerator` writes standard DDL for every `SchemaChange`; `DataProvider.Ddl` exposes it.
- **Implicit defaults:** a NOT NULL column added without a default gets one, so existing rows stay valid.
- **Covering columns:** where the provider has no `INCLUDE`, they are appended to the index key.
- **Schema introspection:** `SchemaIntrospector` (`ReadTableAsync`, `ListTablesAsync`), exposed as `DataProvider.Introspector`.
- **Type normalization:** `StoreTypeNormalizer` converts the type names catalogs report into the names the type mappers write.
- **New query method:** `Database.SqlReadAsync(sql, read)` builds each row of a raw query with your own reader function.
- **Schema conformance:** generated DDL is run, the table is read back, and the diff against the model must be empty; changes apply with the rows kept.
- **Table sync:** `db.Schema.SyncAsync` (and `SyncAsync<T>`, `PlanAsync<T>`) creates and alters tables to match their entity models.
- **Order:** tables referenced by foreign keys come first.
- **Fingerprint skip:** a table whose fingerprint matches its row in `cl_data_schema` is skipped. `Force` inspects every table.
- **Drift:** changes the sync level does not allow are reported as drift (`SchemaDriftDetectedEvent`) and are not recorded, so the next run looks again.
- **One transaction per table** where DDL is transactional. On SQLite, foreign keys are off during the change and checked before commit, so a rebuild never cascades.
- **One process at a time:** the run holds an advisory lock.
- **Advisory locks:** `db.AcquireLockAsync(name, timeout)` returns a `DataLock`, held on its own connection. It uses GET_LOCK on MySQL, pg_try_advisory_lock on PostgreSQL and sp_getapplock on SQL Server; SQLite returns `data.unsupported`.
- **Events:** `DatabaseOptions.Events` receives CL.Data events.
- **Backup before alter:** moved to WP-75, together with backup and restore.
- **Migrations:** `IMigration`/`Migration` with `MigrationVersion` (semantic app version plus order, written `1.2.0/003`).
- **Context:** `IMigrationContext` gives a `Db` bound to the migration's transaction, plus `ExecuteAsync`, `ApplyAsync(SchemaChange)`, `ReadTableAsync` and `SyncTableAsync<T>`.
- **Runner:** `db.Migrations(...)` creates a `MigrationRunner` with `PendingAsync`, `AppliedAsync`, `MigrateAsync` and `RollbackAsync(target)`.
- **History:** each migration commits in one transaction with its row in `cl_data_migrations`.
- **Checksums** (XxHash128 of version and description) detect migrations edited after they were applied.
- **Safety:** `AppVersion` filters which migrations run; a rollback that includes an irreversible migration is refused before anything changes; runs take an advisory lock.
- **Discovery:** `MigrationRunner.FromAssembly` finds migrations by reflection (annotated for trimming).
- **`DataTransaction`:** can run session setup and teardown statements around the transaction (SQLite foreign keys).
- **Administration (`db.Admin`):**
- **Databases:** `DatabaseName`, `DatabaseExistsAsync`, `EnsureDatabaseAsync`, `CreateDatabaseAsync` and `DropDatabaseAsync` (the name is always explicit).
- **Tables:** `ListTablesAsync`, `TableExistsAsync`, `DropTableAsync` and `TruncateAsync`, each by name or entity type.
- **Provider hook:** `DatabaseAdmin`; `ServerDatabaseAdmin` connects to the server-level database for the server providers.
- **Retention (`db.Retention`):** `PurgeAsync<T>` deletes rows past their `[RetainDays]` limit in batches of `BatchSize` and publishes `RetentionPurgedEvent`. `Schedule(scheduler, maps, every)` runs it on `DataScheduler`.
- **Dialect hooks:** `RenderTruncate` and `RenderBatchedDelete`.
- **Backups (`db.Backup`):** `BackupAsync(directory, tables?, schema?)` writes a portable logical backup.
- **Contents:** `manifest.json` holds the table definitions in dependency order, and each table's rows go to a typed JSON-lines file, streamed with sequential access. Values whose type differs from their column's (SQLite) are tagged inline.
- **Restore:** `RestoreAsync(directory)` recreates the tables on the same provider and reinserts the rows in multi-row chunks, one transaction per table, keeping identity values.
- **Snapshot:** `SnapshotAsync(file)` makes a native whole-file copy (SQLite `VACUUM INTO`).
- **Backup before alter:** with `BackupDirectory` set, table sync backs up a table before dropping or altering its columns. If the backup fails, nothing is changed.
- **Provider hooks:** `DdlGenerator.IdentityInsertSetup`/`IdentityInsertTeardown`; `DataProvider.SnapshotSql`.
- **Query plan cache (`QueryPlanCache`):** a lambda whose shape was translated before is not translated again.
- **How it works:** its captured values are read again and bound into the cached SQL tree.
- **Variants:** shapes that depend on a captured value (a null that becomes `IS NULL`, the length of an `IN` list, a null `LIKE` argument, nulls in a params array) keep one plan per variant.
- **Constants:** literals written in code are part of the shape. Values computed at call time (`DateTimeOffset.UtcNow`) are computed again on every use.
- **Not cached:** grouped-query lambdas and unusual expression nodes are translated every time.
- **Settings and stats:** `Enabled`, `Capacity`, `MaxVariantsPerShape`, `Hits`, `Misses`, `Count`, `Clear()`.
- **Effect on a typical predicate:** about 20% less translation time and 56% fewer allocations.
- **Source generator:** every `[Table]` / `[DataRow]` class gets a generated `IEntityAccessor<T>`, registered by a module initializer.
- **Map:** built on first use, from the same attribute instances reflection reads.
- **Typed access:** materialization uses `GeneratedMapping.Read<T>`. Columns that aren't selected keep their property initializers.
- **Constructors and members:** positional records bind through the constructor, `required` members are set in the object initializer, and init-only setters go through `UnsafeAccessor` (no reflection).
- **Writes:** column writes are typed and don't allocate.
- **Fallback:** generic, abstract, private nested and file-local types stay on reflection mapping.
- **Shared mapping logic:** `GeneratedMapping.Column`/`Entity` hold the column and entity rules; reflection mapping uses them too, so both describe a type identically. A parity test compares them for every generated type.
- **Performance (in-memory reader):** about 1.9x faster than reflection when materializing; parameter writes allocate nothing (reflection boxed every value).
- **Mapping analyzer:** mapping mistakes in `[Table]`/`[DataRow]` types are build errors, reported at the offending member, instead of `InvalidOperationException` at first use.
- **CLD002** constructor cannot bind
- **CLD003** bad converter
- **CLD004** nullable key
- **CLD005** composite key without (or with repeated) KeyOrder
- **CLD006** column type doesn't fit its role
- **CLD007** role used twice
- **CLD008** unknown property in an index
- **CLD009** duplicate or unsafe name
- **CLD010** SET NULL on a non-nullable column
- **CLD011** table maps no columns
- **CLD001** (warning): the type cannot use generated mapping, so it falls back to reflection, which Native AOT doesn't support.
- **Native AOT:** the `samples/Data.AotSmoke` app does CRUD, LINQ queries (expression trees run interpreted), plan-cached repeats, transactions, schema sync and raw SQL on SQLite as a 6.6 MB native executable. The publish has no trim/AOT warnings from CL.Data, CL.Common or CodeLogic, and CI (`data-aot`) publishes and runs it.
- **MySqlConnector and Npgsql:** add no warnings either.
- **Changed-column updates:** these write only the columns that changed, plus audit columns and the concurrency token, and send nothing when nothing changed.
- **`Repository.UpdateAsync(entity, change)`:** applies your change, then writes the difference.
- **`UpdateAsync(entity, snapshot)`:** writes what changed since `Repository.Snapshot(entity)`.
- **`UpdateChangedAsync(original, modified)`:** for detached copies.
- **Accessors:** both kinds implement `IEntityAccessor<T>.Snapshot`. Generated snapshots keep typed fields and compare without boxing.
- **In-place changes:** byte arrays are copied and converter columns are compared in stored form, so they are detected.
- **Effect:** a concurrent change to another column is no longer overwritten.
- **Breaking:** `IEntityAccessor<T>` gains `Snapshot`.
- **Compiled queries:** `CompiledQuery.For<T, TArgs>((q, args) => q.Where(p => p.Id == args)...)` plans a query once per database (and tenant) and replays it.
- **Per call:** only the arguments are bound. Building expression trees, translating and rendering SQL are skipped.
- **Terminals:** `ToListAsync`, `FirstOrDefaultAsync`, `CountAsync` and `AnyAsync`.
- **Variants:** shapes that depend on an argument (null checks, `IN` list lengths) get their own variant.
- **Restriction:** non-lambda arguments in the chain (`Take(10)`) must not depend on the arguments.
- **Measured on in-memory SQLite (by id):** 41.7 → 30.4 µs per call and 8.5 → 4.2 KB allocated.
- **Query lambda analyzer:** `QueryBuilder` predicate and ordering lambdas that can't be translated are reported at build time instead of as `data.translation_failed` at runtime.
- **CLD101** unsupported method, or `Contains` on a collection that depends on the row
- **CLD102** unsupported member on a row value
- **CLD103** unmapped or `[Ignore]` property
- **Severity:** warnings (raise them to errors with `.editorconfig`). The checks mirror the translator's supported surface; parts that don't read the row are never flagged.
- **Entity save hooks:** `IDataInterceptor.OnSavingAsync`/`OnSavedAsync` with `EntitySaveContext` (kind, entities, changed properties) run around inserts, updates, upserts and deletes.
- **Before planning:** `OnSavingAsync` runs first, so changes it makes to the entities are saved, and returning an error cancels the save.
- **Cost:** nothing extra without interceptors.
- **Auditing conformance:** `[CreatedAt]`/`[UpdatedAt]` (from `DatabaseOptions.Time`) and `[CreatedBy]`/`[UpdatedBy]` (from `IDataUserAccessor`) are verified on insert, update, both upsert paths, set-based updates and counters. An update never writes the created columns.
- **Global filters:** `DataFilter.For<T>(predicate, name)` in `DatabaseOptions.Filters` adds standing conditions to every read of the targeted entities.
- **Targets:** `T` can be an entity, a base class or an interface; one filter covers every mapped type that implements it.
- **Where they apply:** queries, key lookups, joins, subqueries, set-based updates and deletes, and compiled queries. Entity writes by key aren't filtered, and `UpdateAndGet` returns the rows it updated even if they no longer match.
- **`QueryBuilder.IgnoreFilters()`** lifts all user filters, or only the named ones. The tenant filter always applies.
- **`Database.ForTenant(tenant)`:** a cheap view scoped to one tenant (filters and stamps `[TenantKey]` entities). It shares the connection pools; disposing the view does nothing.
- **Seeding:** `IDataSeed`/`DataSeed` (id, order, version) and `DataSeed.Rows<T>(id, rows)`, an idempotent upsert seed.
- **Runner:** `db.Seeds(...).RunAsync()` runs seeds that are new or have a higher version than recorded, in order. Each seed commits in one transaction with its row in `cl_data_seeds`, under an advisory lock.
- **Failures:** a failed seed rolls back and is retried on the next run.
- **Internal:** schema sync, migrations and seeds share one helper for their bookkeeping tables and locks.
- **Result cache:** `DataCache` (shareable, `DatabaseOptions.Cache`), built on CL.Common's `MemoryCache`.
- **Opting in:** `QueryBuilder.WithCache(policy | ttl)` caches lists, first-or-default, count/any and aggregates, plus projections, groups and joins built from that query. `DataCacheConfig.EntityCache` caches repository key lookups.
- **Fresh objects:** results are stored as raw rows and materialized fresh on every hit, so callers can't change the cache.
- **Invalidation:** every write through CL.Data drops the results of the tables it touches, including the tables a join or subquery reads. Raw `ExecuteSqlAsync` drops the connection's entries.
- **Transactions:** reads inside a transaction bypass the cache, and invalidations wait for commit; a rollback drops nothing.
- **Concurrency:** concurrent misses run once, and a write during a load keeps the stale result out of the cache.
- **Measured on MySQL:** a key lookup takes 1.66 µs from cache against 1.4 ms from the database; a 100-row filter takes 18 µs against 1.6 ms.
- **Warm queries:** set `CachePolicy.Refresh` to keep a result warm.
- The cache re-reads it in the background every period, so hits never wait for the database. It is dropped after `IdleRefreshes` periods without a hit.
---
Release notes truncated to fit NuGet's 35 000 character limit. The complete changelog ships inside this package as CHANGELOG.md.