ZeroAlloc.ORM
1.5.0
dotnet add package ZeroAlloc.ORM --version 1.5.0
NuGet\Install-Package ZeroAlloc.ORM -Version 1.5.0
<PackageReference Include="ZeroAlloc.ORM" Version="1.5.0" />
<PackageVersion Include="ZeroAlloc.ORM" Version="1.5.0" />
<PackageReference Include="ZeroAlloc.ORM" />
paket add ZeroAlloc.ORM --version 1.5.0
#r "nuget: ZeroAlloc.ORM, 1.5.0"
#:package ZeroAlloc.ORM@1.5.0
#addin nuget:?package=ZeroAlloc.ORM&version=1.5.0
#tool nuget:?package=ZeroAlloc.ORM&version=1.5.0
<h1 align="center">ZeroAlloc.ORM</h1>
<p align="center">Source-generator-based, NativeAOT-clean raw-SQL data access for .NET. Annotate <code>partial</code> methods with <code>[Query]</code> / <code>[Command]</code> / <code>[StoredProcedure]</code>; the generator emits typed parameter binding + materialization against <a href="https://github.com/MarcelRoozekrans/AdoNet.Async">AdoNet.Async</a>. Zero runtime reflection.</p>
Status: v1.1.0 — additive release. Adds
MigrationRunnerfor embedded SQL migrations (Sqlite + Postgres dialects); v1.0 public surface still frozen and AOT-clean. The v1.x line is additive-only viaPublicAPI.Shipped.txt. Authoritative design lives atdocs/design/2026-05-30-v1.0-design.md; v1.1 implementation plan atdocs/plans/2026-06-01-v1.1-implementation.md. Working backlog atdocs/plans/za-orm-backlog.md.
What it is
A source-generator-driven data-access library that fills in the gap between two extremes adopters currently choose from:
- EF Core — full LINQ-to-SQL ORM, but its precompile-queries pipeline currently collides with co-resident source generators (e.g. ZA.Rest), blocking NativeAOT publish in template stacks like ZeroAlloc.Templates.
- Hand-written ADO.NET — works under AOT, but every repository becomes a hand-shaped tower of
CreateCommand/CreateParameter/ReadAsynccalls.
ZeroAlloc.ORM is the middle path: write the SQL string in an attribute, declare the partial method signature, let the source generator emit the ADO.NET pipeline. Zero runtime reflection, fully AOT-publishable, idiomatic with the rest of the ZeroAlloc ecosystem (consumes AdoNet.Async, dogfoods ZeroAlloc.ValueObjects, shares the convention catalog with ZeroAlloc.Mapping).
Packages
Quick Start
Every example below assumes a containing partial class that exposes an IAsyncDbConnection (from AdoNet.Async) — usually injected via primary constructor. The source generator emits the open / execute / close pipeline against that connection.
1. Single-row read
[Query("SELECT id, name FROM customers WHERE id = @id")]
public partial Task<Customer?> GetCustomerAsync(int id, CancellationToken ct);
public sealed record Customer(int Id, string Name);
Positional record + matching SELECT column order = no mapping config. Nullable return type = empty result set yields null. See docs/cookbook/multi-result-set.md for the head + lines tuple pattern.
2. Streaming with IAsyncEnumerable
[Query("SELECT id, name FROM customers ORDER BY id")]
public partial IAsyncEnumerable<Customer> StreamCustomersAsync(
[EnumeratorCancellation] CancellationToken ct);
Connection opens lazily on first MoveNextAsync, closes deterministically on DisposeAsync (including early break exits). More in docs/cookbook/streaming.md.
3. Insert returning identity
[Command(
"INSERT INTO orders (customer_id, total) VALUES (@customerId, @total) RETURNING id",
Kind = CommandKind.Identity)]
public partial Task<int> InsertOrderAsync(int customerId, decimal total, CancellationToken ct);
CommandKind.Identity runs the INSERT through ExecuteScalarAsync and unwraps the first column of the first row into your declared identity type. The user-authored SQL supplies the provider-specific identity-capture clause: RETURNING <id-column> (Sqlite 3.35+, Postgres), SCOPE_IDENTITY() (SQL Server), last_insert_rowid() (Sqlite ;-joined fallback). The generator keeps the SQL string verbatim — provider-agnostic by design. More in docs/cookbook/commands.md.
4. Stored procedure with output parameters
[StoredProcedure("usp_insert_order")]
public partial Task<(int rowsAffected, int newOrderId, decimal computedTotal)> InsertOrderSprocAsync(
int customerId, CancellationToken ct);
The first tuple field is the result-set materialization (here: rows-affected). Subsequent named tuple fields map to ParameterDirection.Output SQL parameters by name. More in docs/cookbook/stored-procedures.md.
Capabilities by milestone
Added in v0.1
[Query]with scalar (Task<int>,Task<T?>) and FlatRow (Task<TRow?>) return shapes.- 14 primitive types in parameter binding (int / long / short / byte / bool / decimal / double / float / string / Guid / DateTime / DateTimeOffset / TimeSpan / byte[]) + nullable variants.
[Param(Name = "...")]SQL-side parameter name override.- Compile-time diagnostics (ZAO001–ZAO009 + informational ZAO020–ZAO022) for signature contract violations.
- NativeAOT-clean publish (verified by
aot-smokeCI gate).
Added in v0.2
Value-object columns — types annotated with
[ValueObject]from ZeroAlloc.ValueObjects (with a staticFromfactory and aValueproperty) bind through their underlying primitive. Parameters unwrap toValue; reads wrap viaT.From(primitive).[ValueObject] public readonly partial struct CustomerId { public int Value { get; } public CustomerId(int value) { Value = value; } public static CustomerId From(int value) => new(value); } public sealed record CustomerRow(CustomerId Id, string Name); public sealed partial class CustomerRepo(IAsyncDbConnection connection) { [Query("SELECT Id, Name FROM Customers WHERE Id = @id")] public partial Task<CustomerRow?> GetAsync(CustomerId id, CancellationToken ct); }Enums (default int round-trip) — any
enumparameter or column binds as its underlying integer (reader.GetInt32+ cast on read; cast to underlying primitive on bind).Enums (string round-trip) — annotate the
enumtype with[StoreAsString]to round-trip as the member name (reader.GetString+Enum.Parse<T>on read; member-name bind).Domain-entity classes — plain
classtypes with a single multi-arg public ctor materialize via column-name-keyed reads (__reader.GetOrdinal("ColumnName")). SELECT column order is irrelevant; each ctor parameter resolves to its matching column by name. Records keep the positionalFlatRowpath.Single-arg record discovery + static
Fromfactory discovery — wrappers without[ValueObject]still resolve when ConventionDiscovery can find an obvious construction strategy.New diagnostics ZAO040–ZAO044 — materialization-side failures (no construction strategy, conflicting strategies, unresolved ctor parameters, etc.) surface at build time with focused messages.
Added in v0.3
Multi-result-set tuples (head + lines pattern) — tuple return types map each tuple element to one
;-separated SQL statement. Collapses the 1+N round-trip into a single command. Tuple element kinds: scalar (int, value-object, enum), row (record/ single-ctor class), or list (List<T>/IReadOnlyList<T>/IEnumerable<T>/ ...). Seedocs/cookbook/multi-result-set.md.public sealed record OrderRow(int Id, int CustomerId, decimal Total); public sealed record OrderLineRow(int Id, int OrderId, string Sku, int Qty); public sealed partial class OrderRepo(IAsyncDbConnection connection) { [Query(""" SELECT Id, CustomerId, Total FROM Orders WHERE Id = @id; SELECT Id, OrderId, Sku, Qty FROM OrderLines WHERE OrderId = @id; """)] public partial Task<(OrderRow Head, IReadOnlyList<OrderLineRow> Lines)?> GetWithLinesAsync( int id, CancellationToken ct); }Batch dispatch (
BatchMode.Auto/Always/Never) —[Query(Batch = ...)]picks how multi-statement SQL reaches the provider.Auto(default) branches at runtime onconnection.CanCreateBatch: providers exposingIAsyncDbBatchget the pipelined path; everyone else falls back to a single command with;-joined SQL andNextResultAsync. Both paths produce the same materialized tuple.IAsyncEnumerable<T>streaming — partial methods returningIAsyncEnumerable<T>with a[EnumeratorCancellation] CancellationTokenparameter emit an async iterator that materializes rows lazily. Connection opens on firstMoveNextAsync, closes deterministically onDisposeAsync(including earlybreakexits). Seedocs/cookbook/streaming.md.public sealed partial class OrderRepo(IAsyncDbConnection connection) { [Query("SELECT Id, CustomerId, Total FROM Orders ORDER BY Id")] public partial IAsyncEnumerable<OrderRow> StreamAllAsync( [EnumeratorCancellation] CancellationToken ct); }New diagnostics ZAO032 / ZAO033 — arity mismatch between a tuple return and the number of
;-separated SQL statements. ZAO032 fires when the tuple has more elements than statements; ZAO033 fires when it has fewer. Both at build time, so the wrong shape never ships.
Added in v0.4
[Command]attribute — non-SELECTSQL with three result-shape modes selected viaCommandKind.NonQueryreturns rows-affected (int),Scalarruns throughExecuteScalarAsyncand materializes the first column of the first row,Identityis structurally aScalarwhose user-authored SQL captures the inserted identity via the provider's idiom (RETURNING <col>on Sqlite 3.35+/Postgres,SCOPE_IDENTITY()on SQL Server,;thenlast_insert_rowid()on legacy Sqlite). The generator passes the SQL string through verbatim — provider-agnostic by design. Seedocs/cookbook/commands.md.public sealed partial class OrderRepo(IAsyncDbConnection connection) { [Command("UPDATE Orders SET Total = @total WHERE Id = @id", Kind = CommandKind.NonQuery)] public partial Task<int> UpdateTotalAsync(int id, decimal total, CancellationToken ct); [Command("SELECT COUNT(*) FROM Orders WHERE CustomerId = @customerId", Kind = CommandKind.Scalar)] public partial Task<int> CountByCustomerAsync(int customerId, CancellationToken ct); [Command("INSERT INTO Orders (CustomerId, Total) VALUES (@customerId, @total) RETURNING Id", Kind = CommandKind.Identity)] public partial Task<int> InsertAsync(int customerId, decimal total, CancellationToken ct); }[StoredProcedure]attribute — emitCommandType = StoredProcedurewith the procedure name asCommandText. Result shapes mirror[Query]: scalar, single-row, list, multi-result-set tuples. Parameters bind by name. Seedocs/cookbook/stored-procedures.md.public sealed partial class OrderRepo(IAsyncDbConnection connection) { [StoredProcedure("usp_GetOrderWithLines")] public partial Task<(OrderRow Head, IReadOnlyList<OrderLineRow> Lines)?> GetWithLinesAsync( int orderId, CancellationToken ct); }Named-tuple output parameters — on
[StoredProcedure]methods, tuple return fields beyond the first map toParameterDirection.OutputSQL parameters by name. Output values are copied back into the tuple after execution. The first tuple field is still the result-set materialization (scalar, row, or list); subsequent named fields are output params.public sealed partial class OrderRepo(IAsyncDbConnection connection) { [StoredProcedure("usp_CreateOrder")] public partial Task<(int rowsAffected, int newOrderId, decimal computedTotal)> CreateAsync( int customerId, CancellationToken ct); }The generator emits
Direction = ParameterDirection.Outputon@newOrderIdand@computedTotal, executes the proc, then reads the output values back into the returned tuple.New diagnostics ZAO060 (reserved) / ZAO061 / ZAO062 — sproc-side compile-time guardrails. ZAO060 is reserved for a future
out/ref-on-async check (the C# compiler already rejectsout/refon async-returning partials, so the dedicated ZAO060 message is unnecessary at the source-generator layer for now). ZAO061 fires on[StoredProcedure("")](empty procedure name). ZAO062 fires when a named-tuple output-parameter field doesn't appear as a method parameter that could carry the output back to SQL — the name must match an@paramthe proc declares.
Added in v0.5
Multi-column composites (Money pattern) — declare a positional-ctor type like
Money(decimal Amount, string Currency)and the generator unpacks it into N SQL columns automatically. Each ctor parameter resolves through the existing convention pipeline (primitive / enum / value-object / single-arg-ctor / static-factory). Nested composites flatten transparently —record OrderRow(int Id, Money Total)reads as 3 columns. Seedocs/cookbook/composites.md.public readonly record struct Money(decimal Amount, string Currency); public sealed partial class OrderRepo(IAsyncDbConnection connection) { [Query("SELECT Id, Amount, Currency FROM Orders WHERE Id = @id")] public partial Task<OrderRow?> GetAsync(int id, CancellationToken ct); [Command("UPDATE Orders SET Amount = @total_Amount, Currency = @total_Currency WHERE Id = @id", Kind = CommandKind.NonQuery)] public partial Task<int> UpdateTotalAsync(int id, Money total, CancellationToken ct); }Composite method parameters bind via positional unpacking:
Money total→@total_Amount+@total_Currency. Naming uses the parameter name +_+ ctor-parameter name; the SQL author picks how the columns line up.Nullable composites (all-or-nothing) —
Money? Totalmeans "all composite columns NULL →null; any composite column NULL while others have values →ZeroAllocOrmMaterializationExceptionat runtime." Compile-time warning ZAO050 fires on each nullable-composite materialization site to flag that the partial-null case is undetectable at compile time and is by-design a runtime throw.[Materialize(Factory = "...")]— explicit factory resolution for cases where the SQL shape doesn't match the C# ctor. The namedstaticfactory's parameter list maps to columns by name (positional fallback when SQL column names aren't available). Canonical use case: Sqlite storesdecimalas TEXT, so route through aMoney.FromText(string amountText, string currency)factory.[Materialize(Factory = nameof(FromText))] public readonly record struct Money(decimal Amount, string Currency) { public static Money FromText(string Amount, string Currency) => new(decimal.Parse(Amount, CultureInfo.InvariantCulture), Currency); }Diagnostics: ZAO043 if the named factory doesn't exist; ZAO044 if discovery is ambiguous; ZAO051 if the factory's parameter list cannot be reconciled with the available columns.
New diagnostics ZAO050 / ZAO051 / ZAO052 / ZAO063 — composite + factory + sproc-batch guardrails. ZAO050 (nullable-composite partial-null runtime-only check, see above). ZAO051 (factory parameter list unresolved). ZAO052 (recursive composite — a composite ctor parameter that is itself another composite — explicitly deferred to v0.6+ with a clear error). ZAO063 (informational:
[StoredProcedure(Batch = ...)]with a non-default value is silently ignored — sprocs encapsulate their own batching semantics).
Deferred past v1.0 (see Roadmap beyond v1.0 below): recursive composites (ZAO052 flags them today, v0.5-CLN3); nullable reference-type composite parameter binding (v0.5-CLN2); TVPs / array parameters / SqlBulkCopy (v2 scope); runtime provider routing of identity suffixes beyond Sqlite (v2); SQL Server integration fixture (gated on adopter demand); ZA.Telemetry collision smoke (v0.6-CLN1, blocked on upstream nullable-annotation fix); Postgres benchmark numbers (v1.0-CLN1, gated on Docker availability during the capture window).
Added in v0.6
Postgres integration fixture (Testcontainers) —
tests/ZeroAlloc.ORM.Integration.Tests/Postgres/runs the full integration matrix (FlatRow, multi-result-set with realIAsyncDbBatch, streaming, stored procedures withINOUT/OUTparams,[Materialize(Factory)]againstNUMERICcolumns, composites) against a real Postgres 16 container. Resolves the accumulated v0.3/v0.4/v0.5 deferrals: the runtimeIAsyncDbBatchbranch (v0.3-CLN3), stored-procedure round-trips (v0.4 placeholder), andMoney.FromStorageagainst a real decimal provider (v0.5).Diagnostics catalog audit — every shipping ZAO code now has a dedicated reference page under
docs/diagnostics/with trigger, fix recipe, code example, and related codes. A newDiagnosticHelpLinkTestssuite enforces that everyDiagnosticDescriptor.HelpLinkUriresolves to a real, non-empty markdown file — broken links can't be shipped. Positive/negative test pairs backfilled for ZAO001 and ZAO043. The catalog table (below) is the canonical adopter-facing index.ZA.Telemetry observability cookbook recipe —
docs/cookbook/observability.mdshows the composition pattern at the consumer seam: apartial class OrderRepositoryannotated with both[Query](ZA.ORM) and[Instrument](ZA.Telemetry), with the two generators emitting independently. ZA.ORM ships no built-inActivitySource— observability lives at the adopter boundary so the package graph stays minimal and consumers pick their own tracing stack. Collision smoke deferred to v0.6-CLN1 (blocked on upstream nullable-annotation fix in ZA.Telemetry'sInstrumentGenerator).v0.3-CLN1 perf cleanup — GetOrdinal hoisted once per column — every column-name materialization path (DomainEntity, FlatRow column-name fallback, nullable composite) now emits
var __o_<Col> = __reader.GetOrdinal("<Col>");ONCE before the materialization body and reuses the local in both theIsDBNullandGetXxxcalls. Eliminates the double-lookup in the hot row-materialization loop.v0.5-CLN5 fix — PR-title lint workflow —
.github/workflows/pr-title-lint.ymlenforces conventional-commit prefixes (feat:,fix:,perf:,refactor:,docs:,test:,ci:,chore:, ...) on every PR. Prevents the v0.5 release CHANGELOG hole wherefeat:-less merges silently dropped from release-please's commit aggregation.
Added in v0.7
BenchmarkDotNet suite —
tests/ZeroAlloc.ORM.Benchmarks/ships a comparative micro-benchmark harness with 4 workloads (single-row read, multi-row read, head + lines multi-result, insert) × 3 baselines (hand-written ADO.NET, Dapper.AOT, ZeroAlloc.ORM) × 2 backends (Sqlite in-memory and Postgres via Testcontainers). First Sqlite capture (Windows 11, .NET 10.0.300) lives indocs/benchmarks/v0.7.0-sqlite-results.md: ZA.ORM sits within 5% of hand-written ADO.NET on single-row reads and matches its allocation profile to ~0.5% on 1000-row reads; multi-result-set has a 30% gap that's the next v1.0+ target.ZA.Rest collision smoke (v1.0 release gate) —
tests/ZeroAlloc.ORM.GeneratorCollision.AotSmoke/composes[Query](ZA.ORM) and[Route]/[Query](ZA.Rest) in a single AOT-publishable consumer. Wired into.github/workflows/collision-smoke.yml, so every PR proves the two source generators co-exist and the resulting binary AOT-publishes cleanly. Discovery during Phase B: both libraries ship aQueryAttribute; the collision is resolved cleanly via file-scopedusingaliases at the call site. This is the v1.0 release gate — if collision-smoke ever breaks, v1.0 doesn't ship.README + Quick Start polish (Phase C) — packages table now carries an AOT compatibility column (every shipping package marked ✅), four canonical Quick Start snippets at the top of the doc (single-row read, streaming, insert-returning-identity, stored procedure with output params), and a dedicated NativeAOT compatibility section calling out the AOT smoke + collision smoke gates and pointing at the benchmark suite for performance numbers.
v1.0 public-API surface freeze —
Microsoft.CodeAnalysis.PublicApiAnalyzersis wired acrossZeroAlloc.ORM,ZeroAlloc.ORM.Abstractions, andZeroAlloc.TypeConversions, withPublicAPI.Shipped.txtbaselined at 103 entries across 16 public types. Any accidental addition / change / removal of a public member now breaksdotnet build. The surface lock holds until v1.0 ships, and any v1.x evolution must go through the additivePublicAPI.Unshipped.txtpath with explicit reviewer sign-off.
Added in v1.0
Cookbook completion (8 recipes) — the design Section 5 cookbook target hits all eight adopter-facing pages:
flat-row.md(positional-record single-row reads — promoted from coverage scattered acrossmulti-result-set.md+commands.md) andprovider-quirks.md(Sqlite / PostgreSQL / SQL Server / MySQL differences consolidated in one place) join the six recipes shipped across v0.3-v0.6 (multi-result-set.md,streaming.md,commands.md,stored-procedures.md,composites.md,observability.md). All eight audited for compile-clean code samples + cross-links to diagnostics.Rendered docs at orm.zeroalloc.net — Docusaurus app wired through the shared ZeroAlloc.Website monorepo (canonical per-package pattern:
apps/docs-orm/+repos/orm/). Cookbook, diagnostics catalog, benchmarks, and design doc all surface there. Go-live coupled to ZA.Website PR #25 (currently blocked on ZA.Website-side ruleset config; tracked as v1.0-CLN2).Polish CLNs (4 selected, low-risk) — final pre-freeze sweep:
- ZAO064 (Info) —
[StoredProcedure(Batch = …)]set to anything other thanBatchMode.Neveris silently ignored by the sproc emit path. The new info diagnostic surfaces this so adopters don't reach for it expecting behaviour change. Resolves v0.4-CLN5. - ZAO062 per-element span — when a stored procedure has multiple typo'd named-tuple output fields, ZAO062 now anchors at each offending tuple-element's own syntax span (one squiggle per typo) rather than the whole tuple-return type. Resolves v0.4-CLN6.
- ZAO050 per-position firing — when a row materializes two
Money?fields, ZAO050 fires once per nullable-composite position rather than once per method site. Audit revealed v0.5 Phase C already emitted per-position; v1.0 Phase C added regression tests to pin the contract. Resolves v0.5-CLN1. - Benchmark async-parity —
MultiRowReadBench.Dapper_AOTaudited and confirmed already onQueryAsync<T>(not the syncQuery<T>). The original v0.7-CLN2 concern was filed against an incorrect source reading; the comparison is genuinely async-async. Numbers re-captured indocs/benchmarks/v0.7.0-sqlite-results.mdwithout the Caveat block. Resolves v0.7-CLN2.
- ZAO064 (Info) —
Added in v1.1
MigrationRunnerfor embedded SQL migrations — versioned, idempotent, multi-instance-safe SQL apply. Embed.sqlfiles in your assembly (Migrations/001_initial.sql,Migrations/002_add_orders.sql, ...), instantiateMigrationRunner(connection, source, dialect), callRunAsync(ct)at startup. The runner tracks applied versions in__zaorm_migrations, skips already-applied migrations on re-run, and applies each pending migration in its own transaction. Mid-apply failure rolls back the failing migration only — earlier migrations stay committed, the runner throws, the adopter writes a forward-fix and re-runs. Seedocs/cookbook/migrations.md.var conn = new NpgsqlConnection(connString).AsAsync(); await conn.OpenAsync(ct); var source = new EmbeddedResourceMigrationSource(typeof(Program).Assembly); var dialect = new PostgresMigrationDialect(); var runner = new MigrationRunner(conn, source, dialect); var applied = await runner.RunAsync(ct); logger.LogInformation("Applied {Count} migrations", applied.Count);Sqlite + Postgres dialects ship in v1.1 —
SqliteMigrationDialect(single-writer model, no explicit lock) andPostgresMigrationDialect(usespg_advisory_lock(<constant>)to serialize multi-instance API startup). Adopters can subclassPostgresMigrationDialectwith a custom lock key. SQL Server + MySQL dialects are out-of-scope for v1.1 — tracked as v1.1-CLN1.Unblocks ZA.Templates' EF Core → ZA.ORM swap —
ZeroAlloc.Templates' one-shotschema.sqlbootstrap (ApplyEmbeddedSchemaAsync) becomes the v1.1MigrationRunnerfor versioned, idempotent, multi-instance-safe schema apply. Templates work lives in the separateZeroAlloc.Templatesrepo.
NativeAOT compatibility
ZeroAlloc.ORM is fully NativeAOT-compatible by design:
- Zero reflection at runtime. Source-generator-based emit produces compile-time-known materialization code; no
Activator.CreateInstance, noType.GetMethod, noExpression.Compile. - Globally-qualified type references in every emitted line — AOT publishing trims correctly regardless of consumer
usingdirectives. - Trimming-safe. No
[DynamicallyAccessedMembers]requirements on consumer types. - CI gated. Every PR runs:
tests/ZeroAlloc.ORM.AotSmoke/— single-generator AOT publish.tests/ZeroAlloc.ORM.GeneratorCollision.AotSmoke/— composition withZeroAlloc.Rest.Generator(the v1.0 release gate).
- Performance. ZA.ORM is within 5% of hand-written ADO.NET on single-row reads and matches its allocation profile on multi-row reads; see docs/benchmarks/v0.7.0-sqlite-results.md for the full comparison against hand-written ADO.NET and Dapper.AOT.
Diagnostics catalog
ZeroAlloc.ORM ships a structured catalog of compile-time diagnostics. Every code has a dedicated reference page in docs/diagnostics/ — the IDE help link on each diagnostic resolves to its page directly.
| Code | Severity | Trigger | Link |
|---|---|---|---|
| ZAO001 | Error | Annotated method must be partial | ZAO001 |
| ZAO002 | Error | Unsupported return type | ZAO002 |
| ZAO003 | Error | No IAsyncDbConnection found on containing type |
ZAO003 |
| ZAO004 | Error | Containing type must be partial | ZAO004 |
| ZAO005 | Error | Multiple ORM attributes on one method | ZAO005 |
| ZAO006 | Warning | Method has multiple CancellationToken parameters |
ZAO006 |
| ZAO007 | Error | IAsyncEnumerable<T> return without [EnumeratorCancellation] |
ZAO007 |
| ZAO008 | Error | Multi-statement SQL with single-result return type | ZAO008 |
| ZAO009 | Warning | Redundant async keyword on generated partial |
ZAO009 |
| ZAO020 | Info | [Query](FromResource = true) not yet implemented |
ZAO020 |
| ZAO022 | Info | Return type shape not yet supported | ZAO022 |
| ZAO032 | Error | Tuple arity exceeds SQL statement count | ZAO032 |
| ZAO033 | Error | SQL statement count exceeds tuple arity | ZAO033 |
| ZAO040 | Error | No construction strategy resolved for type | ZAO040 |
| ZAO041 | Error | No binding strategy resolved for parameter | ZAO041 |
| ZAO042 | Error | [StoreAsString] requires an enum type |
ZAO042 |
| ZAO043 | Error | [Materialize(Factory)] references missing method |
ZAO043 |
| ZAO044 | Error | Ambiguous convention discovery | ZAO044 |
| ZAO050 | Warning | Nullable composite type requires runtime all-or-nothing check | ZAO050 |
| ZAO051 | Error | Factory parameter does not match any SELECT column | ZAO051 |
| ZAO052 | Error | Recursive composite types are not supported | ZAO052 |
| ZAO060 | Error | [StoredProcedure] async method has out/ref parameter (reserved) |
ZAO060 |
| ZAO061 | Error | [StoredProcedure] name is empty |
ZAO061 |
| ZAO062 | Warning | Named-tuple field does not match any parameter | ZAO062 |
| ZAO063 | Error | [Param(Name = ...)] override is not supported on composite parameters |
ZAO063 |
| ZAO064 | Info | [StoredProcedure(Batch = ...)] non-default value is ignored |
ZAO064 |
A unit test (DiagnosticHelpLinkTests) enforces that every DiagnosticDescriptor.HelpLinkUri resolves to a real, non-empty markdown page under docs/diagnostics/ — broken links can't be shipped.
Documentation
Rendered docs (cookbook, diagnostics catalog, benchmarks, design doc) live at orm.zeroalloc.net — built from this repo's docs/ directory by the ZA ecosystem website at ZeroAlloc-Net/ZeroAlloc.Website.
Cookbook recipes
Adopter-facing recipes for the eight canonical patterns shipped in v1.0. Each page is paste-into-fresh-project quality with provider notes and diagnostics cross-links.
| Recipe | Description |
|---|---|
| flat-row.md | Single-row reads → positional record (Task<T?> / Task<T>). |
| multi-result-set.md | (head, lines) tuple returns; BatchMode.Auto / Always / Never. |
| streaming.md | IAsyncEnumerable<T> for unbounded result sets. |
| commands.md | [Command] — NonQuery / Scalar / Identity. |
| stored-procedures.md | [StoredProcedure] with output parameters + multi-result-set sprocs. |
| composites.md | Multi-column composites (Money) + [Materialize(Factory)]. |
| observability.md | ZA.Telemetry composition ([Instrument] + [Trace] + [Count] + [Histogram]). |
| provider-quirks.md | Sqlite / PostgreSQL / SQL Server / MySQL differences. |
| migrations.md | SQL migrations via MigrationRunner (versioned, idempotent, multi-instance safe). |
Design + roadmap
- Design:
docs/design/2026-05-30-v1.0-design.md— 5-section v1.0 design covering architecture, generator surface, convention discovery, diagnostics, test strategy, milestones. - Backlog:
docs/plans/za-orm-backlog.md— priority-banded task list. New findings during implementation get appended. - Ecosystem context: sits alongside ZeroAlloc.Mediator, ZeroAlloc.Mapping, ZeroAlloc.ValueObjects, ZeroAlloc.Validation. Substrate is AdoNet.Async (AOT-compatible since v1.x).
Roadmap beyond v1.0
With v1.0 shipped, the project enters maintenance mode under SemVer. The v1.0 public surface (103 entries / 16 types) is locked; v1.x releases are additive-only (v1.1 added the ZeroAlloc.ORM.Migrations namespace) and any breaking change pushes to v2.0.
Carry-forward backlog (post-1.0 polish, none of which blocks adopter use today):
- v0.3-CLN2 — Lift the keeper-connection / shared-cache helper into
SqliteFixture. Single-test pattern today; promote once a second adopter lands. - v0.4-CLN1 — Investigate single-pipeline architecture across
[Query]/[Command]/[StoredProcedure](currentCollect()+SelectManyunion step collapses incremental-cache granularity). - v0.5-CLN2 — Nullable reference-type composite parameter binding (struct case ships in v0.5; reference-type case routes through ZAO041 today).
- v0.5-CLN3 — Recursive composite support (ZAO052 explicitly flags them; generalize the classifier + column-index walk).
- v0.5-CLN4 — Factory parameter-to-column SQL-parser-based name matching (gated on ORM-V2-3).
- v0.6-CLN1 — Re-attempt ZA.Telemetry collision smoke once upstream
InstrumentGeneratorpreserves nullable annotations. - v0.7-CLN1 (Postgres portion) — Capture Postgres BDN numbers once a Docker-reachable machine is available. Sqlite portion shipped.
- v1.0-CLN1 — Capture Postgres benchmark numbers (Docker daemon was not available during the v1.0 capture window).
- v1.0-CLN2 — ZA.Website ruleset /
orm.zeroalloc.netgo-live (tracked in the ZeroAlloc.Website repo, PR #25). - v1.1-CLN1 — SQL Server + MySQL migration dialects (Sqlite + Postgres shipped in v1.1; remaining providers gated on adopter demand).
- v1.1-CLN2 — Migration rollback support (adopter writes forward-fix migrations today).
- v1.1-CLN3 — C# migration DSL (raw SQL is the v1.1 contract).
- v1.1-CLN4 — Generator-emitted migrations from
[Materialize]shape changes (speculative).
v2.0 design space sketched in docs/design/2026-05-30-v1.0-design.md Section 5 (lines 656+):
- Composites with arbitrary recursive nesting.
- Table-valued parameters (SQL Server
READONLYtypes, Postgres array paramsint[]/text[]). SqlBulkCopysemantics + provider equivalents.- Runtime provider routing of identity suffixes beyond Sqlite (SQL Server
SCOPE_IDENTITY()/ PostgresRETURNING). - Schema-drift detection (opt-in analyzer with DB connection at compile time).
- Broader primitive catalog (
BigInteger,Half,Int128/UInt128).
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- AdoNet.Async (>= 1.3.0)
- AdoNet.Async.Adapters (>= 1.3.0)
- ZeroAlloc.ORM.Abstractions (>= 1.5.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.