/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 6 Jun 202614 min readv0.3.2View 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, 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

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.

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.

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

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.

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

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

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