ZeroAlloc.ORM.Abstractions
0.4.0
See the version list below for details.
dotnet add package ZeroAlloc.ORM.Abstractions --version 0.4.0
NuGet\Install-Package ZeroAlloc.ORM.Abstractions -Version 0.4.0
<PackageReference Include="ZeroAlloc.ORM.Abstractions" Version="0.4.0" />
<PackageVersion Include="ZeroAlloc.ORM.Abstractions" Version="0.4.0" />
<PackageReference Include="ZeroAlloc.ORM.Abstractions" />
paket add ZeroAlloc.ORM.Abstractions --version 0.4.0
#r "nuget: ZeroAlloc.ORM.Abstractions, 0.4.0"
#:package ZeroAlloc.ORM.Abstractions@0.4.0
#addin nuget:?package=ZeroAlloc.ORM.Abstractions&version=0.4.0
#tool nuget:?package=ZeroAlloc.ORM.Abstractions&version=0.4.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: Pre-release. v0.4 shipped (
[Command]+[StoredProcedure]+ named-tuple output parameters). Authoritative design lives atdocs/design/2026-05-30-v1.0-design.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
| Package | Description | NativeAOT |
|---|---|---|
| ZeroAlloc.ORM | Runtime helpers + ActivitySource for observability. Depends on AdoNet.Async. |
✅ |
| ZeroAlloc.ORM.Abstractions | Public attribute surface ([Query], [Command], [StoredProcedure], [Param], [StoreAsString]) + exception types. [Materialize] lands in v0.5. |
✅ |
| ZeroAlloc.ORM.Generator | Roslyn incremental source generator. Build-time only. | N/A |
| ZeroAlloc.TypeConversions | Shared convention-discovery catalog (value-objects, enums, composites). Build-time only. | N/A |
Quick Start
Scalar query
using System.Data.Async;
using System.Threading;
using System.Threading.Tasks;
using ZeroAlloc.ORM;
public sealed partial class Repo(IAsyncDbConnection connection)
{
[Query("SELECT count(*) FROM Orders")]
public partial Task<int> CountOrdersAsync(CancellationToken ct);
}
The source generator emits the open / execute / close pipeline against AdoNet.Async's IAsyncDbConnection. Zero runtime reflection; the emit composes through global::-qualified identifiers so it doesn't care about the consumer's using directives.
Row materialization (FlatRow)
public sealed record OrderRow(int Id, int CustomerId, decimal Total);
public sealed partial class OrderRepo(IAsyncDbConnection connection)
{
[Query("SELECT Id, CustomerId, Total FROM Orders WHERE Id = @id")]
public partial Task<OrderRow?> GetByIdAsync(int id, CancellationToken ct);
}
Positional record + matching SELECT column order = no mapping config. Nullable return = empty result set yields null.
Available 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),Scalarreturns the first column of the first row (any materialization-eligible type),Identityreturns the inserted identity value through provider-aware suffixes (SCOPE_IDENTITY()for SQL Server,LAST_INSERT_ROWID()for Sqlite,RETURNINGfor Postgres). 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)", 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.
Deferred to later milestones: composite materialization e.g. Money(decimal, string) (v0.5), [Materialize(Factory)] factory routing (v0.5), provider routing of identity suffixes beyond Sqlite (v2), ActivitySource / built-in observability (v0.6 via ZA.Telemetry composition), TVPs / array parameters / SqlBulkCopy (out of v1.0 scope), stored-procedure integration tests (v0.6 Postgres fixture).
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).
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
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on ZeroAlloc.ORM.Abstractions:
| Package | Downloads |
|---|---|
|
ZeroAlloc.ORM
Source-generator-based, NativeAOT-clean raw-SQL data access for .NET. Annotate partial methods with [Query]; the generator emits typed parameter binding + materialization against AdoNet.Async's IAsyncDbConnection. Zero runtime reflection. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.6.0 | 581 | 6/4/2026 |
| 1.5.0 | 618 | 6/3/2026 |
| 1.4.0 | 113 | 6/3/2026 |
| 1.3.0 | 105 | 6/3/2026 |
| 1.2.0 | 330 | 6/2/2026 |
| 1.1.0 | 190 | 6/1/2026 |
| 1.0.0 | 116 | 6/1/2026 |
| 0.7.0 | 115 | 6/1/2026 |
| 0.6.0 | 114 | 6/1/2026 |
| 0.5.0 | 118 | 5/31/2026 |
| 0.4.0 | 122 | 5/31/2026 |
| 0.3.0 | 108 | 5/31/2026 |
| 0.2.0 | 115 | 5/30/2026 |
| 0.1.0 | 113 | 5/30/2026 |