Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace Microsoft.Extensions.DependencyInjection;
public static class NpgsqlServiceCollectionExtensions
{
/// <summary>
/// Registers an <see cref="NpgsqlDataSource" /> and an <see cref="NpgsqlConnection" /> in the <see cref="IServiceCollection" />,
/// Registers an <see cref="NpgsqlDataSource" /> and an <see cref="NpgsqlConnection" /> in the <see cref="IServiceCollection" />.
/// </summary>
/// <param name="serviceCollection">The <see cref="IServiceCollection" /> to add services to.</param>
/// <param name="connectionString">An Npgsql connection string.</param>
Expand All @@ -38,7 +38,7 @@ public static IServiceCollection AddNpgsqlDataSource(
=> AddNpgsqlDataSourceCore(serviceCollection, connectionString, dataSourceBuilderAction, connectionLifetime, dataSourceLifetime);

/// <summary>
/// Registers an <see cref="NpgsqlDataSource" /> and an <see cref="NpgsqlConnection" /> in the <see cref="IServiceCollection" />,
/// Registers an <see cref="NpgsqlDataSource" /> and an <see cref="NpgsqlConnection" /> in the <see cref="IServiceCollection" />.
/// </summary>
/// <param name="serviceCollection">The <see cref="IServiceCollection" /> to add services to.</param>
/// <param name="connectionString">An Npgsql connection string.</param>
Expand All @@ -59,12 +59,61 @@ public static IServiceCollection AddNpgsqlDataSource(
=> AddNpgsqlDataSourceCore(
serviceCollection, connectionString, dataSourceBuilderAction: null, connectionLifetime, dataSourceLifetime);

static IServiceCollection AddNpgsqlDataSourceCore(
/// <summary>
/// Registers an <see cref="NpgsqlMultiHostDataSource" /> and an <see cref="NpgsqlConnection" /> in the
/// </summary>
/// <param name="serviceCollection">The <see cref="IServiceCollection" /> to add services to.</param>
/// <param name="connectionString">An Npgsql connection string.</param>
/// <param name="dataSourceBuilderAction">
/// An action to configure the <see cref="NpgsqlDataSourceBuilder" /> for further customizations of the <see cref="NpgsqlDataSource" />.
/// </param>
/// <param name="connectionLifetime">
/// The lifetime with which to register the <see cref="NpgsqlConnection" /> in the container.
/// Defaults to <see cref="ServiceLifetime.Scoped" />.
/// </param>
/// <param name="dataSourceLifetime">
/// The lifetime with which to register the <see cref="NpgsqlDataSource" /> service in the container.
/// Defaults to <see cref="ServiceLifetime.Singleton" />.
/// </param>
/// <returns>The same service collection so that multiple calls can be chained.</returns>
public static IServiceCollection AddMultiHostNpgsqlDataSource(
this IServiceCollection serviceCollection,
string connectionString,
Action<NpgsqlDataSourceBuilder> dataSourceBuilderAction,
ServiceLifetime connectionLifetime = ServiceLifetime.Transient,
ServiceLifetime dataSourceLifetime = ServiceLifetime.Singleton)
=> AddNpgsqlMultiHostDataSourceCore(
serviceCollection, connectionString, dataSourceBuilderAction, connectionLifetime, dataSourceLifetime);

/// <summary>
/// Registers an <see cref="NpgsqlMultiHostDataSource" /> and an <see cref="NpgsqlConnection" /> in the
/// <see cref="IServiceCollection" />.
/// </summary>
/// <param name="serviceCollection">The <see cref="IServiceCollection" /> to add services to.</param>
/// <param name="connectionString">An Npgsql connection string.</param>
/// <param name="connectionLifetime">
/// The lifetime with which to register the <see cref="NpgsqlConnection" /> in the container.
/// Defaults to <see cref="ServiceLifetime.Scoped" />.
/// </param>
/// <param name="dataSourceLifetime">
/// The lifetime with which to register the <see cref="NpgsqlDataSource" /> service in the container.
/// Defaults to <see cref="ServiceLifetime.Singleton" />.
/// </param>
/// <returns>The same service collection so that multiple calls can be chained.</returns>
public static IServiceCollection AddMultiHostNpgsqlDataSource(
this IServiceCollection serviceCollection,
string connectionString,
Action<NpgsqlDataSourceBuilder>? dataSourceBuilderAction,
ServiceLifetime connectionLifetime = ServiceLifetime.Transient,
ServiceLifetime dataSourceLifetime = ServiceLifetime.Singleton)
=> AddNpgsqlMultiHostDataSourceCore(
serviceCollection, connectionString, dataSourceBuilderAction: null, connectionLifetime, dataSourceLifetime);

static IServiceCollection AddNpgsqlDataSourceCore(
this IServiceCollection serviceCollection,
string connectionString,
Action<NpgsqlDataSourceBuilder>? dataSourceBuilderAction,
ServiceLifetime connectionLifetime,
ServiceLifetime dataSourceLifetime)
{
serviceCollection.TryAdd(
new ServiceDescriptor(
Expand All @@ -78,6 +127,46 @@ static IServiceCollection AddNpgsqlDataSourceCore(
},
dataSourceLifetime));

AddCommonServices(serviceCollection, connectionLifetime, dataSourceLifetime);

return serviceCollection;
}

static IServiceCollection AddNpgsqlMultiHostDataSourceCore(
this IServiceCollection serviceCollection,
string connectionString,
Action<NpgsqlDataSourceBuilder>? dataSourceBuilderAction,
ServiceLifetime connectionLifetime,
ServiceLifetime dataSourceLifetime)
{
serviceCollection.TryAdd(
new ServiceDescriptor(
typeof(NpgsqlMultiHostDataSource),
sp =>
{
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
dataSourceBuilder.UseLoggerFactory(sp.GetService<ILoggerFactory>());
dataSourceBuilderAction?.Invoke(dataSourceBuilder);
return dataSourceBuilder.BuildMultiHost();
},
dataSourceLifetime));

serviceCollection.TryAdd(
new ServiceDescriptor(
typeof(NpgsqlDataSource),
sp => sp.GetRequiredService<NpgsqlMultiHostDataSource>(),
dataSourceLifetime));

AddCommonServices(serviceCollection, connectionLifetime, dataSourceLifetime);

return serviceCollection;
}

static void AddCommonServices(
IServiceCollection serviceCollection,
ServiceLifetime connectionLifetime,
ServiceLifetime dataSourceLifetime)
{
serviceCollection.TryAdd(
new ServiceDescriptor(
typeof(NpgsqlConnection),
Expand All @@ -95,7 +184,5 @@ static IServiceCollection AddNpgsqlDataSourceCore(
typeof(DbConnection),
sp => sp.GetRequiredService<NpgsqlConnection>(),
connectionLifetime));

return serviceCollection;
}
}
74 changes: 43 additions & 31 deletions src/Npgsql/NpgsqlDataSourceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,36 @@ public NpgsqlDataSourceBuilder UsePhysicalConnectionInitializer(
/// Builds and returns an <see cref="NpgsqlDataSource" /> which is ready for use.
/// </summary>
public NpgsqlDataSource Build()
{
var config = PrepareConfiguration();

if (ConnectionStringBuilder.Host!.Contains(","))
{
ValidateMultiHost();

return new NpgsqlMultiHostDataSource(ConnectionStringBuilder, config);
}

return ConnectionStringBuilder.Multiplexing
? new MultiplexingDataSource(ConnectionStringBuilder, config)
: ConnectionStringBuilder.Pooling
? new PoolingDataSource(ConnectionStringBuilder, config)
: new UnpooledDataSource(ConnectionStringBuilder, config);
}

/// <summary>
/// Builds and returns a <see cref="NpgsqlMultiHostDataSource" /> which is ready for use for load-balancing and failover scenarios.
/// </summary>
public NpgsqlMultiHostDataSource BuildMultiHost()
{
var config = PrepareConfiguration();

ValidateMultiHost();

return new(ConnectionStringBuilder, config);
}

NpgsqlDataSourceConfiguration PrepareConfiguration()
{
ConnectionStringBuilder.PostProcessAndValidate();

Expand All @@ -346,12 +376,10 @@ public NpgsqlDataSource Build()
throw new NotSupportedException(NpgsqlStrings.CannotSetBothPasswordProviderAndPassword);
}

var loggingConfiguration = _loggerFactory is null
? NpgsqlLoggingConfiguration.NullConfiguration
: new NpgsqlLoggingConfiguration(_loggerFactory, _sensitiveDataLoggingEnabled);

var config = new NpgsqlDataSourceConfiguration(
loggingConfiguration,
return new(
_loggerFactory is null
? NpgsqlLoggingConfiguration.NullConfiguration
: new NpgsqlLoggingConfiguration(_loggerFactory, _sensitiveDataLoggingEnabled),
_userCertificateValidationCallback,
_clientCertificatesCallback,
_periodicPasswordProvider,
Expand All @@ -362,31 +390,15 @@ public NpgsqlDataSource Build()
DefaultNameTranslator,
_syncConnectionInitializer,
_asyncConnectionInitializer);

if (ConnectionStringBuilder.Host!.Contains(","))
{
if (ConnectionStringBuilder.TargetSessionAttributes is not null)
throw new InvalidOperationException(NpgsqlStrings.CannotSpecifyTargetSessionAttributes);
if (ConnectionStringBuilder.Multiplexing)
throw new NotSupportedException("Multiplexing is not supported with multiple hosts");
if (ConnectionStringBuilder.ReplicationMode != ReplicationMode.Off)
throw new NotSupportedException("Replication is not supported with multiple hosts");

return new NpgsqlMultiHostDataSource(ConnectionStringBuilder, config);
}

return ConnectionStringBuilder.Multiplexing
? new MultiplexingDataSource(ConnectionStringBuilder, config)
: ConnectionStringBuilder.Pooling
? new PoolingDataSource(ConnectionStringBuilder, config)
: new UnpooledDataSource(ConnectionStringBuilder, config);
}

#pragma warning disable RS0016
/// <summary>
/// Builds and returns a <see cref="NpgsqlMultiHostDataSource" /> which is ready for use for load-balancing and failover scenarios.
/// </summary>
public NpgsqlMultiHostDataSource BuildMultiHost()
=> Build() as NpgsqlMultiHostDataSource ?? throw new InvalidOperationException(NpgsqlStrings.MultipleHostsMustBeSpecified);
#pragma warning restore RS0016
void ValidateMultiHost()
{
if (ConnectionStringBuilder.TargetSessionAttributes is not null)
throw new InvalidOperationException(NpgsqlStrings.CannotSpecifyTargetSessionAttributes);
if (ConnectionStringBuilder.Multiplexing)
throw new NotSupportedException("Multiplexing is not supported with multiple hosts");
if (ConnectionStringBuilder.ReplicationMode != ReplicationMode.Off)
throw new NotSupportedException("Replication is not supported with multiple hosts");
}
}
31 changes: 21 additions & 10 deletions src/Npgsql/NpgsqlMultiHostDataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,25 @@ static bool IsPreferred(DatabaseState state, TargetSessionAttributes preferredTy
{
DatabaseState.Offline => false,
DatabaseState.Unknown => true, // We will check compatibility again after refreshing the database state
DatabaseState.PrimaryReadWrite when preferredType is TargetSessionAttributes.Primary or TargetSessionAttributes.PreferPrimary or TargetSessionAttributes.ReadWrite => true,
DatabaseState.PrimaryReadOnly when preferredType is TargetSessionAttributes.Primary or TargetSessionAttributes.PreferPrimary or TargetSessionAttributes.ReadOnly => true,
DatabaseState.Standby when preferredType is TargetSessionAttributes.Standby or TargetSessionAttributes.PreferStandby or TargetSessionAttributes.ReadOnly => true,

DatabaseState.PrimaryReadWrite when preferredType is
TargetSessionAttributes.Primary or
TargetSessionAttributes.PreferPrimary or
TargetSessionAttributes.ReadWrite
=> true,

DatabaseState.PrimaryReadOnly when preferredType is
TargetSessionAttributes.Primary or
TargetSessionAttributes.PreferPrimary or
TargetSessionAttributes.ReadOnly
=> true,

DatabaseState.Standby when preferredType is
TargetSessionAttributes.Standby or
TargetSessionAttributes.PreferStandby or
TargetSessionAttributes.ReadOnly
=> true,

_ => preferredType == TargetSessionAttributes.Any
};

Expand Down Expand Up @@ -280,9 +296,7 @@ internal override async ValueTask<NpgsqlConnector> Get(

var timeoutPerHost = timeout.IsSet ? timeout.CheckAndGetTimeLeft() : TimeSpan.Zero;
var preferredType = GetTargetSessionAttributes(conn);
var checkUnpreferred =
preferredType == TargetSessionAttributes.PreferPrimary ||
preferredType == TargetSessionAttributes.PreferStandby;
var checkUnpreferred = preferredType is TargetSessionAttributes.PreferPrimary or TargetSessionAttributes.PreferStandby;

var connector = await TryGetIdleOrNew(conn, timeoutPerHost, async, preferredType, IsPreferred, poolIndex, exceptions, cancellationToken) ??
(checkUnpreferred ?
Expand All @@ -293,10 +307,7 @@ await TryGet(conn, timeoutPerHost, async, preferredType, IsPreferred, poolIndex,
await TryGet(conn, timeoutPerHost, async, preferredType, IsOnline, poolIndex, exceptions, cancellationToken)
: null);

if (connector is not null)
return connector;

throw NoSuitableHostsException(exceptions);
return connector ?? throw NoSuitableHostsException(exceptions);
}

static NpgsqlException NoSuitableHostsException(IList<Exception> exceptions)
Expand Down
6 changes: 0 additions & 6 deletions src/Npgsql/Properties/NpgsqlStrings.Designer.cs

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

3 changes: 0 additions & 3 deletions src/Npgsql/Properties/NpgsqlStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@
<data name="ArgumentMustBePositive" xml:space="preserve">
<value>'{0}' must be positive.</value>
</data>
<data name="MultipleHostsMustBeSpecified" xml:space="preserve">
<value>Multiple hosts must be specified.</value>
</data>
<data name="CannotSpecifyTargetSessionAttributes" xml:space="preserve">
<value>When creating a multi-host data source, TargetSessionAttributes cannot be specified. Create without TargetSessionAttributes, and then obtain DataSource wrappers from it. Consult the docs for more information.</value>
</data>
Expand Down
1 change: 1 addition & 0 deletions src/Npgsql/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ Npgsql.NpgsqlDataSourceBuilder.ConnectionString.get -> string!
Npgsql.NpgsqlDataSourceBuilder.ConnectionStringBuilder.get -> Npgsql.NpgsqlConnectionStringBuilder!
Npgsql.NpgsqlDataSourceBuilder.EnableParameterLogging(bool parameterLoggingEnabled = true) -> Npgsql.NpgsqlDataSourceBuilder!
Npgsql.NpgsqlDataSourceBuilder.Build() -> Npgsql.NpgsqlDataSource!
Npgsql.NpgsqlDataSourceBuilder.BuildMultiHost() -> Npgsql.NpgsqlMultiHostDataSource!
Npgsql.NpgsqlDataSourceBuilder.NpgsqlDataSourceBuilder(string? connectionString = null) -> void
Npgsql.NpgsqlDataSourceBuilder.UsePeriodicPasswordProvider(System.Func<Npgsql.NpgsqlConnectionStringBuilder!, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<string!>>? passwordProvider, System.TimeSpan successRefreshInterval, System.TimeSpan failureRefreshInterval) -> Npgsql.NpgsqlDataSourceBuilder!
Npgsql.NpgsqlDataSourceBuilder.UseLoggerFactory(Microsoft.Extensions.Logging.ILoggerFactory? loggerFactory) -> Npgsql.NpgsqlDataSourceBuilder!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@ public async Task NpgsqlDataSource_is_registered_properly([Values] bool async)
: dataSource.OpenConnection();
}

[Test]
public async Task NpgsqlMultiHostDataSource_is_registered_properly([Values] bool async)
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddMultiHostNpgsqlDataSource(TestUtil.ConnectionString);

await using var serviceProvider = serviceCollection.BuildServiceProvider();
var multiHostDataSource = serviceProvider.GetRequiredService<NpgsqlMultiHostDataSource>();
var dataSource = serviceProvider.GetRequiredService<NpgsqlDataSource>();

Assert.That(dataSource, Is.SameAs(multiHostDataSource));

await using var connection = async
? await dataSource.OpenConnectionAsync()
: dataSource.OpenConnection();
}

[Test]
public void NpgsqlDataSource_is_registered_as_singleton_by_default()
{
Expand Down
Loading