Skip to content

Use Dapper as the internal query engine#36

Merged
LucDeCaf merged 10 commits intomainfrom
dapper-query-engine
Jan 22, 2026
Merged

Use Dapper as the internal query engine#36
LucDeCaf merged 10 commits intomainfrom
dapper-query-engine

Conversation

@LucDeCaf
Copy link
Copy Markdown
Contributor

@LucDeCaf LucDeCaf commented Jan 21, 2026

Changes the internal method of deserialising SQL rows into an object, replacing the old JSON Serialize => Deserialize approach with Dapper.

Why change?

The current approach, after querying the database, needs some way to get from a SQLite row to an object T. Writing the logic for this by hand is error-prone and requires maintenance, so the current approach just calls JsonSerializer.Serialize(row) and then JsonSerializer.Deserialize<T>(serializedRow). While this does work, it is wildly inefficient memorywise and is probably also slower than a handwritten/optimised solution.

Why Dapper?

Dapper touts itself as a lightweight and performant "micro-ORM" which can be used to query more efficiently. This PR replaces the current Get/GetAll/Execute/... implementations with calls to Dapper methods, which have builtin logic to generate objects from rows. This moderately increases querying speed and SIGNIFICANTLY cuts down on the memory used per query (see benchmarks below), at the cost of having to support Dapper's way of doing things.

Caveats

Result type structore
To get its savings, Dapper requires that we use it in a certain way. Namely, Dapper doesn't always play nicely with record ListResult(string id, string owner_id, string created_at, ...); syntax. Dapper prefers to work with POCOs (Plain Old CLR Objects), which sometimes necessitates some small changes to code structure:

// Before
public record ListResult(string id, string owner_id, string name, string created_at);
// ^^^ This sometimes works, but if it doesn't, use the below approach

// After
public record ListResult
{
    public string id;
    public string owner_id;
    public string name;
    public DateTime created_at; // Dapper automatically converts into DateTime
}

SELECT * issues
Dapper allows you to run GetAll<ListResult>("SELECT * FROM lists"); however, it then expects the ListResult to have a field/property available for every single column returned by the select. If the select query returns too many or too little columns, Dapper throws a runtime error.

To avoid this, simply make your queries more precise, or use untyped queries, which return dynamic objects.

public record PartialList(string id, string name);

// Before
List<PartialList> lists = await db.GetAll<PartialList>("SELECT * FROM lists"); // Throws error because PartialList doesn't have owner_id or created_at

// After
List<PartialList> lists = await db.GetAll<PartialList>("SELECT id, name FROM lists"); // Success
// OR
List<dynamic> lists = await db.GetAll("SELECT * FROM lists");

Benchmarks

Benchmarks were obtained via BenchmarkDotNet and a local self-host-demo/demos/supabase instance running. The benchmarks consisted of syncing a local database (not timed) and querying all records for a given user into a List<TodoResult>.

Benchmark Details
BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0]
Apple M4 Pro, 1 CPU, 14 logical and 14 physical cores
.NET SDK 9.0.200
[Host] : .NET 9.0.2 (9.0.2, 9.0.225.6610), Arm64 RyuJIT armv8.0-a
ShortRun : .NET 9.0.2 (9.0.2, 9.0.225.6610), Arm64 RyuJIT armv8.0-a

JSON (Old)

Method Mean Error StdDev Gen0 Gen1 Gen2 Allocated
BenchmarkQuery10K 26.37 ms 3.950 ms 0.217 ms 6000.0000 1000.0000 - 50.85 MB
BenchmarkQuery100K 287.21 ms 31.914 ms 1.749 ms 65000.0000 20000.0000 2000.0000 509.06 MB
BenchmarkQuery1M 2,989.54 ms 449.790 ms 24.654 ms 635000.0000 195000.0000 3000.0000 5081.07 MB

Dapper (New)

Method Mean Error StdDev Gen0 Gen1 Gen2 Allocated
BenchmarkQuery10K 9.735 ms 4.267 ms 0.2339 ms - - - 3.3 MB
BenchmarkQuery100K 105.348 ms 6.631 ms 0.3634 ms 3000.0000 1000.0000 - 33.28 MB
BenchmarkQuery1M 1,181.634 ms 132.406 ms 7.2576 ms 39000.0000 20000.0000 1000.0000 328.05 MB

Key takeaways

  • Runtime: ~2.8x faster speed
  • Memory usage: ~15x less memory used

@LucDeCaf LucDeCaf changed the title Dapper query engine Use Dapper as the internal query engine Jan 21, 2026
@LucDeCaf LucDeCaf marked this pull request as ready for review January 22, 2026 09:51
Chriztiaan
Chriztiaan previously approved these changes Jan 22, 2026
Copy link
Copy Markdown
Collaborator

@Chriztiaan Chriztiaan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Changelog entry and we are good.

@LucDeCaf LucDeCaf merged commit 92b0a7e into main Jan 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants