X Tutup
using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Net.Security; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Npgsql.Internal; using Npgsql.Internal.Resolvers; using Npgsql.Properties; using Npgsql.TypeMapping; 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() { DefaultNameTranslator = GlobalTypeMapper.Instance.DefaultNameTranslator }; // 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 => _userTypeMapper.DefaultNameTranslator; set => _userTypeMapper.DefaultNameTranslator = value; } /// 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("Calling MapEnum with a Type can require creating new generic types or methods. This may not work when AOT compiling.")] public INpgsqlTypeMapper MapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null) { _userTypeMapper.MapEnum(clrType, pgName, nameTranslator); return this; } /// public bool UnmapEnum([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null) => _userTypeMapper.UnmapEnum(clrType, 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 LTreeArrayTypeInfoResolver()); AddTypeInfoResolver(new GeometricArrayTypeInfoResolver()); AddTypeInfoResolver(new NetworkArrayTypeInfoResolver()); AddTypeInfoResolver(new FullTextSearchArrayTypeInfoResolver()); AddTypeInfoResolver(new RecordArrayTypeInfoResolver()); 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"); } }
X Tutup