ZeroAlloc.ORM.Abstractions 2.3.2

There is a newer version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ZeroAlloc.ORM.Abstractions" Version="2.3.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ZeroAlloc.ORM.Abstractions" Version="2.3.2" />
                    
Directory.Packages.props
<PackageReference Include="ZeroAlloc.ORM.Abstractions" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ZeroAlloc.ORM.Abstractions --version 2.3.2
                    
#r "nuget: ZeroAlloc.ORM.Abstractions, 2.3.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ZeroAlloc.ORM.Abstractions@2.3.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ZeroAlloc.ORM.Abstractions&version=2.3.2
                    
Install as a Cake Addin
#tool nuget:?package=ZeroAlloc.ORM.Abstractions&version=2.3.2
                    
Install as a Cake Tool

ZeroAlloc.ORM

NuGet Build License: MIT AOT GitHub Sponsors

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 / ReadAsync calls.

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>); every Task<T> shape also has a ValueTask<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, uses IAsyncDbBatch when the provider supports it and falls back to one command with NextResultAsync. 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 static From factory) bind through their primitive.
  • Multi-column composites such as Money(decimal Amount, string Currency) unpack into several columns; a parameter Money total binds @total_Amount and @total_Currency. Composites of composites are not supported yet (ZAO052).
  • [Param] overrides the SQL-side Name and sets DbType, Size, Precision, Scale and Direction.

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 storing decimal as text.
  • A NULL in a non-nullable column throws ZeroAllocOrmMaterializationException.

Commands

  • CommandKind.NonQuery returns rows affected, Scalar returns the first column of the first row, Identity returns the inserted identity, and BulkInsert emits chunked multi-row INSERT ... VALUES returning 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 RETURN values.

Transactions and dialects

  • An IAsyncDbTransaction parameter 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 unscaled decimal output 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.GetMethod or Expression.Compile.
  • Globally-qualified type references in every emitted line, so trimming is correct regardless of the consumer's using directives, and no [DynamicallyAccessedMembers] requirements on your types.
  • CI-gated on every PR:
    • aot-smoke.yml publishes tests/ZeroAlloc.ORM.AotSmoke with PublishAot=true and runs it.
    • collision-smoke.yml AOT-publishes two consumers that combine ZA.ORM with another source generator: tests/ZeroAlloc.ORM.GeneratorCollision.AotSmoke with ZeroAlloc.Rest, whose [Query] attribute shares the name and is disambiguated with a file-scoped using alias, and tests/ZeroAlloc.ORM.TelemetryCollision.AotSmoke with 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 the IMigrationDialect change.
  • 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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
Loading failed