ZeroAlloc.ORM.Abstractions
2.3.2
See the version list below for details.
dotnet add package ZeroAlloc.ORM.Abstractions --version 2.3.2
NuGet\Install-Package ZeroAlloc.ORM.Abstractions -Version 2.3.2
<PackageReference Include="ZeroAlloc.ORM.Abstractions" Version="2.3.2" />
<PackageVersion Include="ZeroAlloc.ORM.Abstractions" Version="2.3.2" />
<PackageReference Include="ZeroAlloc.ORM.Abstractions" />
paket add ZeroAlloc.ORM.Abstractions --version 2.3.2
#r "nuget: ZeroAlloc.ORM.Abstractions, 2.3.2"
#:package ZeroAlloc.ORM.Abstractions@2.3.2
#addin nuget:?package=ZeroAlloc.ORM.Abstractions&version=2.3.2
#tool nuget:?package=ZeroAlloc.ORM.Abstractions&version=2.3.2
ZeroAlloc.ORM
ZeroAlloc.ORM is source-generated, NativeAOT-clean raw-SQL data access for .NET 10. You write the SQL in a [Query], [Command] or [StoredProcedure] attribute on a partial method; the Roslyn generator emits the typed parameter binding and row materialization against AdoNet.Async's IAsyncDbConnection. There is no runtime reflection, no expression compilation and no LINQ provider.
It sits between two choices adopters usually face:
- EF Core — a full LINQ ORM, but its precompiled-queries pipeline collides with co-resident source generators such as ZeroAlloc.Rest, blocking NativeAOT publish in stacks like ZeroAlloc.Templates.
- Hand-written ADO.NET — works under AOT, but every repository becomes a tower of
CreateCommand/CreateParameter/ReadAsynccalls.
ZeroAlloc.ORM keeps the SQL yours and generates the ADO.NET plumbing.
Install
dotnet add package ZeroAlloc.ORM
dotnet add package ZeroAlloc.ORM.Generator
Reference the generator as a build-time analyzer so it does not flow to downstream projects:
<PackageReference Include="ZeroAlloc.ORM.Generator" Version="2.*" PrivateAssets="all" />
Then add your ADO.NET provider, such as Microsoft.Data.Sqlite, Npgsql or Microsoft.Data.SqlClient, and wrap its connection with AdoNet.Async's AsAsync().
Packages
| Package | Description |
|---|---|
ZeroAlloc.ORM |
Runtime: MigrationRunner with the SQLite, PostgreSQL and SQL Server migration dialects, and the ZeroAllocOrm*Exception types. Depends on ZeroAlloc.ORM.Abstractions and AdoNet.Async. |
ZeroAlloc.ORM.Abstractions |
The attributes and enums you annotate with: [Query], [Command], [StoredProcedure], [Param], [Materialize], [StoreAsString], [Dialect], CommandKind, BatchMode, SqlDialect. |
ZeroAlloc.ORM.Generator |
Roslyn incremental source generator and the ZAO diagnostics. Build-time only; bundles ZeroAlloc.TypeConversions. |
ZeroAlloc.TypeConversions |
Build-time type-conversion and convention-discovery catalog used by the generator. You do not reference it directly. |
The runtime packages target net10.0 and set IsAotCompatible. The generator and TypeConversions target netstandard2.0 and only run inside the compiler.
Quick start
Every repository is a partial type that exposes an IAsyncDbConnection, usually through its primary constructor:
using System.Collections.Generic;
using System.Data.Async;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using ZeroAlloc.ORM;
public sealed record Customer(int Id, string Name);
public sealed record OrderRow(int Id, int CustomerId, decimal Total);
public sealed record NewOrder(int CustomerId, decimal Total);
public sealed partial class OrderRepository(IAsyncDbConnection connection)
{
// Single row; a nullable return yields null on an empty result set.
[Query("SELECT Id, Name FROM Customers WHERE Id = @id")]
public partial Task<Customer?> GetCustomerAsync(int id, CancellationToken ct);
// Many rows, materialized eagerly. List<T>, IList<T> and IReadOnlyList<T> all work.
[Query("SELECT Id, CustomerId, Total FROM Orders WHERE CustomerId = @customerId")]
public partial Task<IReadOnlyList<OrderRow>> GetOrdersAsync(int customerId, CancellationToken ct);
// Streaming: the connection opens on the first MoveNextAsync and closes on DisposeAsync.
[Query("SELECT Id, CustomerId, Total FROM Orders ORDER BY Id")]
public partial IAsyncEnumerable<OrderRow> StreamOrdersAsync([EnumeratorCancellation] CancellationToken ct);
// Insert returning the identity; the SQL carries the provider's identity clause.
[Command("INSERT INTO Orders (CustomerId, Total) VALUES (@customerId, @total) RETURNING Id",
Kind = CommandKind.Identity)]
public partial Task<int> InsertOrderAsync(int customerId, decimal total, CancellationToken ct);
// Join a caller's transaction by declaring an IAsyncDbTransaction parameter.
[Command("UPDATE Orders SET Total = @total WHERE Id = @id")]
public partial Task<int> UpdateTotalAsync(int id, decimal total, IAsyncDbTransaction tx, CancellationToken ct);
// Chunked multi-row INSERT; @CustomerId and @Total are NewOrder properties.
[Command("INSERT INTO Orders (CustomerId, Total) VALUES (@CustomerId, @Total)",
Kind = CommandKind.BulkInsert)]
public partial Task<int> InsertOrdersAsync(IReadOnlyList<NewOrder> orders, CancellationToken ct);
// Stored procedure with an output parameter: the NewOrderId tuple field matches the
// newOrderId parameter, so that parameter binds as ParameterDirection.Output and its value
// comes back in the tuple. Result matches no parameter, so it is the result-set row.
[StoredProcedure("usp_InsertOrder")]
public partial Task<(OrderRow Result, int NewOrderId)> InsertViaProcedureAsync(
int customerId, int newOrderId, CancellationToken ct);
}
Every Task<T> return above can be declared as ValueTask<T> instead; a call that completes synchronously then allocates no Task.
The argument passed for an output parameter is not sent. Mark it [Param(Direction = ParameterDirection.InputOutput)] when the procedure reads its initial value, or ParameterDirection.ReturnValue to read a SQL Server RETURN value. See stored-procedures.md.
Features
Return shapes
- Scalars (
Task<int>,Task<T?>), single rows (Task<TRow?>), lists (List<T>,IList<T>,IReadOnlyList<T>) and streams (IAsyncEnumerable<T>); everyTask<T>shape also has aValueTask<T>form. - Multi-result-set tuples, such as
Task<(OrderRow Head, IReadOnlyList<OrderLineRow> Lines)?>, map one tuple element to each;-separated statement.[Query(Batch = BatchMode.Auto)], the default, usesIAsyncDbBatchwhen the provider supports it and falls back to one command withNextResultAsync. Tuple/statement arity mismatches are build errors (ZAO032, ZAO033).
Parameters and columns
- Primitives:
int,long,short,byte,bool,decimal,double,float,string,Guid,DateTime,DateTimeOffset,TimeSpan,DateOnly,TimeOnly,byte[], and their nullable forms. - Enums round-trip as their underlying integer, or as the member name with
[StoreAsString]. - Value objects (
[ValueObject]from ZeroAlloc.ValueObjects, or any type with a single-argument constructor or staticFromfactory) bind through their primitive. - Multi-column composites such as
Money(decimal Amount, string Currency)unpack into several columns; a parameterMoney totalbinds@total_Amountand@total_Currency. Composites of composites are not supported yet (ZAO052). [Param]overrides the SQL-sideNameand setsDbType,Size,Precision,ScaleandDirection.
Materialization
- Positional records materialize by column position. Classes with one public multi-argument constructor materialize by column name, so SELECT order does not matter.
[Materialize(Factory = nameof(...))]routes rows through a static factory when the SQL shape differs from the constructor, for example SQLite storingdecimalas text.- A
NULLin a non-nullable column throwsZeroAllocOrmMaterializationException.
Commands
CommandKind.NonQueryreturns rows affected,Scalarreturns the first column of the first row,Identityreturns the inserted identity, andBulkInsertemits chunked multi-rowINSERT ... VALUESreturning the summed row count or the inserted identities.
Stored procedures
- Result rows, lists and multi-result sets, output and input-output parameters through named tuple fields, and SQL Server
RETURNvalues.
Transactions and dialects
- An
IAsyncDbTransactionparameter enlists the command in the caller's transaction (ZAO080 warns on more than one). [Dialect(SqlDialect.SqlServer | PostgreSql | Sqlite | MySql)]on a repository or the assembly enables provider-specific compile-time checks: ZAO015 for a[Param(DbType)]the provider rejects, ZAO065 for an unscaleddecimaloutput on SQL Server, and ZAO069 for a return value off SQL Server. See provider-quirks.md.
Migrations
MigrationRunner applies embedded .sql migrations in order, each in its own transaction, and records them in __zaorm_migrations per source, so an application and a library can each number from 1 in one database. It ships with SqliteMigrationDialect, PostgresMigrationDialect (pg_advisory_lock) and SqlServerMigrationDialect (sp_getapplock); the last two make concurrent startup across instances safe.
using Npgsql;
using System.Data.Async;
using System.Data.Async.Adapters;
using ZeroAlloc.ORM.Migrations;
IAsyncDbConnection conn = new NpgsqlConnection(connString).AsAsync();
await conn.OpenAsync(ct);
var runner = new MigrationRunner(
conn,
new EmbeddedResourceMigrationSource(typeof(Program).Assembly),
new PostgresMigrationDialect()); // or SqliteMigrationDialect / SqlServerMigrationDialect
IReadOnlyList<Migration> applied = await runner.RunAsync(ct);
PostgresMigrationDialect(lockKey) and SqlServerMigrationDialect(lockName) take a custom lock key or resource name. See migrations.md.
NativeAOT compatibility
- No runtime reflection. The generator emits compile-time-known binding and materialization code; no
Activator.CreateInstance,Type.GetMethodorExpression.Compile. - Globally-qualified type references in every emitted line, so trimming is correct regardless of the consumer's
usingdirectives, and no[DynamicallyAccessedMembers]requirements on your types. - CI-gated on every PR:
aot-smoke.ymlpublishestests/ZeroAlloc.ORM.AotSmokewithPublishAot=trueand runs it.collision-smoke.ymlAOT-publishes two consumers that combine ZA.ORM with another source generator:tests/ZeroAlloc.ORM.GeneratorCollision.AotSmokewith ZeroAlloc.Rest, whose[Query]attribute shares the name and is disambiguated with a file-scopedusingalias, andtests/ZeroAlloc.ORM.TelemetryCollision.AotSmokewith ZeroAlloc.Telemetry's[Instrument]proxy wrapping a ZA.ORM repository.
Performance
BenchmarkDotNet compares ZA.ORM with hand-written ADO.NET and Dapper.AOT, captured at 2.3.1 on 2026-10-15. Allocations are exact. Speed is shown as same-run ratios, because the benchmark host is a shared VM; absolute timings are not published. Time is BenchmarkDotNet's Ratio against hand-written ADO.NET, averaged over three launches.
| Workload | SQLite: time | SQLite: allocation | PostgreSQL: time | PostgreSQL: allocation |
|---|---|---|---|---|
| Single-row read | 1.10× | 1,568 B vs 1,232 B, +27% | 1.07× ± 0.32 | +19% |
| 1000-row read | 0.99× | 88,592 B vs 88,200 B, +0.4% | 1.23× ± 0.36 | +1% |
| Head + lines multi-result | 1.04× | 3,400 B vs 2,680 B, +27% | 0.87× ± 0.28 | +52% |
| Single insert | 1.17× | 1,856 B vs 1,208 B, +54% | 1.11× ± 0.38 | +6% |
PostgreSQL time ratios carry BenchmarkDotNet's RatioSD, because every query is a round trip to a server sharing the same VM; treat anything within it as parity. Full tables, per-launch spreads and methodology: SQLite and PostgreSQL.
Diagnostics
The generator reports signature, binding, materialization, stored-procedure, BulkInsert and transaction problems at build time, from ZAO001 to ZAO081. Every code has a page under docs/diagnostics/, and the IDE help link on each diagnostic opens it. The canonical index of every shipped code, with severity and trigger, is docs/diagnostics/README.md. DiagnosticHelpLinkTests fails the build if a descriptor lacks its page or its index row.
Documentation
Rendered docs: orm.zeroalloc.net, built from this repo's docs/ by ZeroAlloc-Net/.website. The domain is not live yet (.website#95); until it is, read the pages below on GitHub.
| Cookbook recipe | Covers |
|---|---|
| flat-row.md | Single-row reads into positional records |
| 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 |
| bulk-insert.md | CommandKind.BulkInsert — chunked multi-row inserts and identity capture |
| stored-procedures.md | Output, input-output and return-value parameters; multi-result-set procedures |
| composites.md | Multi-column composites (Money) and [Materialize(Factory)] |
| provider-quirks.md | [Dialect] and SQLite / PostgreSQL / SQL Server / MySQL differences |
| migrations.md | MigrationRunner, the three dialects, several sources in one database |
| observability.md | Composing with ZeroAlloc.Telemetry's [Instrument] |
- Migrating to v2 — parameter names without the
@prefix,[Param(DbType)]on inputs, and theIMigrationDialectchange. - CHANGELOG — what shipped in each release.
- Design — the v1.0 architecture, generator surface and convention discovery.
Versioning
ZeroAlloc.ORM follows SemVer. Microsoft.CodeAnalysis.PublicApiAnalyzers guards the public surface of ZeroAlloc.ORM, ZeroAlloc.ORM.Abstractions and ZeroAlloc.TypeConversions, so 2.x releases are additive and a breaking change needs a new major.
Roadmap
Not supported yet, roughly in order of demand: composites of composites (ZAO052 today), SQL loaded from embedded resources (FromResource, ZAO020 today), a MySQL migration dialect, table-valued and array parameters, and provider-native bulk paths such as SqlBulkCopy and PostgreSQL COPY. The working backlog is docs/plans/za-orm-backlog.md.
License
MIT. See 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 |
|---|---|---|
| 2.4.0 | 355 | 10/18/2026 |
| 2.3.4 | 661 | 10/16/2026 |
| 2.3.3 | 304 | 10/16/2026 |
| 2.3.2 | 1,421 | 10/15/2026 |
| 2.3.1 | 1,619 | 10/12/2026 |
| 2.3.0 | 602 | 10/11/2026 |
| 2.2.1 | 194 | 10/11/2026 |
| 2.2.0 | 4,018 | 9/30/2026 |
| 2.1.1 | 1,338 | 9/28/2026 |
| 2.1.0 | 847 | 9/27/2026 |
| 2.0.1 | 704 | 9/26/2026 |
| 2.0.0 | 722 | 9/25/2026 |
| 1.7.1 | 802 | 9/22/2026 |
| 1.7.0 | 711 | 9/22/2026 |
| 1.6.6 | 500 | 9/20/2026 |
| 1.6.5 | 101 | 9/20/2026 |
| 1.6.4 | 822 | 9/14/2026 |
| 1.6.3 | 1,218 | 8/7/2026 |
| 1.6.2 | 159 | 8/7/2026 |
| 1.6.0 | 1,440 | 6/4/2026 |