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 + } }