Home C# / .NET Dapper Micro-ORM in .NET: Fast Data Access Guide
Intermediate 5 min · July 13, 2026

Dapper Micro-ORM in .NET: Fast Data Access Guide

Learn Dapper micro-ORM for .NET with production-ready examples.

N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Drawn from code that ran under real load.

Follow
Production
production tested
July 13, 2026
last updated
2,043
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic knowledge of C# and .NET
  • Familiarity with SQL and relational databases
  • Visual Studio or .NET CLI installed
  • A SQL Server (or other database) instance for testing
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Dapper is a lightweight, high-performance micro-ORM for .NET that extends IDbConnection with extension methods for executing SQL and mapping results to objects.
  • It offers near raw ADO.NET speed with minimal overhead, making it ideal for performance-critical applications.
  • Key features include automatic object mapping, support for stored procedures, multiple result sets, and async operations.
  • Dapper is not a full ORM like Entity Framework; it requires you to write SQL but gives you full control.
  • Common use cases include high-throughput APIs, reporting, and scenarios where EF Core's abstraction is too heavy.
✦ Definition~90s read
What is Dapper Micro-ORM in .NET?

Dapper is a micro-ORM for .NET that provides fast, simple data access by extending IDbConnection with methods to execute SQL and map results to objects.

Think of Dapper as a high-speed courier service for your data.
Plain-English First

Think of Dapper as a high-speed courier service for your data. Instead of using a big truck (Entity Framework) that takes time to load and unload, Dapper is like a motorcycle courier that zips through traffic, delivering exactly what you need with minimal fuss. You tell it the address (SQL query) and it brings back the package (data) in the exact format you want.

In the world of .NET data access, developers often face a trade-off between productivity and performance. Full-featured ORMs like Entity Framework Core offer convenience with automatic change tracking and LINQ queries, but they can introduce overhead that becomes problematic under high load. On the other end, raw ADO.NET gives you maximum speed but requires tedious boilerplate code for mapping results to objects.

Enter Dapper: a micro-ORM created by the Stack Overflow team to handle their massive traffic demands. Dapper strikes a perfect balance by providing a thin wrapper around ADO.NET that eliminates boilerplate while maintaining near-raw performance. It extends the IDbConnection interface with simple methods like Query, Execute, and QueryMultiple, automatically mapping database rows to strongly-typed objects.

In this tutorial, you'll learn how to integrate Dapper into your .NET applications, write efficient SQL queries, handle complex mappings, and avoid common pitfalls. We'll cover real-world scenarios including multi-table joins, stored procedures, and async operations. By the end, you'll be able to use Dapper to build fast, reliable data access layers that scale.

Setting Up Dapper in Your .NET Project

To get started with Dapper, you need to install the NuGet package. Dapper is a single-file library that adds extension methods to IDbConnection. You can install it via the .NET CLI or Package Manager Console.

First, create a new console application or use an existing ASP.NET Core project. Then add the Dapper package:

``bash dotnet add package Dapper ``

Dapper works with any ADO.NET provider (SQL Server, PostgreSQL, MySQL, SQLite, etc.). You'll also need the appropriate data provider package, e.g., Microsoft.Data.SqlClient for SQL Server.

Once installed, you can start using Dapper by opening a connection and calling its extension methods. Here's a minimal example:

```csharp using System.Data; using Microsoft.Data.SqlClient; using Dapper;

string connectionString = "Server=.;Database=MyDb;Trusted_Connection=true;"; using (IDbConnection db = new SqlConnection(connectionString)) { var products = db.Query<Product>("SELECT * FROM Products"); foreach (var product in products) { Console.WriteLine($"{product.Id}: {product.Name}"); } }

public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } } ```

Dapper automatically maps the result columns to the Product properties by name (case-insensitive). No manual mapping or configuration needed.

Key Takeaway: Dapper is easy to set up: install the package, open a connection, and use Query<T> to get strongly-typed results.

SetupExample.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System.Data;
using Microsoft.Data.SqlClient;
using Dapper;

string connectionString = "Server=.;Database=MyDb;Trusted_Connection=true;";
using (IDbConnection db = new SqlConnection(connectionString))
{
    var products = db.Query<Product>("SELECT * FROM Products");
    foreach (var product in products)
    {
        Console.WriteLine($"{product.Id}: {product.Name}");
    }
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}
Output
1: Widget
2: Gadget
3: Doohickey
💡Connection Management
🎯 Key Takeaway
Dapper extends IDbConnection with Query<T> and Execute methods, making data access simple and fast.

Executing Queries and Commands

Dapper provides several extension methods for different operations:

  • Query<T>: Executes a query and maps results to a collection of T.
  • QueryFirst<T>, QuerySingle<T>: Return a single result (throws if not exactly one).
  • QueryFirstOrDefault<T>, QuerySingleOrDefault<T>: Return default if no rows.
  • Execute: Executes a command (INSERT, UPDATE, DELETE) and returns the number of affected rows.
  • ExecuteScalar<T>: Returns a single value (e.g., COUNT, SUM).

All methods have async counterparts: QueryAsync, ExecuteAsync, etc.

Parameterized Queries

Always use parameters to prevent SQL injection. Dapper supports anonymous objects for parameters:

``csharp var product = db.QueryFirstOrDefault<Product>( "SELECT * FROM Products WHERE Id = @Id", new { Id = 42 }); ``

``csharp var parameters = new DynamicParameters(); parameters.Add("@Id", 42, DbType.Int32, ParameterDirection.Input); var product = db.QueryFirstOrDefault<Product>("SELECT * FROM Products WHERE Id = @Id", parameters); ``

Stored Procedures

``csharp var result = db.Query<Product>("usp_GetProducts", new { CategoryId = 5 }, commandType: CommandType.StoredProcedure); ``

Multiple Result Sets

Use QueryMultiple to handle multiple result sets from a single query:

``csharp using (var multi = db.QueryMultiple("SELECT FROM Products; SELECT FROM Categories")) { var products = multi.Read<Product>(); var categories = multi.Read<Category>(); } ``

Key Takeaway: Dapper's methods cover all common data access patterns, with built-in parameterization and async support.

QueryExamples.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using (IDbConnection db = new SqlConnection(connectionString))
{
    // Parameterized query
    var product = db.QueryFirstOrDefault<Product>(
        "SELECT * FROM Products WHERE Id = @Id",
        new { Id = 42 });

    // Execute insert
    int rowsAffected = db.Execute(
        "INSERT INTO Products (Name, Price) VALUES (@Name, @Price)",
        new { Name = "NewProduct", Price = 9.99m });

    // Stored procedure
    var results = db.Query<Product>("usp_GetProducts",
        new { CategoryId = 5 },
        commandType: CommandType.StoredProcedure);

    // Multiple result sets
    using (var multi = db.QueryMultiple("SELECT * FROM Products; SELECT * FROM Categories"))
    {
        var products = multi.Read<Product>();
        var categories = multi.Read<Category>();
    }
}
Output
rowsAffected: 1
products count: 10
categories count: 5
⚠ SQL Injection Prevention
📊 Production Insight
In production, consider using Dapper's async methods (QueryAsync, ExecuteAsync) to avoid blocking threads under load.
🎯 Key Takeaway
Use parameterized queries to prevent SQL injection. Dapper supports anonymous objects and DynamicParameters.

Mapping Results to Objects

Dapper maps columns to properties by name, ignoring case. If your SQL column names don't match your C# properties, you can use column aliases or custom mapping.

Column Aliases

``csharp var products = db.Query<Product>("SELECT Id AS ProductId, Name AS ProductName FROM Products"); ``

Custom Mapping with Dapper's SqlMapper

``csharp SqlMapper.SetTypeMap(typeof(Product), new CustomPropertyTypeMap( typeof(Product), (type, columnName) => type.GetProperty(columnName) )); ``

Handling Nulls

Dapper handles nullable types automatically. If a column is NULL, it maps to null for nullable types (int?, string, etc.). For non-nullable types, it throws an exception.

Multi-Mapping for Joins

When you have a join that returns data from multiple tables, use Dapper's multi-mapping feature:

```csharp var sql = @"SELECT * FROM Orders o INNER JOIN Customers c ON o.CustomerId = c.Id";

var orders = db.Query<Order, Customer, Order>(sql, (order, customer) => { order.Customer = customer; return order; }, splitOn: "Id"); ```

The splitOn parameter tells Dapper where to split the result columns between objects. By default, it splits on "Id". You can specify multiple split columns for more complex mappings.

Key Takeaway: Dapper's automatic mapping works well for simple cases. Use aliases or multi-mapping for complex scenarios.

MappingExamples.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Order
{
    public int Id { get; set; }
    public DateTime OrderDate { get; set; }
    public Customer Customer { get; set; }
}

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
}

// Multi-mapping example
var sql = @"SELECT o.Id, o.OrderDate, c.Id, c.Name 
            FROM Orders o 
            INNER JOIN Customers c ON o.CustomerId = c.Id";

var orders = db.Query<Order, Customer, Order>(sql,
    (order, customer) =>
    {
        order.Customer = customer;
        return order;
    },
    splitOn: "Id").ToList();

Console.WriteLine(orders[0].Customer.Name);
Output
John Doe
🔥SplitOn Behavior
📊 Production Insight
Be careful with splitOn when columns have the same name (e.g., both tables have an Id column). Use aliases to differentiate.
🎯 Key Takeaway
Multi-mapping allows you to map joined results to related objects. Use splitOn to define where each object's columns start.

Working with Transactions

Dapper works seamlessly with database transactions. You can pass a transaction object to any Dapper method.

Basic Transaction

``csharp using (var connection = new SqlConnection(connectionString)) { connection.Open(); using (var transaction = connection.BeginTransaction()) { try { connection.Execute("UPDATE Products SET Price = Price * 1.1", transaction: transaction); connection.Execute("INSERT INTO AuditLog (Action) VALUES (@Action)", new { Action = "PriceUpdate" }, transaction: transaction); transaction.Commit(); } catch { transaction.Rollback(); throw; } } } ``

Async Transactions

``csharp await connection.ExecuteAsync(sql, transaction: transaction); ``

Using TransactionScope

You can also use System.Transactions.TransactionScope for ambient transactions:

``csharp using (var scope = new TransactionScope()) { using (var connection = new SqlConnection(connectionString)) { connection.Execute("UPDATE ..."); } using (var connection2 = new SqlConnection(connectionString2)) { connection2.Execute("UPDATE ..."); } scope.Complete(); } ``

Note that TransactionScope requires the Microsoft.Data.SqlClient package and may need configuration for distributed transactions.

Key Takeaway: Dapper supports transactions via the transaction parameter or TransactionScope, giving you full control over atomicity.

TransactionExample.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using (var connection = new SqlConnection(connectionString))
{
    connection.Open();
    using (var transaction = connection.BeginTransaction())
    {
        try
        {
            connection.Execute("UPDATE Products SET Price = Price * 1.1", transaction: transaction);
            connection.Execute("INSERT INTO AuditLog (Action) VALUES (@Action)", 
                new { Action = "PriceUpdate" }, transaction: transaction);
            transaction.Commit();
            Console.WriteLine("Transaction committed.");
        }
        catch
        {
            transaction.Rollback();
            Console.WriteLine("Transaction rolled back.");
            throw;
        }
    }
}
Output
Transaction committed.
💡Always Open Connection Explicitly
📊 Production Insight
In high-concurrency scenarios, keep transactions short to avoid blocking. Consider using retry logic for deadlocks.
🎯 Key Takeaway
Pass the transaction object to Dapper methods to participate in a transaction. Always commit or rollback explicitly.

