ZeroAlloc.ORM.Abstractions 2.16.1

dotnet add package ZeroAlloc.ORM.Abstractions --version 2.16.1
                    
NuGet\Install-Package ZeroAlloc.ORM.Abstractions -Version 2.16.1
                    
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.16.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ZeroAlloc.ORM.Abstractions" Version="2.16.1" />
                    
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.16.1
                    
#r "nuget: ZeroAlloc.ORM.Abstractions, 2.16.1"
                    
#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.16.1
                    
#: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.16.1
                    
Install as a Cake Addin
#tool nuget:?package=ZeroAlloc.ORM.Abstractions&version=2.16.1
                    
Install as a Cake Tool

ZeroAlloc.ORM

NuGet Build codecov License: MIT AOT GitHub Sponsors

Source-generated raw-SQL data access for .NET: you write the SQL, the generator writes the ADO.NET, with no runtime reflection and NativeAOT-ready.

Why ZeroAlloc.ORM

  • Close to hand-written ADO.NET. A 1,000-row read allocates 88,592 B, 0.4% more than hand-written ADO.NET's 88,200 B, where Dapper.AOT allocates 130,001 B in the same run.
  • Ahead of Dapper.AOT on single-row reads and multi-result sets. A single-row read on SQLite takes 1.06× the time of hand-written ADO.NET; Dapper.AOT takes 1.21× in the same run.
  • Works under NativeAOT and trimming. CI publishes and runs AOT consumers on every PR, including ones that share the build with other source generators, where EF Core's precompiled queries collide.
  • Compile-time diagnostics instead of runtime surprises. A non-partial method, a tuple that does not match the SQL's statement count or a parameter with no binding is a build error, ZAO001 to ZAO082, and a LINQ chain that cannot be translated is one too, ZAO100 to ZAO113.
  • LINQ as a compile-time SQL source. With ZeroAlloc.ORM.Linq, a query's SQL can be written as a LINQ chain that the generator translates to PostgreSQL at build time; the chain never runs. The same goes for set-based updates and deletes, with optional concurrency guards, and for row locks.

Install

dotnet add package ZeroAlloc.ORM
dotnet add package ZeroAlloc.ORM.Generator

Then add your ADO.NET provider, such as Microsoft.Data.Sqlite, Npgsql or Microsoft.Data.SqlClient.

Quick start

using Microsoft.Data.Sqlite;
using System.Data.Async;
using System.Data.Async.Adapters;
using ZeroAlloc.ORM;

await using var db = new SqliteConnection("Data Source=:memory:").AsAsync();
await db.OpenAsync();

var customers = new CustomerRepository(db);
await customers.CreateTableAsync();
int id = await customers.AddAsync("Ada");
Console.WriteLine(await customers.FindAsync(id)); // Customer { Id = 1, Name = Ada }

public sealed record Customer(int Id, string Name);

public sealed partial class CustomerRepository(IAsyncDbConnection connection)
{
    [Command("CREATE TABLE Customers (Id INTEGER PRIMARY KEY, Name TEXT NOT NULL)")]
    public partial Task<int> CreateTableAsync(CancellationToken ct = default);

    [Command("INSERT INTO Customers (Name) VALUES (@name) RETURNING Id", Kind = CommandKind.Identity)]
    public partial Task<int> AddAsync(string name, CancellationToken ct = default);

    [Query("SELECT Id, Name FROM Customers WHERE Id = @id")]
    public partial Task<Customer?> FindAsync(int id, CancellationToken ct = default);
}

The generator fills in each partial method with typed parameter binding and row materialization against AdoNet.Async's IAsyncDbConnection. There is no runtime LINQ provider, no expression compilation and no runtime reflection.

LINQ queries and writes

On PostgreSQL, ZeroAlloc.ORM.Linq lets you write the SQL as LINQ instead. Each query is a partial method plus a companion that holds the chain. The generator reads the companion at build time and emits the SQL as a const string; the companion never runs, and its parameters become the SQL parameters.

