using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Data.Common; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net; using System.Net.Security; using System.Net.Sockets; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using System.Transactions; using Npgsql.Internal; using Npgsql.Logging; using Npgsql.NameTranslation; using Npgsql.Netstandard20; using Npgsql.TypeMapping; using Npgsql.Util; using NpgsqlTypes; using IsolationLevel = System.Data.IsolationLevel; namespace Npgsql; /// /// This class represents a connection to a PostgreSQL server. /// // ReSharper disable once RedundantNameQualifier [System.ComponentModel.DesignerCategory("")] public sealed class NpgsqlConnection : DbConnection, ICloneable, IComponent { #region Fields // Set this when disposed is called. bool _disposed; /// /// The connection string, without the password after open (unless Persist Security Info=true) /// string _userFacingConnectionString = string.Empty; /// /// The original connection string provided by the user, including the password. /// string _connectionString = string.Empty; internal string OriginalConnectionString => _connectionString; ConnectionState _fullState; /// /// The physical connection to the database. This is when the connection is closed, /// and also when it is open in multiplexing mode and unbound (e.g. not in a transaction). /// internal NpgsqlConnector? Connector { get; set; } /// /// The parsed connection string. Set only after the connection is opened. /// public NpgsqlConnectionStringBuilder Settings { get; private set; } = DefaultSettings; static readonly NpgsqlConnectionStringBuilder DefaultSettings = new(); ConnectorSource? _pool; internal ConnectorSource Pool { get { Debug.Assert(_pool is not null); return _pool; } } /// /// A cached command handed out by , which is returned when disposed. Useful for reducing allocations. /// internal NpgsqlCommand? CachedCommand { get; set; } /// /// Flag used to make sure we never double-close a connection, returning it twice to the pool. /// int _closing; internal Transaction? EnlistedTransaction { get; set; } /// /// The global type mapper, which contains defaults used by all new connections. /// Modify mappings on this mapper to affect your entire application. /// public static INpgsqlTypeMapper GlobalTypeMapper => TypeMapping.GlobalTypeMapper.Instance; /// /// The connection-specific type mapper - all modifications affect this connection only, /// and are lost when it is closed. /// public INpgsqlTypeMapper TypeMapper { get { if (Settings.Multiplexing) throw new NotSupportedException("Connection-specific type mapping is unsupported when multiplexing is enabled."); CheckReady(); return Connector!.TypeMapper!; } } /// /// The default TCP/IP port for PostgreSQL. /// public const int DefaultPort = 5432; /// /// Maximum value for connection timeout. /// internal const int TimeoutLimit = 1024; /// /// Tracks when this connection was bound to a physical connector (e.g. at open-time, when a transaction /// was started...). /// internal ConnectorBindingScope ConnectorBindingScope { get; set; } static readonly NpgsqlLogger Log = NpgsqlLogManager.CreateLogger(nameof(NpgsqlConnection)); static readonly StateChangeEventArgs ClosedToOpenEventArgs = new(ConnectionState.Closed, ConnectionState.Open); static readonly StateChangeEventArgs OpenToClosedEventArgs = new(ConnectionState.Open, ConnectionState.Closed); #endregion Fields #region Constructors / Init / Open /// /// Initializes a new instance of the class. /// public NpgsqlConnection() => GC.SuppressFinalize(this); /// /// Initializes a new instance of with the given connection string. /// /// The connection used to open the PostgreSQL database. public NpgsqlConnection(string? connectionString) : this() => ConnectionString = connectionString; /// /// Opens a database connection with the property settings specified by the . /// public override void Open() => Open(false, CancellationToken.None).GetAwaiter().GetResult(); /// /// This is the asynchronous version of . /// /// /// Do not invoke other methods and properties of the object until the returned Task is complete. /// /// /// An optional token to cancel the asynchronous operation. The default value is . /// /// A task representing the asynchronous operation. public override Task OpenAsync(CancellationToken cancellationToken) { using (NoSynchronizationContextScope.Enter()) return Open(true, cancellationToken); } void GetPoolAndSettings() { if (PoolManager.TryGetValue(_connectionString, out _pool)) { Settings = _pool.Settings; // Great, we already have a pool return; } // Connection string hasn't been seen before. Parse it. var settings = new NpgsqlConnectionStringBuilder(_connectionString); settings.Validate(); Settings = settings; var hostsSeparator = settings.Host?.IndexOf(','); if (hostsSeparator is -1) { if (settings.TargetSessionAttributesParsed is not null && settings.TargetSessionAttributesParsed != TargetSessionAttributes.Any) { throw new NotSupportedException("Target Session Attributes other then Any is only supported with multiple hosts"); } if (!NpgsqlConnectionStringBuilder.IsUnixSocket(settings.Host!, settings.Port, out _) && NpgsqlConnectionStringBuilder.TrySplitHostPort(settings.Host!.AsSpan(), out var newHost, out var newPort)) { settings.Host = newHost; settings.Port = newPort; } } // The connection string may be equivalent to one that has already been seen though (e.g. different // ordering). Have NpgsqlConnectionStringBuilder produce a canonical string representation // and recheck. // Note that we remove TargetSessionAttributes and LoadBalanceHosts to make all connection strings // that are otherwise identical point to the same pool. var canonical = settings.ConnectionStringForMultipleHosts; if (PoolManager.TryGetValue(canonical, out _pool)) { // We're wrapping the original pool in the other, as the original doesn't have the TargetSessionAttributes if (_pool is MultiHostConnectorPool mhcpCanonical) _pool = new MultiHostConnectorPoolWrapper(Settings, _connectionString, mhcpCanonical); // The pool was found, but only under the canonical key - we're using a different version // for the first time. Map it via our own key for next time. _pool = PoolManager.GetOrAdd(_connectionString, _pool); return; } // Really unseen, need to create a new pool // The canonical pool is the 'base' pool so we need to set that up first. If someone beats us to it use what they put. // The connection string pool can either be added here or above, if it's added above we should just use that. ConnectorSource newPool; if (hostsSeparator.HasValue && hostsSeparator != -1) { if (settings.Multiplexing) throw new NotSupportedException("Multiplexing is not supported with multiple hosts"); if (settings.ReplicationMode != ReplicationMode.Off) throw new NotSupportedException("Replication is not supported with multiple hosts"); newPool = new MultiHostConnectorPool(settings, canonical); } else if (settings.Multiplexing) newPool = new MultiplexingConnectorPool(settings, canonical); else if (settings.Pooling) newPool = new ConnectorPool(settings, canonical); else newPool = new UnpooledConnectorSource(settings, canonical); _pool = PoolManager.GetOrAdd(canonical, newPool); if (_pool == newPool) { Debug.Assert(_pool is not MultiHostConnectorPoolWrapper); // If the pool we created was the one that ended up being stored we need to increment the appropriate counter. // Avoids a race condition where multiple threads will create a pool but only one will be stored. NpgsqlEventSource.Log.PoolCreated(newPool); } else newPool.Dispose(); // We're wrapping the original pool in the other, as the original doesn't have the TargetSessionAttributes if (_pool is MultiHostConnectorPool mhcp) _pool = new MultiHostConnectorPoolWrapper(Settings, _connectionString, mhcp); _pool = PoolManager.GetOrAdd(_connectionString, _pool); } internal Task Open(bool async, CancellationToken cancellationToken) { CheckClosed(); Debug.Assert(Connector == null); if (_pool == null) { Debug.Assert(string.IsNullOrEmpty(_connectionString)); throw new InvalidOperationException("The ConnectionString property has not been initialized."); } FullState = ConnectionState.Connecting; Log.Trace("Opening connection..."); if (Settings.Multiplexing) { if (Settings.Enlist && Transaction.Current != null) { // TODO: Keep in mind that the TransactionScope can be disposed throw new NotImplementedException(); } // We're opening in multiplexing mode, without a transaction. We don't actually do anything. _userFacingConnectionString = Pool.UserFacingConnectionString; // If we've never connected with this connection string, open a physical connector in order to generate // any exception (bad user/password, IP address...). This reproduces the standard error behavior. if (!((MultiplexingConnectorPool)Pool).IsBootstrapped) return BootstrapMultiplexing(async, cancellationToken); Log.Debug("Connection opened (multipelxing)"); FullState = ConnectionState.Open; return Task.CompletedTask; } return OpenAsync(async, cancellationToken); async Task OpenAsync(bool async, CancellationToken cancellationToken) { Debug.Assert(!Settings.Multiplexing); NpgsqlConnector? connector = null; try { var connectionTimeout = TimeSpan.FromSeconds(ConnectionTimeout); var timeout = new NpgsqlTimeout(connectionTimeout); var enlistToTransaction = Settings.Enlist ? Transaction.Current : null; _userFacingConnectionString = _pool.UserFacingConnectionString; // First, check to see if we there's an ambient transaction, and we have a connection enlisted // to this transaction which has been closed. If so, return that as an optimization rather than // opening a new one and triggering escalation to a distributed transaction. // Otherwise just get a new connector and enlist below. if (enlistToTransaction is not null && _pool.TryRentEnlistedPending(enlistToTransaction, this, out connector)) { EnlistedTransaction = enlistToTransaction; enlistToTransaction = null; } else connector = await _pool.Get(this, timeout, async, cancellationToken); Debug.Assert(connector.Connection is null, $"Connection for opened connector '{Connector?.Id.ToString() ?? "???"}' is bound to another connection"); // Since this connector was last used, PostgreSQL types (e.g. enums) may have been added // (and ReloadTypes() called), or global mappings may have changed by the user. // Bring this up to date if needed. // Note that in multiplexing execution, the pool-wide type mapper is used so no // need to update the connector type mapper (this is why this is here). if (connector.TypeMapper.ChangeCounter != TypeMapping.GlobalTypeMapper.Instance.ChangeCounter) { // LoadDatabaseInfo might attempt to execute a query over a connector, which might run in parallel to KeepAlive. // Start a user action to prevent this. using var _ = connector.StartUserAction(ConnectorState.Executing, cancellationToken); await connector.LoadDatabaseInfo(false, timeout, async, cancellationToken); } ConnectorBindingScope = ConnectorBindingScope.Connection; connector.Connection = this; Connector = connector; if (enlistToTransaction is not null) EnlistTransaction(enlistToTransaction); Log.Debug("Connection opened"); FullState = ConnectionState.Open; } catch { FullState = ConnectionState.Closed; ConnectorBindingScope = ConnectorBindingScope.None; Connector = null; EnlistedTransaction = null; if (connector is not null) { connector.Connection = null; connector.Return(); } throw; } } async Task BootstrapMultiplexing(bool async, CancellationToken cancellationToken) { try { var timeout = new NpgsqlTimeout(TimeSpan.FromSeconds(ConnectionTimeout)); await ((MultiplexingConnectorPool)Pool).BootstrapMultiplexing(this, timeout, async, cancellationToken); Log.Debug("Connection opened (multiplexing)"); FullState = ConnectionState.Open; } catch { FullState = ConnectionState.Closed; throw; } } } #endregion Open / Init #region Connection string management /// /// Gets or sets the string used to connect to a PostgreSQL database. See the manual for details. /// /// The connection string that includes the server name, /// the database name, and other parameters needed to establish /// the initial connection. The default value is an empty string. /// [AllowNull] public override string ConnectionString { get => _userFacingConnectionString; set { CheckClosed(); _userFacingConnectionString = _connectionString = value ?? string.Empty; GetPoolAndSettings(); } } /// /// Gets or sets the delegate used to generate a password for new database connections. /// /// ///

