dotnet / dotnet/efcore

Disposing a RelationalConnection does not call IDbConnectionInterceptor.ConnectionClosing* or IDbConnectionInterceptor.ConnectionClosed*

Open
#36,218 4 comments 0 reactions 1 assignee Claimed by @AndriySvyryd View on GitHub
area-interception customer-reported
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.