using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Npgsql.Internal;
using Npgsql.Internal.Resolvers;
using Npgsql.Properties;
using Npgsql.TypeMapping;
using NpgsqlTypes;
namespace Npgsql;
///
/// Provides a simple API for configuring and creating an , from which database connections can be obtained.
///
///
/// On this builder, various features are disabled by default; unless you're looking to save on code size (e.g. when publishing with
/// NativeAOT), use instead.
///
public sealed class NpgsqlSlimDataSourceBuilder : INpgsqlTypeMapper
{
static UnsupportedTypeInfoResolver UnsupportedTypeInfoResolver { get; } = new();
ILoggerFactory? _loggerFactory;
bool _sensitiveDataLoggingEnabled;
TransportSecurityHandler _transportSecurityHandler = new();
RemoteCertificateValidationCallback? _userCertificateValidationCallback;
Action? _clientCertificatesCallback;
IntegratedSecurityHandler _integratedSecurityHandler = new();
Func>? _periodicPasswordProvider;
TimeSpan _periodicPasswordSuccessRefreshInterval, _periodicPasswordFailureRefreshInterval;
readonly List _resolverChain = new();
readonly UserTypeMapper _userTypeMapper;
Action? _syncConnectionInitializer;
Func? _asyncConnectionInitializer;
///
/// A connection string builder that can be used to configured the connection string on the builder.
///
public NpgsqlConnectionStringBuilder ConnectionStringBuilder { get; }
///
/// Returns the connection string, as currently configured on the builder.
///
public string ConnectionString => ConnectionStringBuilder.ToString();
static NpgsqlSlimDataSourceBuilder()
=> GlobalTypeMapper.Instance.AddGlobalTypeMappingResolvers(new []
{
AdoTypeInfoResolver.Instance
});
///
/// A diagnostics name used by Npgsql when generating tracing, logging and metrics.
///
public string? Name { get; set; }
///
/// Constructs a new , optionally starting out from the given
/// .
///
public NpgsqlSlimDataSourceBuilder(string? connectionString = null)
{
ConnectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString);
_userTypeMapper = new();
// Reverse order
AddTypeInfoResolver(UnsupportedTypeInfoResolver);
AddTypeInfoResolver(new AdoTypeInfoResolver());
// When used publicly we start off with our slim defaults.
var plugins = new List(GlobalTypeMapper.Instance.GetPluginResolvers());
plugins.Reverse();
foreach (var plugin in plugins)
AddTypeInfoResolver(plugin);
}
internal NpgsqlSlimDataSourceBuilder(NpgsqlConnectionStringBuilder connectionStringBuilder)
{
ConnectionStringBuilder = connectionStringBuilder;
_userTypeMapper = new();
}
///
/// Sets the that will be used for logging.
///
/// The logger factory to be used.
/// The same builder instance so that multiple calls can be chained.
public NpgsqlSlimDataSourceBuilder UseLoggerFactory(ILoggerFactory? loggerFactory)
{
_loggerFactory = loggerFactory;
return this;
}
///
/// Enables parameters to be included in logging. This includes potentially sensitive information from data sent to PostgreSQL.
/// You should only enable this flag in development, or if you have the appropriate security measures in place based on the
/// sensitivity of this data.
///
/// If , then sensitive data is logged.
/// The same builder instance so that multiple calls can be chained.
public NpgsqlSlimDataSourceBuilder EnableParameterLogging(bool parameterLoggingEnabled = true)
{
_sensitiveDataLoggingEnabled = parameterLoggingEnabled;
return this;
}
#region Authentication
///
/// When using SSL/TLS, this is a callback that allows customizing how the PostgreSQL-provided certificate is verified. This is an
/// advanced API, consider using or instead.
///
/// The callback containing custom callback verification logic.
///
///
/// Cannot be used in conjunction with , or
/// .
///
///
/// See .
///
///
/// The same builder instance so that multiple calls can be chained.
public NpgsqlSlimDataSourceBuilder UseUserCertificateValidationCallback(
RemoteCertificateValidationCallback userCertificateValidationCallback)
{
_userCertificateValidationCallback = userCertificateValidationCallback;
return this;
}
///
/// Specifies an SSL/TLS certificate which Npgsql will send to PostgreSQL for certificate-based authentication.
///
/// The client certificate to be sent to PostgreSQL when opening a connection.
/// The same builder instance so that multiple calls can be chained.
public NpgsqlSlimDataSourceBuilder UseClientCertificate(X509Certificate? clientCertificate)
{
if (clientCertificate is null)
return UseClientCertificatesCallback(null);
var clientCertificates = new X509CertificateCollection { clientCertificate };
return UseClientCertificates(clientCertificates);
}
///
/// Specifies a collection of SSL/TLS certificates which Npgsql will send to PostgreSQL for certificate-based authentication.
///
/// The client certificate collection to be sent to PostgreSQL when opening a connection.
/// The same builder instance so that multiple calls can be chained.
public NpgsqlSlimDataSourceBuilder UseClientCertificates(X509CertificateCollection? clientCertificates)
=> UseClientCertificatesCallback(clientCertificates is null ? null : certs => certs.AddRange(clientCertificates));
///
/// Specifies a callback to modify the collection of SSL/TLS client certificates which Npgsql will send to PostgreSQL for
/// certificate-based authentication. This is an advanced API, consider using or
/// instead.
///
/// The callback to modify the client certificate collection.
///
///
/// The callback is invoked every time a physical connection is opened, and is therefore suitable for rotating short-lived client
/// certificates. Simply make sure the certificate collection argument has the up-to-date certificate(s).
///
///
/// The callback's collection argument already includes any client certificates specified via the connection string or environment
/// variables.
///
///
/// The same builder instance so that multiple calls can be chained.
public NpgsqlSlimDataSourceBuilder UseClientCertificatesCallback(Action? clientCertificatesCallback)
{
_clientCertificatesCallback = clientCertificatesCallback;
return this;
}
///
/// Sets the that will be used validate SSL certificate, received from the server.
///
/// The CA certificate.
/// The same builder instance so that multiple calls can be chained.
public NpgsqlSlimDataSourceBuilder UseRootCertificate(X509Certificate2? rootCertificate)
=> rootCertificate is null
? UseRootCertificateCallback(null)
: UseRootCertificateCallback(() => rootCertificate);
///
/// Specifies a callback that will be used to validate SSL certificate, received from the server.
///
/// The callback to get CA certificate.
/// The same builder instance so that multiple calls can be chained.
///
/// This overload, which accepts a callback, is suitable for scenarios where the certificate rotates
/// and might change during the lifetime of the application.
/// When that's not the case, use the overload which directly accepts the certificate.
///
/// The same builder instance so that multiple calls can be chained.
public NpgsqlSlimDataSourceBuilder UseRootCertificateCallback(Func? rootCertificateCallback)
{
_transportSecurityHandler.RootCertificateCallback = rootCertificateCallback;
return this;
}
///
/// Configures a periodic password provider, which is automatically called by the data source at some regular interval. This is the
/// recommended way to fetch a rotating access token.
///
/// A callback which returns the password to be sent to PostgreSQL.
/// How long to cache the password before re-invoking the callback.
///
/// If a password refresh attempt fails, it will be re-attempted with this interval.
/// This should typically be much lower than .
///
/// The same builder instance so that multiple calls can be chained.
///
///
/// The provided callback is invoked in a timer, and not when opening connections. It therefore doesn't affect opening time.
///
///
/// The provided cancellation token is only triggered when the entire data source is disposed. If you'd like to apply a timeout to the
/// token fetching, do so within the provided callback.
///
///
/// The same builder instance so that multiple calls can be chained.
public NpgsqlSlimDataSourceBuilder UsePeriodicPasswordProvider(
Func>? passwordProvider,
TimeSpan successRefreshInterval,
TimeSpan failureRefreshInterval)
{
if (successRefreshInterval < TimeSpan.Zero)
throw new ArgumentException(
string.Format(NpgsqlStrings.ArgumentMustBePositive, nameof(successRefreshInterval)), nameof(successRefreshInterval));
if (failureRefreshInterval < TimeSpan.Zero)
throw new ArgumentException(
string.Format(NpgsqlStrings.ArgumentMustBePositive, nameof(failureRefreshInterval)), nameof(failureRefreshInterval));
_periodicPasswordProvider = passwordProvider;
_periodicPasswordSuccessRefreshInterval = successRefreshInterval;
_periodicPasswordFailureRefreshInterval = failureRefreshInterval;
return this;
}
#endregion Authentication
#region Type mapping
///
public INpgsqlNameTranslator DefaultNameTranslator { get; set; } = GlobalTypeMapper.Instance.DefaultNameTranslator;
///
public INpgsqlTypeMapper MapEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] TEnum>(string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
where TEnum : struct, Enum
{
_userTypeMapper.MapEnum(pgName, nameTranslator);
return this;
}
///
public bool UnmapEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] TEnum>(string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
where TEnum : struct, Enum
=> _userTypeMapper.UnmapEnum(pgName, nameTranslator);
///
[RequiresDynamicCode("Mapping composite types involves serializing arbitrary types, requiring require creating new generic types or methods. This is currently unsupported with NativeAOT, vote on issue #5303 if this is important to you.")]
public INpgsqlTypeMapper MapComposite<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)] T>(
string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
{
_userTypeMapper.MapComposite(typeof(T), pgName, nameTranslator);
return this;
}
///
[RequiresDynamicCode("Mapping composite types involves serializing arbitrary types, requiring require creating new generic types or methods. This is currently unsupported with NativeAOT, vote on issue #5303 if this is important to you.")]
public bool UnmapComposite<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)] T>(
string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
=> _userTypeMapper.UnmapComposite(typeof(T), pgName, nameTranslator);
///
[RequiresDynamicCode("Mapping composite types involves serializing arbitrary types, requiring require creating new generic types or methods. This is currently unsupported with NativeAOT, vote on issue #5303 if this is important to you.")]
public INpgsqlTypeMapper MapComposite([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)]
Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
{
_userTypeMapper.MapComposite(clrType, pgName, nameTranslator);
return this;
}
///
[RequiresDynamicCode("Mapping composite types involves serializing arbitrary types, requiring require creating new generic types or methods. This is currently unsupported with NativeAOT, vote on issue #5303 if this is important to you.")]
public bool UnmapComposite([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)]
Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
=> _userTypeMapper.UnmapComposite(clrType, pgName, nameTranslator);
///
/// Adds a type info resolver which can add or modify support for PostgreSQL types.
/// Typically used by plugins.
///
/// The type resolver to be added.
public void AddTypeInfoResolver(IPgTypeInfoResolver resolver)
{
var type = resolver.GetType();
for (var i = 0; i < _resolverChain.Count; i++)
if (_resolverChain[i].GetType() == type)
{
_resolverChain.RemoveAt(i);
break;
}
_resolverChain.Insert(0, resolver);
}
void INpgsqlTypeMapper.Reset()
=> ResetTypeMappings();
internal void ResetTypeMappings()
{
_resolverChain.Clear();
_resolverChain.AddRange(GlobalTypeMapper.Instance.GetPluginResolvers());
}
#endregion Type mapping
#region Optional opt-ins
///
/// Sets up mappings for the PostgreSQL array types.
///
/// The same builder instance so that multiple calls can be chained.
public NpgsqlSlimDataSourceBuilder EnableArrays()
{
AddTypeInfoResolver(new RangeArrayTypeInfoResolver());
AddTypeInfoResolver(new ExtraConversionsArrayTypeInfoResolver());
AddTypeInfoResolver(new AdoArrayTypeInfoResolver());
return this;
}
///
/// Sets up mappings for the PostgreSQL range types.
///
/// The same builder instance so that multiple calls can be chained.
public NpgsqlSlimDataSourceBuilder EnableRanges()
{
AddTypeInfoResolver(new RangeTypeInfoResolver());
return this;
}
///
/// Sets up mappings for the PostgreSQL multirange types.
///
/// The same builder instance so that multiple calls can be chained.
public NpgsqlSlimDataSourceBuilder EnableMultiranges()
{
AddTypeInfoResolver(new RangeTypeInfoResolver());
return this;
}
///
/// Sets up mappings for the PostgreSQL record type as a .NET object[].
///
/// The same builder instance so that multiple calls can be chained.
public NpgsqlSlimDataSourceBuilder EnableRecords()
{
AddTypeInfoResolver(new RecordTypeInfoResolver());
return this;
}
///
/// Sets up mappings for the PostgreSQL tsquery and tsvector types.
///
/// The same builder instance so that multiple calls can be chained.
public NpgsqlSlimDataSourceBuilder EnableFullTextSearch()
{
AddTypeInfoResolver(new FullTextSearchTypeInfoResolver());
return this;
}
///
/// Sets up mappings for the PostgreSQL ltree extension types.
///
/// The same builder instance so that multiple calls can be chained.
public NpgsqlSlimDataSourceBuilder EnableLTree()
{
AddTypeInfoResolver(new LTreeTypeInfoResolver());
return this;
}
///
/// Sets up mappings for extra conversions from PostgreSQL to .NET types.
///
/// The same builder instance so that multiple calls can be chained.
public NpgsqlSlimDataSourceBuilder EnableExtraConversions()
{
AddTypeInfoResolver(new ExtraConversionsResolver());
return this;
}
///
/// Enables the possibility to use TLS/SSl encryption for connections to PostgreSQL. This does not guarantee that encryption will
/// actually be used; see for more details.
///
/// The same builder instance so that multiple calls can be chained.
public NpgsqlSlimDataSourceBuilder EnableTransportSecurity()
{
_transportSecurityHandler = new RealTransportSecurityHandler();
return this;
}
///
/// Enables the possibility to use GSS/SSPI authentication for connections to PostgreSQL. This does not guarantee that it will
/// actually be used; see for more details.
///
/// The same builder instance so that multiple calls can be chained.
public NpgsqlSlimDataSourceBuilder EnableIntegratedSecurity()
{
_integratedSecurityHandler = new RealIntegratedSecurityHandler();
return this;
}
#endregion Optional opt-ins
///
/// Register a connection initializer, which allows executing arbitrary commands when a physical database connection is first opened.
///
///
/// A synchronous connection initialization lambda, which will be called from when a new physical
/// connection is opened.
///
///
/// An asynchronous connection initialization lambda, which will be called from
/// when a new physical connection is opened.
///
///
/// If an initializer is registered, both sync and async versions must be provided. If you do not use sync APIs in your code, simply
/// throw , which would also catch accidental cases of sync opening.
///
///
/// Take care that the setting you apply in the initializer does not get reverted when the connection is returned to the pool, since
/// Npgsql sends DISCARD ALL by default. The option can be used to
/// turn this off.
///
/// The same builder instance so that multiple calls can be chained.
public NpgsqlSlimDataSourceBuilder UsePhysicalConnectionInitializer(
Action? connectionInitializer,
Func? connectionInitializerAsync)
{
if (connectionInitializer is null != connectionInitializerAsync is null)
throw new ArgumentException(NpgsqlStrings.SyncAndAsyncConnectionInitializersRequired);
_syncConnectionInitializer = connectionInitializer;
_asyncConnectionInitializer = connectionInitializerAsync;
return this;
}
///
/// Builds and returns an which is ready for use.
///
public NpgsqlDataSource Build()
{
var config = PrepareConfiguration();
var connectionStringBuilder = ConnectionStringBuilder.Clone();
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);
}
///
/// Builds and returns a which is ready for use for load-balancing and failover scenarios.
///
public NpgsqlMultiHostDataSource BuildMultiHost()
{
var config = PrepareConfiguration();
ValidateMultiHost();
return new(ConnectionStringBuilder.Clone(), config);
}
NpgsqlDataSourceConfiguration PrepareConfiguration()
{
ConnectionStringBuilder.PostProcessAndValidate();
if (!_transportSecurityHandler.SupportEncryption && (_userCertificateValidationCallback is not null || _clientCertificatesCallback is not null))
{
throw new InvalidOperationException(NpgsqlStrings.TransportSecurityDisabled);
}
if (_periodicPasswordProvider is not null &&
(ConnectionStringBuilder.Password is not null || ConnectionStringBuilder.Passfile is not null))
{
throw new NotSupportedException(NpgsqlStrings.CannotSetBothPasswordProviderAndPassword);
}
return new(
Name,
_loggerFactory is null
? NpgsqlLoggingConfiguration.NullConfiguration
: new NpgsqlLoggingConfiguration(_loggerFactory, _sensitiveDataLoggingEnabled),
_transportSecurityHandler,
_integratedSecurityHandler,
_userCertificateValidationCallback,
_clientCertificatesCallback,
_periodicPasswordProvider,
_periodicPasswordSuccessRefreshInterval,
_periodicPasswordFailureRefreshInterval,
Resolvers(),
HackyEnumMappings(),
DefaultNameTranslator,
_syncConnectionInitializer,
_asyncConnectionInitializer);
IEnumerable Resolvers()
{
var resolvers = new List();
if (_userTypeMapper.Items.Count > 0)
resolvers.Add(_userTypeMapper.Build());
if (GlobalTypeMapper.Instance.GetUserMappingsResolver() is { } globalUserTypeMapper)
resolvers.Add(globalUserTypeMapper);
resolvers.AddRange(_resolverChain);
return resolvers;
}
List HackyEnumMappings()
{
var mappings = new List();
if (_userTypeMapper.Items.Count > 0)
foreach (var userTypeMapping in _userTypeMapper.Items)
if (userTypeMapping is UserTypeMapper.EnumMapping enumMapping)
mappings.Add(new(enumMapping.ClrType, enumMapping.PgTypeName, enumMapping.NameTranslator));
if (GlobalTypeMapper.Instance.HackyEnumTypeMappings.Count > 0)
mappings.AddRange(GlobalTypeMapper.Instance.HackyEnumTypeMappings);
return mappings;
}
}
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");
}
}