diff --git a/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs b/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs index 1434cbc8e..3bcd31e53 100644 --- a/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs +++ b/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs @@ -171,14 +171,6 @@ public static string StoredProcedureReturnValueNotSupported(object? entityType, GetString("StoredProcedureReturnValueNotSupported", nameof(entityType), nameof(sproc)), entityType, sproc); - /// - /// Different connection strings are being used, but the provider uses has been configured with a feature that requires a singleton data source internally: {dataSourceFeature} - /// - public static string DataSourceWithMultipleConnectionStrings(object? dataSourceFeature) - => string.Format( - GetString("DataSourceWithMultipleConnectionStrings", nameof(dataSourceFeature)), - dataSourceFeature); - /// /// An exception has been raised that is likely due to a transient failure. Consider enabling transient error resiliency by adding 'EnableRetryOnFailure()' to the 'UseSqlServer' call. /// diff --git a/src/EFCore.PG/Properties/NpgsqlStrings.resx b/src/EFCore.PG/Properties/NpgsqlStrings.resx index 35a84a1d3..1759bc4a9 100644 --- a/src/EFCore.PG/Properties/NpgsqlStrings.resx +++ b/src/EFCore.PG/Properties/NpgsqlStrings.resx @@ -247,7 +247,4 @@ The entity type '{entityType}' is mapped to the stored procedure '{sproc}', which is configured with result columns. PostgreSQL stored procedures do not support return values; use output parameters instead. - - Different connection strings are being used, but the provider uses has been configured with a feature that requires a singleton data source internally: {dataSourceFeature} - \ No newline at end of file diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlDataSourceManager.cs b/src/EFCore.PG/Storage/Internal/NpgsqlDataSourceManager.cs index 8820eda24..9859e2cd0 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlDataSourceManager.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlDataSourceManager.cs @@ -1,7 +1,7 @@ +using System.Collections.Concurrent; using System.Data.Common; using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; @@ -27,11 +27,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; /// public class NpgsqlDataSourceManager : IDisposable, IAsyncDisposable { - private bool _isInitialized; - private string? _connectionString; private readonly IEnumerable _plugins; - private NpgsqlDataSource? _dataSource; - private readonly object _lock = new(); + private readonly ConcurrentDictionary _dataSources = new(); + private volatile int _isDisposed; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -70,8 +68,8 @@ public NpgsqlDataSourceManager(IEnumerable { ConnectionString: null } or null => null, // The following are features which require an NpgsqlDataSource, since they require configuration on NpgsqlDataSourceBuilder. - { EnumDefinitions.Count: > 0 } => GetSingletonDataSource(npgsqlOptionsExtension, "MapEnum"), - _ when _plugins.Any() => GetSingletonDataSource(npgsqlOptionsExtension, _plugins.First().GetType().Name), + { EnumDefinitions.Count: > 0 } => GetSingletonDataSource(npgsqlOptionsExtension), + _ when _plugins.Any() => GetSingletonDataSource(npgsqlOptionsExtension), // If there's no configured feature which requires us to use a data source internally, don't use one; this causes // NpgsqlRelationalConnection to use the connection string as before (no data source), allowing switching connection strings @@ -79,30 +77,30 @@ _ when _plugins.Any() => GetSingletonDataSource(npgsqlOptionsExtension, _plugins _ => null }; - private DbDataSource GetSingletonDataSource(NpgsqlOptionsExtension npgsqlOptionsExtension, string dataSourceFeature) + private DbDataSource GetSingletonDataSource(NpgsqlOptionsExtension npgsqlOptionsExtension) { - if (!_isInitialized) + var connectionString = npgsqlOptionsExtension.ConnectionString; + Check.DebugAssert(connectionString is not null, "Connection string can't be null"); + + if (_dataSources.TryGetValue(connectionString, out var dataSource)) { - lock (_lock) - { - if (!_isInitialized) - { - _dataSource = CreateSingletonDataSource(npgsqlOptionsExtension); - _connectionString = npgsqlOptionsExtension.ConnectionString; - _isInitialized = true; - return _dataSource; - } - } + return dataSource; } - Check.DebugAssert(_dataSource is not null, "_dataSource cannot be null at this point"); + var newDataSource = CreateDataSource(npgsqlOptionsExtension); - if (_connectionString != npgsqlOptionsExtension.ConnectionString) + var addedDataSource = _dataSources.GetOrAdd(connectionString, newDataSource); + if (!ReferenceEquals(addedDataSource, newDataSource)) { - throw new InvalidOperationException(NpgsqlStrings.DataSourceWithMultipleConnectionStrings(dataSourceFeature)); + newDataSource.Dispose(); + } + else if (_isDisposed == 1) + { + newDataSource.Dispose(); + throw new ObjectDisposedException(nameof(NpgsqlDataSourceManager)); } - return _dataSource; + return addedDataSource; } /// @@ -111,7 +109,7 @@ private DbDataSource GetSingletonDataSource(NpgsqlOptionsExtension npgsqlOptions /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected virtual NpgsqlDataSource CreateSingletonDataSource(NpgsqlOptionsExtension npgsqlOptionsExtension) + protected virtual NpgsqlDataSource CreateDataSource(NpgsqlOptionsExtension npgsqlOptionsExtension) { var dataSourceBuilder = new NpgsqlDataSourceBuilder(npgsqlOptionsExtension.ConnectionString); @@ -151,7 +149,15 @@ enumDefinition.StoreTypeSchema is null /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public void Dispose() - => _dataSource?.Dispose(); + { + if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0) + { + foreach (var dataSource in _dataSources.Values) + { + dataSource.Dispose(); + } + } + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -161,9 +167,12 @@ public void Dispose() /// public async ValueTask DisposeAsync() { - if (_dataSource != null) + if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0) { - await _dataSource.DisposeAsync().ConfigureAwait(false); + foreach (var dataSource in _dataSources.Values) + { + await dataSource.DisposeAsync().ConfigureAwait(false); + } } } } diff --git a/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs b/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs index 93a522922..e5ce3d183 100644 --- a/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs +++ b/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs @@ -103,34 +103,60 @@ public void DbDataSource_from_application_service_provider_does_not_used_if_conn } [Fact] - public void Multiple_connection_strings_with_plugin_is_not_supported() + public void Multiple_connection_strings_with_plugin() { var context1 = new ConnectionStringSwitchingContext("Host=FakeHost1", withNetTopologySuite: true); - _ = context1.GetService(); - var context2 = new ConnectionStringSwitchingContext("Host=FakeHost2", withNetTopologySuite: true); - - var exception = Assert.Throws(() => context2.GetService()); - Assert.Equal(NpgsqlStrings.DataSourceWithMultipleConnectionStrings("NetTopologySuiteDataSourceConfigurationPlugin"), exception.Message); + var connection1 = (NpgsqlRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1", connection1.ConnectionString); + Assert.NotNull(connection1.DbDataSource); + + var context2 = new ConnectionStringSwitchingContext("Host=FakeHost1", withNetTopologySuite: true); + var connection2 = (NpgsqlRelationalConnection)context2.GetService(); + Assert.Equal("Host=FakeHost1", connection2.ConnectionString); + Assert.Same(connection1.DbDataSource, connection2.DbDataSource); + + var context3 = new ConnectionStringSwitchingContext("Host=FakeHost2", withNetTopologySuite: true); + var connection3 = (NpgsqlRelationalConnection)context3.GetService(); + Assert.Equal("Host=FakeHost2", connection3.ConnectionString); + Assert.NotSame(connection1.DbDataSource, connection3.DbDataSource); } [Fact] - public void Multiple_connection_strings_with_enum_is_not_supported() + public void Multiple_connection_strings_with_enum() { var context1 = new ConnectionStringSwitchingContext("Host=FakeHost1", withEnum: true); - _ = context1.GetService(); - var context2 = new ConnectionStringSwitchingContext("Host=FakeHost2", withEnum: true); - - var exception = Assert.Throws(() => context2.GetService()); - Assert.Equal(NpgsqlStrings.DataSourceWithMultipleConnectionStrings("MapEnum"), exception.Message); + var connection1 = (NpgsqlRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1", connection1.ConnectionString); + Assert.NotNull(connection1.DbDataSource); + + var context2 = new ConnectionStringSwitchingContext("Host=FakeHost1", withEnum: true); + var connection2 = (NpgsqlRelationalConnection)context2.GetService(); + Assert.Equal("Host=FakeHost1", connection2.ConnectionString); + Assert.Same(connection1.DbDataSource, connection2.DbDataSource); + + var context3 = new ConnectionStringSwitchingContext("Host=FakeHost2", withEnum: true); + var connection3 = (NpgsqlRelationalConnection)context3.GetService(); + Assert.Equal("Host=FakeHost2", connection3.ConnectionString); + Assert.NotSame(connection1.DbDataSource, connection3.DbDataSource); } [Fact] - public void Multiple_connection_strings_without_data_source_features_is_supported() + public void Multiple_connection_strings_without_data_source_features() { var context1 = new ConnectionStringSwitchingContext("Host=FakeHost1"); - _ = context1.GetService(); - var context2 = new ConnectionStringSwitchingContext("Host=FakeHost2"); - _ = context2.GetService(); + var connection1 = (NpgsqlRelationalConnection)context1.GetService(); + Assert.Equal("Host=FakeHost1", connection1.ConnectionString); + Assert.Null(connection1.DbDataSource); + + var context2 = new ConnectionStringSwitchingContext("Host=FakeHost1"); + var connection2 = (NpgsqlRelationalConnection)context2.GetService(); + Assert.Equal("Host=FakeHost1", connection2.ConnectionString); + Assert.Null(connection2.DbDataSource); + + var context3 = new ConnectionStringSwitchingContext("Host=FakeHost2"); + var connection3 = (NpgsqlRelationalConnection)context3.GetService(); + Assert.Equal("Host=FakeHost2", connection3.ConnectionString); + Assert.Null(connection3.DbDataSource); } private class ConnectionStringSwitchingContext(string connectionString, bool withNetTopologySuite = false, bool withEnum = false)