diff --git a/src/MySqlConnector/Core/ServerSession.cs b/src/MySqlConnector/Core/ServerSession.cs index 5d3e09dfd..cae38cd4b 100644 --- a/src/MySqlConnector/Core/ServerSession.cs +++ b/src/MySqlConnector/Core/ServerSession.cs @@ -65,10 +65,12 @@ public ServerSession(ILogger logger, IConnectionPoolMetadata pool) public bool ProcAccessDenied { get; set; } public ICollection> ActivityTags => m_activityTags; public MySqlDataReader DataReader { get; set; } + public MySqlConnectionOpenedConditions Conditions { get; private set; } public ValueTask ReturnToPoolAsync(IOBehavior ioBehavior, MySqlConnection? owningConnection) { Log.ReturningToPool(m_logger, Id, Pool?.Id ?? 0); + Conditions = MySqlConnectionOpenedConditions.None; LastReturnedTimestamp = Stopwatch.GetTimestamp(); if (Pool is null) return default; @@ -414,6 +416,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella } } + Conditions = MySqlConnectionOpenedConditions.New; var connected = cs.ConnectionProtocol switch { MySqlConnectionProtocol.Sockets => await OpenTcpSocketAsync(cs, loadBalancer ?? throw new ArgumentNullException(nameof(loadBalancer)), activity, ioBehavior, cancellationToken).ConfigureAwait(false), @@ -747,6 +750,7 @@ public static async ValueTask ConnectAndRedirectAsync(ILogger con public async Task TryResetConnectionAsync(ConnectionSettings cs, MySqlConnection connection, IOBehavior ioBehavior, CancellationToken cancellationToken) { VerifyState(State.Connected); + Conditions |= MySqlConnectionOpenedConditions.Reset; try { @@ -829,6 +833,7 @@ public async Task TryResetConnectionAsync(ConnectionSettings cs, MySqlConn Log.IgnoringFailureInTryResetConnectionAsync(m_logger, ex, Id, "SocketException"); } + Conditions |= ~MySqlConnectionOpenedConditions.Reset; return false; } diff --git a/src/MySqlConnector/MySqlConnection.cs b/src/MySqlConnector/MySqlConnection.cs index a037a3eed..383ac0172 100644 --- a/src/MySqlConnector/MySqlConnection.cs +++ b/src/MySqlConnector/MySqlConnection.cs @@ -582,6 +582,9 @@ internal async Task OpenAsync(IOBehavior? ioBehavior, CancellationToken cancella if (m_connectionSettings.AutoEnlist && System.Transactions.Transaction.Current is not null) EnlistTransaction(System.Transactions.Transaction.Current); + + if (ConnectionOpenedCallback is { } connectionOpenedCallback) + await connectionOpenedCallback(this, m_session.Conditions).ConfigureAwait(false); } catch (Exception ex) when (activity is { IsAllDataRequested: true }) { @@ -917,6 +920,7 @@ internal void Cancel(ICancellableCommand command, int commandId, bool isCancel) using var connection = CloneWith(csb.ConnectionString); connection.m_connectionSettings = connectionSettings; + connection.ConnectionOpenedCallback = null; // clear the callback because the user doesn't need to execute any setup logic on this connection connection.Open(); #if NET6_0_OR_GREATER var killQuerySql = string.Create(CultureInfo.InvariantCulture, $"KILL QUERY {command.Connection!.ServerThread}"); @@ -992,6 +996,7 @@ internal void Cancel(ICancellableCommand command, int commandId, bool isCancel) internal MySqlTransaction? CurrentTransaction { get; set; } internal MySqlConnectorLoggingConfiguration LoggingConfiguration { get; } internal ZstandardPlugin? ZstandardPlugin { get; set; } + internal MySqlConnectionOpenedCallback? ConnectionOpenedCallback { get; set; } internal bool AllowLoadLocalInfile => GetInitializedConnectionSettings().AllowLoadLocalInfile; internal bool AllowUserVariables => GetInitializedConnectionSettings().AllowUserVariables; internal bool AllowZeroDateTime => GetInitializedConnectionSettings().AllowZeroDateTime; @@ -1142,6 +1147,7 @@ private MySqlConnection(MySqlConnection other, MySqlDataSource? dataSource, stri ProvideClientCertificatesCallback = other.ProvideClientCertificatesCallback; ProvidePasswordCallback = other.ProvidePasswordCallback; RemoteCertificateValidationCallback = other.RemoteCertificateValidationCallback; + ConnectionOpenedCallback = other.ConnectionOpenedCallback; } private void VerifyNotDisposed() diff --git a/src/MySqlConnector/MySqlConnectionOpenedCallback.cs b/src/MySqlConnector/MySqlConnectionOpenedCallback.cs new file mode 100644 index 000000000..cf554c3b8 --- /dev/null +++ b/src/MySqlConnector/MySqlConnectionOpenedCallback.cs @@ -0,0 +1,31 @@ +namespace MySqlConnector; + +/// +/// A callback that is invoked when a new is opened. +/// +/// The that was opened. +/// Bitflags giving the conditions under which a connection was opened. +/// A representing the result of the possibly-asynchronous operation. +public delegate ValueTask MySqlConnectionOpenedCallback(MySqlConnection connection, MySqlConnectionOpenedConditions conditions); + +/// +/// Bitflags giving the conditions under which a connection was opened. +/// +[Flags] +public enum MySqlConnectionOpenedConditions +{ + /// + /// No specific conditions apply. This value may be used when an existing pooled connection is reused without being reset. + /// + None = 0, + + /// + /// A new physical connection to a MySQL Server was opened. This value is mutually exclusive with . + /// + New = 1, + + /// + /// An existing pooled connection to a MySQL Server was reset. This value is mutually exclusive with . + /// + Reset = 2, +} diff --git a/src/MySqlConnector/MySqlDataSource.cs b/src/MySqlConnector/MySqlDataSource.cs index 0c4372fd4..56e9718af 100644 --- a/src/MySqlConnector/MySqlDataSource.cs +++ b/src/MySqlConnector/MySqlDataSource.cs @@ -19,7 +19,7 @@ public sealed class MySqlDataSource : DbDataSource /// The connection string for the MySQL Server. This parameter is required. /// Thrown if is null. public MySqlDataSource(string connectionString) - : this(connectionString ?? throw new ArgumentNullException(nameof(connectionString)), MySqlConnectorLoggingConfiguration.NullConfiguration, null, null, null, null, default, default, default) + : this(connectionString ?? throw new ArgumentNullException(nameof(connectionString)), MySqlConnectorLoggingConfiguration.NullConfiguration, null, null, null, null, default, default, default, default) { } @@ -31,7 +31,8 @@ internal MySqlDataSource(string connectionString, Func>? periodicPasswordProvider, TimeSpan periodicPasswordProviderSuccessRefreshInterval, TimeSpan periodicPasswordProviderFailureRefreshInterval, - ZstandardPlugin? zstandardPlugin) + ZstandardPlugin? zstandardPlugin, + MySqlConnectionOpenedCallback? connectionOpenedCallback) { m_connectionString = connectionString; LoggingConfiguration = loggingConfiguration; @@ -40,6 +41,7 @@ internal MySqlDataSource(string connectionString, m_remoteCertificateValidationCallback = remoteCertificateValidationCallback; m_logger = loggingConfiguration.DataSourceLogger; m_zstandardPlugin = zstandardPlugin; + m_connectionOpenedCallback = connectionOpenedCallback; Pool = ConnectionPool.CreatePool(m_connectionString, LoggingConfiguration, name); m_id = Interlocked.Increment(ref s_lastId); @@ -142,6 +144,7 @@ protected override DbConnection CreateDbConnection() ProvideClientCertificatesCallback = m_clientCertificatesCallback, ProvidePasswordCallback = m_providePasswordCallback, RemoteCertificateValidationCallback = m_remoteCertificateValidationCallback, + ConnectionOpenedCallback = m_connectionOpenedCallback, }; } @@ -225,6 +228,7 @@ private string ProvidePasswordFromInitialRefreshTask(MySqlProvidePasswordContext private readonly TimeSpan m_periodicPasswordProviderSuccessRefreshInterval; private readonly TimeSpan m_periodicPasswordProviderFailureRefreshInterval; private readonly ZstandardPlugin? m_zstandardPlugin; + private readonly MySqlConnectionOpenedCallback? m_connectionOpenedCallback; private readonly MySqlProvidePasswordContext? m_providePasswordContext; private readonly CancellationTokenSource? m_passwordProviderTimerCancellationTokenSource; private readonly Timer? m_passwordProviderTimer; diff --git a/src/MySqlConnector/MySqlDataSourceBuilder.cs b/src/MySqlConnector/MySqlDataSourceBuilder.cs index e1d15d183..d75d6c687 100644 --- a/src/MySqlConnector/MySqlDataSourceBuilder.cs +++ b/src/MySqlConnector/MySqlDataSourceBuilder.cs @@ -89,6 +89,22 @@ public MySqlDataSourceBuilder UseRemoteCertificateValidationCallback(RemoteCerti return this; } + public MySqlDataSourceBuilder UseConnectionOpenedCallback(MySqlConnectionOpenedCallback callback) + { + m_connectionOpenedCallback = callback; + return this; + } + + public MySqlDataSourceBuilder UseConnectionOpenedCallback(Action callback) + { + m_connectionOpenedCallback = (connection, conditions) => + { + callback(connection, conditions); + return default; + }; + return this; + } + /// /// Builds a which is ready for use. /// @@ -104,7 +120,8 @@ public MySqlDataSource Build() m_periodicPasswordProvider, m_periodicPasswordProviderSuccessRefreshInterval, m_periodicPasswordProviderFailureRefreshInterval, - ZstandardPlugin + ZstandardPlugin, + m_connectionOpenedCallback ); } @@ -122,4 +139,5 @@ public MySqlDataSource Build() private Func>? m_periodicPasswordProvider; private TimeSpan m_periodicPasswordProviderSuccessRefreshInterval; private TimeSpan m_periodicPasswordProviderFailureRefreshInterval; + private MySqlConnectionOpenedCallback? m_connectionOpenedCallback; }