Disposing a RelationalConnection does not call IDbConnectionInterceptor.ConnectionClosing* or IDbConnectionInterceptor.ConnectionClosed*
- Dominant language
- C#
- Stars
- 14.8k
- Forks
- 3.4k
- PR merge metrics
- PR metrics pending
Description
### Bug description
Run the attached code once just to initialize the database file. On subsequent runs, observe that the opening/opened count (from the interceptor) is 3, while the closing/closed count (from the interceptor) is only 1. However, the connection state change transition counts (from `DbConnection.StateChange` event handler) is 3 for both Closed->Open as well as Open->Closed:
```
Opening count = 3
Opened count = 3
Closing count = 1
Closed count = 1
Connection state change transition counts:
Closed->Open: 3
Open->Closed: 3
```
What I would expect to see in this case is that the closing/closed count from the interceptor is 3 as well to match the opening/opened count from the interceptor. The problem seems to be that `SqliteDatabaseCreator.Exists()` implicitly disposes of `readOnlyConnection` at the end of the scope. `RelationalConnection.Dispose()` then calls `RelationalConnection.ResetState(disposeDbConnection: true)`, which calls `RelationalConnection.CloseDbConnection()`, which calls `DbConnection.Close()`. Notably, `RelationalConnection.Dependencies.ConnectionLogger.{ConnectionClosing, ConnectionClosed}` are never invoked in this code path and so the interceptor is never called.
### Your code
```csharp
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.Concurrent;
using System.Data;
using System.Data.Common;
var connectionInterceptor = new ConnectionInterceptor();
var dbContextOptions = new DbContextOptionsBuilder()
.UseSqlite(
new SqliteConnectionStringBuilder
{
DataSource = "test.db",
Pooling = false,
}.ConnectionString
).AddInterceptors(connectionInterceptor)
.Options;
await using var db = new MyContext(dbContextOptions);
await db.Database.MigrateAsync();
Console.WriteLine($"Opening count = {connectionInterceptor.ConnectionOpeningCount}");
Console.WriteLine($"Opened count = {connectionInterceptor.ConnectionOpenedCount}");
Console.WriteLine($"Closing count = {connectionInterceptor.ConnectionClosingCount}");
Console.WriteLine($"Closed count = {connectionInterceptor.ConnectionClosedCount}");
Console.WriteLine("Connection state change transition counts:");
foreach (var (stateChange, count) in connectionInterceptor.StateChangeCounts)
Console.WriteLine($" {stateChange.Item1}->{stateChange.Item2}: {count}");
sealed class MyContext(DbContextOptions options) : DbContext(options)
{
}
sealed class ConnectionInterceptor : DbConnectionInterceptor
{
public ConcurrentDictionary<(ConnectionState, ConnectionState), int> StateChangeCounts { get; } = new();
private int _connectionClosedCount = 0;
public int ConnectionClosedCount => _connectionClosedCount;
private int _connectionClosingCount = 0;
public int ConnectionClosingCount => _connectionClosingCount;
private int _connectionOpenedCount = 0;
public int ConnectionOpenedCount => _connectionOpenedCount;
private int _connectionOpeningCount = 0;
public int ConnectionOpeningCount => _connectionOpeningCount;
public override DbConnection ConnectionCreated(ConnectionCreatedEventData eventData, DbConnection connection)
{
connection.StateChange += (sender, e) =>
{
Console.WriteLine($"Connection state change: {e.OriginalState}->{e.CurrentState}");
StateChangeCounts.AddOrUpdate((e.OriginalState, e.CurrentState), 1, (key, currentValue) => currentValue + 1);
};
return base.ConnectionCreated(eventData, connection);
}
public override void ConnectionClosed(DbConnection connection, ConnectionEndEventData eventData)
{
Console.WriteLine(nameof(ConnectionClosed));
Interlocked.Increment(ref _connectionClosedCount);
base.ConnectionClosed(connection, eventData);
}
public override Task ConnectionClosedAsync(DbConnection connection, ConnectionEndEventData eventData)
{
ConnectionClosed(connection, eventData);
return base.ConnectionClosedAsync(connection, eventData);
}
public override InterceptionResult ConnectionClosing(DbConnection connection, ConnectionEventData eventData, InterceptionResult result)
{
Console.WriteLine(nameof(ConnectionClosing));
Interlocked.Increment(ref _connectionClosingCount);
return base.ConnectionClosing(connection, eventData, result);
}
public override ValueTask ConnectionClosingAsync(DbConnection connection, ConnectionEventData eventData, InterceptionResult result)
{
ConnectionClosing(connection, eventData, result);
return base.ConnectionClosingAsync(connection, eventData, result);
}
public override void ConnectionOpened(DbConnection connection, ConnectionEndEventData eventData)
{
Console.WriteLine(nameof(ConnectionOpened));
Interlocked.Increment(ref _connectionOpenedCount);
base.ConnectionOpened(connection, eventData);
}
public override Task ConnectionOpenedAsync(DbConnection connection, ConnectionEndEventData eventData, CancellationToken cancellationToken = default)
{
ConnectionOpened(connection, eventData);
return base.ConnectionOpenedAsync(connection, eventData, cancellationToken);
}
public override InterceptionResult ConnectionOpening(DbConnection connection, ConnectionEventData eventData, InterceptionResult result)
{
Console.WriteLine(nameof(ConnectionOpening));
Interlocked.Increment(ref _connectionOpeningCount);
return base.ConnectionOpening(connection, eventData, result);
}
public override ValueTask ConnectionOpeningAsync(DbConnection connection, ConnectionEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default)
{
ConnectionOpening(connection, eventData, result);
return base.ConnectionOpeningAsync(connection, eventData, result, cancellationToken);
}
}
```
### Stack traces
```text
```
### Verbose output
```text
```
### EF Core version
9.0.5
### Database provider
Microsoft.EntityFrameworkCore.Sqlite
### Target framework
.NET 9.0
### Operating system
Windows 10
### IDE
Visual Studio 2022
Contributor guide
Assessment
This issue has not been assessed yet.