Data Layer at Scale — series:

  1. EF Core 10 AOT: What's broken

  2. Dapper.AOT broken on .NET 10 (link TBD)

  3. Extern alias benchmarking (link TBD)

  4. Cold start vs density (link TBD)

  5. Allocation numbers for k8s (this article)

  6. The benchmark lied (link TBD)

  7. The data layer as a line item (link TBD)

  8. Measure, then migrate (link TBD)

Dapper.AOT promises zero-reflection database access through compile-time code generation. The pitch is compelling: keep your familiar Dapper syntax, get NativeAOT compatibility for free. On .NET 10, that promise doesn't hold. The interceptors silently stop firing, the runtime falls back to SqlMapper, hits Reflection.Emit, and your AOT binary crashes on the first query.

Here is what is actually happening, why no configuration fixes it, and what the measurements say you should do instead.

How Dapper.AOT Is Supposed to Work

Dapper.AOT uses the C# interceptors feature. During compilation, a source generator scans your call sites — every connection.Query<T>(...), connection.Execute(...) — and emits a replacement method tagged with [InterceptsLocation]. The compiler then routes the call to the generated method instead of the original SqlMapper reflection path.

The original attribute form points at the call site by file path, line, and column:

// What the generator emits today (deprecated form)
[InterceptsLocation("C:/MyApp/Repos/UserRepo.cs", line: 42, column: 18)]
public static IEnumerable<User> Query_Users_42_18(
    this IDbConnection connection, string sql, object? param = null)
{
    // generated IDataReader mapping, no reflection
}

That three-argument overload was experimental from the start. .NET 10's Roslyn deprecated it in favor of a versioned, checksum-based form:

// The form .NET 10's Roslyn expects
[InterceptsLocation(version: 1, data: "AQAAACMAAAA...")]

The Version Matrix

VersionCompiles on net10Interceptors wireAOT safe
Dapper.AOT 1.0.31No — CS9270 errorN/ANo
Dapper.AOT 1.0.52 (latest)YesNo (deprecated form, silently ignored)No
Devedse community fork 1.0.84YesNo (same deprecated form)No
Dapper (plain, no AOT)YesN/ANo (JIT only)

CS9270 is what 1.0.31 gives you outright. Upgrading to 1.0.52 clears the build error. That feels like progress. It is not — the failure just moved from compile time to runtime, which is strictly worse.

What Happens at Runtime

With 1.0.52 on a NativeAOT binary, the compiler sees the deprecated attribute form, ignores the interceptors entirely, and the calls silently route to the original Dapper.SqlMapper.Query<T>. That path builds column-to-property mappings with Reflection.Emit at runtime. NativeAOT does not ship the IL emit pipeline.

First query, first crash:

Unhandled exception. System.NotSupportedException:
  System.Reflection.Emit is not supported on this platform.
   at System.Reflection.Emit.DynamicMethod..ctor(...)
   at Dapper.SqlMapper.GetDeserializer(...)
   at Dapper.SqlMapper.Query[T](IDbConnection, String, Object, ...)

The tracking issue is DapperAOT#148, open and unaddressed as of this writing.

No Config Workaround Exists

To be direct about what was tested:

<!-- None of these fix it -->
<PropertyGroup>
  <InterceptorsPreviewNamespaces>Dapper.AOT.Generated</InterceptorsPreviewNamespaces>
  <InterceptorsNamespaces>$(InterceptorsNamespaces);Dapper.AOT</InterceptorsNamespaces>
</PropertyGroup>

These properties control whether the analyzer runs — not which attribute form the generator emits. We verified against the generator's own source: the emitted form is hardcoded. You can confirm the interceptors are not wiring by putting a log line inside a generated interceptor method — with the deprecated form on .NET 10, it never executes. Control goes straight to SqlMapper.

The fix has to come from the generator emitting [InterceptsLocation(version: 1, data: ...)]. Until #148 resolves and a release ships, the package cannot be used in NativeAOT binaries on .NET 10.

The Measurement That Changes the Conclusion

Here's the part most write-ups miss. The obvious question is "so I lose Dapper's convenience under AOT — how much performance do I lose staying on JIT?" We measured all of it, same service, same schema, sustained HTTP load (8 workers × 60 s, self-reported runtime counters):

SetupCPU cores usedreq/sreq/s per coreRSS peak
Dapper — JIT0.682,5413,73775 MB
ADO.NET — JIT0.672,3683,53472 MB
ADO.NET — AOT0.502,5805,16037 MB

Two conclusions:

  1. Dapper-JIT ≈ ADO-JIT within noise. Dapper's mapping convenience costs essentially nothing at runtime — it's a fine JIT choice.

  2. The AOT prize goes only to raw ADO — and it's substantial: −25% CPU and −49% RSS versus the same code on JIT. Since Dapper can't ride along, hand-rolled (or better, generated) reader mapping is what unlocks it.

So the honest framing isn't "Dapper is broken, tolerate boilerplate." It's: for AOT services, the mapping layer must be reflection-free by construction — written by you or emitted by a source generator you control:

public async Task<User?> GetUserAsync(int id, CancellationToken ct)
{
    await using var cmd = _dataSource.CreateCommand(
        "SELECT id, name, email FROM users WHERE id = @id");
    cmd.Parameters.AddWithValue("id", id);

    await using var reader = await cmd.ExecuteReaderAsync(ct);
    if (!await reader.ReadAsync(ct)) return null;

    return new User(
        Id:    reader.GetInt32(0),
        Name:  reader.GetString(1),
        Email: reader.GetString(2));
}

Deterministic, trimmable, no late binding. It survives NativeAOT publication without modification. In our framework this mapping is generated from the same declaration that produces the DDL, so it cannot drift from the schema — but even hand-written, the pattern is bounded and testable.

Summary

Dapper.AOT's interceptor mechanism is sound in principle, but its generator still emits the [InterceptsLocation] form that .NET 10's Roslyn deprecated and now ignores. The result is the worst failure mode: a clean build whose interceptors never wire, falling back to Reflection.Emit, and crashing only at the first query of your published AOT binary.

Based on my experience: for JIT, Dapper remains excellent — and measurably free. For NativeAOT, own the mapping — raw or generated ADO.NET.

Verified against .NET 10 (net10.0), Dapper 2.1.66, Dapper.AOT 1.0.31/1.0.52, DevedseDapperAotFork 1.0.84. Tracking issue: github.com/DapperLib/DapperAOT/issues/148.

Part of the Data Layer at Scale series — research notes from building Turboservices, a .NET framework for rebuilding legacy systems onto AI-native delivery rails.

About the author — Vitalii Honcharuk is a hands-on Distinguished Engineer, Architect and CTO with 15+ years of experience building frameworks and high-reliability backend systems for enterprises and startups. His current work, Turboservices, turns engineering discipline into compile-time guarantees: unsafe states refuse to build, quality gates are code, and every architectural decision ships with measured evidence.