Performance Optimization and Best Practices

Dapper is already fast, but you can optimize further with these techniques:

1. Use Buffered vs Unbuffered Queries

By default, Dapper buffers query results (loads all into memory). For large result sets, use unbuffered to stream:

``csharp var products = db.Query<Product>("SELECT * FROM Products", buffered: false); // Iterate without loading all into memory ``

2. Avoid SELECT *

Select only the columns you need to reduce data transfer and mapping overhead.

3. Use Async Methods

``csharp var products = await db.QueryAsync<Product>("SELECT * FROM Products"); ``

4. Cache Compiled Queries

Dapper caches the generated IL code for mapping. You can pre-compile queries for repeated use:

``csharp var query = SqlMapper.Query<Product>(connection, "SELECT * FROM Products"); ``

5. Use DynamicParameters for Complex Parameters

DynamicParameters allows you to specify types and directions explicitly, which can improve performance.

6. Connection Pooling

Rely on ADO.NET connection pooling. Open and close connections quickly; don't hold them open.

7. Batch Operations

For multiple inserts/updates, use table-valued parameters or bulk insert via SqlBulkCopy. Dapper's Execute can handle multiple statements in one call if you pass a list:

``csharp var products = new List<Product> { ... }; db.Execute("INSERT INTO Products (Name, Price) VALUES (@Name, @Price)", products); ``

Key Takeaway: Optimize by selecting only needed columns, using unbuffered queries for large sets, and leveraging async I/O.

PerformanceExample.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Unbuffered query for large result set
var products = db.Query<Product>("SELECT Id, Name FROM Products", buffered: false);
foreach (var product in products)
{
    // Process one at a time
}

// Batch insert
var newProducts = new List<Product>
{
    new Product { Name = "A", Price = 1.0m },
    new Product { Name = "B", Price = 2.0m }
};
int rows = db.Execute("INSERT INTO Products (Name, Price) VALUES (@Name, @Price)", newProducts);
Console.WriteLine($"Inserted {rows} rows.");
Output
Inserted 2 rows.
🔥Buffered vs Unbuffered
📊 Production Insight
Always use async methods in ASP.NET Core to avoid thread pool starvation. Monitor query performance with a profiler.
🎯 Key Takeaway
Use buffered: false for large result sets to reduce memory usage. Batch multiple operations into single Execute calls.

Handling Dynamic Results and ExpandoObject

Sometimes you don't have a predefined POCO for the result. Dapper supports dynamic results using dynamic or ExpandoObject.

QueryDynamic

Use Query without a type parameter to get a collection of dynamic objects:

``csharp var rows = db.Query("SELECT Id, Name FROM Products"); foreach (dynamic row in rows) { Console.WriteLine($"{row.Id}: {row.Name}"); } ``

Using ExpandoObject

``csharp var rows = db.Query<ExpandoObject>("SELECT Id, Name FROM Products"); ``

When to Use Dynamic

  • When the result schema is unknown at compile time.
  • For quick prototyping or ad-hoc queries.
  • When working with NoSQL-like data or varying columns.

However, dynamic results lose compile-time safety and performance (no IL caching). Use sparingly in production.

Key Takeaway: Dapper can return dynamic objects for flexible scenarios, but prefer strongly-typed POCOs for performance and safety.

DynamicExample.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
var rows = db.Query("SELECT Id, Name FROM Products");
foreach (dynamic row in rows)
{
    Console.WriteLine($"{row.Id}: {row.Name}");
}

