Introducción

En aplicaciones EF Core, cada instancia de DbContext establece internamente conexiones y servicios que conllevan cierta sobrecarga. Aunque crear y eliminar un DbContext es relativamente ligero (no implica operar con la base de datos por sí solo), en escenarios de alta concurrencia la repetición continua de esa inicialización puede notarse en rendimiento. Además, las bases de datos (especialmente en la nube) imponen límites de sesiones/ conexiones. Optimizar el ciclo de vida del DbContext y la gestión de conexiones es clave para evitar errores de límite (por ejemplo “session limit reached”) y asegurar que la aplicación escala bien tanto en Azure SQL como en entornos on-premises.

AddDbContext vs AddDbContextPool

Por defecto en ASP.NET Core usamos AddDbContext<TContext>(), que registra el contexto como scoped (una instancia por petición) y lo elimina al final de cada petición. Esto es sencillo y seguro, pero crea un nuevo objeto DbContext en cada unidad de trabajo. EF Core ofrece AddDbContextPool<TContext>() para reutilizar instancias de contexto: al disponer un contexto, EF Core restablece su estado y lo guarda en un pool interno, devolviendo instancias recicladas en lugar de crear nuevaslearn.microsoft.com. Esto reduce significativamente la sobrecarga de inicialización si la aplicación recibe muchas peticiones por segundo.

// Registro sin pooling (nuevo DbContext por petición)
builder.Services.AddDbContext<MiContexto>(options =>
    options.UseSqlServer(connectionString));

// Registro con pooling (reutiliza instancias de DbContext)
builder.Services.AddDbContextPool<MiContexto>(options =>
    options.UseSqlServer(connectionString));

En términos prácticos, AddDbContextPool ofrece ventaja de rendimiento en cargas altas. Sin embargo, tiene algunas restricciones: el DbContext debe poder reiniciarse con seguridad. EF Core advierte que si el DbContext define propiedades privadas con estado que no deban compartirse entre usos, entonces no conviene usar pooling. Por ejemplo, evitar guardar en el contexto colecciones o configuraciones cambiantes que persistan tras SaveChanges. En general, los escenarios comunes (entidades, consultas LINQ, transacciones estándar) funcionan bien con pooling.

Ventajas de AddDbContextPool: ahorro de CPU/GC al no reconstruir servicios internos, mejor throughput bajo alto load.
Precauciones: no es independiente de la agrupación de conexiones ADO.NET (una cosa es el pool de DbContext y otra el pool de conexiones SQL). Si tu contexto tiene lógica extra (hooks, eventos) asegúrate de que EF pueda resetearlo correctamente. Y recuerda que bajo el capó, al devolver un contexto al pool, EF llama a Dispose() en su conexión subyacente para reiniciarlo.

Impacto en Azure SQL

En Azure SQL Database existen límites estrictos según el nivel de servicio. Por ejemplo, el nivel Basic permite sólo 30 solicitudes concurrentes (workers) y hasta 300 sesiones simultáneas. Superar estos límites causa errores como «The request limit for the database is 30 and has been reached» o «session limit 300 has been reached». Estos errores son comunes si una aplicación abre muchas conexiones en paralelo. Por ejemplo, un bucle de Azure Data Factory que lanza 30 hilos fue bloqueado por el error “request limit 30”medium.com. Subir el nivel (p. ej. Standard/S0 permite ~60 conexiones concurrentes) es una solución cuando se requiere más concurrenciamedium.com.

Para evitar saturar el servicio Azure SQL, conviene:

  • Controlar conexiones concurrentes: Conectar/deconectar rápido. Usar using o dejar que DI elimine el contexto para que la conexión se libere de inmediato.

  • Pooling de conexiones: .NET tiene por defecto pool de 100 conexiones. Puede ajustarse vía cadena, e.g. Max Pool Size=200.

  • Retries en conexiones: Habilitar connection resiliency ayuda con cortes intermitentes. Por ejemplo:

options.UseSqlServer(connStr, sqlOpts => sqlOpts.EnableRetryOnFailure());

Además, en Elastic Pools de Azure se comparten recursos entre varias bases. El límite total de sesiones por pool suele ser muy alto (ej. 30 000), pero cada base dentro sigue limitada individualmente por su tier. En cualquier caso, se recomienda monitorear el uso de conexiones. Por ejemplo, en T-SQL se puede consultar el conteo de sesiones activas:

SELECT COUNT(*) AS SesionesActivas
FROM sys.dm_exec_sessions
WHERE is_user_process = 1;

También el portal de Azure ofrece métricas de conexiones activas, DTU/CPU, etc. Estas herramientas ayudan a detectar cuándo se está cerca del límite y a dimensionar el servicio de forma adecuada.

Consideraciones en entornos on-premises

En SQL Server on-premises o en máquinas virtuales no existen los límites de SNAT/PUERTOS que Azure impone, pero sí hay buenas prácticas similares. El servidor on-prem típicamente admite muchos más usuarios concurrentes (hasta decenas de miles), aunque el pool de ADO.NET por defecto (100 conexiones por string) sigue aplicando. Si tu aplicación genera muchas conexiones simultáneas, puedes aumentar Max Pool Size en la cadena de conexión o revisar el número máximo de conexiones del servidor.

