Benchmarks
Socigy.OpenSource.DB delivers Dapper-class speed, hand-written-ADO.NET allocations, and full NativeAOT support, measured against Dapper and EF Core. Reproduce every number yourself.
Dapper-class. AOT-native. Zero ceremony.
Socigy.OpenSource.DB is a source-generated data layer. Your queries become real C# at compile time, so there's no reflection, no runtime IL emit, and nothing to warm up. The result is performance that matches hand-written Dapper, with a fully typed API.
The headline, in three numbers (PostgreSQL, .NET 10, 1 000-row read)
- ⚡ Matches hand-written Dapper on speed (statistically tied at every size), with a fully typed API instead of hand-written SQL.
- 🪶 ~120 KB allocated: on par with hand-written ADO.NET, 1.6× less than Dapper, and up to 8× less than EF Core.
- 🚀 Runs under NativeAOT with no performance penalty, something Dapper and EF Core cannot do at all.
You get the ergonomics of an ORM, micro-ORM-class speed, lower read allocations than Dapper, far less memory than EF Core, and the only typed query layer that survives a NativeAOT publish.
Why it's fast
| Socigy | Dapper | EF Core | |
|---|---|---|---|
| Row materialization | Source-generated | Reflection.Emit (runtime IL) |
Runtime codegen |
| LINQ→SQL translation | Compiled once, cached per shape | n/a (you write SQL) | Cached |
| Boxing on read | None (GetFieldValue<T>) |
None | Varies |
| NativeAOT | ✅ Runs | ❌ | ❌ |
Column ordinals are resolved once per result set and values are read with the allocation-free
GetFieldValue<T>, so materialization is as lean as a hand-written reader loop: by ordinal, no boxing.
Predicates are translated to SQL once per query shape and cached, so repeat calls skip translation
entirely and just re-bind the parameter values, with no per-call reflection or string building. (That
first-time translation costs ~450 ns; after that the shape is served from cache.)
The numbers
Environment: BenchmarkDotNet v0.15.8 · Intel Core Ultra 9 275HX · Windows 11 · .NET 10.0.8. Each call opens a pooled connection (a realistic per-request pattern), so connection acquisition is part of every measurement. Run them yourself (below) for figures on your own hardware.
How to read these tables: the best result per metric in each group is highlighted in green (lower is better: less time, less memory), Socigy's own rows are tinted, and the bars show relative cost within a group. Within noise means two results' 99.9% confidence intervals overlap: the gap is smaller than normal run-to-run variance, so neither is reliably faster.
Reads: typed `Query`, vs Dapper & EF Core
| Method | Mean | Allocated | vs Socigy | |
|---|---|---|---|---|
| 1 row | Socigy (typed Query) | 351 µs | 4.6 KB | 1.00× |
| Dapper (raw SQL) | ✓340 µs | ✓3.1 KB | 0.97× | |
| EF Core (no tracking) | 424 µs | 56 KB | 1.21× | |
| EF Core (tracking) | 441 µs | 56 KB | 1.26× | |
| 100 rows | Socigy (typed Query) | 375 µs | ✓16.7 KB | 1.00× |
| Dapper (raw SQL) | ✓365 µs | 22.9 KB | 0.97× | |
| EF Core (no tracking) | 474 µs | 87 KB | 1.26× | |
| EF Core (tracking) | 527 µs | 148 KB | 1.41× | |
| 1000 rows | Socigy (typed Query) | 611 µs | ✓122 KB | 1.00× |
| Dapper (raw SQL) | ✓593 µs | 199 KB | 0.97× | |
| EF Core (no tracking) | 762 µs | 361 KB | 1.25× | |
| EF Core (tracking) | 1166 µs | 979 KB | 1.91× |
Socigy matches Dapper on speed and beats it on memory. Queries are translated once and cached per
shape, so typed Query lands within ~3% of hand-written Dapper at every size (their confidence
intervals overlap, a statistical tie). It also allocates less than Dapper throughout: at 1 000
rows 122 KB vs Dapper's 199 KB (1.6× less), and 8× less than EF Core tracking's 979 KB, while
running up to 1.9× faster than EF Core.
Reads: `.sql` procedure, vs Dapper & EF Core
A fixed-SQL Socigy procedure (no LINQ translation at all) against Dapper raw SQL and EF Core FromSqlRaw.
| Method | Mean | Allocated | vs Socigy | |
|---|---|---|---|---|
| 1 row | Socigy (.sql procedure) | 314 µs | 3.5 KB | 1.00× |
| Dapper (raw SQL) | ✓304 µs | ✓3.1 KB | 0.97× | |
| EF Core (FromSqlRaw, no tracking) | 408 µs | 59 KB | 1.30× | |
| 100 rows | Socigy (.sql procedure) | 384 µs | ✓15.6 KB | 1.00× |
| Dapper (raw SQL) | ✓371 µs | 22.9 KB | 0.97× | |
| EF Core (FromSqlRaw, no tracking) | 506 µs | 90 KB | 1.32× | |
| 1000 rows | Socigy (.sql procedure) | 649 µs | ✓121 KB | 1.00× |
| Dapper (raw SQL) | ✓593 µs | 199 KB | 0.91× | |
| EF Core (FromSqlRaw, no tracking) | 803 µs | 364 KB | 1.24× |
Within ~3-9% of Dapper, and well ahead of EF Core, at the same allocation as the cached typed
Query (≈121 KB at 1 000 rows) and ~3× lighter than EF's FromSqlRaw.
Reads: defined vs dynamic table
The same filtered read + materialization on bench_users, comparing the typed Query
(table fixed at build time) against a DynamicTable (table name
bound at runtime), with Dapper and EF Core for reference.
| Method | Mean | Allocated | vs Socigy | |
|---|---|---|---|---|
| 1 row | Socigy (typed Query, defined) | 359 µs | 4.6 KB | 1.00× |
| Socigy (DynamicTable, runtime name) | 333 µs | 5.7 KB | 0.93× | |
| Dapper (raw SQL, runtime name) | ✓325 µs | ✓3.3 KB | 0.91× | |
| EF Core (static model) | 465 µs | 57 KB | 1.30× | |
| 100 rows | Socigy (typed Query, defined) | 395 µs | ✓16.7 KB | 1.00× |
| Socigy (DynamicTable, runtime name) | 390 µs | 17.9 KB | 0.99× | |
| Dapper (raw SQL, runtime name) | ✓373 µs | 23.2 KB | 0.94× | |
| EF Core (static model) | 506 µs | 87 KB | 1.28× | |
| 1000 rows | Socigy (typed Query, defined) | ✓603 µs | ✓122 KB | 1.00× |
| Socigy (DynamicTable, runtime name) | 697 µs | 123 KB | 1.16× | |
| Dapper (raw SQL, runtime name) | 696 µs | 199 KB | 1.16× | |
| EF Core (static model) | 834 µs | 362 KB | 1.39× |
Dynamic binding costs almost nothing on the hot path. A DynamicTable rebuilds its SQL on each call
(the typed Query caches it per shape), yet it shares the exact same materialization, so allocations track
the typed path almost perfectly (123 KB vs 122 KB at 1 000 rows). At small result sets the two are within
noise, with the DynamicTable even a touch ahead at a single row (333 µs vs 359 µs). At 1 000 rows the
per-call SQL rebuild and higher run-to-run variance put it about 16% behind the cached path (697 µs vs
603 µs). Both Socigy paths stay ~1.3–1.4× faster than EF Core and allocate far less (EF: 57 KB to
362 KB). Dapper, writing raw SQL by hand, ties on the hot path but gives up the typed API. EF Core can't
actually bind a runtime table name (it needs a build-time model); it's shown reading its statically-mapped
table purely as a reference point.
Reads: two-table join, vs Dapper & EF Core
A 1:1 inner join (bench_users to bench_logins) filtered to age < Rows, materializing both sides:
Socigy's typed Join builder vs Dapper multi-mapping vs EF Core's
no-tracking Join.
| Method | Mean | Allocated | vs Socigy | |
|---|---|---|---|---|
| 1 row | Socigy (typed Join) | ✓487 µs | 14.4 KB | 1.00× |
| Dapper (multi-map join) | 491 µs | ✓3.9 KB | 1.01× | |
| EF Core (no tracking join) | 593 µs | 61 KB | 1.22× | |
| 100 rows | Socigy (typed Join) | ✓542 µs | ✓34.5 KB | 1.00× |
| Dapper (multi-map join) | 557 µs | 37.5 KB | 1.03× | |
| EF Core (no tracking join) | 694 µs | 101 KB | 1.28× | |
| 1000 rows | Socigy (typed Join) | 994 µs | ✓218 KB | 1.00× |
| Dapper (multi-map join) | ✓904 µs | 340 KB | 0.91× | |
| EF Core (no tracking join) | 1179 µs | 460 KB | 1.19× |
Joins materialize both sides through the same fast path as Query (ordinals resolved once, GetFieldValue<T>,
no per-row dictionary or boxing). The result: the lowest allocations at scale (218 KB at 1 000 rows vs
Dapper's 340 KB and EF's 460 KB) and ~1.2× faster than EF Core. Dapper is ~9% faster at 1 000 rows
because its mapper here returns a single value, while Socigy builds two typed entities per row; at a single
row Dapper's flat map also allocates less (3.9 KB vs 14.4 KB of fixed join setup). Across 100–1 000 rows
Socigy and Dapper are otherwise within noise.
Writes: INSERT, vs Dapper & EF Core
| Method | Mean | Allocated | vs Socigy |
|---|---|---|---|
| Socigy (insert builder) | ✓2.03 ms | 4.2 KB | 1.00× |
| Dapper (ExecuteAsync) | 2.06 ms | ✓3.7 KB | 1.01× |
| EF Core (Add + SaveChanges) | 2.74 ms | 66 KB | 1.35× |
Single-row writes are dominated by the database commit (fsync), so times are close and noisy. Socigy and Dapper are tied within noise. Socigy's insert builder caches its statement and column plan, so it allocates just 4.2 KB, a hair above Dapper's 3.7 KB and ~16× less than EF Core's 66 KB.
Writes: bulk INSERT (`InsertMultipleAsync`), vs a per-row loop, Dapper & EF Core
InsertMultipleAsync batches a whole collection into one multi-row
INSERT … VALUES (…),(…) command (auto-chunked under PostgreSQL's 65,535-parameter limit) instead of a
command per row.
| Method | Mean | Allocated | vs Socigy | |
|---|---|---|---|---|
| 100 rows | Socigy (InsertMultipleAsync) | ✓2.88 ms | ✓136 KB | 1.00× |
| Socigy (per-row loop, 1 tx) | 30.9 ms | 233 KB | 10.81× | |
| Dapper (ExecuteAsync over list) | 141.8 ms | 154 KB | 49.68× | |
| EF Core (AddRange + SaveChanges) | 7.0 ms | 707 KB | 2.45× | |
| 1000 rows | Socigy (InsertMultipleAsync) | ✓6.67 ms | ✓1432 KB | 1.00× |
| Socigy (per-row loop, 1 tx) | 262 ms | 2314 KB | 39.54× | |
| Dapper (ExecuteAsync over list) | 1527 ms | 1525 KB | 230.23× | |
| EF Core (AddRange + SaveChanges) | 23.9 ms | 6295 KB | 3.60× |
Batched multi-row INSERT is in a different league. Because the whole batch becomes a single command,
InsertMultipleAsync is 10–40× faster than a per-row loop and far ahead of anything that issues one
command per row. Dapper's ExecuteAsync over a list does exactly that (N round-trips), landing
~50–230× slower. EF Core batches its inserts so it stays within 2–4×, but allocates ~5× more
(707 KB / 6.3 MB vs Socigy's 136 KB / 1.4 MB). Socigy is both the fastest and the leanest at every size.
Writes: UPDATE (by primary key), vs Dapper & EF Core
| Method | Mean | Allocated | vs Socigy |
|---|---|---|---|
| Socigy (update builder) | ✓1.57 ms | 6.0 KB | 1.00× |
| Dapper (ExecuteAsync) | 1.58 ms | ✓3.2 KB | 1.01× |
| EF Core (ExecuteUpdate) | 1.66 ms | 58 KB | 1.06× |
Dead-even with Dapper, both fsync-bound; EF Core trails slightly and allocates ~10× more.
NativeAOT: the one nobody else can match 🚀
Publish your app with NativeAOT and Socigy just works, running as fast as it does under the JIT:
| Socigy workload | JIT | NativeAOT | Delta |
|---|---|---|---|
typed Query, 1 000 rows |
616 µs | 641 µs | within noise |
typed Query, 1 row |
358 µs | 322 µs | NativeAOT faster |
.sql procedure, 1 000 rows |
586 µs | 643 µs | within noise |
| INSERT, 1 row | 2.15 ms | 1.91 ms | NativeAOT faster |
Allocations are identical across JIT and NativeAOT (≈122 KB at 1 000 rows). The parity is no accident: materialization is entirely source-generated, so there's no runtime codegen to fall back to an interpreter.
And here's the floor: under NativeAOT, Socigy against a hand-written, ordinal-based ADO.NET reader
loop (GetInt32/GetGuid by ordinal, no abstraction). This is as fast as managed data access gets:
| Workload (NativeAOT) | Socigy Mean | ADO.NET Mean | Socigy Alloc | ADO.NET Alloc |
|---|---|---|---|---|
typed Query, 1 row |
322 µs | 301 µs | 4.9 KB | 2.6 KB |
typed Query, 1 000 rows |
641 µs | 600 µs | 123 KB | 120 KB |
.sql procedure, 1 000 rows |
643 µs | 591 µs | 121 KB | 120 KB |
| INSERT, 1 row | 1.91 ms | 1.87 ms | 4.7 KB | 3.2 KB |
Raw ADO.NET is the theoretical floor, fastest on every row (the green cells), yet Socigy stays within ~9% of it while giving you a fully typed API, and matches it on allocations at scale (≈121 KB vs 120 KB at 1 000 rows; the per-row gap is essentially gone).
No other typed library even gets to play this game. The alternatives don't run under NativeAOT at all:
| Library | NativeAOT | Why |
|---|---|---|
| Socigy.OpenSource.DB | ✅ Runs | Source-generated materialization, no runtime codegen |
| Dapper | ❌ Fails | Materializes via System.Reflection.Emit (runtime IL) |
| EF Core | ❌ Fails | Query pipeline relies on runtime code generation |
In an AOT-published service, Socigy is the typed data layer, because it's the only one that boots.
Run it yourself
Don't take our word for it. Every number above is reproducible in minutes.
1. Prerequisites
- .NET 10 SDK (
dotnet --version≥ 10). - A PostgreSQL instance for every suite except
ParseBenchmarks. - NativeAOT only: a C/C++ toolchain for the native link step. On Windows, the Desktop development
with C++ workload (or Build Tools); on Linux,
clang+zlib. See the NativeAOT prerequisites. BenchmarkDotNet pulls theMicrosoft.DotNet.ILCompilerpackage automatically.
net10.0 target. Older
versions fail with Invalid TFM: 'net10.0'. The repo pins a compatible version, so a fresh clone works.2. Start a PostgreSQL (example: Docker)
docker run --rm -d --name socigy-bench -p 5432:5432 \
-e POSTGRES_PASSWORD=1234 -e POSTGRES_DB=postgres postgres:163. Point the benchmarks at it
The connection string is read from the BENCH_DB environment variable; if unset it defaults to
Host=localhost;Port=5432;Username=postgres;Password=1234;Database=postgres.
# PowerShell
$env:BENCH_DB = "Host=localhost;Port=5432;Username=postgres;Password=1234;Database=postgres"# bash
export BENCH_DB="Host=localhost;Port=5432;Username=postgres;Password=1234;Database=postgres"The bench_users (1000 rows) and bench_writes tables are created and seeded automatically on the first
run, with no manual SQL needed.
4. Run a suite (Release only)
# Socigy vs Dapper vs EF Core
dotnet run -c Release --project Benchmarks # everything
dotnet run -c Release --project Benchmarks -- --filter *QueryBenchmarks*
dotnet run -c Release --project Benchmarks -- --filter *ProcedureBenchmarks*
dotnet run -c Release --project Benchmarks -- --filter *InsertBenchmarks*
dotnet run -c Release --project Benchmarks -- --filter *UpdateBenchmarks*
dotnet run -c Release --project Benchmarks -- --filter *ParseBenchmarks* # no database needed5. Run the NativeAOT comparison
Publishes a native build per benchmark and runs the suites under JIT and NativeAOT side by side (Socigy vs raw ADO.NET):
dotnet run -c Release --project Benchmarks.Aot6. Find the results
BenchmarkDotNet prints a summary table and writes report files to BenchmarkResults/results/ next
to the project: GitHub-flavoured Markdown (*-report-github.md), JSON (*-report-full.json), CSV and
HTML.
Benchmarks/ and Benchmarks.Aot/ projects, each with a
README.md documenting every suite.