// Or with ExpandoObject
var expandoRows = db.Query<ExpandoObject>("SELECT Id, Name FROM Products");
foreach (var row in expandoRows)
{
    var dict = (IDictionary<string, object>)row;
    Console.WriteLine($"{dict["Id"]}: {dict["Name"]}");
}
Output
1: Widget
2: Gadget
3: Doohickey
⚠ Performance Impact
📊 Production Insight
If you need dynamic results frequently, consider using a JSON column or a document database instead.
🎯 Key Takeaway
Dynamic results are flexible but slower. Prefer strongly-typed POCOs for production code.

Integrating Dapper with ASP.NET Core

In ASP.NET Core applications, you typically register the database connection as a service and inject it into controllers or services.

Registering Dapper Services

``csharp builder.Services.AddTransient<IDbConnection>(sp => new SqlConnection(builder.Configuration.GetConnectionString("DefaultConnection"))); ``

```csharp public class ProductRepository { private readonly IDbConnection _db; public ProductRepository(IDbConnection db) { _db = db; }

public async Task<IEnumerable<Product>> GetAllAsync() { return await _db.QueryAsync<Product>("SELECT * FROM Products"); } } ```

Using Dapper in Controllers

```csharp [ApiController] [Route("api/[controller]")] public class ProductsController : ControllerBase { private readonly IDbConnection _db; public ProductsController(IDbConnection db) { _db = db; }

[HttpGet] public async Task<IActionResult> Get() { var products = await _db.QueryAsync<Product>("SELECT * FROM Products"); return Ok(products); } } ```

Best Practices

  • Use transient or scoped lifetime for connections. Scoped is common for web apps.
  • Avoid holding connections open across requests.
  • Use async methods to avoid blocking.
  • Consider using a repository pattern to abstract Dapper calls.

Key Takeaway: Dapper integrates easily with ASP.NET Core via dependency injection. Register IDbConnection and inject it where needed.

Startup.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddTransient<IDbConnection>(sp =>
    new SqlConnection(builder.Configuration.GetConnectionString("DefaultConnection")));

var app = builder.Build();
app.MapControllers();
app.Run();

// ProductsController.cs
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IDbConnection _db;
    public ProductsController(IDbConnection db)
    {
        _db = db;
    }

    [HttpGet]
    public async Task<IActionResult> Get()
    {
        var products = await _db.QueryAsync<Product>("SELECT * FROM Products");
        return Ok(products);
    }
}
Output
HTTP 200 with JSON array of products
💡Connection Lifetime
📊 Production Insight
Consider using a connection factory pattern to manage connection strings and provider selection, especially if you support multiple databases.
🎯 Key Takeaway
Register IDbConnection in DI and inject into your services. Use async methods for scalability.
● Production incidentPOST-MORTEMseverity: high

The N+1 Query Nightmare in a High-Traffic API

Symptom
API response times increased from 50ms to over 5 seconds, causing timeouts and user complaints.
Assumption
The developer assumed Dapper's Query method would batch all queries automatically when called in a loop.
Root cause
A foreach loop over a list of IDs executed a separate SQL query for each ID, resulting in N+1 queries instead of a single IN clause.
Fix
Replaced the loop with a single Query call using WHERE Id IN @Ids, passing the list of IDs as a parameter.
Key lesson
  • Always batch queries using IN clauses or JOINs instead of looping.
  • Use Dapper's built-in support for IEnumerable parameters to pass lists.
  • Monitor database query counts in production to detect N+1 patterns.
  • Consider using QueryMultiple for multiple result sets from a single query.
  • Profile your data access layer under realistic load before deployment.
