Using the Singleton Connection Pool with EF

This post explains how connection pooling works with EF Core, the benefits of implementing a Singleton connection pool, and how to set it up correctly.

Using the Singleton Connection Pool with Entity Framework Core

Getting Started

Efficient database connectivity is a critical part of building scalable .NET applications. In high-traffic scenarios—such as web APIs or background services—creating and disposing of database connections too frequently can lead to significant performance bottlenecks.

This is where connection pooling and the singleton connection pool pattern come into play. In this article, we’ll explore how connection pooling works in Entity Framework Core (EF Core), why you might consider using a singleton connection pool, and how to implement it correctly.

Understanding Connection Pooling in EF Core

When you use EF Core with a relational provider like SQL Server, PostgreSQL, or MySQL, the underlying ADO.NET provider manages connection pooling automatically.

A connection pool keeps a cache of open database connections that can be reused, instead of opening a new connection for every request. This drastically reduces overhead from repeated connection establishment.

For example, when EF Core executes a query:
using var context = new AppDbContext();
var data = context.Users.ToList();

The context requests a connection from the pool. Once disposed, the connection is returned to the pool for reuse.

The Singleton DbContext Is Not the Same Thing

A common misconception is that a singleton DbContext improves performance. In reality, DbContext should never be registered as a singleton because it is not thread-safe and is designed to be short-lived (per operation or per request).

Instead, the correct approach is to make sure the underlying connection pool (not the context) is shared efficiently across requests.

Why Consider a Singleton Connection Pool?

Under normal circumstances, EF Core and ADO.NET already provide connection pooling automatically, as long as your connection string is identical across instances.

However, there are cases where you might explicitly want to manage a singleton connection pool:
  • Custom connection management: You need to control connection lifetimes, max pool size, or timeouts.
  • Database multiplexing or dynamic connection strings: You’re switching between multiple databases and need separate pools for each.
  • High-frequency service calls: Your application opens and closes many connections per second.
In these cases, creating a singleton DbContext factory or using a pooled DbContext pattern can improve performance.

Using EF Core’s Built-in Context Pooling

The simplest and safest way to enable efficient pooling in EF Core is to use AddDbContextPool instead of AddDbContext in your dependency injection setup.

Example:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add EF Core DbContext with pooling
builder.Services.AddDbContextPool<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
);

How It Works
  • EF Core maintains an internal pool of AddDbContext instances.
  • Each time a context is requested, EF Core reuses a pre-warmed instance instead of creating a new one.
  • When the context is released, EF Core resets its state and returns it to the pool.

This approach avoids the cost of building and disposing contexts repeatedly, while still maintaining thread safety and the unit-of-work pattern.

Implementing a Singleton Connection Pool Manually

If you need finer control—such as using a shared connection factory—you can manually create a singleton DbConnection pool. Here’s a simplified example using SQL Server:

using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
public static class ServiceCollectionExtensions
{
  public static IServiceCollection AddSingletonConnectionPool(this IServiceCollection services, string connectionString)
  {
    // Create a shared SqlConnectionFactory
    services.AddSingleton(() => new SqlConnection(connectionString));
    services.AddDbContext<AppDbContext>((provider, options) =>
    {
      var connection = provider.GetRequiredService<SqlConnection>();
      options.UseSqlServer(connection);
      });
      return services;
    }
  }

Caution:

Sharing a single SqlConnection across multiple threads is not safe. If you do this, you must ensure access synchronization or use a proper connection factory that retrieves connections from ADO.NET’s internal pool.

Thread-Safe Singleton Connection Pool Example (C#)

Here’s a complete, thread-safe, and production-safe implementation that works with SQL Server or PostgreSQL.

using System;
using System.Data.Common;
using Microsoft.Data.SqlClient; // or Npgsql for PostgreSQL
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
public sealed class DbConnectionPoolManager
{
  private static readonly Lazy<DbConnectionPoolManager> _instance =
  new(() => new DbConnectionPoolManager());
  private string? _connectionString;
  private Func<DbConnection>? _connectionFactory;
  private readonly object _lock = new();
  private DbConnectionPoolManager() { }
  public static DbConnectionPoolManager Instance => _instance.Value;
  public void Initialize(string connectionString)
  {
    if (_connectionFactory != null)
    return; // already initialized safely
    lock (_lock)
    {
      if (_connectionFactory == null)
      {
        _connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
        // Create factory lazily; this ensures thread-safe init
        _connectionFactory = () => new SqlConnection(_connectionString);
      }
    }
  }
  /// <summary>
  /// Returns a new (pooled) connection. The ADO.NET provider will handle pooling.
  /// </summary>
  public DbConnection GetOpenConnection()
  {
    if (_connectionFactory == null)
    throw new InvalidOperationException("ConnectionPoolManager not initialized. Call Initialize() first.");
    var conn = _connectionFactory();
    conn.Open(); // pulls from ADO.NET pool
    return conn;
  }
}

Why This Is Thread Safe
  • DbConnectionPoolManager is a true singleton, created via Lazy<T>.
  • Initialization uses a lock to prevent race conditions.
  • Each thread/request calls GetOpenConnection() → receives a fresh connection from ADO.NET’s pool.
  • The pool is managed by the driver, not manually by your code.
  • No shared mutable connection objects are reused across threads.

A better pattern is to let the ADO.NET provider handle pooling automatically, while you manage only the configuration.

Configuring ADO.NET Connection Pool Settings

You can control connection pooling behavior directly in your connection string Instead of managing your own singleton, you can often just use:

Server=myServer;Database=myDb;User Id=myUser;Password=myPass;
Max Pool Size=100;Min Pool Size=5;Pooling=true;

Adjusting these values allows you to optimize resource usage for your workload.
Setting Description
Pooling Enables or disables connection pooling (default: true)
Max Pool Size Maximum number of connections in the pool
Min Pool Size Minimum number of connections maintained
Connection Lifetime Maximum lifetime (in seconds) for a connection in the pool

Best Practices
  1. Use AddDbContextPool whenever possible — it’s safe, simple, and efficient.
  2. Avoid singleton DbContext instances — they cause concurrency issues.
  3. Use a consistent connection string so the same pool is reused.
  4. Monitor your pool — use SQL Server’s DMVs (sys.dm_exec_connections, sys.dm_os_performance_counters) or Application Insights.
  5. Benchmark your configuration under realistic load to tune pool sizes

Summary

While Entity Framework Core and ADO.NET already manage connection pooling for you, understanding and controlling the singleton connection pool pattern can help you optimize performance in demanding scenarios.

Thanks

Kailash Chandra Behera

I am an IT professional with over 13 years of experience in the full software development life cycle for Windows, services, and web-based applications using Microsoft .NET technologies.

Previous Post Next Post

نموذج الاتصال