In the world of high-performance .NET applications, Dapper is often the "Goldilocks" choice: it provides the raw speed of ADO.NET with the developer-friendly mapping of an ORM. However, while Dapper makes querying easy, it doesn't automate infrastructure.
The most critical piece of that infrastructure is connection pooling. If managed incorrectly, your API will suffer from latency spikes, "pool exhaustion" errors, and eventual crashes under load.
What is connection pooling?
Opening a physical connection to a database engine (like SQL Server or PostgreSQL) is an expensive operation involving network handshakes and authentication. To solve this, the .NET data provider maintains a Pool of connections.
When you call connection.Open(), the provider checks the pool for an idle connection. When you call Dispose() or Close(), the connection isn't killed; it’s simply wiped and returned to the pool for the next caller.
1. The Architecture of a Healthy Connection
In an ASP.NET Core environment, you must ensure that connections are short-lived. The lifecycle should follow this strict sequence:
Instantiate the connection.
Open the connection (Dapper does this automatically if you haven't).
Execute the query.
Dispose (Return to pool).
The Implementation Pattern
The using declaration is your best friend. It ensures that even if an exception occurs during a database call, the connection is safely returned to the pool.
public async Task<User?> GetUserByIdAsync(int id)
{
using var connection = new SqlConnection(_connectionString);
const string sql = "SELECT * FROM Users WHERE Id = @Id";
// Dapper handles the Open/Close logic internally if the state is Closed
return await connection.QueryFirstOrDefaultAsync<User>(sql, new { Id = id });
}
2. Managing Connections via Dependency Injection
Hardcoding connection strings inside your repositories is a recipe for maintenance nightmares. Instead, leverage ASP.NET Core’s Dependency Injection (DI).
Option A: The Scoped Connection (Simple)
This approach provides a single connection instance per HTTP request.
// Program.cs
builder.Services.AddScoped<IDbConnection>(sp =>
new SqlConnection(builder.Configuration.GetConnectionString("DefaultConnection")));
Option B: The Connection Factory (Scalable)
For more complex scenarios—like connecting to multiple databases or handling transient faults—a Factory is superior.
public interface IDbConnectionFactory
{
IDbConnection CreateConnection();
}
public class SqlServerConnectionFactory : IDbConnectionFactory
{
private readonly string _connectionString;
public SqlServerConnectionFactory(string connectionString) => _connectionString = connectionString;
public IDbConnection CreateConnection() => new SqlConnection(_connectionString);
}
// Program.cs
builder.Services.AddSingleton<IDbConnectionFactory>(new SqlServerConnectionFactory(connectionString));
3. Tuning the Pool in the Connection String
Connection pooling is configured via the connection string. Here are the levers you can pull to optimize performance:
Join the conversation! Your thoughts help the community grow.