Production debug guideSymptom to Action3 entries
Symptom · 01
Slow query execution
Fix
Enable Dapper's logging or use a profiler to capture the actual SQL generated. Check for missing indexes or parameter sniffing.
Symptom · 02
Mapping exceptions (InvalidCastException, NullReferenceException)
Fix
Verify that the SQL result columns match the property names/types of your POCO. Use column aliases if needed.
Symptom · 03
Connection pooling exhaustion
Fix
Ensure every IDbConnection is properly disposed (use 'using' statements). Check for async deadlocks where connections are not released.
★ Quick Debug Cheat SheetCommon Dapper issues and immediate fixes.
No data returned but SQL works in SSMS
Immediate action
Check parameter names: Dapper uses positional or named parameters matching SQL parameter names exactly.
Commands
SELECT * FROM Users WHERE Id = @Id
connection.Query<User>("SELECT * FROM Users WHERE Id = @Id", new { Id = 1 })
Fix now
Ensure the anonymous object property name matches the SQL parameter name (case-insensitive).
Mapping fails for complex types+
Immediate action
Use Dapper's multi-mapping feature (Query<T1,T2,TResult>) for joins.
Commands
SELECT * FROM Orders o INNER JOIN Customers c ON o.CustomerId = c.Id
connection.Query<Order, Customer, Order>(sql, (order, customer) => { order.Customer = customer; return order; }, splitOn: "Id")
Fix now
Specify the splitOn parameter to indicate where the first object ends and the second begins.
Async operations hanging+
Immediate action
Check for .Result or .Wait() calls on async methods causing deadlocks.
Commands
var result = await connection.QueryAsync<...>(sql);
Avoid blocking calls; use async all the way.
Fix now
Replace .Result with await in all calling methods.
FeatureDapperEntity Framework Core
TypeMicro-ORMFull ORM
PerformanceNear ADO.NET speedSlower due to abstraction
SQL ControlFull control (write raw SQL)LINQ and SQL generation
Change TrackingNoYes
MigrationsNoYes
Learning CurveLowMedium to High
Best ForHigh-performance, simple queriesComplex object graphs, rapid development
⚙ Quick Reference
7 commands from this guide
FileCommand / CodePurpose
SetupExample.csusing System.Data;Setting Up Dapper in Your .NET Project
QueryExamples.csusing (IDbConnection db = new SqlConnection(connectionString))Executing Queries and Commands
MappingExamples.cspublic class OrderMapping Results to Objects
TransactionExample.csusing (var connection = new SqlConnection(connectionString))Working with Transactions
PerformanceExample.csvar products = db.Query("SELECT Id, Name FROM Products", buffered: fals...Performance Optimization and Best Practices
DynamicExample.csvar rows = db.Query("SELECT Id, Name FROM Products");Handling Dynamic Results and ExpandoObject
Startup.csvar builder = WebApplication.CreateBuilder(args);Integrating Dapper with ASP.NET Core

Key takeaways

1
Dapper is a fast, lightweight micro-ORM that extends IDbConnection with simple methods for data access.
2
Always use parameterized queries to prevent SQL injection.
3
Use async methods in ASP.NET Core for better scalability.
4
Leverage multi-mapping for joins and dynamic results for flexible schemas.
5
Optimize performance by selecting only needed columns, using unbuffered queries, and batching operations.

Common mistakes to avoid

3 patterns
×

Not disposing the connection properly

×

Using SELECT * in production

×

Forgetting to open the connection before a transaction

INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain how Dapper maps query results to objects.
Q02JUNIOR
How do you prevent SQL injection when using Dapper?
Q03SENIOR
What is the difference between Query and QueryAsync?
Q01 of 03SENIOR

Explain how Dapper maps query results to objects.

ANSWER
Dapper uses reflection and IL generation to map column names to property names (case-insensitive). It caches the generated mapping IL for performance. For complex mappings, it supports multi-mapping with splitOn and custom type maps.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
What is Dapper and how does it differ from Entity Framework?
02
Can Dapper handle stored procedures?
03
Is Dapper thread-safe?
N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Drawn from code that ran under real load.

Follow
Verified
production tested
July 13, 2026
last updated
2,043
articles · all by Naren
🔥

That's ASP.NET. Mark it forged?

5 min read · try the examples if you haven't

Previous
Containerizing .NET Applications with Docker
31 / 35 · ASP.NET
Next
Dapper vs EF Core: Decision Guide