/DB

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.

updated 5 Jun 202610 min readv0.3.0View as Markdown

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 — 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

MethodMeanAllocatedvs Socigy
1 rowSocigy (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 rowsSocigy (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 rowsSocigy (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 (this library) best in group · lower is betterbars are relative within each group

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.

MethodMeanAllocatedvs Socigy
1 rowSocigy (.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 rowsSocigy (.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 rowsSocigy (.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×
Socigy (this library) best in group · lower is betterbars are relative within each group

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.

Writes — INSERT, vs Dapper & EF Core

MethodMeanAllocatedvs 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×
Socigy (this library) best in group · lower is betterbars are relative within each group

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.

MethodMeanAllocatedvs Socigy
100 rowsSocigy (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 rowsSocigy (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×
Socigy (this library) best in group · lower is betterbars are relative within each group

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

MethodMeanAllocatedvs 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×
Socigy (this library) best in group · lower is betterbars are relative within each group

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 — and runs 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 the Microsoft.DotNet.ILCompiler package automatically.
NOTE
The NativeAOT job needs BenchmarkDotNet ≥ 0.15 to recognise the 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:16

3. 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 — 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 needed

5. 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.Aot

6. 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.


TIP
The benchmark source lives in the Benchmarks/ and Benchmarks.Aot/ projects, each with a README.md documenting every suite.