Skip to content

Dapper

Databases & CachingDatabaseC#

What it is

Dapper is a micro-ORM that maps SQL results onto objects with almost no overhead, leaving the SQL entirely under your control.

Extension methods on IDbConnection execute SQL and map rows to types. Parameters are passed as anonymous objects and always parameterised.

Installation

dotnet add package Dapper

Getting started

The smallest useful thing you can do with it, and what each part means.

Query and execute
using var conn = new SqlConnection(connectionString);

var book = await conn.QuerySingleOrDefaultAsync<Book>(
    "SELECT Id, Title, Year FROM Books WHERE Id = @Id",
    new { Id = 42 });

var recent = await conn.QueryAsync<Book>(
    "SELECT Id, Title, Year FROM Books WHERE Year >= @Year",
    new { Year = 1990 });

var rows = await conn.ExecuteAsync(
    "UPDATE Books SET Year = @Year WHERE Id = @Id",
    new { Year = 1966, Id = 42 });
The anonymous object becomes real SQL parameters, so this is injection-safe. Dapper never builds SQL for you — what you write is what runs.
Multi-mapping a join
var sql = @"SELECT b.Id, b.Title, a.Id, a.Name
            FROM Books b JOIN Authors a ON a.Id = b.AuthorId";

var books = await conn.QueryAsync<Book, Author, Book>(
    sql,
    (book, author) => { book.Author = author; return book; },
    splitOn: "Id");   // where the second object's columns begin
splitOn tells Dapper which column starts the next object. Getting it wrong is the most common Dapper bug — it silently maps the wrong columns.

Advanced usage

Where the library earns its place over a simpler alternative.

Multiple result sets and bulk insert
// One round trip, several result sets.
using var multi = await conn.QueryMultipleAsync(@"
    SELECT * FROM Books WHERE Id = @Id;
    SELECT * FROM Reviews WHERE BookId = @Id;", new { Id = 42 });

var book = await multi.ReadSingleAsync<Book>();
var reviews = (await multi.ReadAsync<Review>()).ToList();

// Passing a collection executes the statement once per item.
await conn.ExecuteAsync(
    "INSERT INTO Books (Title, Year) VALUES (@Title, @Year)",
    booksToInsert);   // batched, but still one statement each
QueryMultiple saves round trips when a page needs several related sets. For genuinely large inserts, use SqlBulkCopy — Dapper's list form is still one statement per row.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

When using the multi-mapping APIs ensure you set the splitOn parameter
Dapper cannot infer the boundary. Set splitOn to the first column name of the second object.
Properties are null after a query
Column names do not match property names. Alias them in the SQL, or enable MatchNamesWithUnderscores for snake_case columns.

Best practices

  • Always pass parameters as an object; never interpolate values into the SQL string.
  • Get splitOn right when multi-mapping, and verify the result rather than assuming.
  • Use Dapper for read-heavy hot paths and EF Core where change tracking and migrations help.
  • Let the connection pool work — create and dispose connections per operation rather than holding one open.

Background

Why it exists, and what it was reacting to.

Built by the Stack Overflow team for their own hot paths, Dapper exists because full ORMs added measurable latency to their highest-traffic queries. It does one thing: turn a result set into objects, quickly.