A pesar de la mayor capacidad, no conviene descuidar el control de conexiones: siempre cierre el contexto al terminar de usarlo. Dejar conexiones abiertas puede agotar el pool local o dejar locks/pesos en la base. En on-prem podemos usar herramientas nativas para monitorear, por ejemplo:

SELECT session_id, login_name, status
FROM sys.dm_exec_sessions
WHERE is_user_process = 1;

También sp_who2 o el Monitor de actividad de SSMS muestran conexiones abiertas. En resumen, aunque on-prem tiene más holgura, las mismas buenas prácticas de disposing y lifetimes aplican: no use DbContext como objeto de larga duración, cierre recursos a tiempo y configure el pool de ADO.NET si es necesario.

 

Buenas prácticas del ciclo de vida de DbContext

  • Lifetime apropiado: El DbContext debe usarse como unidad de trabajo. Esto significa crear una instancia por operación lógica (p.ej. petición HTTP, tarea o transacción). En ASP.NET Core se recomienda el scope por petición: AddDbContext lo registra como scoped y el contenedor de DI lo eliminará al finalizar la petición. No intente usar un mismo DbContext para toda la aplicación.

  • Evitar Singleton: No registre DbContext como AddSingleton. Un contexto no es thread-safe, por lo que provocaría corrupción de datos si varias peticiones lo usan simultáneamente. Siempre use AddDbContext (scoped) o AddDbContextPool (también scoped, pero reciclable).

  • Uso de using / Dispose: Como regla general, “dispose lo que creas”. Si de alguna forma crea un contexto fuera del DI (por ejemplo, new MiContexto() o usando un DbContextFactory), envuélvalo en un bloque using (o await using) para asegurarse de que se liberen conexiones. En una API con DI esto suele ser automático (al terminar la petición, el contenedor llama a Dispose en el contexto), pero en otros escenarios (console apps, background services) es responsabilidad del desarrollador.

  • DbContextFactory: En .NET 6+ está disponible AddDbContextFactory<TContext>(). Esto registra una fábrica de contextos que puede inyectarse como IDbContextFactory<TContext>. Es útil si necesita crear múltiples contextos de forma manual (por ejemplo, en un bucle dentro de una misma petición, o en un IHostedService). Ejemplo de uso:

// En Program.cs
builder.Services.AddDbContextFactory<MiContexto>(options =>
    options.UseSqlServer(connectionString));

// En alguna clase
public class MiServicio {
    private readonly IDbContextFactory<MiContexto> _factory;
    public MiServicio(IDbContextFactory<MiContexto> factory) => _factory = factory;

    public void Procesar() {
        using var context = _factory.CreateDbContext();
        // ... usar context ...
    }
}
  • Otros consejos: Evite mantener el contexto vivo innecesariamente (por ej. en variables estáticas o propiedades de clase larga duración). Use EnableSensitiveDataLogging sólo en desarrollo, para no cargar el contexto con información privada. En escenarios de alta latencia (p. ej. Azure), combine pooling de contexto con políticas de reintento (EnableRetryOnFailure) para mayor resiliencia.

Resumen práctico

En resumen, AddDbContextPool es una herramienta valiosa para optimizar el rendimiento de EF Core bajo cargas intensas. Use pooling cuando su aplicación reciba muchas peticiones concurrentes por segundo y sea crítico reducir la sobrecarga de inicialización de contextos. Tenga en cuenta que al habilitar pooling el DbContext debe diseñarse para ser “restablecido” de forma segura: si su contexto conserva estado privado entre consultas, es mejor no usarlo. Si no está seguro o su aplicación es pequeña/mediana, AddDbContext tradicional suele ser suficiente y más sencillo.

En cualquier caso, asegúrese de gestionar bien las conexiones: disponga el contexto tras cada uso (el contenedor DI lo hace automáticamente en las peticiones web) o use using si lo crea manualmente. Ajuste el tamaño del pool de conexiones ADO.NET (Max Pool Size) según necesite y monitoree los límites de su servidor (especialmente en Azure SQL). Para entornos en la nube, use EnableRetryOnFailure y esté al tanto de los límites de servicio (concurrentes y sesiones) para evitar errores de saturación. De esta forma logrará un equilibrio entre eficiencia y claridad de diseño: use pooling cuando aporte mejoras tangibles, pero mantenga siempre un ciclo de vida de DbContext controlado y predecible.

var connectionString = builder.Configuration.GetConnectionString("MiBaseDatos");

// Registro con pooling y retry:
builder.Services.AddDbContextPool<MiContexto>(options =>
    options.UseSqlServer(connectionString, sqlOpts =>
        sqlOpts.EnableRetryOnFailure()));

app = builder.Build();
// ...

Con esto, EF Core administrará contextos eficientes y conexiones robustas tanto en Azure como on-premises, evitando errores comunes por malas prácticas.