diff --git a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs
index 285ed2989..e4abca786 100644
--- a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs
+++ b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs
@@ -36,6 +36,11 @@ public virtual Version PostgresVersion
public virtual bool IsPostgresVersionSet
=> _postgresVersion is not null;
+ ///
+ /// A lambda to configure Npgsql options on .
+ ///
+ public virtual Action? DataSourceBuilderAction { get; private set; }
+
///
/// The , or if a connection string or was used
/// instead of a .
@@ -126,6 +131,21 @@ public NpgsqlOptionsExtension(NpgsqlOptionsExtension copyFrom)
public override int? MinBatchSize
=> base.MinBatchSize ?? 2;
+ ///
+ /// Creates a new instance with all options the same as for this instance, but with the given option changed.
+ /// It is unusual to call this method directly. Instead use .
+ ///
+ /// A lambda to configure Npgsql options on .
+ /// A new instance with the option changed.
+ public virtual NpgsqlOptionsExtension WithDataSourceConfiguration(Action dataSourceBuilderAction)
+ {
+ var clone = (NpgsqlOptionsExtension)Clone();
+
+ clone.DataSourceBuilderAction = dataSourceBuilderAction;
+
+ return clone;
+ }
+
///
/// Creates a new instance with all options the same as for this instance, but with the given option changed.
/// It is unusual to call this method directly. Instead use .
diff --git a/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs b/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs
index b0a8e9921..12b3fc599 100644
--- a/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs
+++ b/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs
@@ -18,6 +18,19 @@ public NpgsqlDbContextOptionsBuilder(DbContextOptionsBuilder optionsBuilder)
{
}
+ ///
+ /// Configures lower-level Npgsql options at the ADO.NET driver level.
+ ///
+ /// A lambda to configure Npgsql options on .
+ ///
+ /// Changes made by are untracked; When using , EF Core
+ /// will by default resolve the same internally, disregarding differing configuration across calls
+ /// to . Either make sure that always sets the same
+ /// configuration, or pass externally-provided, pre-configured data source instances when configuring the provider.
+ ///
+ public virtual NpgsqlDbContextOptionsBuilder ConfigureDataSource(Action dataSourceBuilderAction)
+ => WithOption(e => e.WithDataSourceConfiguration(dataSourceBuilderAction));
+
///
/// Connect to this database for administrative operations (creating/dropping databases).
///
@@ -48,6 +61,8 @@ public virtual NpgsqlDbContextOptionsBuilder SetPostgresVersion(int major, int m
public virtual NpgsqlDbContextOptionsBuilder UseRedshift(bool useRedshift = true)
=> WithOption(e => e.WithRedshift(useRedshift));
+ #region MapRange
+
///
/// Maps a user-defined PostgreSQL range type for use.
///
@@ -95,6 +110,10 @@ public virtual NpgsqlDbContextOptionsBuilder MapRange(
string? subtypeName = null)
=> WithOption(e => e.WithUserRangeDefinition(rangeName, schemaName, subtypeClrType, subtypeName));
+ #endregion MapRange
+
+ #region MapEnum
+
///
/// Maps a PostgreSQL enum type for use.
///
@@ -122,6 +141,8 @@ public virtual NpgsqlDbContextOptionsBuilder MapEnum(
INpgsqlNameTranslator? nameTranslator = null)
=> WithOption(e => e.WithEnumMapping(clrType, enumName, schemaName, nameTranslator));
+ #endregion MapEnum
+
///
/// Appends NULLS FIRST to all ORDER BY clauses. This is important for the tests which were written
/// for SQL Server. Note that to fully implement null-first ordering indexes also need to be generated
@@ -131,12 +152,13 @@ public virtual NpgsqlDbContextOptionsBuilder MapEnum(
internal virtual NpgsqlDbContextOptionsBuilder ReverseNullOrdering(bool reverseNullOrdering = true)
=> WithOption(e => e.WithReverseNullOrdering(reverseNullOrdering));
- #region Authentication
+ #region Authentication (obsolete)
///
/// Configures the to use the specified .
///
/// The callback to use.
+ [Obsolete("Call ConfigureDataSource() and configure the client certificates on the NpgsqlDataSourceBuilder, or pass an externally-built, pre-configured NpgsqlDataSource to UseNpgsql().")]
public virtual NpgsqlDbContextOptionsBuilder ProvideClientCertificatesCallback(ProvideClientCertificatesCallback? callback)
=> WithOption(e => e.WithProvideClientCertificatesCallback(callback));
@@ -144,6 +166,7 @@ public virtual NpgsqlDbContextOptionsBuilder ProvideClientCertificatesCallback(P
/// Configures the to use the specified .
///
/// The callback to use.
+ [Obsolete("Call ConfigureDataSource() and configure remote certificate validation on the NpgsqlDataSourceBuilder, or pass an externally-built, pre-configured NpgsqlDataSource to UseNpgsql().")]
public virtual NpgsqlDbContextOptionsBuilder RemoteCertificateValidationCallback(RemoteCertificateValidationCallback? callback)
=> WithOption(e => e.WithRemoteCertificateValidationCallback(callback));
@@ -151,12 +174,11 @@ public virtual NpgsqlDbContextOptionsBuilder RemoteCertificateValidationCallback
/// Configures the to use the specified .
///
/// The callback to use.
-#pragma warning disable CS0618 // ProvidePasswordCallback is obsolete
+ [Obsolete("Call ConfigureDataSource() and configure the password callback on the NpgsqlDataSourceBuilder, or pass an externally-built, pre-configured NpgsqlDataSource to UseNpgsql().")]
public virtual NpgsqlDbContextOptionsBuilder ProvidePasswordCallback(ProvidePasswordCallback? callback)
=> WithOption(e => e.WithProvidePasswordCallback(callback));
-#pragma warning restore CS0618
- #endregion Authentication
+ #endregion Authentication (obsolete)
#region Retrying execution strategy
diff --git a/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs b/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs
index 6e39cfd0e..23e9455d7 100644
--- a/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs
+++ b/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs
@@ -20,12 +20,10 @@ private static readonly ResourceManager _resourceManager
= new ResourceManager("Npgsql.EntityFrameworkCore.PostgreSQL.Properties.NpgsqlStrings", typeof(NpgsqlStrings).Assembly);
///
- /// Using two distinct data sources within a service provider is not supported, and Entity Framework is not building its own internal service provider. Either allow Entity Framework to build the service provider by removing the call to '{useInternalServiceProvider}', or ensure that the same data source is used for all uses of a given service provider passed to '{useInternalServiceProvider}'.
+ /// ConfigureDataSource() cannot be used when an externally-provided NpgsqlDataSource is passed to UseNpgsql(). Either perform all data source configuration on the external NpgsqlDataSource, or pass a connection string to UseNpgsql() and specify the data source configuration there.
///
- public static string TwoDataSourcesInSameServiceProvider(object? useInternalServiceProvider)
- => string.Format(
- GetString("TwoDataSourcesInSameServiceProvider", nameof(useInternalServiceProvider)),
- useInternalServiceProvider);
+ public static string DataSourceAndConfigNotSupported
+ => GetString("DataSourceAndConfigNotSupported");
///
/// '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured with different compression methods.
@@ -171,6 +169,14 @@ public static string StoredProcedureReturnValueNotSupported(object? entityType,
GetString("StoredProcedureReturnValueNotSupported", nameof(entityType), nameof(sproc)),
entityType, sproc);
+ ///
+ /// Using two distinct data sources within a service provider is not supported, and Entity Framework is not building its own internal service provider. Either allow Entity Framework to build the service provider by removing the call to '{useInternalServiceProvider}', or ensure that the same data source is used for all uses of a given service provider passed to '{useInternalServiceProvider}'.
+ ///
+ public static string TwoDataSourcesInSameServiceProvider(object? useInternalServiceProvider)
+ => string.Format(
+ GetString("TwoDataSourcesInSameServiceProvider", nameof(useInternalServiceProvider)),
+ useInternalServiceProvider);
+
///
/// 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 1759bc4a9..d40cedf4e 100644
--- a/src/EFCore.PG/Properties/NpgsqlStrings.resx
+++ b/src/EFCore.PG/Properties/NpgsqlStrings.resx
@@ -117,8 +117,8 @@
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
- Using two distinct data sources within a service provider is not supported, and Entity Framework is not building its own internal service provider. Either allow Entity Framework to build the service provider by removing the call to '{useInternalServiceProvider}', or ensure that the same data source is used for all uses of a given service provider passed to '{useInternalServiceProvider}'.
+
+ ConfigureDataSource() cannot be used when an externally-provided NpgsqlDataSource is passed to UseNpgsql(). Either perform all data source configuration on the external NpgsqlDataSource, or pass a connection string to UseNpgsql() and specify the data source configuration there.
'{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}' but are configured with different compression methods.
@@ -247,4 +247,7 @@
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.
+
+ Using two distinct data sources within a service provider is not supported, and Entity Framework is not building its own internal service provider. Either allow Entity Framework to build the service provider by removing the call to '{useInternalServiceProvider}', or ensure that the same data source is used for all uses of a given service provider passed to '{useInternalServiceProvider}'.
+
\ 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 9859e2cd0..323935116 100644
--- a/src/EFCore.PG/Storage/Internal/NpgsqlDataSourceManager.cs
+++ b/src/EFCore.PG/Storage/Internal/NpgsqlDataSourceManager.cs
@@ -2,6 +2,7 @@
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;
@@ -52,7 +53,12 @@ public NpgsqlDataSourceManager(IEnumerable
// If the user has explicitly passed in a data source via UseNpgsql(), use that.
// Note that in this case, the data source is scoped (not singleton), and so can change between different
// DbContext instances using the same internal service provider.
- { DataSource: DbDataSource dataSource } => dataSource,
+ { DataSource: DbDataSource dataSource }
+ => npgsqlOptionsExtension.DataSourceBuilderAction is null
+ ? dataSource
+ // If the user has explicitly passed in a data source via UseNpgsql(), but also supplied a data source configuration
+ // lambda, throw - we're unable to apply the configuration lambda to the externally-provided, already-built data source.
+ : throw new NotSupportedException(NpgsqlStrings.DataSourceAndConfigNotSupported),
// If the user has passed in a DbConnection, never use a data source - even if e.g. MapEnum() was called.
// This is to avoid blocking and allow continuing using enums in conjunction with DbConnections (which
@@ -68,6 +74,7 @@ public NpgsqlDataSourceManager(IEnumerable
{ ConnectionString: null } or null => null,
// The following are features which require an NpgsqlDataSource, since they require configuration on NpgsqlDataSourceBuilder.
+ { DataSourceBuilderAction: not null } => GetSingletonDataSource(npgsqlOptionsExtension),
{ EnumDefinitions.Count: > 0 } => GetSingletonDataSource(npgsqlOptionsExtension),
_ when _plugins.Any() => GetSingletonDataSource(npgsqlOptionsExtension),
@@ -139,6 +146,10 @@ enumDefinition.StoreTypeSchema is null
dataSourceBuilder.UseUserCertificateValidationCallback(npgsqlOptionsExtension.RemoteCertificateValidationCallback);
}
+ // Finally, if the user has provided a data source builder configuration action, invoke it.
+ // Do this last, to allow the user to override anything set above.
+ npgsqlOptionsExtension.DataSourceBuilderAction?.Invoke(dataSourceBuilder);
+
return dataSourceBuilder.Build();
}
diff --git a/test/EFCore.PG.FunctionalTests/LoggingNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/LoggingNpgsqlTest.cs
index 5432f3364..a495a96cb 100644
--- a/test/EFCore.PG.FunctionalTests/LoggingNpgsqlTest.cs
+++ b/test/EFCore.PG.FunctionalTests/LoggingNpgsqlTest.cs
@@ -18,6 +18,7 @@ public void Logs_context_initialization_postgres_version()
ExpectedMessage($"PostgresVersion=10.7 {DefaultOptions}"),
ActualMessage(s => CreateOptionsBuilder(s, b => ((NpgsqlDbContextOptionsBuilder)b).SetPostgresVersion(Version.Parse("10.7")))));
+#pragma warning disable CS0618 // Authentication APIs on NpgsqlDbContextOptionsBuilder are obsolete
[Fact]
public void Logs_context_initialization_provide_client_certificates_callback()
=> Assert.Equal(
@@ -42,6 +43,7 @@ public void Logs_context_initialization_remote_certificate_validation_callback()
s => CreateOptionsBuilder(
s,
b => ((NpgsqlDbContextOptionsBuilder)b).RemoteCertificateValidationCallback((_, _, _, _) => true))));
+#pragma warning restore CS0618 // Authentication APIs on NpgsqlDbContextOptionsBuilder are obsolete
[Fact]
public void Logs_context_initialization_reverse_null_ordering()
diff --git a/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs b/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs
index 8b72df045..ee6f3033c 100644
--- a/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs
+++ b/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs
@@ -4,6 +4,7 @@
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Diagnostics.Internal;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
using Npgsql.EntityFrameworkCore.PostgreSQL.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities;
@@ -11,6 +12,8 @@
namespace Npgsql.EntityFrameworkCore.PostgreSQL;
+#nullable enable
+
public class NpgsqlRelationalConnectionTest
{
[Fact]
@@ -30,13 +33,14 @@ public void Uses_DbDataSource_from_DbContextOptions()
serviceCollection
.AddNpgsqlDataSource("Host=FakeHost")
+ // ReSharper disable once AccessToDisposedClosure
.AddDbContext(o => o.UseNpgsql(dataSource));
using var serviceProvider = serviceCollection.BuildServiceProvider();
using var scope1 = serviceProvider.CreateScope();
var context1 = scope1.ServiceProvider.GetRequiredService();
- var relationalConnection1 = (NpgsqlRelationalConnection)context1.GetService()!;
+ var relationalConnection1 = (NpgsqlRelationalConnection)context1.GetService();
Assert.Same(dataSource, relationalConnection1.DbDataSource);
var connection1 = context1.GetService().Database.GetDbConnection();
@@ -44,7 +48,7 @@ public void Uses_DbDataSource_from_DbContextOptions()
using var scope2 = serviceProvider.CreateScope();
var context2 = scope2.ServiceProvider.GetRequiredService();
- var relationalConnection2 = (NpgsqlRelationalConnection)context2.GetService()!;
+ var relationalConnection2 = (NpgsqlRelationalConnection)context2.GetService();
Assert.Same(dataSource, relationalConnection2.DbDataSource);
var connection2 = context2.GetService().Database.GetDbConnection();
@@ -66,7 +70,7 @@ public void Uses_DbDataSource_from_application_service_provider()
using var scope1 = serviceProvider.CreateScope();
var context1 = scope1.ServiceProvider.GetRequiredService();
- var relationalConnection1 = (NpgsqlRelationalConnection)context1.GetService()!;
+ var relationalConnection1 = (NpgsqlRelationalConnection)context1.GetService();
Assert.Same(dataSource, relationalConnection1.DbDataSource);
var connection1 = context1.GetService().Database.GetDbConnection();
@@ -74,7 +78,7 @@ public void Uses_DbDataSource_from_application_service_provider()
using var scope2 = serviceProvider.CreateScope();
var context2 = scope2.ServiceProvider.GetRequiredService();
- var relationalConnection2 = (NpgsqlRelationalConnection)context2.GetService()!;
+ var relationalConnection2 = (NpgsqlRelationalConnection)context2.GetService();
Assert.Same(dataSource, relationalConnection2.DbDataSource);
var connection2 = context2.GetService().Database.GetDbConnection();
@@ -94,7 +98,7 @@ public void DbDataSource_from_application_service_provider_does_not_used_if_conn
using var scope1 = serviceProvider.CreateScope();
var context1 = scope1.ServiceProvider.GetRequiredService();
- var relationalConnection1 = (NpgsqlRelationalConnection)context1.GetService()!;
+ var relationalConnection1 = (NpgsqlRelationalConnection)context1.GetService();
Assert.Null(relationalConnection1.DbDataSource);
var connection1 = context1.GetService().Database.GetDbConnection();
@@ -102,82 +106,183 @@ public void DbDataSource_from_application_service_provider_does_not_used_if_conn
}
[Fact]
- public void Multiple_connection_strings_with_plugin()
+ public void Data_source_config_with_same_connection_string()
+ {
+ // The connection string is the same, so the same data source gets resolved.
+ // This works well as long as ConfigureDataSource() has the same lambda.
+ var context1 = new ConfigurableContext(
+ "Host=FakeHost1", no => no.ConfigureDataSource(dsb => dsb.ConnectionStringBuilder.ApplicationName = "App1"));
+ var connection1 = (NpgsqlRelationalConnection)context1.GetService();
+ Assert.Equal("Host=FakeHost1;Application Name=App1", connection1.ConnectionString);
+ Assert.NotNull(connection1.DbDataSource);
+
+ var context2 = new ConfigurableContext(
+ "Host=FakeHost1", no => no.ConfigureDataSource(dsb => dsb.ConnectionStringBuilder.ApplicationName = "App1"));
+ var connection2 = (NpgsqlRelationalConnection)context2.GetService();
+ Assert.Equal("Host=FakeHost1;Application Name=App1", connection1.ConnectionString);
+ Assert.Same(connection1.DbDataSource, connection2.DbDataSource);
+ }
+
+ [Fact]
+ public void Data_source_config_with_different_connection_strings()
+ {
+ // When different connection strings are used, different data sources are created internally.
+ var context1 = new ConfigurableContext(
+ "Host=FakeHost1", no => no.ConfigureDataSource(dsb => dsb.ConnectionStringBuilder.ApplicationName = "App1"));
+ var connection1 = (NpgsqlRelationalConnection)context1.GetService();
+ Assert.Equal("Host=FakeHost1;Application Name=App1", connection1.ConnectionString);
+ Assert.NotNull(connection1.DbDataSource);
+
+ var context2 = new ConfigurableContext(
+ "Host=FakeHost2", no => no.ConfigureDataSource(dsb => dsb.ConnectionStringBuilder.ApplicationName = "App2"));
+ var connection2 = (NpgsqlRelationalConnection)context2.GetService();
+ Assert.Equal("Host=FakeHost2;Application Name=App2", connection2.ConnectionString);
+ Assert.NotSame(connection1.DbDataSource, connection2.DbDataSource);
+ }
+
+ [Fact]
+ public void Data_source_config_with_same_connection_string_and_different_lambda()
+ {
+ // Bad case: same connection string but with a different data source config lambda.
+ // Same data source gets reused, and so the differing data source config gets ignored.
+ var context1 = new ConfigurableContext(
+ "Host=FakeHost1", no => no.ConfigureDataSource(dsb => dsb.ConnectionStringBuilder.ApplicationName = "App1"));
+ var connection1 = (NpgsqlRelationalConnection)context1.GetService();
+ Assert.Equal("Host=FakeHost1;Application Name=App1", connection1.ConnectionString);
+ Assert.NotNull(connection1.DbDataSource);
+
+ var context2 = new ConfigurableContext(
+ "Host=FakeHost1", no => no.ConfigureDataSource(dsb => dsb.ConnectionStringBuilder.ApplicationName = "App2"));
+ var connection2 = (NpgsqlRelationalConnection)context2.GetService();
+ // Note the incorrect Application Name below, because the 1st data source was resolved based on the connection string only
+ Assert.Equal("Host=FakeHost1;Application Name=App1", connection2.ConnectionString);
+ Assert.Same(connection1.DbDataSource, connection2.DbDataSource);
+ }
+
+ [Fact]
+ public void Plugin_config_with_same_connection_string()
{
- var context1 = new ConnectionStringSwitchingContext("Host=FakeHost1", withNetTopologySuite: true);
+ // The connection string and plugin config are the same, so the same data source gets resolved.
+ var context1 = new ConfigurableContext("Host=FakeHost1", no => no.UseNetTopologySuite());
var connection1 = (NpgsqlRelationalConnection)context1.GetService();
Assert.Equal("Host=FakeHost1", connection1.ConnectionString);
Assert.NotNull(connection1.DbDataSource);
- var context2 = new ConnectionStringSwitchingContext("Host=FakeHost1", withNetTopologySuite: true);
+ var context2 = new ConfigurableContext("Host=FakeHost1", no => no.UseNetTopologySuite());
var connection2 = (NpgsqlRelationalConnection)context2.GetService();
- Assert.Equal("Host=FakeHost1", connection2.ConnectionString);
+ Assert.Equal("Host=FakeHost1", connection1.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 Plugin_config_with_different_connection_strings()
+ {
+ // When different connection strings are used, different data sources are created internally.
+ var context1 = new ConfigurableContext("Host=FakeHost1", no => no.UseNetTopologySuite());
+ var connection1 = (NpgsqlRelationalConnection)context1.GetService();
+ Assert.Equal("Host=FakeHost1", connection1.ConnectionString);
+ Assert.NotNull(connection1.DbDataSource);
+
+ var context2 = new ConfigurableContext("Host=FakeHost2", no => no.UseNetTopologySuite());
+ var connection2 = (NpgsqlRelationalConnection)context2.GetService();
+ Assert.Equal("Host=FakeHost2", connection2.ConnectionString);
+ Assert.NotSame(connection1.DbDataSource, connection2.DbDataSource);
}
[Fact]
- public void Multiple_connection_strings_with_enum()
+ public void Plugin_config_with_different_connection_strings_and_different_plugins()
{
- var context1 = new ConnectionStringSwitchingContext("Host=FakeHost1", withEnum: true);
+ // Since the plugin configuration is a singleton option, a different service provider gets resolved and we have different data
+ // sources.
+ var context1 = new ConfigurableContext("Host=FakeHost1", no => no.UseNetTopologySuite());
var connection1 = (NpgsqlRelationalConnection)context1.GetService();
Assert.Equal("Host=FakeHost1", connection1.ConnectionString);
Assert.NotNull(connection1.DbDataSource);
- var context2 = new ConnectionStringSwitchingContext("Host=FakeHost1", withEnum: true);
+ var context2 = new ConfigurableContext("Host=FakeHost1", no => no.UseNodaTime());
var connection2 = (NpgsqlRelationalConnection)context2.GetService();
Assert.Equal("Host=FakeHost1", connection2.ConnectionString);
+ Assert.NotSame(connection1.DbDataSource, connection2.DbDataSource);
+ }
+
+ [Fact]
+ public void Enum_config_with_same_connection_string()
+ {
+ // The connection string and plugin config are the same, so the same data source gets resolved.
+ var context1 = new ConfigurableContext("Host=FakeHost1", no => no.MapEnum("mood"));
+ var connection1 = (NpgsqlRelationalConnection)context1.GetService();
+ Assert.Equal("Host=FakeHost1", connection1.ConnectionString);
+ Assert.NotNull(connection1.DbDataSource);
+
+ var context2 = new ConfigurableContext("Host=FakeHost1", no => no.MapEnum("mood"));
+ var connection2 = (NpgsqlRelationalConnection)context2.GetService();
+ Assert.Equal("Host=FakeHost1", connection1.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 Enum_config_with_different_connection_strings()
+ {
+ // When different connection strings are used, different data sources are created internally.
+ var context1 = new ConfigurableContext("Host=FakeHost1", no => no.MapEnum("mood"));
+ var connection1 = (NpgsqlRelationalConnection)context1.GetService();
+ Assert.Equal("Host=FakeHost1", connection1.ConnectionString);
+ Assert.NotNull(connection1.DbDataSource);
+
+ var context2 = new ConfigurableContext("Host=FakeHost2", no => no.MapEnum("mood"));
+ var connection2 = (NpgsqlRelationalConnection)context2.GetService();
+ Assert.Equal("Host=FakeHost2", connection2.ConnectionString);
+ Assert.NotSame(connection1.DbDataSource, connection2.DbDataSource);
+ }
+
+ [Fact]
+ public void Enum_config_with_different_connection_strings_and_different_enums()
+ {
+ // Since the enum configuration is a singleton option, a different service provider gets resolved, and we have different data
+ // sources.
+ var context1 = new ConfigurableContext("Host=FakeHost1", no => no.MapEnum("mood"));
+ var connection1 = (NpgsqlRelationalConnection)context1.GetService();
+ Assert.Equal("Host=FakeHost1", connection1.ConnectionString);
+ Assert.NotNull(connection1.DbDataSource);
+
+ var context2 = new ConfigurableContext("Host=FakeHost1", _ => { /* no enums */});
+ var connection2 = (NpgsqlRelationalConnection)context2.GetService();
+ Assert.Equal("Host=FakeHost1", connection2.ConnectionString);
+ Assert.NotSame(connection1.DbDataSource, connection2.DbDataSource);
+ }
+
+ [Fact]
+ public void Data_source_and_data_source_config_are_incompatible()
+ {
+ using var dataSource = NpgsqlDataSource.Create("Host=FakeHost");
+
+ var optionsBuilder = new DbContextOptionsBuilder();
+ optionsBuilder.UseNpgsql(dataSource, no => no.ConfigureDataSource(dsb => dsb.ConnectionStringBuilder.ApplicationName = "foo"));
+
+ var context1 = new FakeDbContext(optionsBuilder.Options);
+ var exception = Assert.Throws(() => context1.GetService());
+ Assert.Equal(NpgsqlStrings.DataSourceAndConfigNotSupported, exception.Message);
}
[Fact]
public void Multiple_connection_strings_without_data_source_features()
{
- var context1 = new ConnectionStringSwitchingContext("Host=FakeHost1");
+ var context1 = new ConfigurableContext("Host=FakeHost1");
var connection1 = (NpgsqlRelationalConnection)context1.GetService();
Assert.Equal("Host=FakeHost1", connection1.ConnectionString);
Assert.Null(connection1.DbDataSource);
- var context2 = new ConnectionStringSwitchingContext("Host=FakeHost1");
+ var context2 = new ConfigurableContext("Host=FakeHost1");
var connection2 = (NpgsqlRelationalConnection)context2.GetService();
Assert.Equal("Host=FakeHost1", connection2.ConnectionString);
Assert.Null(connection2.DbDataSource);
- var context3 = new ConnectionStringSwitchingContext("Host=FakeHost2");
+ var context3 = new ConfigurableContext("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)
- : DbContext
- {
- protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
- => optionsBuilder.UseNpgsql(connectionString, b =>
- {
- if (withNetTopologySuite)
- {
- b.UseNetTopologySuite();
- }
-
- if (withEnum)
- {
- b.MapEnum("mood");
- }
- });
-
- private enum Mood { Happy, Sad }
- }
-
[Fact]
public void Can_create_master_connection_with_connection_string()
{
@@ -259,7 +364,7 @@ public void CloneWith_with_connection_and_connection_string()
Assert.Equal("Host=localhost;Database=DummyDatabase;Application Name=foo", clone.ConnectionString);
}
- public static NpgsqlRelationalConnection CreateConnection(DbContextOptions options = null, DbDataSource dataSource = null)
+ public static NpgsqlRelationalConnection CreateConnection(DbContextOptions? options = null, DbDataSource? dataSource = null)
{
options ??= new DbContextOptionsBuilder()
.UseNpgsql(@"Host=localhost;Database=NpgsqlConnectionTest;Username=some_user;Password=some_password")
@@ -308,8 +413,7 @@ public static NpgsqlRelationalConnection CreateConnection(DbContextOptions optio
private const string ConnectionString = "Fake Connection String";
- private static IDbContextOptions CreateOptions(
- RelationalOptionsExtension optionsExtension = null)
+ private static IDbContextOptions CreateOptions(RelationalOptionsExtension? optionsExtension = null)
{
var optionsBuilder = new DbContextOptionsBuilder();
@@ -332,4 +436,18 @@ public FakeDbContext(DbContextOptions options)
{
}
}
+
+ private class ConfigurableContext(string connectionString, Action? npgsqlOptionsAction = null) : DbContext
+ {
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ => optionsBuilder.UseNpgsql(connectionString, npgsqlOptionsAction);
+ }
+
+ private enum Mood
+ {
+ // ReSharper disable once UnusedMember.Local
+ Happy,
+ // ReSharper disable once UnusedMember.Local
+ Sad
+ }
}