dotnet add package ZeroAlloc.ORM.Linq
using System.Data.Async;
using ZeroAlloc.ORM;
using ZeroAlloc.ORM.Linq;

[TableMap(typeof(TaskEntity), "Tasks")]
[ColumnMap(nameof(TaskEntity.Kind), EnumAsString = true)]
internal static class TaskMap;

[Dialect(SqlDialect.PostgreSql)]
public sealed partial class TaskStore(IAsyncDbConnection connection)
{
    [LinqQuery(nameof(OpenTasksQuery))]
    public partial Task<List<TaskSummary>> OpenTasks(Guid projectId, WorkKind? kind, CancellationToken ct);

    // SELECT "Id", "Title" FROM "Tasks"
    // WHERE "ProjectId" = @projectId AND (@kind IS NULL OR "Kind" = @kind) ORDER BY "CreatedAt", "Id"
    private static IQuery<TaskSummary> OpenTasksQuery(Guid projectId, WorkKind? kind) =>
        Sql.Table<TaskEntity>()
            .Where(t => t.ProjectId == projectId)
            .Where(t => kind == null || t.Kind == kind)
            .OrderBy(t => t.CreatedAt)
            .ThenBy(t => t.Id)
            .Select(t => new TaskSummary(t.Id, t.Title));
}

TaskEntity is any class with the mapped properties, such as a Domain entity; columns default to the property names, as in EF Core. Filters, ordering, Skip/Take, Sql.Row keyset paging, Count, LongCount, Any, Sum, Min, Max, Average, GroupBy with aggregates and .Date, and inner Join translate today. [LinqCommand] writes rows in one statement with ExecuteUpdate and ExecuteDelete, ThrowOnNoRows with Sql.Xmin guards a write against a concurrent change, and ForUpdate, ForUpdateSkipLocked and ForUpdateNoWait lock the rows a query reads. See the LINQ recipe for every operator, result shape and diagnostic.

Table and column placeholders

A library that ships repositories cannot know the table its adopters use, so the SQL of a [Query] or [Command] can name it with a {table} placeholder. The generator binds it to a string member of the repository: a primary-constructor parameter, a field or a property with a getter.

[Dialect(SqlDialect.Sqlite)]
public sealed partial class OrderRepository(IAsyncDbConnection connection, string table, bool quoteColumns)
{
    [Query("SELECT COUNT(*) FROM {table} WHERE Status = @status")]
    public partial Task<int> CountAsync(string status, CancellationToken ct);

    [Query("SELECT {col:Id}, {col:Name} FROM {table} WHERE {col:Id} = @id")]
    public partial Task<OrderRow?> GetAsync(int id, CancellationToken ct);
}

{table} binds to table, ignoring case and one leading underscore, so _table works too. {col:Id} writes a column name. It is quoted in the style of the declared [Dialect] when the repository has a bool member named QuoteColumns that is true, and bare otherwise: "Id" for SQLite and PostgreSQL, [Id] for SQL Server, a backtick on both sides for MySQL. Without a declared dialect {col:Id} is a build error. Here the member quoteColumns carries the flag.

