Skip to content

Commit

Permalink
Add ConfigureDataSource() to NpgsqlDbContextOptionsBuilder (#3277)
Browse files Browse the repository at this point in the history
Closes #2542
Closes #2704
  • Loading branch information
roji authored Sep 14, 2024
1 parent f7d7910 commit 30cebf0
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 58 deletions.
20 changes: 20 additions & 0 deletions src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ public virtual Version PostgresVersion
public virtual bool IsPostgresVersionSet
=> _postgresVersion is not null;

/// <summary>
/// A lambda to configure Npgsql options on <see cref="NpgsqlDataSourceBuilder" />.
/// </summary>
public virtual Action<NpgsqlDataSourceBuilder>? DataSourceBuilderAction { get; private set; }

/// <summary>
/// The <see cref="DbDataSource" />, or <see langword="null" /> if a connection string or <see cref="DbConnection" /> was used
/// instead of a <see cref="DbDataSource" />.
Expand Down Expand Up @@ -126,6 +131,21 @@ public NpgsqlOptionsExtension(NpgsqlOptionsExtension copyFrom)
public override int? MinBatchSize
=> base.MinBatchSize ?? 2;

/// <summary>
/// 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 <see cref="DbContextOptionsBuilder" />.
/// </summary>
/// <param name="dataSourceBuilderAction">A lambda to configure Npgsql options on <see cref="NpgsqlDataSourceBuilder" />.</param>
/// <returns>A new instance with the option changed.</returns>
public virtual NpgsqlOptionsExtension WithDataSourceConfiguration(Action<NpgsqlDataSourceBuilder> dataSourceBuilderAction)
{
var clone = (NpgsqlOptionsExtension)Clone();

clone.DataSourceBuilderAction = dataSourceBuilderAction;

return clone;
}

/// <summary>
/// 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 <see cref="DbContextOptionsBuilder" />.
Expand Down
30 changes: 26 additions & 4 deletions src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ public NpgsqlDbContextOptionsBuilder(DbContextOptionsBuilder optionsBuilder)
{
}

/// <summary>
/// Configures lower-level Npgsql options at the ADO.NET driver level.
/// </summary>
/// <param name="dataSourceBuilderAction">A lambda to configure Npgsql options on <see cref="NpgsqlDataSourceBuilder" />.</param>
/// <remarks>
/// Changes made by <see cref="ConfigureDataSource" /> are untracked; When using <see cref="DbContext.OnConfiguring" />, EF Core
/// will by default resolve the same <see cref="NpgsqlDataSource" /> internally, disregarding differing configuration across calls
/// to <see cref="ConfigureDataSource" />. Either make sure that <see cref="ConfigureDataSource" /> always sets the same
/// configuration, or pass externally-provided, pre-configured data source instances when configuring the provider.
/// </remarks>
public virtual NpgsqlDbContextOptionsBuilder ConfigureDataSource(Action<NpgsqlDataSourceBuilder> dataSourceBuilderAction)
=> WithOption(e => e.WithDataSourceConfiguration(dataSourceBuilderAction));

/// <summary>
/// Connect to this database for administrative operations (creating/dropping databases).
/// </summary>
Expand Down Expand Up @@ -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

/// <summary>
/// Maps a user-defined PostgreSQL range type for use.
/// </summary>
Expand Down Expand Up @@ -95,6 +110,10 @@ public virtual NpgsqlDbContextOptionsBuilder MapRange(
string? subtypeName = null)
=> WithOption(e => e.WithUserRangeDefinition(rangeName, schemaName, subtypeClrType, subtypeName));

#endregion MapRange

#region MapEnum

/// <summary>
/// Maps a PostgreSQL enum type for use.
/// </summary>
Expand Down Expand Up @@ -122,6 +141,8 @@ public virtual NpgsqlDbContextOptionsBuilder MapEnum(
INpgsqlNameTranslator? nameTranslator = null)
=> WithOption(e => e.WithEnumMapping(clrType, enumName, schemaName, nameTranslator));

#endregion MapEnum

/// <summary>
/// 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
Expand All @@ -131,32 +152,33 @@ public virtual NpgsqlDbContextOptionsBuilder MapEnum(
internal virtual NpgsqlDbContextOptionsBuilder ReverseNullOrdering(bool reverseNullOrdering = true)
=> WithOption(e => e.WithReverseNullOrdering(reverseNullOrdering));

#region Authentication
#region Authentication (obsolete)

/// <summary>
/// Configures the <see cref="DbContext" /> to use the specified <see cref="ProvideClientCertificatesCallback" />.
/// </summary>
/// <param name="callback">The callback to use.</param>
[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));

/// <summary>
/// Configures the <see cref="DbContext" /> to use the specified <see cref="RemoteCertificateValidationCallback" />.
/// </summary>
/// <param name="callback">The callback to use.</param>
[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));

/// <summary>
/// Configures the <see cref="DbContext" /> to use the specified <see cref="ProvidePasswordCallback" />.
/// </summary>
/// <param name="callback">The callback to use.</param>
#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

Expand Down
16 changes: 11 additions & 5 deletions src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions src/EFCore.PG/Properties/NpgsqlStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="TwoDataSourcesInSameServiceProvider" xml:space="preserve">
<value>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}'.</value>
<data name="DataSourceAndConfigNotSupported" xml:space="preserve">
<value>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.</value>
</data>
<data name="DuplicateColumnCompressionMethodMismatch" xml:space="preserve">
<value>'{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}' but are configured with different compression methods.</value>
Expand Down Expand Up @@ -247,4 +247,7 @@
<data name="StoredProcedureReturnValueNotSupported" xml:space="preserve">
<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>
</data>
<data name="TwoDataSourcesInSameServiceProvider" xml:space="preserve">
<value>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}'.</value>
</data>
</root>
13 changes: 12 additions & 1 deletion src/EFCore.PG/Storage/Internal/NpgsqlDataSourceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -52,7 +53,12 @@ public NpgsqlDataSourceManager(IEnumerable<INpgsqlDataSourceConfigurationPlugin>
// 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
Expand All @@ -68,6 +74,7 @@ public NpgsqlDataSourceManager(IEnumerable<INpgsqlDataSourceConfigurationPlugin>
{ 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),

Expand Down Expand Up @@ -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();
}

Expand Down
2 changes: 2 additions & 0 deletions test/EFCore.PG.FunctionalTests/LoggingNpgsqlTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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()
Expand Down
Loading

0 comments on commit 30cebf0

Please sign in to comment.