Skip to content

Commit 79f8abe

Browse files
committed
Support multiple connection strings by managing multiple data sources internally
1 parent 983ab15 commit 79f8abe

File tree

4 files changed

+78
-54
lines changed

4 files changed

+78
-54
lines changed

src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs

Lines changed: 0 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.PG/Properties/NpgsqlStrings.resx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,4 @@
247247
<data name="StoredProcedureReturnValueNotSupported" xml:space="preserve">
248248
<value>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.</value>
249249
</data>
250-
<data name="DataSourceWithMultipleConnectionStrings" xml:space="preserve">
251-
<value>Different connection strings are being used, but the provider uses has been configured with a feature that requires a singleton data source internally: {dataSourceFeature}</value>
252-
</data>
253250
</root>

src/EFCore.PG/Storage/Internal/NpgsqlDataSourceManager.cs

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
using System.Collections.Concurrent;
12
using System.Data.Common;
23
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
34
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal;
4-
using Npgsql.EntityFrameworkCore.PostgreSQL.Internal;
55

66
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal;
77

@@ -27,11 +27,9 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal;
2727
/// </remarks>
2828
public class NpgsqlDataSourceManager : IDisposable, IAsyncDisposable
2929
{
30-
private bool _isInitialized;
31-
private string? _connectionString;
3230
private readonly IEnumerable<INpgsqlDataSourceConfigurationPlugin> _plugins;
33-
private NpgsqlDataSource? _dataSource;
34-
private readonly object _lock = new();
31+
private readonly ConcurrentDictionary<string, NpgsqlDataSource> _dataSources = new();
32+
private volatile int _isDisposed;
3533

3634
/// <summary>
3735
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -70,39 +68,39 @@ public NpgsqlDataSourceManager(IEnumerable<INpgsqlDataSourceConfigurationPlugin>
7068
{ ConnectionString: null } or null => null,
7169

7270
// The following are features which require an NpgsqlDataSource, since they require configuration on NpgsqlDataSourceBuilder.
73-
{ EnumDefinitions.Count: > 0 } => GetSingletonDataSource(npgsqlOptionsExtension, "MapEnum"),
74-
_ when _plugins.Any() => GetSingletonDataSource(npgsqlOptionsExtension, _plugins.First().GetType().Name),
71+
{ EnumDefinitions.Count: > 0 } => GetSingletonDataSource(npgsqlOptionsExtension),
72+
_ when _plugins.Any() => GetSingletonDataSource(npgsqlOptionsExtension),
7573

7674
// If there's no configured feature which requires us to use a data source internally, don't use one; this causes
7775
// NpgsqlRelationalConnection to use the connection string as before (no data source), allowing switching connection strings
7876
// with the same service provider etc.
7977
_ => null
8078
};
8179

82-
private DbDataSource GetSingletonDataSource(NpgsqlOptionsExtension npgsqlOptionsExtension, string dataSourceFeature)
80+
private DbDataSource GetSingletonDataSource(NpgsqlOptionsExtension npgsqlOptionsExtension)
8381
{
84-
if (!_isInitialized)
82+
var connectionString = npgsqlOptionsExtension.ConnectionString;
83+
Check.DebugAssert(connectionString is not null, "Connection string can't be null");
84+
85+
if (_dataSources.TryGetValue(connectionString, out var dataSource))
8586
{
86-
lock (_lock)
87-
{
88-
if (!_isInitialized)
89-
{
90-
_dataSource = CreateSingletonDataSource(npgsqlOptionsExtension);
91-
_connectionString = npgsqlOptionsExtension.ConnectionString;
92-
_isInitialized = true;
93-
return _dataSource;
94-
}
95-
}
87+
return dataSource;
9688
}
9789

98-
Check.DebugAssert(_dataSource is not null, "_dataSource cannot be null at this point");
90+
var newDataSource = CreateDataSource(npgsqlOptionsExtension);
9991

100-
if (_connectionString != npgsqlOptionsExtension.ConnectionString)
92+
var addedDataSource = _dataSources.GetOrAdd(connectionString, newDataSource);
93+
if (!ReferenceEquals(addedDataSource, newDataSource))
10194
{
102-
throw new InvalidOperationException(NpgsqlStrings.DataSourceWithMultipleConnectionStrings(dataSourceFeature));
95+
newDataSource.Dispose();
96+
}
97+
else if (_isDisposed == 1)
98+
{
99+
newDataSource.Dispose();
100+
throw new ObjectDisposedException(nameof(NpgsqlDataSourceManager));
103101
}
104102

105-
return _dataSource;
103+
return addedDataSource;
106104
}
107105

108106
/// <summary>
@@ -111,7 +109,7 @@ private DbDataSource GetSingletonDataSource(NpgsqlOptionsExtension npgsqlOptions
111109
/// any release. You should only use it directly in your code with extreme caution and knowing that
112110
/// doing so can result in application failures when updating to a new Entity Framework Core release.
113111
/// </summary>
114-
protected virtual NpgsqlDataSource CreateSingletonDataSource(NpgsqlOptionsExtension npgsqlOptionsExtension)
112+
protected virtual NpgsqlDataSource CreateDataSource(NpgsqlOptionsExtension npgsqlOptionsExtension)
115113
{
116114
var dataSourceBuilder = new NpgsqlDataSourceBuilder(npgsqlOptionsExtension.ConnectionString);
117115

@@ -151,7 +149,15 @@ enumDefinition.StoreTypeSchema is null
151149
/// doing so can result in application failures when updating to a new Entity Framework Core release.
152150
/// </summary>
153151
public void Dispose()
154-
=> _dataSource?.Dispose();
152+
{
153+
if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0)
154+
{
155+
foreach (var dataSource in _dataSources.Values)
156+
{
157+
dataSource.Dispose();
158+
}
159+
}
160+
}
155161

156162
/// <summary>
157163
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -161,9 +167,12 @@ public void Dispose()
161167
/// </summary>
162168
public async ValueTask DisposeAsync()
163169
{
164-
if (_dataSource != null)
170+
if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0)
165171
{
166-
await _dataSource.DisposeAsync().ConfigureAwait(false);
172+
foreach (var dataSource in _dataSources.Values)
173+
{
174+
await dataSource.DisposeAsync().ConfigureAwait(false);
175+
}
167176
}
168177
}
169178
}

test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -103,34 +103,60 @@ public void DbDataSource_from_application_service_provider_does_not_used_if_conn
103103
}
104104

105105
[Fact]
106-
public void Multiple_connection_strings_with_plugin_is_not_supported()
106+
public void Multiple_connection_strings_with_plugin()
107107
{
108108
var context1 = new ConnectionStringSwitchingContext("Host=FakeHost1", withNetTopologySuite: true);
109-
_ = context1.GetService<IRelationalConnection>();
110-
var context2 = new ConnectionStringSwitchingContext("Host=FakeHost2", withNetTopologySuite: true);
111-
112-
var exception = Assert.Throws<InvalidOperationException>(() => context2.GetService<IRelationalConnection>());
113-
Assert.Equal(NpgsqlStrings.DataSourceWithMultipleConnectionStrings("NetTopologySuiteDataSourceConfigurationPlugin"), exception.Message);
109+
var connection1 = (NpgsqlRelationalConnection)context1.GetService<IRelationalConnection>();
110+
Assert.Equal("Host=FakeHost1", connection1.ConnectionString);
111+
Assert.NotNull(connection1.DbDataSource);
112+
113+
var context2 = new ConnectionStringSwitchingContext("Host=FakeHost1", withNetTopologySuite: true);
114+
var connection2 = (NpgsqlRelationalConnection)context2.GetService<IRelationalConnection>();
115+
Assert.Equal("Host=FakeHost1", connection2.ConnectionString);
116+
Assert.Same(connection1.DbDataSource, connection2.DbDataSource);
117+
118+
var context3 = new ConnectionStringSwitchingContext("Host=FakeHost2", withNetTopologySuite: true);
119+
var connection3 = (NpgsqlRelationalConnection)context3.GetService<IRelationalConnection>();
120+
Assert.Equal("Host=FakeHost2", connection3.ConnectionString);
121+
Assert.NotSame(connection1.DbDataSource, connection3.DbDataSource);
114122
}
115123