/// This delegate is executed when a new database connection is opened that requires a password. ///

///

/// The and connection /// string properties have precedence over this delegate: it will not be executed if a password is specified, or if the specified or /// default Passfile contains a valid entry. ///

///

/// Due to connection pooling this delegate is only executed when a new physical connection is opened, not when reusing a connection /// that was previously opened from the pool. ///

///
public ProvidePasswordCallback? ProvidePasswordCallback { get; set; } /// /// Gets or sets the delegate used to setup a connection whenever a physical connection is opened synchronously. /// [RequiresPreviewFeatures("Physical open callback is an experimental API, and its exact shape may change in the future")] public PhysicalOpenCallback? PhysicalOpenCallback { get; set; } /// /// Gets or sets the delegate used to setup a connection whenever a physical connection is opened asynchronously. /// [RequiresPreviewFeatures("Physical open callback is an experimental API, and its exact shape may change in the future")] public PhysicalOpenAsyncCallback? PhysicalOpenAsyncCallback { get; set; } #endregion Connection string management #region Configuration settings /// /// Backend server host name. /// [Browsable(true)] public string? Host => Connector?.Host; /// /// Backend server port. /// [Browsable(true)] public int Port => Connector?.Port ?? 0; /// /// Gets the time (in seconds) to wait while trying to establish a connection /// before terminating the attempt and generating an error. /// /// The time (in seconds) to wait for a connection to open. The default value is 15 seconds. public override int ConnectionTimeout => Settings.Timeout; /// /// Gets the time (in seconds) to wait while trying to execute a command /// before terminating the attempt and generating an error. /// /// The time (in seconds) to wait for a command to complete. The default value is 20 seconds. public int CommandTimeout => Settings.CommandTimeout; /// /// Gets the name of the current database or the database to be used after a connection is opened. /// /// The name of the current database or the name of the database to be /// used after a connection is opened. The default value is the empty string. public override string Database => Settings.Database ?? Settings.Username ?? ""; /// /// Gets the string identifying the database server (host and port) /// /// /// The name of the database server (host and port). If the connection uses a Unix-domain socket, /// the path to that socket is returned. The default value is the empty string. /// public override string DataSource => Connector?.Settings.DataSourceCached ?? string.Empty; /// /// Whether to use Windows integrated security to log in. /// public bool IntegratedSecurity => Settings.IntegratedSecurity; /// /// User name. /// public string? UserName => Settings.Username; internal string? Password => Settings.Password; // The following two lines are here for backwards compatibility with the EF6 provider // ReSharper disable UnusedMember.Global internal string? EntityTemplateDatabase => Settings.EntityTemplateDatabase; internal string? EntityAdminDatabase => Settings.EntityAdminDatabase; // ReSharper restore UnusedMember.Global #endregion Configuration settings #region State management /// /// Gets the current state of the connection. /// /// A bitwise combination of the values. The default is Closed. [Browsable(false)] public ConnectionState FullState { // Note: we allow accessing the state after dispose, #164 get => _fullState switch { ConnectionState.Open => Connector == null ? ConnectionState.Open // When unbound, we only know we're open : Connector.State switch { ConnectorState.Ready => ConnectionState.Open, ConnectorState.Executing => ConnectionState.Open | ConnectionState.Executing, ConnectorState.Fetching => ConnectionState.Open | ConnectionState.Fetching, ConnectorState.Copy => ConnectionState.Open | ConnectionState.Fetching, ConnectorState.Replication => ConnectionState.Open | ConnectionState.Fetching, ConnectorState.Waiting => ConnectionState.Open | ConnectionState.Fetching, ConnectorState.Connecting => ConnectionState.Connecting, ConnectorState.Broken => ConnectionState.Broken, ConnectorState.Closed => throw new InvalidOperationException("Internal Npgsql bug: connection is in state Open but connector is in state Closed"), _ => throw new InvalidOperationException($"Internal Npgsql bug: unexpected value {Connector.State} of enum {nameof(ConnectorState)}. Please file a bug.") }, _ => _fullState }; internal set { var originalOpen = _fullState.HasFlag(ConnectionState.Open); _fullState = value; var currentOpen = _fullState.HasFlag(ConnectionState.Open); if (currentOpen != originalOpen) { OnStateChange(currentOpen ? ClosedToOpenEventArgs : OpenToClosedEventArgs); } } } /// /// Gets whether the current state of the connection is Open or Closed /// /// ConnectionState.Open, ConnectionState.Closed or ConnectionState.Connecting [Browsable(false)] public override ConnectionState State { get { var fullState = FullState; if (fullState.HasFlag(ConnectionState.Connecting)) return ConnectionState.Connecting; if (fullState.HasFlag(ConnectionState.Open)) return ConnectionState.Open; return ConnectionState.Closed; } } #endregion State management #region Command / Batch creation /// /// Creates and returns a /// object associated with the . /// /// A object. protected override DbCommand CreateDbCommand() => CreateCommand(); /// /// Creates and returns a object associated with the . /// /// A object. public new NpgsqlCommand CreateCommand() { CheckDisposed(); var cachedCommand = CachedCommand; if (cachedCommand is not null) { CachedCommand = null; cachedCommand.State = CommandState.Idle; return cachedCommand; } return NpgsqlCommand.CreateCachedCommand(this); } #if NET6_0_OR_GREATER /// public override bool CanCreateBatch => true; /// protected override DbBatch CreateDbBatch() => CreateBatch(); /// public new NpgsqlBatch CreateBatch() => new(this); #else /// /// Creates and returns a object associated with the . /// /// A object. public NpgsqlBatch CreateBatch() => new(this); #endif #endregion Command / Batch creation #region Transactions /// /// Begins a database transaction with the specified isolation level. /// /// The isolation level under which the transaction should run. /// A object representing the new transaction. /// Nested transactions are not supported. protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => BeginTransaction(isolationLevel); /// /// Begins a database transaction. /// /// A object representing the new transaction. /// /// Nested transactions are not supported. /// Transactions created by this method will have the isolation level. /// public new NpgsqlTransaction BeginTransaction() => BeginTransaction(IsolationLevel.Unspecified); /// /// Begins a database transaction with the specified isolation level. /// /// The isolation level under which the transaction should run. /// A object representing the new transaction. /// Nested transactions are not supported. public new NpgsqlTransaction BeginTransaction(IsolationLevel level) => BeginTransaction(level, async: false, CancellationToken.None).GetAwaiter().GetResult(); async ValueTask BeginTransaction(IsolationLevel level, bool async, CancellationToken cancellationToken) { if (level == IsolationLevel.Chaos) throw new NotSupportedException("Unsupported IsolationLevel: " + level); CheckReady(); if (Connector != null && Connector.InTransaction) throw new InvalidOperationException("A transaction is already in progress; nested/concurrent transactions aren't supported."); // There was a commited/rollbacked transaction, but it was not disposed var connector = ConnectorBindingScope == ConnectorBindingScope.Transaction ? Connector : await StartBindingScope(ConnectorBindingScope.Transaction, NpgsqlTimeout.Infinite, async, cancellationToken); Debug.Assert(connector != null); try { // Note that beginning a transaction doesn't actually send anything to the backend (only prepends). // But we start a user action to check the cancellation token and generate exceptions using var _ = connector.StartUserAction(cancellationToken); connector.Transaction ??= new NpgsqlTransaction(connector); connector.Transaction.Init(level); return connector.Transaction; } catch { EndBindingScope(ConnectorBindingScope.Transaction); throw; } } #if !NETSTANDARD2_0 /// /// Asynchronously begins a database transaction. /// /// The isolation level under which the transaction should run. /// /// An optional token to cancel the asynchronous operation. The default value is . /// /// A task whose property is an object representing the new transaction. /// /// Nested transactions are not supported. /// protected override async ValueTask BeginDbTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken) => await BeginTransactionAsync(isolationLevel, cancellationToken); /// /// Asynchronously begins a database transaction. /// /// /// An optional token to cancel the asynchronous operation. The default value is . /// /// A task whose Result property is an object representing the new transaction. /// /// Nested transactions are not supported. /// Transactions created by this method will have the isolation level. /// public new ValueTask BeginTransactionAsync(CancellationToken cancellationToken = default) => BeginTransactionAsync(IsolationLevel.Unspecified, cancellationToken); /// /// Asynchronously begins a database transaction. /// /// The isolation level under which the transaction should run. /// /// An optional token to cancel the asynchronous operation. The default value is . /// /// A task whose property is an object representing the new transaction. /// /// Nested transactions are not supported. /// public new ValueTask BeginTransactionAsync(IsolationLevel level, CancellationToken cancellationToken = default) { using (NoSynchronizationContextScope.Enter()) return BeginTransaction(level, async: true, cancellationToken); } #endif /// /// Enlist transaction. /// public override void EnlistTransaction(Transaction? transaction) { if (Settings.Multiplexing) throw new NotSupportedException("Ambient transactions aren't yet implemented for multiplexing"); if (EnlistedTransaction != null) { if (EnlistedTransaction.Equals(transaction)) return; try { if (EnlistedTransaction.TransactionInformation.Status == System.Transactions.TransactionStatus.Active) throw new InvalidOperationException($"Already enlisted to transaction (localid={EnlistedTransaction.TransactionInformation.LocalIdentifier})"); } catch (ObjectDisposedException) { // The MSDTC 2nd phase is asynchronous, so we may end up checking the TransactionInformation on // a disposed transaction. To be extra safe we catch that, and understand that the transaction // has ended - no problem for reenlisting. } } CheckReady(); var connector = StartBindingScope(ConnectorBindingScope.Transaction); EnlistedTransaction = transaction; if (transaction == null) { EnlistedTransaction = null; return; } // Until #1378 is implemented, we have no recovery, and so no need to enlist as a durable resource manager // (or as promotable single phase). // Note that even when #1378 is implemented in some way, we should check for mono and go volatile in any case - // distributed transactions aren't supported. var volatileResourceManager = new VolatileResourceManager(this, transaction); transaction.EnlistVolatile(volatileResourceManager, EnlistmentOptions.None); volatileResourceManager.Init(); EnlistedTransaction = transaction; Log.Debug($"Enlisted volatile resource manager (localid={transaction.TransactionInformation.LocalIdentifier})", connector.Id); } #endregion #region Close /// /// Releases the connection. If the connection is pooled, it will be returned to the pool and made available for re-use. /// If it is non-pooled, the physical connection will be closed. /// public override void Close() => Close(async: false).GetAwaiter().GetResult(); /// /// Releases the connection. If the connection is pooled, it will be returned to the pool and made available for re-use. /// If it is non-pooled, the physical connection will be closed. /// #if NETSTANDARD2_0 public Task CloseAsync() #else public override Task CloseAsync() #endif { using (NoSynchronizationContextScope.Enter()) return Close(async: true); } internal bool TakeCloseLock() => Interlocked.Exchange(ref _closing, 1) == 0; internal void ReleaseCloseLock() => Volatile.Write(ref _closing, 0); internal Task Close(bool async) { // Even though NpgsqlConnection isn't thread safe we'll make sure this part is. // Because we really don't want double returns to the pool. if (!TakeCloseLock()) return Task.CompletedTask; switch (FullState) { case ConnectionState.Open: case ConnectionState.Open | ConnectionState.Executing: case ConnectionState.Open | ConnectionState.Fetching: break; case ConnectionState.Broken: FullState = ConnectionState.Closed; goto case ConnectionState.Closed; case ConnectionState.Closed: ReleaseCloseLock(); return Task.CompletedTask; case ConnectionState.Connecting: ReleaseCloseLock(); throw new InvalidOperationException("Can't close, connection is in state " + FullState); default: ReleaseCloseLock(); throw new ArgumentOutOfRangeException("Unknown connection state: " + FullState); } // TODO: The following shouldn't exist - we need to flow down the regular path to close any // open reader / COPY. See test CloseDuringRead with multiplexing. if (Settings.Multiplexing && ConnectorBindingScope == ConnectorBindingScope.None) { // TODO: Consider falling through to the regular reset logic. This adds some unneeded conditions // and assignment but actual perf impact should be negligible (measure). Debug.Assert(Connector == null); ReleaseCloseLock(); FullState = ConnectionState.Closed; Log.Debug("Connection closed (multiplexing)"); return Task.CompletedTask; } return CloseAsync(async); } async Task CloseAsync(bool async) { Debug.Assert(Connector != null); Debug.Assert(ConnectorBindingScope != ConnectorBindingScope.None); try { var connector = Connector; Log.Trace("Closing connection...", connector.Id); if (connector.CurrentReader != null || connector.CurrentCopyOperation != null) { // This method could re-enter connection.Close() due to an underlying connection failure. await connector.CloseOngoingOperations(async); if (ConnectorBindingScope == ConnectorBindingScope.None) { Debug.Assert(Settings.Multiplexing); Debug.Assert(Connector is null); FullState = ConnectionState.Closed; Log.Debug("Connection closed (multiplexing, after closing reader)", connector.Id); return; } } Debug.Assert(connector.IsReady || connector.IsBroken); Debug.Assert(connector.CurrentReader == null); Debug.Assert(connector.CurrentCopyOperation == null); if (EnlistedTransaction != null) { // A System.Transactions transaction is still in progress connector.Connection = null; // Close the connection and disconnect it from the resource manager but leave the // connector in an enlisted pending list in the pool. If another connection is opened within // the same transaction scope, we will reuse this connector to avoid escalating to a distributed // transaction _pool?.AddPendingEnlistedConnector(connector, EnlistedTransaction); EnlistedTransaction = null; } else { if (Settings.Pooling) { // Clear the buffer, roll back any pending transaction and prepend a reset message if needed // Also returns the connector to the pool, if there is an open transaction and multiplexing is on // Note that we're doing this only for pooled connections await connector.Reset(async); } else { // We're already doing the same in the NpgsqlConnector.Reset for pooled connections // TODO: move reset logic to ConnectorSource.Return connector.Transaction?.UnbindIfNecessary(); } if (Settings.Multiplexing) { // We've already closed ongoing operations rolled back any transaction and the connector is already in the pool, // so we must be unbound. Nothing to do. Debug.Assert(ConnectorBindingScope == ConnectorBindingScope.None, $"When closing a multiplexed connection, the connection was supposed to be unbound, but {nameof(ConnectorBindingScope)} was {ConnectorBindingScope}"); } else { connector.Connection = null; connector.Return(); } } Connector = null; ConnectorBindingScope = ConnectorBindingScope.None; FullState = ConnectionState.Closed; Log.Debug("Connection closed", connector.Id); } finally { ReleaseCloseLock(); } } /// /// Releases all resources used by the . /// /// when called from ; /// when being called from the finalizer. protected override void Dispose(bool disposing) { if (_disposed) return; if (disposing) Close(); _disposed = true; } /// /// Releases all resources used by the . /// #if NETSTANDARD2_0 public ValueTask DisposeAsync() #else public override ValueTask DisposeAsync() #endif { using (NoSynchronizationContextScope.Enter()) return DisposeAsyncCore(); [MethodImpl(MethodImplOptions.AggressiveInlining)] async ValueTask DisposeAsyncCore() { if (_disposed) return; await CloseAsync(); _disposed = true; } } #endregion #region Notifications and Notices /// /// Fires when PostgreSQL notices are received from PostgreSQL. /// /// /// PostgreSQL notices are non-critical messages generated by PostgreSQL, either as a result of a user query /// (e.g. as a warning or informational notice), or due to outside activity (e.g. if the database administrator /// initiates a "fast" database shutdown). /// /// Note that notices are very different from notifications (see the event). /// public event NoticeEventHandler? Notice; /// /// Fires when PostgreSQL notifications are received from PostgreSQL. /// /// /// PostgreSQL notifications are sent when your connection has registered for notifications on a specific channel via the /// LISTEN command. NOTIFY can be used to generate such notifications, allowing for an inter-connection communication channel. /// /// Note that notifications are very different from notices (see the event). /// public event NotificationEventHandler? Notification; internal void OnNotice(PostgresNotice e) { try { Notice?.Invoke(this, new NpgsqlNoticeEventArgs(e)); } catch (Exception ex) { // Block all exceptions bubbling up from the user's event handler Log.Error("User exception caught when emitting notice event", ex); } } internal void OnNotification(NpgsqlNotificationEventArgs e) { try { Notification?.Invoke(this, e); } catch (Exception ex) { // Block all exceptions bubbling up from the user's event handler Log.Error("User exception caught when emitting notification event", ex); } } #endregion Notifications and Notices #region SSL /// /// Returns whether SSL is being used for the connection. /// internal bool IsSecure => CheckOpenAndRunInTemporaryScope(c => c.IsSecure); /// /// Returns whether SCRAM-SHA256 is being user for the connection /// internal bool IsScram => CheckOpenAndRunInTemporaryScope(c => c.IsScram); /// /// Returns whether SCRAM-SHA256-PLUS is being user for the connection /// internal bool IsScramPlus => CheckOpenAndRunInTemporaryScope(c => c.IsScramPlus); /// /// Selects the local Secure Sockets Layer (SSL) certificate used for authentication. /// /// /// See /// public ProvideClientCertificatesCallback? ProvideClientCertificatesCallback { get; set; } /// /// /// Verifies the remote Secure Sockets Layer (SSL) certificate used for authentication. /// /// /// Cannot be used in conjunction with , and /// . /// /// /// /// See /// public RemoteCertificateValidationCallback? UserCertificateValidationCallback { get; set; } #endregion SSL #region Backend version, capabilities, settings // TODO: We should probably move DatabaseInfo from each connector to the pool (but remember unpooled) /// /// The version of the PostgreSQL server we're connected to. /// ///