The rules:

  • Placeholders work in [Command] and [Query] SQL, outside quotes and comments. '{1,2}' and -- {x} are not placeholders. BulkInsert and [StoredProcedure] names do not take them (ZAO117).
  • Members are read on every call, so a changed value is honoured on the next call. A repository instance holds no state for placeholders.
  • The SQL for each distinct value is built once and cached process-wide, so a call allocates the same as a call with literal SQL.
  • A null or blank table throws ArgumentException on each call.
  • A placeholder belongs where a name belongs. Use @parameter for values; the build refuses a placeholder after =, LIKE or IN ( or in a VALUES tuple (ZAO116).

The table value is configuration, not input: it is spliced into the SQL verbatim and is never escaped or validated, so never pass user input.

The SQL for each distinct value is cached for the life of the process and never evicted, so keep the set of values small and fixed, such as one per deployment or pipeline, not one per request or per tenant.

ID Severity Fires when
ZAO115 Error A placeholder finds no string member, finds several, or QuoteColumns is not a bool.
ZAO116 Error A placeholder sits where a value belongs.
ZAO117 Error A placeholder is in BulkInsert or stored-procedure SQL.
ZAO118 Error {col:Name} is used on a repository with no declared dialect.

Upgrading. This is a behaviour change: a {identifier} or {col:identifier} in existing [Query] or [Command] SQL outside quotes and comments is now a placeholder. A {identifier} binds silently when the repository has a string member of the same name, and fails the build with ZAO115 otherwise. A {col:identifier} becomes a column name, and fails the build with ZAO118 when no dialect is declared. The scanner does not honour backslash escapes: in MySQL, or a PostgreSQL E'...' string, a \' inside a literal ends the literal for the scanner, so brace text after it can become a placeholder. Move such brace text inside a string literal that escapes quotes by doubling them, or remove it.

What you get

  • Every common result shape: scalars, single rows, lists, IAsyncEnumerable<T> streams and multi-result-set tuples such as (Order Head, IReadOnlyList<OrderLine> Lines).
  • Commands: rows affected, scalars, inserted identities and chunked multi-row bulk inserts.
  • Stored procedures with output, input-output and return-value parameters.
  • Rich types: enums as integers or names, value objects, multi-column composites such as Money, and custom row factories.
  • Transactions by declaring an IAsyncDbTransaction parameter.
  • Provider-aware checks for SQLite, PostgreSQL, SQL Server and MySQL.
  • Migrations: MigrationRunner applies embedded .sql files, with locking for safe concurrent startup on PostgreSQL and SQL Server.
  • Observability through ZeroAlloc.Telemetry's [Instrument]; see the observability recipe.

Every shape, option and diagnostic is in the reference.

Performance

SQLite, BenchmarkDotNet, captured 2026-10-30 on a shared VM. Time is the same-run ratio against hand-written ADO.NET, and rough; allocations are exact bytes per call.

Workload ZA.ORM time Dapper.AOT time ZA.ORM alloc Dapper.AOT alloc Hand-written alloc
Single-row read 0.84× 0.99× 1,376 B 1,800 B 1,232 B
1,000-row read 1.05× 0.94× 88,592 B 130,000 B 88,200 B
Head + 10 lines 1.01× 1.26× 3,016 B 4,264 B 2,872 B
Insert 0.93× 1.24× 1,256 B 1,888 B 1,208 B

On the single-statement workloads ZA.ORM allocates exactly what the same hand-written code over IAsyncDbConnection does; the rest is the AdoNet.Async substrate's.

Full tables, the insert workload, PostgreSQL and methodology: SQLite and PostgreSQL.

Learn more

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 (2)

Showing the top 2 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.

ZeroAlloc.ORM.Linq

Compile-time LINQ authoring surface for ZeroAlloc.ORM: the [LinqQuery] companion pattern with Sql.Table, QueryOperators and SqlRow. The chain is read by the ZeroAlloc.ORM generator at build time and emitted as SQL; the methods never run.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.16.1 2,625 11/11/2026
2.16.0 679 11/10/2026
2.15.0 217 11/10/2026
2.14.0 1,840 11/6/2026
2.13.0 1,530 10/31/2026
2.12.0 304 10/30/2026
2.11.2 184 10/30/2026
2.11.1 121 10/30/2026
2.11.0 281 10/30/2026
2.10.2 798 10/29/2026
2.10.0 1,705 10/23/2026
2.9.0 596 10/23/2026
2.8.0 388 10/22/2026
2.7.0 191 10/22/2026
2.6.0 190 10/22/2026
2.5.0 405 10/21/2026
2.4.1 394 10/21/2026
2.4.0 1,087 10/18/2026
2.3.4 720 10/16/2026
2.3.3 371 10/16/2026
Loading failed