Skip to content

CloneWith and CreateAdminConnection should own newly created connections #1814

Closed

Description

@roji

NpgsqlRelationalConnection.CloneWith returns an instance with _connectionOwned=false, since the NpgsqlConnection is externally-provided; this means that disposing the instance doesn't close/dispose the connection leading to potential leaks (e.g. #1812). Fixing this properly requires upstream changes.

When this is done, #1813 can be reverted (can remove the Close, Dispose should be enough).

Activity

  1. added this to the 6.0.0 milestone on Apr 17, 2021
  2. self-assigned this
    on Apr 17, 2021
  3. modified the milestones: 6.0.0, Backlog on Oct 10, 2021
  4. added theissue type on Jun 14, 2025
  5. roji commented on Oct 20, 2026

    @roji
    MemberAuthor

    AI Triage

    The below is an AI-generated analysis and may contain inaccuracies.

    Confirmed provider bug; the existing Bug classification is appropriate. NpgsqlRelationalConnection.CloneWith creates a new NpgsqlConnection, but registers it through the WithConnection(connection) overload, which defaults to non-owning. Disposing the relational wrapper therefore does not close or dispose that new connection, and there is no external owner responsible for it.

    Cause and scope

    The ownership omission is still present in the current main implementation. It is a C# EF provider issue, not a driver disposal bug.

    The issue's original upstream prerequisite no longer applies: EF's RelationalOptionsExtension.WithConnection(connection, owned: true) now supports expressing this ownership directly. No change to EF's Close behavior is needed. Close balances a connection opened by EF, including a caller-owned connection; disposal ownership is a separate contract.

    Correction after checking the rebased implementation: #3648 already replaced the cloned-connection path in NpgsqlDatabaseCreator.Exists with SELECT 1 on the context's existing connection. The #1813 workaround therefore no longer exists on current main. The remaining Close/CloseAsync calls balance opening that existing connection and must stay; they are not redundant disposal cleanup. CloneWith currently has no production callers within the provider. The confirmed internal API ownership defect should not be read as evidence that ordinary CanConnectAsync calls still reproduce the original leak in #1812.

    Minimal reproduction

    Against a locally running PostgreSQL server, obtain the provider's internal connection service and dispose a clone without separately closing it:

    using Microsoft.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore.Infrastructure;
    using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal;
    
    const string cs = "Host=localhost;Database=postgres;Pooling=false";
    await using var context = new DbContext(
        new DbContextOptionsBuilder().UseNpgsql(cs).Options);
    
    var original = context.GetService<INpgsqlRelationalConnection>();
    var clone = await original.CloneWith(cs, async: true);
    var dbConnection = clone.DbConnection;
    
    try
    {
        await clone.OpenAsync(CancellationToken.None);
        await clone.DisposeAsync();
        Console.WriteLine(dbConnection.State);
    }
    finally
    {
        await dbConnection.DisposeAsync();
    }

    The existing non-owning implementation leaves the connection open. The expected result is Closed, with the underlying NpgsqlConnection also disposed.

    Validation and next step

    Reviewed the original implementation and manually exercised #3565 against a real PostgreSQL server, first with its original EF Core 10 preview / Npgsql 9.0.3 dependencies, then after a local rebase onto main with EF Core 11.0.0-rc.2.26504.105 / Npgsql 10.0.3. Its owned: true change correctly closes and disposes clones for connection-string, supplied-connection, and data-source configuration, in both synchronous and asynchronous paths. The caller's original connection and data source remain usable.

    The focused NpgsqlRelationalConnectionTest suite also passes all 24 tests after rebasing, but those tests do not currently assert cloned-connection disposal. Add regression coverage for disposal ownership before considering this issue complete.

    This is a longstanding defect documented by this issue and #1813, not a newly established regression. A last-known-good release was not identified.

    Posted by copilot.

  6. roji commented on Oct 20, 2026

    @roji
    MemberAuthor

    The same ownership defect also affects NpgsqlRelationalConnection.CreateAdminConnection(), particularly when the context is configured with an NpgsqlDataSource. That path creates a new connection but registers it with the non-owning WithConnection(connection) overload. There is no external owner responsible for disposing the new connection.

    This path is still unfixed in #3565: its admin-connection change adds owned: true only to the supplied-connection branch, not the data-source branch. The latter needs the same ownership flag.

    Minimal reproduction against PostgreSQL:

    using Microsoft.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore.Infrastructure;
    using Npgsql;
    using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal;
    
    const string cs = "Host=localhost;Database=postgres";
    using var dataSource = NpgsqlDataSource.Create(cs);
    using var context = new DbContext(
        new DbContextOptionsBuilder().UseNpgsql(dataSource).Options);
    
    var admin = context.GetService<INpgsqlRelationalConnection>().CreateAdminConnection();
    var raw = admin.DbConnection;
    
    try
    {
        admin.Open();
        admin.Dispose();
        Console.WriteLine(raw.State); // Actual: Open. Expected: Closed.
    }
    finally
    {
        raw.Dispose(); // Clean up the connection left open by the wrapper.
    }

    I manually reproduced this against PostgreSQL with EF Core 11.0.0-rc.2.26504.105 / Npgsql 10.0.3, including the equivalent OpenAsync / DisposeAsync path. The admin connection has pooling disabled, so leaving it open leaks a physical database connection until some other cleanup occurs; repeated leaks can exhaust PostgreSQL's connection limit.

    Scope qualification: this demonstrates a leak through the internal admin-connection API, not that every ordinary database-creation or migration call leaks. Callers which explicitly close the connection can mask the defect. Correction after checking current main: #3648 replaced the cloned-connection path in CanConnect / Exists entirely; its remaining close calls balance opening the context's existing connection, rather than implementing the old #1813 workaround. This is not evidence that the original health-check leak in #1812 still occurs.

    Regression coverage should verify that disposing both clones and admin wrappers closes and disposes their new connections, in synchronous and asynchronous paths, without disposing the caller's original connection or data source.

    Posted by copilot.

  7. modified the milestones: Backlog, 10.0.4 on Oct 20, 2026
  8. assigned and unassigned on Oct 20, 2026
  9. changed the title [-]NpgsqlRelationalConnection.CloneWith should return instance owning the connection[/-] [+]CloneWith and CreateAdminConnection should own newly created connections[/+] on Oct 20, 2026
  10. marcusber commented on Oct 20, 2026

    @marcusber
    Contributor

    Thanks @roji !

  11. linked a pull request that will close this issue[10.0] Own newly cloned connections #4056on Oct 20, 2026
  12. linked a pull request that will close this issue[9.0] Fix cloned connection ownership #4055on Oct 20, 2026
  13. added a commit that references this issue on Oct 20, 2026
    df25c1b
  14. assigned and unassigned on Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions