Skip to content

v6.4.0

Choose a tag to compare

@github-actions github-actions released this 24 Jul 09:24
· 11 commits to master since this release

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 registrationAddPostgreSqlRepository<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 activationAddPostgreSqlUnitOfWork(factory) replaces typeof-based activation with a real new.
  • Operator rooting — a module initializer's [DynamicDependency] roots the decimal / DateTime / DateTimeOffset / TimeSpan / DateOnly / TimeOnly operators, so Expression.MakeBinary(LessThan, …) no longer trims decimal.op_LessThan out 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.Compile falls back to the interpreter, jsonb awaits a JSON source-gen context, and the wire-format ExpressionSerializer is [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