Native AOT and trimming
Reference the package and publish. There is nothing to install and nothing to opt into - no companion
package, no source generator, no attribute, no registration call. Everything below works in an application
published with PublishAot exactly as it does on the just-in-time compiler.
DbConnectionPlus targets net8.0 and net10.0. net8.0 is the supported floor; net10.0 is recommended
for AOT, because from net9.0 on the trim and AOT analyzers recognise RuntimeFeature.IsDynamicCodeSupported
as a feature guard and stop reporting code your own guard has already made unreachable. Either way this
library's own publish is warning-free on both — see What you will see in your own
build.
What works
| Feature | Native AOT |
|---|---|
ExecuteNonQuery, ExecuteReader, Exists |
✅ |
ExecuteScalar<T> and scalar Query<T> |
✅ |
Query<T> for entities - property setters and constructor injection (records, immutable entities) |
✅ |
Query<T> for value tuples, including tuples with more than seven fields |
✅ |
Non-generic Query / QueryFirst / … returning DataRow, read with row["Id"] |
✅ |
InsertEntity, UpdateEntity, DeleteEntity and their bulk counterparts |
✅ |
TemporaryTable(...) for scalar values and for complex objects |
✅ |
Fluent-API mapping, [Column]/[Key] attributes, EnumSerializationMode |
✅ |
dynamic row.Id member access on a DataRow |
❌ - use the row["Id"] indexer instead |
Mapping is somewhat slower under Native AOT, because reflection replaces the compiled expression tree. In the
benchmark suite - an in-memory SQLite database, the worst case, because statement execution is
almost free there and nothing dilutes the mapping cost - querying entities takes ~1.31x as long end to end
and querying value tuples ~1.35x. Part of that is the ahead-of-time runtime rather than this library: the
raw DbCommand baseline in the same run slows by 1.08x and 1.12x respectively. Against a real database server,
where the query itself dominates, the difference is correspondingly smaller.
Reading rows without a type: row["Id"], not row.Id
The non-generic query methods return DataRow. The string indexer is the AOT-safe way to read a column and is
what the examples in this README use:
var product = connection.QueryFirst($"SELECT * FROM Product WHERE Id = {Parameter(id)}");
var name = product["Name"];
Member access through a dynamic reference still works wherever the runtime supports dynamic code generation,
but not under Native AOT - the Dynamic Language Runtime cannot bind without generating code. DataRow
itself is AOT-safe either way, and costs you nothing if you never write dynamic; the incompatibility is
reported by the compiler at your own call site. See Query methods for the full comparison.
Supported providers
End-to-end AOT support is also bounded by your ADO.NET provider, which this library cannot fix:
| Database | Provider | Native AOT |
|---|---|---|
| SQLite | Microsoft.Data.Sqlite |
✅ Verified trim-clean, and the provider this library's own AOT smoke test runs against |
| MySQL | MySqlConnector |
✅ Fully managed and trim-friendly |
| SQL Server | Microsoft.Data.SqlClient |
✅ Publishes and runs clean. |
| PostgreSQL | Npgsql |
⚠️ Core is AOT-capable; some type plug-ins reflect |
| Oracle | Oracle.ManagedDataAccess.Core |
❌ Not AOT-ready. This is a limitation of the provider |
What you will see in your own build
No warnings. Publishing with PublishAot or PublishTrimmed reports no IL2xxx and no IL3xxx diagnostic
for any scenario in the table above, on either target framework.
The public API carries no [RequiresUnreferencedCode] and no [RequiresDynamicCode], so nothing is reported at
your call sites. The three underlying reflection sites are answered inside the library, where they occur:
| Site | How it is answered |
|---|---|
| Specializing the value converter over the column's type | The generic method declares no [DynamicallyAccessedMembers], so a runtime specialization has no requirements trimming could fail to preserve |
| Compiling the expression tree | [RequiresDynamicCode] stays on the expression-tree materializer, and its only caller reaches it from inside a RuntimeFeature.IsDynamicCodeSupported branch that the AOT compiler removes |
| Finding the constructor of a nested value tuple | An ILLink.Descriptors.xml embedded in the package preserves System.ValueTuple\1-`8`, so the constructors survive trimming |
This is verified rather than asserted: the repository publishes a Native AOT smoke test on net8.0 and
net10.0 and gates on zero IL diagnostics plus every asserted value coming back correctly, including
nested value tuples and enum fields inside them.