In a high-traffic ASP.NET Core Web API, the database is often the bottleneck. While Dapper is famous for its speed, it is a "Micro-ORM," meaning it handles the mapping but leaves the connection management to you. If you don't handle connections correctly, your application will suffer from "Connection Pool Exhaustion," leading to site-wide timeouts and crashes.
1. How Connection Pooling Works in .NET
When you request a connection (e.g., new SqlConnection()), the ADO.NET driver doesn't immediately build a physical pipe to the database. Instead, it looks into a Pool.
If an idle connection exists: It is "leased" to your code.
If no idle connection exists: A new one is created (up to the
Max Pool Size).When you Dispose: The connection isn't closed; it’s cleaned and returned to the pool for the next request.
2. The Implementation: Step-by-Step
Step 1: Configure the Connection String
In your appsettings.json, define your pool limits. For production, it's wise to set a Min Pool Size to avoid "cold start" latency.
{
"ConnectionStrings": {
"DefaultConnection": "Server=myServer;Database=myDb;User Id=myUser;Password=myPassword;Min Pool Size=10;Max Pool Size=200;Connect Timeout=20;"
}
}
Step 2: Create a Connection Factory
We avoid injecting IDbConnection directly to prevent lifecycle issues. Instead, we inject a Factory that creates a connection exactly when we need it.
using Microsoft.Data.SqlClient;
using System.Data;
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);
}
Step 3: Register in Dependency Injection
In Program.cs, register the factory as a Singleton.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddSingleton<IDbConnectionFactory>(new SqlServerConnectionFactory(connectionString));
Step 4: Building the Repository (The "Safe" Way)
This is the most critical part. We use the using var statement to ensure the connection is returned to the pool the moment the method finishes, even if an error occurs.
public class ProductRepository
{
private readonly IDbConnectionFactory _connectionFactory;
public ProductRepository(IDbConnectionFactory connectionFactory)
{
_connectionFactory = connectionFactory;
}
public async Task<IEnumerable<Product>> GetAllAsync()
{
// 1. Connection is pulled from the pool
using var connection = _connectionFactory.CreateConnection();
const string sql = "SELECT * FROM Products";
// 2. Dapper handles the .Open() and .Close() internally
// if the connection state is closed.
return await connection.QueryAsync<Product>(sql);
// 3. At the '}', Dispose() is called, and the connection
// returns to the pool immediately.
}
}
Join the conversation! Your thoughts help the community grow.