v6.4.0
v6.4.0 — the native-binary release
Three features land together, and they share a theme: the stack now runs as a NativeAOT binary, and the two escape hatches you reach for at scale — bulk load and raw SQL — are here, honest about what they cost.
🚀 The stack runs as a native binary
The PostgreSQL stack provably publishes and runs under NativeAOT: samples/AotProbe is a PublishAot app that opens a real PostgreSQL connection, migrates, and round-trips an entity — a ~12 MB self-contained binary with no JIT. Getting there meant finding and closing every wall the linker hit, at the library level, so your app code stays plain:
- Closed-generic registration —
AddPostgreSqlRepository<TEntity, TKey>()/AddRelationalRepository<…>register over factories instead of open generics over a value-type key (which the AOT DI graph cannot honour). - Explicit unit-of-work activation —
AddPostgreSqlUnitOfWork(factory)replacestypeof-based activation with a realnew. - Operator rooting — a module initializer's
[DynamicDependency]roots thedecimal/DateTime/DateTimeOffset/TimeSpan/DateOnly/TimeOnlyoperators, soExpression.MakeBinary(LessThan, …)no longer trimsdecimal.op_LessThanout from under a query.
🗂️ Explicit migration registration — the last AOT wall
Assembly-scanning migration discovery trimmed the migration constructors. It now has a reflection-free sibling on all four providers:
services.AddPostgreSqlMigrations(source => source
.Add<ProductsSetup>()
.Add<ProductsBackfill>());Both the scan form and the explicit form funnel through one shared MigrationDiscovery.Pending — merge, dedupe, throw on two migrations claiming the same id, order by timestamp. AotProbe is now plain app code: no [DynamicDependency], no descriptor, no ILLink XML anywhere.
Still not clean-AOT (and honestly documented as such):
Expression.Compilefalls back to the interpreter,jsonbawaits a JSON source-gen context, and the wire-formatExpressionSerializeris[RequiresDynamicCode]upstream. The DAM-propagation warnings that remain are warnings, not failures. See Trimming and NativeAOT.
📦 Native bulk load + typed raw SQL
Two escape hatches for the moments the ordinary write model and the pushdown engine are the wrong tool.
BulkInsertAsync streams entities through the store's native bulk mechanism:
| Store | Mechanism |
|---|---|
| PostgreSQL | binary COPY … FROM STDIN |
| SQL Server | SqlBulkCopy |
| MySQL / MariaDB | MySqlBulkCopy |
Loading 1 000 rows costs 6.5 ms against 13.2 ms for a hand-written 1 000-statement DbBatch (0.49×) and 40.3 ms for EF Core (6.2× faster), at 0.28× the baseline's allocations.
A dialect with no native path refuses rather than quietly running a row-by-row batch — a "bulk" API that is secretly per-row is exactly the hidden cost this engine does not ship. MySQL surfaces the LOAD DATA LOCAL INFILE requirement (both AllowLoadLocalInfile=true and the server's local_infile=1) instead of flipping a security-relevant switch behind your back.
QueryAsync<TResult> runs arbitrary SQL and materializes by column name — case-insensitive, snake_case-tolerant, plan cached per result shape — with ExecuteAsync for non-queries:
var totals = await uow.QueryAsync<CategoryTotal>(
"SELECT category, COUNT(*) AS orders, SUM(total) AS total FROM sale_orders GROUP BY category");The SQL is yours, so the guarantees the engine normally makes — query filters, soft-delete, pushdown analysis, Explain() — deliberately do not apply. Parameters bind positionally as @p0, @p1…; never interpolate. That trade is documented where the API is.
Packages (all at 6.4.0): Core · Relational · PostgreSql · MySql · SqlServer · MongoDb · CosmosDb · Cassandra. Requires DataModel 4.0.0 + Domain 4.0.0.
Covered by integration tests on all three relational stores and a bulk-vs-batch benchmark. Full changelog: v6.3.0...v6.4.0