/// This can only be called when the connection is open. ///

///

/// In case of a development or pre-release version this field will contain /// the version of the next version to be released from this branch. ///

///
///
[Browsable(false)] public Version PostgreSqlVersion => CheckOpenAndRunInTemporaryScope(c => c.DatabaseInfo.Version); /// /// The PostgreSQL server version as returned by the server_version option. /// /// This can only be called when the connection is open. /// /// public override string ServerVersion => CheckOpenAndRunInTemporaryScope( c => c.DatabaseInfo.ServerVersion); /// /// Process id of backend server. /// This can only be called when there is an active connection. /// [Browsable(false)] // ReSharper disable once InconsistentNaming public int ProcessID { get { CheckOpen(); return TryGetBoundConnector(out var connector) ? connector.BackendProcessId : throw new InvalidOperationException("No bound physical connection (using multiplexing)"); } } /// /// Reports whether the backend uses the newer integer timestamp representation. /// Note that the old floating point representation is not supported. /// Meant for use by type plugins (e.g. NodaTime) /// [Browsable(false)] public bool HasIntegerDateTimes => CheckOpenAndRunInTemporaryScope(c => c.DatabaseInfo.HasIntegerDateTimes); /// /// The connection's timezone as reported by PostgreSQL, in the IANA/Olson database format. /// [Browsable(false)] public string Timezone => CheckOpenAndRunInTemporaryScope(c => c.Timezone); /// /// Holds all PostgreSQL parameters received for this connection. Is updated if the values change /// (e.g. as a result of a SET command). /// [Browsable(false)] public IReadOnlyDictionary PostgresParameters => CheckOpenAndRunInTemporaryScope(c => c.PostgresParameters); #endregion Backend version, capabilities, settings #region Copy /// /// Begins a binary COPY FROM STDIN operation, a high-performance data import mechanism to a PostgreSQL table. /// /// A COPY FROM STDIN SQL command /// A which can be used to write rows and columns /// /// See https://www.postgresql.org/docs/current/static/sql-copy.html. /// public NpgsqlBinaryImporter BeginBinaryImport(string copyFromCommand) => BeginBinaryImport(copyFromCommand, async: false, CancellationToken.None).GetAwaiter().GetResult(); /// /// Begins a binary COPY FROM STDIN operation, a high-performance data import mechanism to a PostgreSQL table. /// /// A COPY FROM STDIN SQL command /// An optional token to cancel the asynchronous operation. The default value is None. /// A which can be used to write rows and columns /// /// See https://www.postgresql.org/docs/current/static/sql-copy.html. /// public Task BeginBinaryImportAsync(string copyFromCommand, CancellationToken cancellationToken = default) { using (NoSynchronizationContextScope.Enter()) return BeginBinaryImport(copyFromCommand, async: true, cancellationToken); } async Task BeginBinaryImport(string copyFromCommand, bool async, CancellationToken cancellationToken = default) { if (copyFromCommand == null) throw new ArgumentNullException(nameof(copyFromCommand)); if (!copyFromCommand.TrimStart().ToUpper().StartsWith("COPY", StringComparison.Ordinal)) throw new ArgumentException("Must contain a COPY FROM STDIN command!", nameof(copyFromCommand)); CheckReady(); var connector = StartBindingScope(ConnectorBindingScope.Copy); Log.Debug("Starting binary import", connector.Id); // no point in passing a cancellationToken here, as we register the cancellation in the Init method connector.StartUserAction(ConnectorState.Copy, attemptPgCancellation: false); try { var importer = new NpgsqlBinaryImporter(connector); await importer.Init(copyFromCommand, async, cancellationToken); connector.CurrentCopyOperation = importer; return importer; } catch { connector.EndUserAction(); EndBindingScope(ConnectorBindingScope.Copy); throw; } } /// /// Begins a binary COPY TO STDOUT operation, a high-performance data export mechanism from a PostgreSQL table. /// /// A COPY TO STDOUT SQL command /// A which can be used to read rows and columns /// /// See https://www.postgresql.org/docs/current/static/sql-copy.html. /// public NpgsqlBinaryExporter BeginBinaryExport(string copyToCommand) => BeginBinaryExport(copyToCommand, async: false, CancellationToken.None).GetAwaiter().GetResult(); /// /// Begins a binary COPY TO STDOUT operation, a high-performance data export mechanism from a PostgreSQL table. /// /// A COPY TO STDOUT SQL command /// An optional token to cancel the asynchronous operation. The default value is None. /// A which can be used to read rows and columns /// /// See https://www.postgresql.org/docs/current/static/sql-copy.html. /// public Task BeginBinaryExportAsync(string copyToCommand, CancellationToken cancellationToken = default) { using (NoSynchronizationContextScope.Enter()) return BeginBinaryExport(copyToCommand, async: true, cancellationToken); } async Task BeginBinaryExport(string copyToCommand, bool async, CancellationToken cancellationToken = default) { if (copyToCommand == null) throw new ArgumentNullException(nameof(copyToCommand)); if (!copyToCommand.TrimStart().ToUpper().StartsWith("COPY", StringComparison.Ordinal)) throw new ArgumentException("Must contain a COPY TO STDOUT command!", nameof(copyToCommand)); CheckReady(); var connector = StartBindingScope(ConnectorBindingScope.Copy); Log.Debug("Starting binary export", connector.Id); // no point in passing a cancellationToken here, as we register the cancellation in the Init method connector.StartUserAction(ConnectorState.Copy, attemptPgCancellation: false); try { var exporter = new NpgsqlBinaryExporter(connector); await exporter.Init(copyToCommand, async, cancellationToken); connector.CurrentCopyOperation = exporter; return exporter; } catch { connector.EndUserAction(); EndBindingScope(ConnectorBindingScope.Copy); throw; } } /// /// Begins a textual COPY FROM STDIN operation, a data import mechanism to a PostgreSQL table. /// It is the user's responsibility to send the textual input according to the format specified /// in . /// /// A COPY FROM STDIN SQL command /// /// A TextWriter that can be used to send textual data. /// /// See https://www.postgresql.org/docs/current/static/sql-copy.html. /// public TextWriter BeginTextImport(string copyFromCommand) => BeginTextImport(copyFromCommand, async: false, CancellationToken.None).GetAwaiter().GetResult(); /// /// Begins a textual COPY FROM STDIN operation, a data import mechanism to a PostgreSQL table. /// It is the user's responsibility to send the textual input according to the format specified /// in . /// /// A COPY FROM STDIN SQL command /// An optional token to cancel the asynchronous operation. The default value is None. /// /// A TextWriter that can be used to send textual data. /// /// See https://www.postgresql.org/docs/current/static/sql-copy.html. /// public Task BeginTextImportAsync(string copyFromCommand, CancellationToken cancellationToken = default) { using (NoSynchronizationContextScope.Enter()) return BeginTextImport(copyFromCommand, async: true, cancellationToken); } async Task BeginTextImport(string copyFromCommand, bool async, CancellationToken cancellationToken = default) { if (copyFromCommand == null) throw new ArgumentNullException(nameof(copyFromCommand)); if (!copyFromCommand.TrimStart().ToUpper().StartsWith("COPY", StringComparison.Ordinal)) throw new ArgumentException("Must contain a COPY FROM STDIN command!", nameof(copyFromCommand)); CheckReady(); var connector = StartBindingScope(ConnectorBindingScope.Copy); Log.Debug("Starting text import", connector.Id); // no point in passing a cancellationToken here, as we register the cancellation in the Init method connector.StartUserAction(ConnectorState.Copy, attemptPgCancellation: false); try { var copyStream = new NpgsqlRawCopyStream(connector); await copyStream.Init(copyFromCommand, async, cancellationToken); var writer = new NpgsqlCopyTextWriter(connector, copyStream); connector.CurrentCopyOperation = writer; return writer; } catch { connector.EndUserAction(); EndBindingScope(ConnectorBindingScope.Copy); throw; } } /// /// Begins a textual COPY TO STDOUT operation, a data export mechanism from a PostgreSQL table. /// It is the user's responsibility to parse the textual input according to the format specified /// in . /// /// A COPY TO STDOUT SQL command /// /// A TextReader that can be used to read textual data. /// /// See https://www.postgresql.org/docs/current/static/sql-copy.html. /// public TextReader BeginTextExport(string copyToCommand) => BeginTextExport(copyToCommand, async: false, CancellationToken.None).GetAwaiter().GetResult(); /// /// Begins a textual COPY TO STDOUT operation, a data export mechanism from a PostgreSQL table. /// It is the user's responsibility to parse the textual input according to the format specified /// in . /// /// A COPY TO STDOUT SQL command /// An optional token to cancel the asynchronous operation. The default value is None. /// /// A TextReader that can be used to read textual data. /// /// See https://www.postgresql.org/docs/current/static/sql-copy.html. /// public Task BeginTextExportAsync(string copyToCommand, CancellationToken cancellationToken = default) { using (NoSynchronizationContextScope.Enter()) return BeginTextExport(copyToCommand, async: true, cancellationToken); } async Task BeginTextExport(string copyToCommand, bool async, CancellationToken cancellationToken = default) { if (copyToCommand == null) throw new ArgumentNullException(nameof(copyToCommand)); if (!copyToCommand.TrimStart().ToUpper().StartsWith("COPY", StringComparison.Ordinal)) throw new ArgumentException("Must contain a COPY TO STDOUT command!", nameof(copyToCommand)); CheckReady(); var connector = StartBindingScope(ConnectorBindingScope.Copy); Log.Debug("Starting text export", connector.Id); // no point in passing a cancellationToken here, as we register the cancellation in the Init method connector.StartUserAction(ConnectorState.Copy, attemptPgCancellation: false); try { var copyStream = new NpgsqlRawCopyStream(connector); await copyStream.Init(copyToCommand, async, cancellationToken); var reader = new NpgsqlCopyTextReader(connector, copyStream); connector.CurrentCopyOperation = reader; return reader; } catch { connector.EndUserAction(); EndBindingScope(ConnectorBindingScope.Copy); throw; } } /// /// Begins a raw binary COPY operation (TO STDOUT or FROM STDIN), a high-performance data export/import mechanism to a PostgreSQL table. /// Note that unlike the other COPY API methods, doesn't implement any encoding/decoding /// and is unsuitable for structured import/export operation. It is useful mainly for exporting a table as an opaque /// blob, for the purpose of importing it back later. /// /// A COPY TO STDOUT or COPY FROM STDIN SQL command /// A that can be used to read or write raw binary data. /// /// See https://www.postgresql.org/docs/current/static/sql-copy.html. /// public NpgsqlRawCopyStream BeginRawBinaryCopy(string copyCommand) => BeginRawBinaryCopy(copyCommand, async: false, CancellationToken.None).GetAwaiter().GetResult(); /// /// Begins a raw binary COPY operation (TO STDOUT or FROM STDIN), a high-performance data export/import mechanism to a PostgreSQL table. /// Note that unlike the other COPY API methods, doesn't implement any encoding/decoding /// and is unsuitable for structured import/export operation. It is useful mainly for exporting a table as an opaque /// blob, for the purpose of importing it back later. /// /// A COPY TO STDOUT or COPY FROM STDIN SQL command /// An optional token to cancel the asynchronous operation. The default value is None. /// A that can be used to read or write raw binary data. /// /// See https://www.postgresql.org/docs/current/static/sql-copy.html. /// public Task BeginRawBinaryCopyAsync(string copyCommand, CancellationToken cancellationToken = default) { using (NoSynchronizationContextScope.Enter()) return BeginRawBinaryCopy(copyCommand, async: true, cancellationToken); } async Task BeginRawBinaryCopy(string copyCommand, bool async, CancellationToken cancellationToken = default) { if (copyCommand == null) throw new ArgumentNullException(nameof(copyCommand)); if (!copyCommand.TrimStart().ToUpper().StartsWith("COPY", StringComparison.Ordinal)) throw new ArgumentException("Must contain a COPY TO STDOUT OR COPY FROM STDIN command!", nameof(copyCommand)); CheckReady(); var connector = StartBindingScope(ConnectorBindingScope.Copy); Log.Debug("Starting raw COPY operation", connector.Id); // no point in passing a cancellationToken here, as we register the cancellation in the Init method connector.StartUserAction(ConnectorState.Copy, attemptPgCancellation: false); try { var stream = new NpgsqlRawCopyStream(connector); await stream.Init(copyCommand, async, cancellationToken); if (!stream.IsBinary) { // TODO: Stop the COPY operation gracefully, no breaking throw connector.Break(new ArgumentException( "copyToCommand triggered a text transfer, only binary is allowed", nameof(copyCommand))); } connector.CurrentCopyOperation = stream; return stream; } catch { connector.EndUserAction(); EndBindingScope(ConnectorBindingScope.Copy); throw; } } #endregion #region Enum mapping /// /// Maps a CLR enum to a PostgreSQL enum type for use with this connection. /// /// /// CLR enum labels are mapped by name to PostgreSQL enum labels. /// The translation strategy can be controlled by the parameter, /// which defaults to . /// You can also use the on your enum fields to manually specify a PostgreSQL enum label. /// If there is a discrepancy between the .NET and database labels while an enum is read or written, /// an exception will be raised. /// /// Can only be invoked on an open connection; if the connection is closed the mapping is lost. /// /// To avoid mapping the type for each connection, use the method. /// /// /// A PostgreSQL type name for the corresponding enum type in the database. /// If null, the name translator given in will be used. /// /// /// A component which will be used to translate CLR names (e.g. SomeClass) into database names (e.g. some_class). /// Defaults to /// /// The .NET enum type to be mapped [Obsolete("Use NpgsqlConnection.TypeMapper.MapEnum() instead")] public void MapEnum(string? pgName = null, INpgsqlNameTranslator? nameTranslator = null) where TEnum : struct, Enum => TypeMapper.MapEnum(pgName, nameTranslator); /// /// Maps a CLR enum to a PostgreSQL enum type for use with all connections created from now on. Existing connections aren't affected. /// /// /// CLR enum labels are mapped by name to PostgreSQL enum labels. /// The translation strategy can be controlled by the parameter, /// which defaults to . /// You can also use the on your enum fields to manually specify a PostgreSQL enum label. /// If there is a discrepancy between the .NET and database labels while an enum is read or written, /// an exception will be raised. /// /// To map the type for a specific connection, use the method. /// /// /// A PostgreSQL type name for the corresponding enum type in the database. /// If null, the name translator given in will be used. /// /// /// A component which will be used to translate CLR names (e.g. SomeClass) into database names (e.g. some_class). /// Defaults to /// /// The .NET enum type to be mapped [Obsolete("Use NpgsqlConnection.GlobalTypeMapper.MapEnum() instead")] public static void MapEnumGlobally(string? pgName = null, INpgsqlNameTranslator? nameTranslator = null) where TEnum : struct, Enum => GlobalTypeMapper.MapEnum(pgName, nameTranslator); /// /// Removes a previous global enum mapping. /// /// /// A PostgreSQL type name for the corresponding enum type in the database. /// If null, the name translator given in will be used. /// /// /// A component which will be used to translate CLR names (e.g. SomeClass) into database names (e.g. some_class). /// Defaults to /// [Obsolete("Use NpgsqlConnection.GlobalTypeMapper.UnmapEnum() instead")] public static void UnmapEnumGlobally(string? pgName = null, INpgsqlNameTranslator? nameTranslator = null) where TEnum : struct, Enum => GlobalTypeMapper.UnmapEnum(pgName, nameTranslator); #endregion #region Composite registration /// /// Maps a CLR type to a PostgreSQL composite type for use with this connection. /// /// /// CLR fields and properties by string to PostgreSQL enum labels. /// The translation strategy can be controlled by the parameter, /// which defaults to . /// You can also use the on your members to manually specify a PostgreSQL enum label. /// If there is a discrepancy between the .NET and database labels while a composite is read or written, /// an exception will be raised. /// /// Can only be invoked on an open connection; if the connection is closed the mapping is lost. /// /// To avoid mapping the type for each connection, use the method. /// /// /// A PostgreSQL type name for the corresponding enum type in the database. /// If null, the name translator given in will be used. /// /// /// A component which will be used to translate CLR names (e.g. SomeClass) into database names (e.g. some_class). /// Defaults to /// /// The .NET type to be mapped [Obsolete("Use NpgsqlConnection.TypeMapper.MapComposite() instead")] [RequiresUnreferencedCode("Composite type mapping currently isn't trimming-safe.")] public void MapComposite(string? pgName = null, INpgsqlNameTranslator? nameTranslator = null) where T : new() => TypeMapper.MapComposite(pgName, nameTranslator); /// /// Maps a CLR type to a PostgreSQL composite type for use with all connections created from now on. Existing connections aren't affected. /// /// /// CLR fields and properties by string to PostgreSQL enum labels. /// The translation strategy can be controlled by the parameter, /// which defaults to . /// You can also use the on your members to manually specify a PostgreSQL enum label. /// If there is a discrepancy between the .NET and database labels while a composite is read or written, /// an exception will be raised. /// /// To map the type for a specific connection, use the method. /// /// /// A PostgreSQL type name for the corresponding enum type in the database. /// If null, the name translator given in will be used. /// /// /// A component which will be used to translate CLR names (e.g. SomeClass) into database names (e.g. some_class). /// Defaults to /// /// The .NET type to be mapped [Obsolete("Use NpgsqlConnection.GlobalTypeMapper.MapComposite() instead")] [RequiresUnreferencedCode("Composite type mapping currently isn't trimming-safe.")] public static void MapCompositeGlobally(string? pgName = null, INpgsqlNameTranslator? nameTranslator = null) where T : new() => GlobalTypeMapper.MapComposite(pgName, nameTranslator); /// /// Removes a previous global enum mapping. /// /// /// A PostgreSQL type name for the corresponding enum type in the database. /// If null, the name translator given in will be used. /// /// /// A component which will be used to translate CLR names (e.g. SomeClass) into database names (e.g. some_class). /// Defaults to /// [Obsolete("Use NpgsqlConnection.GlobalTypeMapper.UnmapComposite() instead")] [RequiresUnreferencedCode("Composite type mapping currently isn't trimming-safe.")] public static void UnmapCompositeGlobally(string pgName, INpgsqlNameTranslator? nameTranslator = null) where T : new() => GlobalTypeMapper.UnmapComposite(pgName, nameTranslator); #endregion #region Wait /// /// Waits until an asynchronous PostgreSQL messages (e.g. a notification) arrives, and /// exits immediately. The asynchronous message is delivered via the normal events /// (, ). /// /// /// The time-out value, in milliseconds, passed to . /// The default value is 0, which indicates an infinite time-out period. /// Specifying -1 also indicates an infinite time-out period. /// /// true if an asynchronous message was received, false if timed out. public bool Wait(int timeout) { if (timeout != -1 && timeout < 0) throw new ArgumentException("Argument must be -1, 0 or positive", nameof(timeout)); if (Settings.Multiplexing) throw new NotSupportedException($"{nameof(Wait)} isn't supported in multiplexing mode"); CheckReady(); Log.Debug($"Starting to wait (timeout={timeout})...", Connector!.Id); return Connector!.Wait(async: false, timeout, CancellationToken.None).GetAwaiter().GetResult(); } /// /// Waits until an asynchronous PostgreSQL messages (e.g. a notification) arrives, and /// exits immediately. The asynchronous message is delivered via the normal events /// (, ). /// /// /// The time-out value is passed to . /// /// true if an asynchronous message was received, false if timed out. public bool Wait(TimeSpan timeout) => Wait((int)timeout.TotalMilliseconds); /// /// Waits until an asynchronous PostgreSQL messages (e.g. a notification) arrives, and /// exits immediately. The asynchronous message is delivered via the normal events /// (, ). /// public void Wait() => Wait(0); /// /// Waits asynchronously until an asynchronous PostgreSQL messages (e.g. a notification) /// arrives, and exits immediately. The asynchronous message is delivered via the normal events /// (, ). /// /// /// The time-out value, in milliseconds. /// The default value is 0, which indicates an infinite time-out period. /// Specifying -1 also indicates an infinite time-out period. /// /// /// An optional token to cancel the asynchronous operation. The default value is . /// /// true if an asynchronous message was received, false if timed out. public Task WaitAsync(int timeout, CancellationToken cancellationToken = default) { if (Settings.Multiplexing) throw new NotSupportedException($"{nameof(Wait)} isn't supported in multiplexing mode"); CheckReady(); Log.Debug("Starting to wait asynchronously...", Connector!.Id); using (NoSynchronizationContextScope.Enter()) return Connector!.Wait(async: true, timeout, cancellationToken); } /// /// Waits asynchronously until an asynchronous PostgreSQL messages (e.g. a notification) /// arrives, and exits immediately. The asynchronous message is delivered via the normal events /// (, ). /// /// /// The time-out value as /// /// /// An optional token to cancel the asynchronous operation. The default value is . /// /// true if an asynchronous message was received, false if timed out. public Task WaitAsync(TimeSpan timeout, CancellationToken cancellationToken = default) => WaitAsync((int)timeout.TotalMilliseconds, cancellationToken); /// /// Waits asynchronously until an asynchronous PostgreSQL messages (e.g. a notification) /// arrives, and exits immediately. The asynchronous message is delivered via the normal events /// (, ). /// /// /// An optional token to cancel the asynchronous operation. The default value is . /// public Task WaitAsync(CancellationToken cancellationToken = default) => WaitAsync(0, cancellationToken); #endregion #region State checks [MethodImpl(MethodImplOptions.AggressiveInlining)] void CheckOpen() { CheckDisposed(); switch (FullState) { case ConnectionState.Open: case ConnectionState.Open | ConnectionState.Executing: case ConnectionState.Open | ConnectionState.Fetching: case ConnectionState.Connecting: break; case ConnectionState.Closed: case ConnectionState.Broken: throw new InvalidOperationException("Connection is not open"); default: throw new ArgumentOutOfRangeException(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] void CheckClosed() { CheckDisposed(); switch (FullState) { case ConnectionState.Closed: case ConnectionState.Broken: break; case ConnectionState.Open: case ConnectionState.Connecting: case ConnectionState.Open | ConnectionState.Executing: case ConnectionState.Open | ConnectionState.Fetching: throw new InvalidOperationException("Connection already open"); default: throw new ArgumentOutOfRangeException(); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] void CheckDisposed() { if (_disposed) throw new ObjectDisposedException(typeof(NpgsqlConnection).Name); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void CheckReady() { CheckDisposed(); switch (FullState) { case ConnectionState.Open: case ConnectionState.Connecting: // We need to do type loading as part of connecting break; case ConnectionState.Closed: case ConnectionState.Broken: throw new InvalidOperationException("Connection is not open"); case ConnectionState.Open | ConnectionState.Executing: case ConnectionState.Open | ConnectionState.Fetching: throw new InvalidOperationException("Connection is busy"); default: throw new ArgumentOutOfRangeException(); } } #endregion State checks #region Connector binding /// /// Checks whether the connection is currently bound to a connector, and if so, returns it via /// . /// internal bool TryGetBoundConnector([NotNullWhen(true)] out NpgsqlConnector? connector) { if (ConnectorBindingScope == ConnectorBindingScope.None) { Debug.Assert(Connector == null, $"Binding scope is None but {Connector} exists"); connector = null; return false; } Debug.Assert(Connector != null, $"Binding scope is {ConnectorBindingScope} but {Connector} is null"); Debug.Assert(Connector.Connection == this, $"Bound connector {Connector} does not reference this connection"); connector = Connector; return true; } /// /// Binds this connection to a physical connector. This happens when opening a non-multiplexing connection, /// or when starting a transaction on a multiplexed connection. /// internal ValueTask StartBindingScope( ConnectorBindingScope scope, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken) { // If the connection is around bound at a higher scope, we do nothing (e.g. copy operation started // within a transaction on a multiplexing connection). // Note that if we're in an ambient transaction, that means we're already bound and so we do nothing here. if (ConnectorBindingScope != ConnectorBindingScope.None) { Debug.Assert(Connector != null, $"Connection bound with scope {ConnectorBindingScope} but has no connector"); Debug.Assert(scope != ConnectorBindingScope, $"Binding scopes aren't reentrant ({ConnectorBindingScope})"); return new ValueTask(Connector); } return StartBindingScopeAsync(); async ValueTask StartBindingScopeAsync() { try { Debug.Assert(Settings.Multiplexing); Debug.Assert(_pool != null); var connector = await _pool.Get(this, timeout, async, cancellationToken); Connector = connector; connector.Connection = this; ConnectorBindingScope = scope; return connector; } catch { FullState = ConnectionState.Broken; throw; } } } internal NpgsqlConnector StartBindingScope(ConnectorBindingScope scope) => StartBindingScope(scope, NpgsqlTimeout.Infinite, async: false, CancellationToken.None) .GetAwaiter().GetResult(); internal EndScopeDisposable StartTemporaryBindingScope(out NpgsqlConnector connector) { connector = StartBindingScope(ConnectorBindingScope.Temporary); return new EndScopeDisposable(this); } internal T CheckOpenAndRunInTemporaryScope(Func func) { CheckOpen(); using var _ = StartTemporaryBindingScope(out var connector); var result = func(connector); return result; } /// /// Ends binding scope to the physical connection and returns it to the pool. Only useful with multiplexing on. /// /// /// After this method is called, under no circumstances the physical connection (connector) should ever be used if multiplexing is on. /// See #3249. /// internal void EndBindingScope(ConnectorBindingScope scope) { Debug.Assert(ConnectorBindingScope != ConnectorBindingScope.None || FullState == ConnectionState.Broken, $"Ending binding scope {scope} but connection's scope is null"); if (scope != ConnectorBindingScope) return; Debug.Assert(Connector != null, $"Ending binding scope {scope} but connector is null"); Debug.Assert(_pool != null, $"Ending binding scope {scope} but _pool is null"); Debug.Assert(Settings.Multiplexing, $"Ending binding scope {scope} but multiplexing is disabled"); // TODO: If enlisted transaction scope is still active, need to AddPendingEnlistedConnector, just like Close var connector = Connector; Connector = null; connector.Connection = null; connector.Transaction?.UnbindIfNecessary(); connector.Return(); ConnectorBindingScope = ConnectorBindingScope.None; } #endregion Connector binding #region Schema operations /// /// Returns the supported collections /// [UnconditionalSuppressMessage( "Composite type mapping currently isn't trimming-safe, and warnings are generated at the MapComposite level.", "IL2026")] public override DataTable GetSchema() => GetSchema("MetaDataCollections", null); /// /// Returns the schema collection specified by the collection name. /// /// The collection name. /// The collection specified. public override DataTable GetSchema(string? collectionName) => GetSchema(collectionName, null); /// /// Returns the schema collection specified by the collection name filtered by the restrictions. /// /// The collection name. /// /// The restriction values to filter the results. A description of the restrictions is contained /// in the Restrictions collection. /// /// The collection specified. public override DataTable GetSchema(string? collectionName, string?[]? restrictions) => NpgsqlSchema.GetSchema(this, collectionName, restrictions, async: false).GetAwaiter().GetResult(); /// /// Asynchronously returns the supported collections. /// /// /// An optional token to cancel the asynchronous operation. The default value is . /// /// The collection specified. #if NET5_0_OR_GREATER public override Task GetSchemaAsync(CancellationToken cancellationToken = default) #else public Task GetSchemaAsync(CancellationToken cancellationToken = default) #endif => GetSchemaAsync("MetaDataCollections", null, cancellationToken); /// /// Asynchronously returns the schema collection specified by the collection name. /// /// The collection name. /// /// An optional token to cancel the asynchronous operation. The default value is . /// /// The collection specified. #if NET5_0_OR_GREATER public override Task GetSchemaAsync(string collectionName, CancellationToken cancellationToken = default) #else public Task GetSchemaAsync(string collectionName, CancellationToken cancellationToken = default) #endif => GetSchemaAsync(collectionName, null, cancellationToken); /// /// Asynchronously returns the schema collection specified by the collection name filtered by the restrictions. /// /// The collection name. /// /// The restriction values to filter the results. A description of the restrictions is contained /// in the Restrictions collection. /// /// /// An optional token to cancel the asynchronous operation. The default value is . /// /// The collection specified. #if NET5_0_OR_GREATER public override Task GetSchemaAsync(string collectionName, string?[]? restrictions, CancellationToken cancellationToken = default) #else public Task GetSchemaAsync(string collectionName, string?[]? restrictions, CancellationToken cancellationToken = default) #endif { using (NoSynchronizationContextScope.Enter()) return NpgsqlSchema.GetSchema(this, collectionName, restrictions, async: true, cancellationToken); } #endregion Schema operations #region Misc /// /// Creates a closed connection with the connection string and authentication details of this message. /// object ICloneable.Clone() { CheckDisposed(); var conn = new NpgsqlConnection(_connectionString) { ProvideClientCertificatesCallback = ProvideClientCertificatesCallback, UserCertificateValidationCallback = UserCertificateValidationCallback, ProvidePasswordCallback = ProvidePasswordCallback, _userFacingConnectionString = _userFacingConnectionString }; return conn; } /// /// Clones this connection, replacing its connection string with the given one. /// This allows creating a new connection with the same security information /// (password, SSL callbacks) while changing other connection parameters (e.g. /// database or pooling) /// public NpgsqlConnection CloneWith(string connectionString) { CheckDisposed(); var csb = new NpgsqlConnectionStringBuilder(connectionString); if (csb.Password == null && Password != null) csb.Password = Password; if (csb.PersistSecurityInfo && !Settings.PersistSecurityInfo) csb.PersistSecurityInfo = false; return new NpgsqlConnection(csb.ToString()) { ProvideClientCertificatesCallback = ProvideClientCertificatesCallback, UserCertificateValidationCallback = UserCertificateValidationCallback, ProvidePasswordCallback = ProvidePasswordCallback, }; } /// /// This method changes the current database by disconnecting from the actual /// database and connecting to the specified. /// /// The name of the database to use in place of the current database. public override void ChangeDatabase(string dbName) { if (dbName == null) throw new ArgumentNullException(nameof(dbName)); if (string.IsNullOrEmpty(dbName)) throw new ArgumentOutOfRangeException(nameof(dbName), dbName, $"Invalid database name: {dbName}"); CheckOpen(); Close(); _pool = null; Settings = Settings.Clone(); Settings.Database = dbName; ConnectionString = Settings.ToString(); Open(); } /// /// DB provider factory. /// protected override DbProviderFactory DbProviderFactory => NpgsqlFactory.Instance; /// /// Clears the connection pool. All idle physical connections in the pool of the given connection are /// immediately closed, and any busy connections which were opened before was called /// will be closed when returned to the pool. /// public static void ClearPool(NpgsqlConnection connection) => PoolManager.Clear(connection._connectionString); /// /// Clear all connection pools. All idle physical connections in all pools are immediately closed, and any busy /// connections which were opened before was called will be closed when returned /// to their pool. /// public static void ClearAllPools() => PoolManager.ClearAll(); /// /// Unprepares all prepared statements on this connection. /// public void UnprepareAll() { if (Settings.Multiplexing) throw new NotSupportedException("Explicit preparation not supported with multiplexing"); CheckReady(); using (Connector!.StartUserAction()) Connector.UnprepareAll(); } /// /// Flushes the type cache for this connection's connection string and reloads the types for this connection only. /// Type changes will appear for other connections only after they are re-opened from the pool. /// public void ReloadTypes() { CheckReady(); using var scope = StartTemporaryBindingScope(out var connector); using var _ = connector.StartUserAction(ConnectorState.Executing); connector.LoadDatabaseInfo( forceReload: true, NpgsqlTimeout.Infinite, async: false, CancellationToken.None).GetAwaiter().GetResult(); // Increment the change counter on the global type mapper. This will make conn.Open() pick up the // new DatabaseInfo and set up a new connection type mapper TypeMapping.GlobalTypeMapper.Instance.RecordChange(); if (Settings.Multiplexing) { var multiplexingTypeMapper = ((MultiplexingConnectorPool)Pool).MultiplexingTypeMapper!; Debug.Assert(multiplexingTypeMapper == connector.TypeMapper, "A connector must reference the exact same TypeMapper the MultiplexingConnectorPool does"); // It's very probable that we've called ReloadTypes on the different connection than // the MultiplexingConnectorPool references. // Which means, we have to explicitly call Reset after we change the connector's DatabaseInfo to reload type mappings. multiplexingTypeMapper.Connector.DatabaseInfo = connector.TypeMapper.DatabaseInfo; multiplexingTypeMapper.Reset(); } } /// /// This event is unsupported by Npgsql. Use instead. /// [EditorBrowsable(EditorBrowsableState.Never)] public new event EventHandler? Disposed { add => throw new NotSupportedException("The Disposed event isn't supported by Npgsql. Use DbConnection.StateChange instead."); remove => throw new NotSupportedException("The Disposed event isn't supported by Npgsql. Use DbConnection.StateChange instead."); } event EventHandler? IComponent.Disposed { add => Disposed += value; remove => Disposed -= value; } #endregion Misc } enum ConnectorBindingScope { /// /// The connection is currently not bound to a connector. /// None, /// /// The connection is bound to its connector for the scope of the entire connection /// (i.e. non-multiplexed connection). /// Connection, /// /// The connection is bound to its connector for the scope of a transaction. /// Transaction, /// /// The connection is bound to its connector for the scope of a COPY operation. /// Copy, /// /// The connection is bound to its connector for the scope of a single reader. /// Reader, /// /// The connection is bound to its connector for an unspecified, temporary scope; the code that initiated /// the binding is also responsible to unbind it. /// Temporary } readonly struct EndScopeDisposable : IDisposable { readonly NpgsqlConnection _connection; public EndScopeDisposable(NpgsqlConnection connection) => _connection = connection; public void Dispose() => _connection.EndBindingScope(ConnectorBindingScope.Temporary); } #region Delegates /// /// Represents a method that handles the event. /// /// The source of the event. /// A that contains the notice information (e.g. message, severity...). public delegate void NoticeEventHandler(object sender, NpgsqlNoticeEventArgs e); /// /// Represents a method that handles the event. /// /// The source of the event. /// A that contains the notification payload. public delegate void NotificationEventHandler(object sender, NpgsqlNotificationEventArgs e); /// /// Represents a method that allows the application to provide a certificate collection to be used for SSL client authentication /// /// /// A to be filled with one or more client /// certificates. /// public delegate void ProvideClientCertificatesCallback(X509CertificateCollection certificates); /// /// Represents a method that allows the application to provide a password at connection time in code rather than configuration /// /// Hostname /// Port /// Database Name /// User /// A valid password for connecting to the database public delegate string ProvidePasswordCallback(string host, int port, string database, string username); /// /// Represents a method that allows the application to setup a connection with custom commands. /// /// Physical connection to the database [RequiresPreviewFeatures("Physical open callback is an experimental API, and its exact shape may change in the future")] public delegate void PhysicalOpenCallback(NpgsqlConnector connection); /// /// Represents an asynchronous method that allows the application to setup a connection with custom commands. /// /// Physical connection to the database [RequiresPreviewFeatures("Physical open callback is an experimental API, and its exact shape may change in the future")] public delegate Task PhysicalOpenAsyncCallback(NpgsqlConnector connection); #endregion