ZeroAlloc.ORM.Linq
2.10.0
dotnet add package ZeroAlloc.ORM.Linq --version 2.10.0
NuGet\Install-Package ZeroAlloc.ORM.Linq -Version 2.10.0
<PackageReference Include="ZeroAlloc.ORM.Linq" Version="2.10.0" />
<PackageVersion Include="ZeroAlloc.ORM.Linq" Version="2.10.0" />
<PackageReference Include="ZeroAlloc.ORM.Linq" />
paket add ZeroAlloc.ORM.Linq --version 2.10.0
#r "nuget: ZeroAlloc.ORM.Linq, 2.10.0"
#:package ZeroAlloc.ORM.Linq@2.10.0
#addin nuget:?package=ZeroAlloc.ORM.Linq&version=2.10.0
#tool nuget:?package=ZeroAlloc.ORM.Linq&version=2.10.0
ZeroAlloc.ORM
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,
ZAO001toZAO082, and a LINQ chain that cannot be translated is one too,ZAO100toZAO113. - 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.
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
IAsyncDbTransactionparameter. - Provider-aware checks for SQLite, PostgreSQL, SQL Server and MySQL.
- Migrations:
MigrationRunnerapplies embedded.sqlfiles, 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 at 2.4.0. Time is the same-run ratio against hand-written ADO.NET; 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 | 1.06× | 1.21× | 1,568 B | 1,800 B | 1,232 B |
| 1,000-row read | 1.06× | 0.94× | 88,592 B | 130,001 B | 88,200 B |
| Head + 10 lines | 1.09× | 1.27× | 3,400 B | 4,072 B | 2,680 B |
Full tables, the insert workload, PostgreSQL and methodology: SQLite and PostgreSQL.
Learn more
- Documentation site: orm.zeroalloc.net
- Documentation source: the reference, cookbook, diagnostics and migrating to v2.
- Related packages: AdoNet.Async, ZeroAlloc.ValueObjects, ZeroAlloc.Telemetry and ZeroAlloc.Templates.
- Contributing: issues and pull requests are welcome on GitHub.
- CHANGELOG.
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
- ZeroAlloc.ORM.Abstractions (>= 2.10.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.