Connection open count survives the reset of a pooled DbContext, so later leases keep the connection open
Los mantenedores suelen responder en 2 días
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 42/100
Línea de trabajo
Start in src/EFCore.Relational/Storage/RelationalConnection.cs and compare ResetState with ResetStateAsync, especially the handling of _openedCount and openedInternally. Then read the existing RelationalConnectionTest cases and the DbContextPoolingTest cases for Handle_open_connection_when_returning_to_pool_for*. Add coverage for a later lease after returning a context with the connection open; done means both reset paths clear EF's open count while preserving the expected state of owned and external connections.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Bug description
When a pooled context goes back to the pool while EF still counts its connection as open (for example after Database.OpenConnectionAsync() without a matching CloseConnectionAsync()), the reset closes the DbConnection but leaves RelationalConnection's _openedCount and _openedInternally unchanged. Every later lease of that context instance then starts with the count at 1: its first command opens the connection and raises the count to 2, and the close after the command only lowers it to 1, so the connection stays open until the lease ends. As a result:
- each later lease holds a physical connection for its whole duration instead of per command;
DbConnectionInterceptor.ConnectionOpening/ConnectionOpenedrun only for the first command of such a lease instead of for every command;- the stale count carries over from lease to lease, so every later lease of that instance is affected (in 10.0, until a lease is returned through the synchronous path).
One lease that misses the close, for example after an exception, is enough. A connection opened directly with GetDbConnection().Open() does not trigger it, since EF does not count that open.
Which reset path is affected depends on the version:
- 10.0 (10.0.13; 8.0 and 9.0 have the same code): only the asynchronous path.
ResetState(disposeDbConnection: false)resets both fields (release/10.0, lines 1043-1044), butResetStateAsync(disposeDbConnection: false)only does so insideif (disposeDbConnection)(lines 1068-1092). So the context is affected afterDisposeAsync()/await using, not afterDispose(). WithAddDbContextPoolin ASP.NET Core the asynchronous path is the usual one, since request scopes are disposed asynchronously.ResetStateAsynchas not reset the count since #24207 separated it fromDisposein 6.0; since 7.0 (#28519, for #27308) it closes an owned connection, which leads to this symptom. - main and 11.0 (since 11.0.0-preview.3, including rc.2): both paths. #37916 removed the unconditional
_openedCount = 0; _openedInternally = false;fromResetState(main, lines 1027-1047), soDispose()is now affected too, a regression from 10.0. The Copilot review on that PR pointed out this consequence. The same reset runs whenSetDbConnectionreplaces a connection that the context does not own, so in main and 11.0 a count EF still holds for the old connection carries over to the new one.
A command's close only closes the connection when ShouldClose() brings the count to zero (lines 994-998), which no longer happens once a lease starts with the count at 1.
The repro below prints, with 10.0.13 and DisposeAsync(), and with 11.0.0-rc.2 and either DisposeAsync() or Dispose():
Lease 2, command 1: Open, ConnectionOpened calls: 2
Lease 2, command 2: Open, ConnectionOpened calls: 2
Lease 3, command 1: Open, ConnectionOpened calls: 3
Lease 3, command 2: Open, ConnectionOpened calls: 3
Expected, as 10.0.13 prints with Dispose():
Lease 2, command 1: Closed, ConnectionOpened calls: 2
Lease 2, command 2: Closed, ConnectionOpened calls: 3
Lease 3, command 1: Closed, ConnectionOpened calls: 4
Lease 3, command 2: Closed, ConnectionOpened calls: 5
Workaround: call CloseConnection(Async) for every OpenConnection(Async) before the context is returned to the pool (closing the DbConnection directly does not lower EF's count); in 10.0, returning it with Dispose() also clears the count.
Proposed fix: reset _openedCount and _openedInternally in both ResetState and ResetStateAsync regardless of disposeDbConnection and of whether the context owns the connection, as ResetState did from 6.0 until #37916. I have this change with tests in RelationalConnectionTest and, in DbContextPoolingTest, a query in the second lease of the Handle_open_connection_when_returning_to_pool_for_* tests added for #27308: afterwards an owned connection must be closed and an external one still open. Those tests checked the state on return, but not what a later lease does. I'd be happy to send a pull request against main if this approach works for you. Since the synchronous part is a regression from 10.0 in 11.0, which hasn't shipped yet, you may want the fix in 11.0 as well; if you also fix this in 10.0, only the two lines in ResetStateAsync are needed there.
Related but different: #36218 (disposing a connection calls no closing interceptor) and #38983 (the concurrency detector was not reset when a pooled context was returned).
Your code
// Console app: <TargetFramework>net10.0</TargetFramework>, <ImplicitUsings>enable</ImplicitUsings>,
// <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.13" />
// (for 11.0: net11.0 and 11.0.0-rc.2.26504.105)
using System.Data.Common;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
// true: DisposeAsync (affected in 10.0 and 11.0); false: Dispose (affected in 11.0)
var disposeAsync = true;
var interceptor = new ConnectionOpenedCounter();
var options = new DbContextOptionsBuilder<BlogContext>()
.UseSqlite("Data Source=:memory:")
.AddInterceptors(interceptor)
.Options;
var factory = new PooledDbContextFactory<BlogContext>(options);
// Lease 1: open the connection and return the context to the pool without closing it.
var context = factory.CreateDbContext();
await context.Database.OpenConnectionAsync();
await DisposeContext(context);
// Leases 2 and 3 get the same instance; each command should open and close the connection.
for (var lease = 2; lease <= 3; lease++)
{
context = factory.CreateDbContext();
var connection = context.Database.GetDbConnection();
for (var command = 1; command <= 2; command++)
{
await context.Database.ExecuteSqlRawAsync("SELECT 1");
Console.WriteLine($"Lease {lease}, command {command}: {connection.State}, ConnectionOpened calls: {interceptor.Count}");
}
await DisposeContext(context);
}
async Task DisposeContext(DbContext context)
{
if (disposeAsync)
{
await context.DisposeAsync();
}
else
{
context.Dispose();
}
}
public class BlogContext(DbContextOptions<BlogContext> options) : DbContext(options);
public class ConnectionOpenedCounter : DbConnectionInterceptor
{
public int Count { get; private set; }
public override void ConnectionOpened(DbConnection connection, ConnectionEndEventData eventData)
=> Count++;
public override Task ConnectionOpenedAsync(
DbConnection connection, ConnectionEndEventData eventData, CancellationToken cancellationToken = default)
{
Count++;
return Task.CompletedTask;
}
}
Stack traces
Verbose output
EF Core version
10.0.13, 11.0.0-rc.2.26504.105
Database provider
Microsoft.EntityFrameworkCore.Sqlite (the code is in Microsoft.EntityFrameworkCore.Relational)
Target framework
.NET 10.0, .NET 11.0 (rc.1)
Operating system
Windows 11
IDE
dotnet CLI
- Lenguaje dominante
- C#
- Estrellas
- 14.8k
- Forks
- 3.4k
- Merge medio
- 3 d 18 h
- PR fusionados (30 d)
- 159
Preparar el entorno
Inicia el contenedor de desarrollo del proyecto en tu navegador, con tu propia cuenta de GitHub.
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de dotnet/efcore
-
customer-reported
Dificultad 4/5 3-5 días Aptitud para principiantes 54/100
Los mantenedores suelen responder en 2 días
-
Dificultad 4/5 3-5 días Aptitud para principiantes 48/100
Los mantenedores suelen responder en 2 días
-
Dificultad 4/5 3-5 días Aptitud para principiantes 48/100
Los mantenedores suelen responder en 2 días
-
Regression in 11.0: replacing an owned instance or re-adding an entity with the same key sets unchanged OnAdd properties to nullPosiblemente ocupada @seragAhmed la tomó hoy. Abierto
Dificultad 4/5 3-5 días Aptitud para principiantes 48/100
dotnet/efcore#39617 · 1 comentario ·
Los mantenedores suelen responder en 2 días
-
customer-reported
Dificultad 4/5 3-5 días Aptitud para principiantes 48/100
Los mantenedores suelen responder en 2 días
Todos los issues de dotnet/efcore
Issues similares
-
bug emitter:client:csharp
Dificultad 2/5 1-3 horas Aptitud para principiantes 84/100
microsoft/typespec#12572 · 1 comentario · 1 reacción ·
Los mantenedores suelen responder en 1 día
-
needs-area-label
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
Los mantenedores suelen responder en 2 días
-
AWS Lambda: KeyNotFoundException drops HTTP-triggered transactions when allowAllHeaders is disabledAbiertobug
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
newrelic/newrelic-dotnet-agent#3949 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
area-networking
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
dotnet/aspnetcore#70545 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 87/100
Los mantenedores suelen responder en 1 día