diff --git a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs index 5f61329fe..30b842722 100644 --- a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs +++ b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs @@ -208,6 +208,17 @@ public override RelationalOptionsExtension WithConnection(DbConnection? connecti return clone; } + /// + public override RelationalOptionsExtension WithConnection(DbConnection? connection, bool owned) + { + var clone = (NpgsqlOptionsExtension)base.WithConnection(connection, owned); + + // A previously configured data source would otherwise take precedence over the supplied connection. + clone.DataSource = null; + + return clone; + } + /// /// Returns a copy of the current instance configured with the specified range mapping. /// diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlRelationalConnection.cs b/src/EFCore.PG/Storage/Internal/NpgsqlRelationalConnection.cs index 89df883b0..3788670e0 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlRelationalConnection.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlRelationalConnection.cs @@ -202,10 +202,11 @@ public virtual INpgsqlRelationalConnection CreateAdminConnection() Multiplexing = false }.ToString(); + // Cloned connections must be owned so disposing the wrapper also closes and disposes them. var adminNpgsqlOptions = DataSource is not null - ? npgsqlOptions.WithConnection(((NpgsqlConnection)CreateDbConnection()).CloneWith(adminConnectionString)) + ? npgsqlOptions.WithConnection(((NpgsqlConnection)CreateDbConnection()).CloneWith(adminConnectionString), owned: true) : npgsqlOptions.Connection is not null - ? npgsqlOptions.WithConnection(DbConnection.CloneWith(adminConnectionString)) + ? npgsqlOptions.WithConnection(DbConnection.CloneWith(adminConnectionString), owned: true) : npgsqlOptions.WithConnectionString(adminConnectionString); var optionsBuilder = new DbContextOptionsBuilder(); @@ -243,7 +244,7 @@ public virtual async ValueTask CloneWith( var relationalOptions = RelationalOptionsExtension.Extract(Dependencies.ContextOptions) .WithConnectionString(null) - .WithConnection(clonedDbConnection); + .WithConnection(clonedDbConnection, owned: true); var optionsBuilder = new DbContextOptionsBuilder(); ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(relationalOptions); diff --git a/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs b/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs index 5f8f170e4..186958c1c 100644 --- a/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs +++ b/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs @@ -55,6 +55,26 @@ public void Uses_DbDataSource_from_DbContextOptions() Assert.Equal("Host=FakeHost", connection2.ConnectionString); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Supplied_connection_replaces_data_source(bool contextOwnsConnection) + { + using var dataSource = NpgsqlDataSource.Create("Host=FakeHost1"); + using var connection = new NpgsqlConnection("Host=FakeHost2"); + var options = new DbContextOptionsBuilder() + .UseNpgsql(dataSource) + .UseNpgsql(connection, contextOwnsConnection) + .Options; + + using var context = new DbContext(options); + var relationalConnection = (NpgsqlRelationalConnection)context.GetService(); + + Assert.Null(relationalConnection.DataSource); + Assert.Same(connection, relationalConnection.DbConnection); + Assert.Equal("Host=FakeHost2", relationalConnection.ConnectionString); + } + [Fact] public void Uses_DbDataSource_from_application_service_provider() { @@ -322,6 +342,36 @@ public void Can_create_master_connection_with_connection_string_and_alternate_ad master.ConnectionString); } + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public async Task Admin_connection_is_owned(bool async, bool useDataSource) + { + using var dataSource = NpgsqlDataSource.Create("Host=FakeHost"); + using var dbConnection = new NpgsqlConnection("Host=FakeHost"); + var optionsBuilder = new DbContextOptionsBuilder(); + var options = (useDataSource ? optionsBuilder.UseNpgsql(dataSource) : optionsBuilder.UseNpgsql(dbConnection)).Options; + using var context = new DbContext(options); + var connection = context.GetService(); + using var admin = connection.CreateAdminConnection(); + using var adminDbConnection = admin.DbConnection; + + if (async) + { + await admin.DisposeAsync(); + } + else + { + admin.Dispose(); + } + + Assert.Throws(() => adminDbConnection.ConnectionString = "Host=FakeHost"); + using var originalClone = ((NpgsqlConnection)connection.DbConnection).CloneWith("Host=FakeHost"); + using var dataSourceConnection = dataSource.CreateConnection(); + } + [Theory] [InlineData("false")] [InlineData("False")] @@ -360,19 +410,34 @@ public void CurrentAmbientTransaction_returns_transaction_with_enlist_enabled(st Transaction.Current = null; } - [ConditionalFact] - public async Task CloneWith_with_connection_and_connection_string() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public async Task CloneWith_with_connection_and_connection_string(bool async) { var services = NpgsqlTestHelpers.Instance.CreateContextServices( new DbContextOptionsBuilder() .UseNpgsql("Host=localhost;Database=DummyDatabase") .Options); - var relationalConnection = (NpgsqlRelationalConnection)services.GetRequiredService(); + using var relationalConnection = (NpgsqlRelationalConnection)services.GetRequiredService(); - var clone = await relationalConnection.CloneWith("Host=localhost;Database=DummyDatabase;Application Name=foo", async: true); + using var clone = await relationalConnection.CloneWith("Host=localhost;Database=DummyDatabase;Application Name=foo", async); + using var clonedDbConnection = clone.DbConnection; Assert.Equal("Host=localhost;Database=DummyDatabase;Application Name=foo", clone.ConnectionString); + + if (async) + { + await clone.DisposeAsync(); + } + else + { + clone.Dispose(); + } + + Assert.Throws(() => clonedDbConnection.ConnectionString = "Host=localhost"); + using var originalClone = relationalConnection.DbConnection.CloneWith("Host=localhost"); } public static NpgsqlRelationalConnection CreateConnection(DbContextOptions? options = null, DbDataSource? dataSource = null)