NativeAOT
Publish an application using this library with PublishAot. What is already AOT-safe, the one API shape you have to swap, and the single dependency that currently rules AOT out.
Overview
The library is built for NativeAOT: the source generator emits typed materialisation and command building at
compile time, so there is no runtime reflection or IL emit in the hot path. An application referencing
Socigy.OpenSource.DB publishes with PublishAot=true and runs at the same speed it does under the JIT — see
Benchmarks.
dotnet publish -c Release -p:PublishAot=trueThere are exactly two things to know before you do: one API shape in your own code that has to change, and one optional package that currently cannot be published this way.
Use the `string[]` overloads for column selectors
Every API that takes a column selector has two forms:
row.Update().WithFields(x => new object?[] { x.Name, x.Age }) // expression selector
row.Update().WithFields(nameof(Row.Name), nameof(Row.Age)) // string selectorThey produce identical SQL. The expression form is fine under the JIT, but the C# compiler lowers
new object?[] { ... } inside an expression tree to Expression.NewArrayInit, which is annotated
[RequiresDynamicCode]. Under NativeAOT that produces an IL3050 at your call site, and with
TreatWarningsAsErrors the publish produces no binary at all.
dotnet build — only when ILC runs at publish time. A project can build perfectly cleanly and still fail to
publish.Swap the selector; leave everything else alone.
| Expression form | AOT-safe form |
|---|---|
InsertAsync(row, keep: r => new object?[] { r.Id }) |
InsertAsync(row, [nameof(Row.Id)]) |
InsertMultipleAsync(rows, conn, fields, keep: …) |
InsertMultipleAsync(rows, conn, [nameof(Row.Id)], …) |
InsertMultipleCopyAsync(rows, conn, fields, keep: …) |
InsertMultipleCopyAsync(rows, conn, [nameof(Row.Id)], …) |
.Select(x => new object?[] { x.Id, x.Name }) |
.Select(nameof(Row.Id), nameof(Row.Name)) |
.OrderBy(x => new object?[] { x.CreatedAt }) |
.OrderBy(nameof(Row.CreatedAt)) |
.OrderByDesc(x => new object?[] { x.CreatedAt }) |
.OrderByDesc(nameof(Row.CreatedAt)) |
.WithFields(x => new object?[] { x.Name }) |
.WithFields(nameof(Row.Name)) |
.ExceptFields(x => new object?[] { x.CreatedAt }) |
.ExceptFields(nameof(Row.CreatedAt)) |
.ExcludeAutoFields(x => new object?[] { x.Id }) |
.ExcludeAutoFields(nameof(Row.Id)) |
The string overloads accept either the C# property name (nameof(Row.CreatedAt), recommended — the
compiler checks it) or the database column name ("created_at").
Predicates are unaffected
Only selectors are a problem. A WHERE predicate is an Expression<Func<T, bool>> with no array
construction in it, and is translated to SQL without runtime code generation:
// Both fine under NativeAOT — no change needed.
await foreach (var u in User.Query(x => x.Age >= 18 && x.TenantId == tenant)
.OrderBy(nameof(User.CreatedAt))
.WithConnection(conn)
.ExecuteAsync()) { }
await row.Update().WithFields(nameof(User.Email)).Where(x => x.Id == id).WithConnection(conn).ExecuteAsync();Collection predicates (ids.Contains(x.Id), which becomes = ANY(@p)) are AOT-safe too.
Finding your remaining call sites
IL3050 names the containing method rather than the line, and the default publish output collapses every
warning into a single IL3053. Turn that off to get one message per site:
dotnet publish -c Release -p:PublishAot=true -p:TrimmerSingleWarn=falseFor a large codebase, a plain text search is faster than a publish cycle. The expression form always contains
new object?[] (or new object[]) inside a lambda:
grep -rn "new object?\[\]" --include=*.cs src/The HashiCorp Vault package rules out NativeAOT
Socigy.OpenSource.DB.HashiCorp currently prevents a NativeAOT publish. This is not
something an application can work around.The package depends on VaultSharp, which serialises its request and response models with reflection-based
System.Text.Json. That produces IL2026 warnings from inside VaultSharp itself
(VaultSharp.Core.Polymath.MakeRequestAsync and VaultApiException), and:
<TrimmerRootAssembly Include="VaultSharp" />makes the reflection safe but does not silenceIL2026.- Suppressing
IL2026project-wide would suppress it for your own code too, which defeats the point of running the analysis at all.
Under TreatWarningsAsErrors the publish fails and leaves an empty output directory.
The call sites are inside VaultSharp, so nothing in this library or in your application can fix them. If you
need both Vault-managed encryption or credentials and NativeAOT today, run the Vault integration in a
separate, JIT-compiled process — a small sidecar that holds the Vault client and hands the AOT-published
application what it needs.
Everything else in the library, including field encryption with the built-in AES encryptor, publishes cleanly.
How this is verified
The library does not take AOT-safety on trust. Three gates run on every build:
- Analyser gate. The Core sources are compiled a second time under a modern target framework with the
trim and AOT analysers enabled and every
IL2xxx/IL3xxxpromoted to an error. Core itself targetsnetstandard2.0, where those analysers cannot run at all, so without this second compilation no trim warning in the library would ever be visible before it reached an application. - Generated-code gate. A project exercising the generated query, insert, update, and procedure APIs is
built with
IsAotCompatible, so the emitted code is analysed the way an application's would be. - Publish gate. A sample application that uses only the
string[]overloads is published withPublishAot=true, running the real ILC compiler, and must produce a native binary with zero IL warnings.
The third exists because the first two are not sufficient on their own: the Roslyn analysers never flag expression-tree construction, so a project can build with zero warnings and still fail to publish. That is exactly the gap this page's first section is about.