116124
[Fact]
117-
public void Multiple_connection_strings_with_enum_is_not_supported()
125+
public void Multiple_connection_strings_with_enum()
118126
{
119127
var context1 = new ConnectionStringSwitchingContext("Host=FakeHost1", withEnum: true);
120-
_ = context1.GetService<IRelationalConnection>();
121-
var context2 = new ConnectionStringSwitchingContext("Host=FakeHost2", withEnum: true);
122-
123-
var exception = Assert.Throws<InvalidOperationException>(() => context2.GetService<IRelationalConnection>());
124-
Assert.Equal(NpgsqlStrings.DataSourceWithMultipleConnectionStrings("MapEnum"), exception.Message);
128+
var connection1 = (NpgsqlRelationalConnection)context1.GetService<IRelationalConnection>();
129+
Assert.Equal("Host=FakeHost1", connection1.ConnectionString);
130+
Assert.NotNull(connection1.DbDataSource);
131+
132+
var context2 = new ConnectionStringSwitchingContext("Host=FakeHost1", withEnum: true);
133+
var connection2 = (NpgsqlRelationalConnection)context2.GetService<IRelationalConnection>();
134+
Assert.Equal("Host=FakeHost1", connection2.ConnectionString);
135+
Assert.Same(connection1.DbDataSource, connection2.DbDataSource);
136+
137+
var context3 = new ConnectionStringSwitchingContext("Host=FakeHost2", withEnum: true);
138+
var connection3 = (NpgsqlRelationalConnection)context3.GetService<IRelationalConnection>();
139+
Assert.Equal("Host=FakeHost2", connection3.ConnectionString);
140+
Assert.NotSame(connection1.DbDataSource, connection3.DbDataSource);
125141
}
126142

127143
[Fact]
128-
public void Multiple_connection_strings_without_data_source_features_is_supported()
144+
public void Multiple_connection_strings_without_data_source_features()
129145
{
130146
var context1 = new ConnectionStringSwitchingContext("Host=FakeHost1");
131-
_ = context1.GetService<IRelationalConnection>();
132-
var context2 = new ConnectionStringSwitchingContext("Host=FakeHost2");
133-
_ = context2.GetService<IRelationalConnection>();
147+
var connection1 = (NpgsqlRelationalConnection)context1.GetService<IRelationalConnection>();
148+
Assert.Equal("Host=FakeHost1", connection1.ConnectionString);
149+
Assert.Null(connection1.DbDataSource);
150+
151+
var context2 = new ConnectionStringSwitchingContext("Host=FakeHost1");
152+
var connection2 = (NpgsqlRelationalConnection)context2.GetService<IRelationalConnection>();
153+
Assert.Equal("Host=FakeHost1", connection2.ConnectionString);
154+
Assert.Null(connection2.DbDataSource);
155+
156+
var context3 = new ConnectionStringSwitchingContext("Host=FakeHost2");
157+
var connection3 = (NpgsqlRelationalConnection)context3.GetService<IRelationalConnection>();
158+
Assert.Equal("Host=FakeHost2", connection3.ConnectionString);
159+
Assert.Null(connection3.DbDataSource);
134160
}
135161

136162
private class ConnectionStringSwitchingContext(string connectionString, bool withNetTopologySuite = false, bool withEnum = false)

0 commit comments

Comments
 (0)