Dapper
.NET
Database Connections
ORM
C#

How do I handle Database Connections with Dapper in .NET?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Dapper is lightweight because it works directly on top of ADO.NET connections instead of hiding them behind a heavy abstraction. That means connection handling is your responsibility, and good Dapper code depends on short-lived pooled connections, explicit transactions, and clear ownership of when a connection is opened and disposed.

The Core Rule: Use Short-Lived Connections

In most applications, you should not keep one open database connection around as a singleton. ADO.NET already provides connection pooling, so the normal Dapper pattern is:

  1. create a connection
  2. open it
  3. execute the query or command
  4. dispose it

Example:

csharp
1using System.Data.SqlClient;
2using Dapper;
3
4public sealed class UserRepository
5{
6    private readonly string _connectionString;
7
8    public UserRepository(string connectionString)
9    {
10        _connectionString = connectionString;
11    }
12
13    public async Task<User?> GetByIdAsync(int id, CancellationToken ct)
14    {
15        const string sql = "SELECT Id, Email, DisplayName FROM Users WHERE Id = @Id";
16
17        await using var connection = new SqlConnection(_connectionString);
18        await connection.OpenAsync(ct);
19
20        return await connection.QuerySingleOrDefaultAsync<User>(
21            new CommandDefinition(sql, new { Id = id }, cancellationToken: ct)
22        );
23    }
24}

Even though you create a new SqlConnection object per call, the underlying physical connection is usually reused from the pool.

Transactions Belong Around Multi-Step Writes

If one operation spans several statements, wrap them in a transaction and pass that transaction into each Dapper call:

csharp
1public async Task CreateOrderAsync(Order order, CancellationToken ct)
2{
3    await using var connection = new SqlConnection(_connectionString);
4    await connection.OpenAsync(ct);
5
6    await using var tx = await connection.BeginTransactionAsync(ct);
7    try
8    {
9        const string insertOrder = @"
10            INSERT INTO Orders(CustomerId, CreatedAtUtc)
11            VALUES (@CustomerId, @CreatedAtUtc);
12            SELECT CAST(SCOPE_IDENTITY() as int);";
13
14        var orderId = await connection.QuerySingleAsync<int>(
15            new CommandDefinition(insertOrder, order, transaction: tx, cancellationToken: ct)
16        );
17
18        const string insertLine = @"
19            INSERT INTO OrderLines(OrderId, Sku, Quantity)
20            VALUES (@OrderId, @Sku, @Quantity);";
21
22        foreach (var line in order.Lines)
23        {
24            await connection.ExecuteAsync(
25                new CommandDefinition(
26                    insertLine,
27                    new { OrderId = orderId, line.Sku, line.Quantity },
28                    transaction: tx,
29                    cancellationToken: ct
30                )
31            );
32        }
33
34        await tx.CommitAsync(ct);
35    }
36    catch
37    {
38        await tx.RollbackAsync(ct);
39        throw;
40    }
41}

Without this, partial writes are easy to create.

Centralize Connection Creation

In larger codebases, repeating new SqlConnection(...) everywhere makes it harder to change behavior consistently. A small factory helps:

csharp
1using System.Data;
2using System.Data.SqlClient;
3
4public interface IDbConnectionFactory
5{
6    Task<IDbConnection> CreateOpenConnectionAsync(CancellationToken ct);
7}
8
9public sealed class SqlConnectionFactory : IDbConnectionFactory
10{
11    private readonly string _connectionString;
12
13    public SqlConnectionFactory(string connectionString)
14    {
15        _connectionString = connectionString;
16    }
17
18    public async Task<IDbConnection> CreateOpenConnectionAsync(CancellationToken ct)
19    {
20        var connection = new SqlConnection(_connectionString);
21        await connection.OpenAsync(ct);
22        return connection;
23    }
24}

This makes repositories easier to test and keeps connection setup consistent.

Let Connection Pooling Do Its Job

A common anti-pattern is trying to "optimize" by keeping one shared connection open. That usually creates threading and resilience problems instead of performance gains.

With Dapper, you normally want:

  • short-lived connection objects
  • a stable connection string
  • pooling left to the provider

That is the path ADO.NET is already optimized for.

Parameterize Everything

Dapper makes parameterized SQL easy, and connection-handling discipline goes together with query discipline:

csharp
1const string sql = "SELECT * FROM Users WHERE Email = @Email";
2var user = await connection.QuerySingleOrDefaultAsync<User>(
3    sql,
4    new { Email = email }
5);

Do not build SQL by string concatenation. That creates correctness issues and opens the door to injection vulnerabilities.

Cancellation and Timeouts Matter

In web apps and background services, pass cancellation tokens through your commands. If the request has been canceled or the job is shutting down, the database call should not keep running unnecessarily.

Also remember that long-running queries are not a connection-management success story. If connections stay occupied for too long, the pool becomes a bottleneck.

Common Pitfalls

  • Holding one open connection as a singleton service.
  • Forgetting to dispose the connection and slowly exhausting the pool.
  • Running several related write statements without a transaction.
  • Building SQL by string concatenation instead of parameterization.
  • Ignoring cancellation, timeouts, and query duration when diagnosing pool pressure.

Summary

  • Use short-lived pooled connections and dispose them promptly.
  • Wrap multi-step writes in explicit transactions.
  • Centralize connection creation if the codebase is large enough to benefit from it.
  • Let ADO.NET pooling handle reuse instead of keeping one global connection open.
  • Good Dapper code is mostly disciplined ADO.NET code with better query mapping.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.