diff --git a/Orm/Xtensive.Orm.Firebird/Sql.Drivers.Firebird/DriverFactory.cs b/Orm/Xtensive.Orm.Firebird/Sql.Drivers.Firebird/DriverFactory.cs index 90c07849ca..6a272eb8e0 100644 --- a/Orm/Xtensive.Orm.Firebird/Sql.Drivers.Firebird/DriverFactory.cs +++ b/Orm/Xtensive.Orm.Firebird/Sql.Drivers.Firebird/DriverFactory.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2011-2020 Xtensive LLC. +// Copyright (C) 2011-2021 Xtensive LLC. // This code is distributed under MIT license terms. // See the License.txt file in the project root for more information. // Created by: Csaba Beer @@ -33,8 +33,10 @@ public class DriverFactory : SqlDriverFactory protected override SqlDriver CreateDriver(string connectionString, SqlDriverConfiguration configuration) { using var connection = new FbConnection(connectionString); - connection.Open(); - SqlHelper.ExecuteInitializationSql(connection, configuration); + if (configuration.DbConnectionAccessors.Count > 0) + OpenConnectionWithNotification(connection, configuration, false).GetAwaiter().GetResult(); + else + OpenConnectionFast(connection, configuration, false).GetAwaiter().GetResult(); var defaultSchema = GetDefaultSchema(connection); return CreateDriverInstance( connectionString, GetVersionFromServerVersionString(connection.ServerVersion), defaultSchema); @@ -45,8 +47,10 @@ protected override async Task CreateDriverAsync( { var connection = new FbConnection(connectionString); await using (connection.ConfigureAwait(false)) { - await connection.OpenAsync(token).ConfigureAwait(false); - await SqlHelper.ExecuteInitializationSqlAsync(connection, configuration, token).ConfigureAwait(false); + if (configuration.DbConnectionAccessors.Count > 0) + await OpenConnectionWithNotification(connection, configuration, true, token).ConfigureAwait(false); + else + await OpenConnectionFast(connection, configuration, true, token).ConfigureAwait(false); var defaultSchema = await GetDefaultSchemaAsync(connection, token: token).ConfigureAwait(false); return CreateDriverInstance( connectionString, GetVersionFromServerVersionString(connection.ServerVersion), defaultSchema); @@ -118,6 +122,58 @@ protected override Task ReadDefaultSchemaAsync( DbConnection connection, DbTransaction transaction, CancellationToken token) => SqlHelper.ReadDatabaseAndSchemaAsync(DatabaseAndSchemaQuery, connection, transaction, token); + private static async ValueTask OpenConnectionFast(FbConnection connection, + SqlDriverConfiguration configuration, bool isAsync, CancellationToken cancellationToken = default) + { + if (!isAsync) { + connection.Open(); + SqlHelper.ExecuteInitializationSql(connection, configuration); + } + else { + await connection.OpenAsync().ConfigureAwait(false); + await SqlHelper.ExecuteInitializationSqlAsync(connection, configuration, cancellationToken).ConfigureAwait(false); + } + } + + private static async ValueTask OpenConnectionWithNotification(FbConnection connection, + SqlDriverConfiguration configuration, bool isAsync, CancellationToken cancellationToken = default) + { + var accessors = configuration.DbConnectionAccessors; + if (!isAsync) { + SqlHelper.NotifyConnectionOpening(accessors, connection); + try { + connection.Open(); + if (!string.IsNullOrEmpty(configuration.ConnectionInitializationSql)) + SqlHelper.NotifyConnectionInitializing(accessors, connection, configuration.ConnectionInitializationSql); + SqlHelper.ExecuteInitializationSql(connection, configuration); + SqlHelper.NotifyConnectionOpened(accessors, connection); + } + catch(Exception ex) { + SqlHelper.NotifyConnectionOpeningFailed(accessors, connection, ex); + throw; + } + } + else { + await SqlHelper.NotifyConnectionOpeningAsync(accessors, connection, false, cancellationToken).ConfigureAwait(false); + try { + await connection.OpenAsync(cancellationToken).ConfigureAwait(false); + + if (!string.IsNullOrEmpty(configuration.ConnectionInitializationSql)) { + await SqlHelper.NotifyConnectionInitializingAsync(accessors, + connection, configuration.ConnectionInitializationSql, false, cancellationToken) + .ConfigureAwait(false); + } + + await SqlHelper.ExecuteInitializationSqlAsync(connection, configuration, cancellationToken).ConfigureAwait(false); + await SqlHelper.NotifyConnectionOpenedAsync(accessors, connection, false, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) { + await SqlHelper.NotifyConnectionOpeningFailedAsync(accessors, connection, ex, false, cancellationToken).ConfigureAwait(false); + throw; + } + } + } + private static Version GetVersionFromServerVersionString(string serverVersionString) { var matcher = new Regex(ServerVersionParser); diff --git a/Orm/Xtensive.Orm.MySql/Sql.Drivers.MySql/DriverFactory.cs b/Orm/Xtensive.Orm.MySql/Sql.Drivers.MySql/DriverFactory.cs index d50eb049c7..ab35623afc 100644 --- a/Orm/Xtensive.Orm.MySql/Sql.Drivers.MySql/DriverFactory.cs +++ b/Orm/Xtensive.Orm.MySql/Sql.Drivers.MySql/DriverFactory.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2011-2020 Xtensive LLC. +// Copyright (C) 2011-2021 Xtensive LLC. // This code is distributed under MIT license terms. // See the License.txt file in the project root for more information. // Created by: Malisa Ncube @@ -70,8 +70,10 @@ private static Version ParseVersion(string version) protected override SqlDriver CreateDriver(string connectionString, SqlDriverConfiguration configuration) { using (var connection = new MySqlConnection(connectionString)) { - connection.Open(); - SqlHelper.ExecuteInitializationSql(connection, configuration); + if (configuration.DbConnectionAccessors.Count > 0) + OpenConnectionWithNotification(connection, configuration, false).GetAwaiter().GetResult(); + else + OpenConnectionFast(connection, configuration, false).GetAwaiter().GetResult(); var versionString = string.IsNullOrEmpty(configuration.ForcedServerVersion) ? connection.ServerVersion : configuration.ForcedServerVersion; @@ -88,8 +90,10 @@ protected override async Task CreateDriverAsync( { var connection = new MySqlConnection(connectionString); await using (connection.ConfigureAwait(false)) { - await connection.OpenAsync(token).ConfigureAwait(false); - await SqlHelper.ExecuteInitializationSqlAsync(connection, configuration, token).ConfigureAwait(false); + if (configuration.DbConnectionAccessors.Count > 0) + await OpenConnectionWithNotification(connection, configuration, true, token).ConfigureAwait(false); + else + await OpenConnectionFast(connection, configuration, true, token).ConfigureAwait(false); var versionString = string.IsNullOrEmpty(configuration.ForcedServerVersion) ? connection.ServerVersion : configuration.ForcedServerVersion; @@ -131,5 +135,61 @@ protected override DefaultSchemaInfo ReadDefaultSchema(DbConnection connection, protected override Task ReadDefaultSchemaAsync( DbConnection connection, DbTransaction transaction, CancellationToken token) => SqlHelper.ReadDatabaseAndSchemaAsync(DatabaseAndSchemaQuery, connection, transaction, token); + + private async ValueTask OpenConnectionFast(MySqlConnection connection, + SqlDriverConfiguration configuration, + bool isAsync, + CancellationToken cancellationToken = default) + { + if (!isAsync) { + connection.Open(); + SqlHelper.ExecuteInitializationSql(connection, configuration); + } + else { + await connection.OpenAsync().ConfigureAwait(false); + await SqlHelper.ExecuteInitializationSqlAsync(connection, configuration, cancellationToken).ConfigureAwait(false); + } + } + + private async ValueTask OpenConnectionWithNotification(MySqlConnection connection, + SqlDriverConfiguration configuration, + bool isAsync, + CancellationToken cancellationToken = default) + { + var acessors = configuration.DbConnectionAccessors; + if (!isAsync) { + SqlHelper.NotifyConnectionOpening(acessors, connection); + try { + connection.Open(); + if (!string.IsNullOrEmpty(configuration.ConnectionInitializationSql)) + SqlHelper.NotifyConnectionInitializing(acessors, connection, configuration.ConnectionInitializationSql); + SqlHelper.ExecuteInitializationSql(connection, configuration); + SqlHelper.NotifyConnectionOpened(acessors, connection); + } + catch (Exception ex) { + SqlHelper.NotifyConnectionOpeningFailed(acessors, connection, ex); + throw; + } + } + else { + await SqlHelper.NotifyConnectionOpeningAsync(acessors, connection, false, cancellationToken).ConfigureAwait(false); + try { + await connection.OpenAsync(cancellationToken).ConfigureAwait(false); + + if (!string.IsNullOrEmpty(configuration.ConnectionInitializationSql)) { + await SqlHelper.NotifyConnectionInitializingAsync(acessors, + connection, configuration.ConnectionInitializationSql, false, cancellationToken) + .ConfigureAwait(false); + } + + await SqlHelper.ExecuteInitializationSqlAsync(connection, configuration, cancellationToken).ConfigureAwait(false); + await SqlHelper.NotifyConnectionOpenedAsync(acessors, connection, false, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) { + await SqlHelper.NotifyConnectionOpeningFailedAsync(acessors, connection, ex, false, cancellationToken).ConfigureAwait(false); + throw; + } + } + } } } \ No newline at end of file diff --git a/Orm/Xtensive.Orm.Oracle/Sql.Drivers.Oracle/DriverFactory.cs b/Orm/Xtensive.Orm.Oracle/Sql.Drivers.Oracle/DriverFactory.cs index 41b0f471b2..18ed9dc7f9 100644 --- a/Orm/Xtensive.Orm.Oracle/Sql.Drivers.Oracle/DriverFactory.cs +++ b/Orm/Xtensive.Orm.Oracle/Sql.Drivers.Oracle/DriverFactory.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2009-2020 Xtensive LLC. +// Copyright (C) 2009-2021 Xtensive LLC. // This code is distributed under MIT license terms. // See the License.txt file in the project root for more information. // Created by: Denis Krjuchkov @@ -71,8 +71,10 @@ protected override string BuildConnectionString(UrlInfo url) protected override SqlDriver CreateDriver(string connectionString, SqlDriverConfiguration configuration) { using var connection = new OracleConnection(connectionString); - connection.Open(); - SqlHelper.ExecuteInitializationSql(connection, configuration); + if (configuration.DbConnectionAccessors.Count > 0) + OpenConnectionWithNotification(connection, configuration, false).GetAwaiter().GetResult(); + else + OpenConnectionFast(connection, configuration, false).GetAwaiter().GetResult(); var version = string.IsNullOrEmpty(configuration.ForcedServerVersion) ? ParseVersion(connection.ServerVersion) : new Version(configuration.ForcedServerVersion); @@ -86,8 +88,10 @@ protected override async Task CreateDriverAsync( { var connection = new OracleConnection(connectionString); await using (connection.ConfigureAwait(false)) { - await connection.OpenAsync(token).ConfigureAwait(false); - await SqlHelper.ExecuteInitializationSqlAsync(connection, configuration, token).ConfigureAwait(false); + if (configuration.DbConnectionAccessors.Count > 0) + await OpenConnectionWithNotification(connection, configuration, true, token).ConfigureAwait(false); + else + await OpenConnectionFast(connection, configuration, true, token).ConfigureAwait(false); var version = string.IsNullOrEmpty(configuration.ForcedServerVersion) ? ParseVersion(connection.ServerVersion) : new Version(configuration.ForcedServerVersion); @@ -124,5 +128,61 @@ protected override DefaultSchemaInfo ReadDefaultSchema(DbConnection connection, protected override Task ReadDefaultSchemaAsync( DbConnection connection, DbTransaction transaction, CancellationToken token) => SqlHelper.ReadDatabaseAndSchemaAsync(DatabaseAndSchemaQuery, connection, transaction, token); + + private async ValueTask OpenConnectionFast(OracleConnection connection, + SqlDriverConfiguration configuration, + bool isAsync, + CancellationToken cancellationToken = default) + { + if (!isAsync) { + connection.Open(); + SqlHelper.ExecuteInitializationSql(connection, configuration); + } + else { + await connection.OpenAsync().ConfigureAwait(false); + await SqlHelper.ExecuteInitializationSqlAsync(connection, configuration, cancellationToken).ConfigureAwait(false); + } + } + + private async ValueTask OpenConnectionWithNotification(OracleConnection connection, + SqlDriverConfiguration configuration, + bool isAsync, + CancellationToken cancellationToken = default) + { + var accessors = configuration.DbConnectionAccessors; + if (!isAsync) { + SqlHelper.NotifyConnectionOpening(accessors, connection); + try { + connection.Open(); + if (!string.IsNullOrEmpty(configuration.ConnectionInitializationSql)) + SqlHelper.NotifyConnectionInitializing(accessors, connection, configuration.ConnectionInitializationSql); + SqlHelper.ExecuteInitializationSql(connection, configuration); + SqlHelper.NotifyConnectionOpened(accessors, connection); + } + catch (Exception ex) { + SqlHelper.NotifyConnectionOpeningFailed(accessors, connection, ex); + throw; + } + } + else { + await SqlHelper.NotifyConnectionOpeningAsync(accessors, connection, false, cancellationToken).ConfigureAwait(false); + try { + await connection.OpenAsync(cancellationToken).ConfigureAwait(false); + + if (!string.IsNullOrEmpty(configuration.ConnectionInitializationSql)) { + await SqlHelper.NotifyConnectionInitializingAsync(accessors, + connection, configuration.ConnectionInitializationSql, false, cancellationToken) + .ConfigureAwait(false); + } + + await SqlHelper.ExecuteInitializationSqlAsync(connection, configuration, cancellationToken).ConfigureAwait(false); + await SqlHelper.NotifyConnectionOpenedAsync(accessors, connection, false, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) { + await SqlHelper.NotifyConnectionOpeningFailedAsync(accessors, connection, ex, false, cancellationToken).ConfigureAwait(false); + throw; + } + } + } } } \ No newline at end of file diff --git a/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/DriverFactory.cs b/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/DriverFactory.cs index 3a55931398..bf4db22e50 100644 --- a/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/DriverFactory.cs +++ b/Orm/Xtensive.Orm.PostgreSql/Sql.Drivers.PostgreSql/DriverFactory.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2009-2020 Xtensive LLC. +// Copyright (C) 2009-2021 Xtensive LLC. // This code is distributed under MIT license terms. // See the License.txt file in the project root for more information. // Created by: Denis Krjuchkov @@ -61,8 +61,10 @@ protected override string BuildConnectionString(UrlInfo url) protected override SqlDriver CreateDriver(string connectionString, SqlDriverConfiguration configuration) { using var connection = new NpgsqlConnection(connectionString); - connection.Open(); - SqlHelper.ExecuteInitializationSql(connection, configuration); + if (configuration.DbConnectionAccessors.Count > 0) + OpenConnectionWithNotification(connection, configuration, false).GetAwaiter().GetResult(); + else + OpenConnectionFast(connection, configuration, false).GetAwaiter().GetResult(); var version = GetVersion(configuration, connection); var defaultSchema = GetDefaultSchema(connection); return CreateDriverInstance(connectionString, version, defaultSchema); @@ -74,8 +76,10 @@ protected override async Task CreateDriverAsync( { var connection = new NpgsqlConnection(connectionString); await using (connection.ConfigureAwait(false)) { - await connection.OpenAsync(token).ConfigureAwait(false); - await SqlHelper.ExecuteInitializationSqlAsync(connection, configuration, token).ConfigureAwait(false); + if (configuration.DbConnectionAccessors.Count > 0) + await OpenConnectionWithNotification(connection, configuration, true, token).ConfigureAwait(false); + else + await OpenConnectionFast(connection, configuration, true, token).ConfigureAwait(false); var version = GetVersion(configuration, connection); var defaultSchema = await GetDefaultSchemaAsync(connection, token: token).ConfigureAwait(false); return CreateDriverInstance(connectionString, version, defaultSchema); @@ -130,5 +134,63 @@ protected override DefaultSchemaInfo ReadDefaultSchema(DbConnection connection, protected override Task ReadDefaultSchemaAsync( DbConnection connection, DbTransaction transaction, CancellationToken token) => SqlHelper.ReadDatabaseAndSchemaAsync(DatabaseAndSchemaQuery, connection, transaction, token); + + private async ValueTask OpenConnectionFast(NpgsqlConnection connection, + SqlDriverConfiguration configuration, + bool isAsync, + CancellationToken cancellationToken = default) + { + if (!isAsync) { + connection.Open(); + SqlHelper.ExecuteInitializationSql(connection, configuration); + } + else { + await connection.OpenAsync().ConfigureAwait(false); + await SqlHelper.ExecuteInitializationSqlAsync(connection, configuration, cancellationToken).ConfigureAwait(false); + } + } + + private async ValueTask OpenConnectionWithNotification(NpgsqlConnection connection, + SqlDriverConfiguration configuration, + bool isAsync, + CancellationToken cancellationToken = default) + { + var accessors = configuration.DbConnectionAccessors; + if (!isAsync) { + SqlHelper.NotifyConnectionOpening(accessors, connection); + try { + connection.Open(); + if (!string.IsNullOrEmpty(configuration.ConnectionInitializationSql)) { + SqlHelper.NotifyConnectionInitializing(accessors, connection, configuration.ConnectionInitializationSql); + } + + SqlHelper.ExecuteInitializationSql(connection, configuration); + SqlHelper.NotifyConnectionOpened(accessors, connection); + } + catch (Exception ex) { + SqlHelper.NotifyConnectionOpeningFailed(accessors, connection, ex); + throw; + } + } + else { + await SqlHelper.NotifyConnectionOpeningAsync(accessors, connection, false, cancellationToken).ConfigureAwait(false); + try { + await connection.OpenAsync(cancellationToken).ConfigureAwait(false); + + if (!string.IsNullOrEmpty(configuration.ConnectionInitializationSql)) { + await SqlHelper.NotifyConnectionInitializingAsync(accessors, + connection, configuration.ConnectionInitializationSql, false, cancellationToken) + .ConfigureAwait(false); + } + + await SqlHelper.ExecuteInitializationSqlAsync(connection, configuration, cancellationToken).ConfigureAwait(false); + await SqlHelper.NotifyConnectionOpenedAsync(accessors, connection, false, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) { + await SqlHelper.NotifyConnectionOpeningFailedAsync(accessors, connection, ex, false, cancellationToken).ConfigureAwait(false); + throw; + } + } + } } } diff --git a/Orm/Xtensive.Orm.SqlServer/Sql.Drivers.SqlServer/Connection.cs b/Orm/Xtensive.Orm.SqlServer/Sql.Drivers.SqlServer/Connection.cs index 3f674dc0fd..1bbc6f39ad 100644 --- a/Orm/Xtensive.Orm.SqlServer/Sql.Drivers.SqlServer/Connection.cs +++ b/Orm/Xtensive.Orm.SqlServer/Sql.Drivers.SqlServer/Connection.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2009-2020 Xtensive LLC. +// Copyright (C) 2009-2021 Xtensive LLC. // This code is distributed under MIT license terms. // See the License.txt file in the project root for more information. // Created by: Denis Krjuchkov @@ -39,7 +39,13 @@ public override void Open() base.Open(); } else { - OpenWithCheck(DefaultCheckConnectionQuery); + var connectionAccessorEx = Extensions.Get(); + if (connectionAccessorEx == null) { + OpenWithCheckFast(DefaultCheckConnectionQuery); + } + else { + OpenWithCheckAndNotification(DefaultCheckConnectionQuery, connectionAccessorEx); + } } } @@ -51,7 +57,13 @@ public override Task OpenAsync(CancellationToken cancellationToken) return base.OpenAsync(cancellationToken); } - return OpenWithCheckAsync(DefaultCheckConnectionQuery, cancellationToken); + var connectionAccessorEx = Extensions.Get(); + if (connectionAccessorEx == null) { + return OpenWithCheckFastAsync(DefaultCheckConnectionQuery, cancellationToken); + } + else { + return OpenWithCheckAndNotificationAsync(DefaultCheckConnectionQuery, connectionAccessorEx, cancellationToken); + } } /// @@ -65,7 +77,13 @@ public override void OpenAndInitialize(string initializationScript) var script = string.IsNullOrEmpty(initializationScript.Trim()) ? DefaultCheckConnectionQuery : initializationScript; - OpenWithCheck(script); + var connectionAccessorEx = Extensions.Get(); + if (connectionAccessorEx == null) { + OpenWithCheckFast(script); + } + else { + OpenWithCheckAndNotification(script, connectionAccessorEx); + } } /// @@ -78,7 +96,10 @@ public override Task OpenAndInitializeAsync(string initializationScript, Cancell var script = string.IsNullOrEmpty(initializationScript.Trim()) ? DefaultCheckConnectionQuery : initializationScript; - return OpenWithCheckAsync(script, token); + var connectionAccessorEx = Extensions.Get(); + return connectionAccessorEx == null + ? OpenWithCheckFastAsync(script, token) + : OpenWithCheckAndNotificationAsync(script, connectionAccessorEx, token); } /// @@ -160,16 +181,16 @@ public override void ReleaseSavepoint(string name) /// protected override void ClearActiveTransaction() => activeTransaction = null; - private void OpenWithCheck(string checkQueryString) + private void OpenWithCheckFast(string checkQueryString) { var connectionChecked = false; var restoreTriggered = false; while (!connectionChecked) { - base.Open(); + underlyingConnection.Open(); try { using (var command = underlyingConnection.CreateCommand()) { command.CommandText = checkQueryString; - command.ExecuteNonQuery(); + _ = command.ExecuteNonQuery(); } connectionChecked = true; } @@ -196,23 +217,119 @@ private void OpenWithCheck(string checkQueryString) } } - private async Task OpenWithCheckAsync(string checkQueryString, CancellationToken cancellationToken) + private void OpenWithCheckAndNotification(string checkQueryString, DbConnectionAccessorExtension connectionAccessorEx) { var connectionChecked = false; var restoreTriggered = false; + var accessors = connectionAccessorEx.Accessors; + while (!connectionChecked) { + SqlHelper.NotifyConnectionOpening(accessors, UnderlyingConnection, (!connectionChecked && !restoreTriggered)); + underlyingConnection.Open(); + try { + SqlHelper.NotifyConnectionInitializing(accessors, UnderlyingConnection, checkQueryString, (!connectionChecked && !restoreTriggered)); + using (var command = underlyingConnection.CreateCommand()) { + command.CommandText = checkQueryString; + _ = command.ExecuteNonQuery(); + } + connectionChecked = true; + SqlHelper.NotifyConnectionOpened(accessors, UnderlyingConnection, (!connectionChecked && !restoreTriggered)); + } + catch (Exception exception) { + SqlHelper.NotifyConnectionOpeningFailed(accessors, UnderlyingConnection, exception, (!connectionChecked && !restoreTriggered)); + if (InternalHelpers.ShouldRetryOn(exception)) { + if (restoreTriggered) { + throw; + } + + var newConnection = new SqlServerConnection(underlyingConnection.ConnectionString); + try { + underlyingConnection.Close(); + underlyingConnection.Dispose(); + } + catch { } + + underlyingConnection = newConnection; + restoreTriggered = true; + continue; + } + + throw; + } + } + } + + private async Task OpenWithCheckFastAsync(string checkQueryString, CancellationToken cancellationToken) + { + var connectionChecked = false; + var restoreTriggered = false; + + while (!connectionChecked) { + cancellationToken.ThrowIfCancellationRequested(); + await underlyingConnection.OpenAsync(cancellationToken).ConfigureAwait(false); + try { + var command = underlyingConnection.CreateCommand(); + await using (command.ConfigureAwait(false)) { + command.CommandText = checkQueryString; + _ = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + } + connectionChecked = true; + } + catch (Exception exception) { + if (InternalHelpers.ShouldRetryOn(exception)) { + if (restoreTriggered) { + throw; + } + var newConnection = new SqlServerConnection(underlyingConnection.ConnectionString); + try { + underlyingConnection.Close(); + underlyingConnection.Dispose(); + } + catch { } + + underlyingConnection = newConnection; + restoreTriggered = true; + continue; + } + + throw; + } + } + } + + private async Task OpenWithCheckAndNotificationAsync(string checkQueryString, + DbConnectionAccessorExtension connectionAccessorEx, CancellationToken cancellationToken) + { + var connectionChecked = false; + var restoreTriggered = false; + var accessors = connectionAccessorEx.Accessors; while (!connectionChecked) { cancellationToken.ThrowIfCancellationRequested(); - await base.OpenAsync(cancellationToken).ConfigureAwait(false); + + await SqlHelper.NotifyConnectionOpeningAsync(accessors, + UnderlyingConnection, (!connectionChecked && !restoreTriggered), cancellationToken) + .ConfigureAwait(false); + + await underlyingConnection.OpenAsync(cancellationToken).ConfigureAwait(false); try { + await SqlHelper.NotifyConnectionInitializingAsync(accessors, + UnderlyingConnection, checkQueryString, (!connectionChecked && !restoreTriggered), cancellationToken) + .ConfigureAwait(false); + var command = underlyingConnection.CreateCommand(); await using (command.ConfigureAwait(false)) { command.CommandText = checkQueryString; - await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + _ = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); } connectionChecked = true; + await SqlHelper.NotifyConnectionOpenedAsync(accessors, UnderlyingConnection, (!connectionChecked && !restoreTriggered), cancellationToken) + .ConfigureAwait(false); } catch (Exception exception) { + await SqlHelper.NotifyConnectionOpeningFailedAsync(accessors, + UnderlyingConnection, exception, (!connectionChecked && !restoreTriggered), cancellationToken) + .ConfigureAwait(false); + if (InternalHelpers.ShouldRetryOn(exception)) { if (restoreTriggered) { throw; diff --git a/Orm/Xtensive.Orm.SqlServer/Sql.Drivers.SqlServer/DriverFactory.cs b/Orm/Xtensive.Orm.SqlServer/Sql.Drivers.SqlServer/DriverFactory.cs index 5775af9eef..042f1c20ed 100644 --- a/Orm/Xtensive.Orm.SqlServer/Sql.Drivers.SqlServer/DriverFactory.cs +++ b/Orm/Xtensive.Orm.SqlServer/Sql.Drivers.SqlServer/DriverFactory.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2009-2020 Xtensive LLC. +// Copyright (C) 2009-2021 Xtensive LLC. // This code is distributed under MIT license terms. // See the License.txt file in the project root for more information. // Created by: Denis Krjuchkov @@ -37,6 +37,8 @@ FROM [master].[sys].[sysmessages] msg private const string VersionQuery = "SELECT @@VERSION"; + private const string ForcedAzureVersion = "12.0.0.0"; + private static ErrorMessageParser CreateMessageParser(SqlServerConnection connection) { bool isEnglish; @@ -151,7 +153,7 @@ protected override SqlDriver CreateDriver(string connectionString, SqlDriverConf var parser = isAzure ? new ErrorMessageParser() : CreateMessageParser(connection); var versionString = isForcedVersion - ? isForcedAzure ? "10.0.0.0" : forcedServerVersion + ? isForcedAzure ? ForcedAzureVersion : forcedServerVersion : connection.ServerVersion ?? string.Empty; var version = new Version(versionString); var defaultSchema = GetDefaultSchema(connection); @@ -229,130 +231,337 @@ private static SqlServerConnection CreateAndOpenConnection( string connectionString, SqlDriverConfiguration configuration) { var connection = new SqlServerConnection(connectionString); + var initScript = configuration.ConnectionInitializationSql; + if (!configuration.EnsureConnectionIsAlive) { - connection.Open(); - SqlHelper.ExecuteInitializationSql(connection, configuration); + if (configuration.DbConnectionAccessors.Count == 0) + OpenConnectionFast(connection, initScript, false).GetAwaiter().GetResult(); + else + OpenConnectionWithNotification(connection, configuration, false).GetAwaiter().GetResult(); return connection; } - var testQuery = string.IsNullOrEmpty(configuration.ConnectionInitializationSql) + var testQuery = string.IsNullOrEmpty(initScript) ? CheckConnectionQuery - : configuration.ConnectionInitializationSql; - return EnsureConnectionIsAlive(connection, testQuery); + : initScript; + if (configuration.DbConnectionAccessors.Count == 0) + return EnsureConnectionIsAliveFast(connection, testQuery, false).GetAwaiter().GetResult(); + else + return EnsureConnectionIsAliveWithNotification(connection, testQuery, configuration.DbConnectionAccessors, false) + .GetAwaiter().GetResult(); } private static async Task CreateAndOpenConnectionAsync( string connectionString, SqlDriverConfiguration configuration, CancellationToken token) { var connection = new SqlServerConnection(connectionString); + var initScript = configuration.ConnectionInitializationSql; + if (!configuration.EnsureConnectionIsAlive) { - await connection.OpenAsync(token).ConfigureAwait(false); - await SqlHelper.ExecuteInitializationSqlAsync(connection, configuration, token).ConfigureAwait(false); + if (configuration.DbConnectionAccessors.Count == 0) + await OpenConnectionFast(connection, initScript, true, token).ConfigureAwait(false); + else + await OpenConnectionWithNotification(connection, configuration, true, token).ConfigureAwait(false); return connection; } - var testQuery = string.IsNullOrEmpty(configuration.ConnectionInitializationSql) + var testQuery = string.IsNullOrEmpty(initScript) ? CheckConnectionQuery - : configuration.ConnectionInitializationSql; - return await EnsureConnectionIsAliveAsync(connection, testQuery, token).ConfigureAwait(false); + : initScript; + if (configuration.DbConnectionAccessors.Count == 0) + return await EnsureConnectionIsAliveFast(connection, testQuery, true, token).ConfigureAwait(false); + else + return await EnsureConnectionIsAliveWithNotification(connection, testQuery, configuration.DbConnectionAccessors, true, token) + .ConfigureAwait(false); } - private static SqlServerConnection EnsureConnectionIsAlive(SqlServerConnection connection, string query) + private static async ValueTask OpenConnectionFast(SqlServerConnection connection, + string sqlScript, bool isAsync, CancellationToken token = default) { - try { + if (!isAsync) { connection.Open(); - using var command = connection.CreateCommand(); - command.CommandText = query; - command.ExecuteNonQuery(); + SqlHelper.ExecuteInitializationSql(connection, sqlScript); + } + else { + await connection.OpenAsync(token).ConfigureAwait(false); + await SqlHelper.ExecuteInitializationSqlAsync(connection, sqlScript, token).ConfigureAwait(false); + } + } - return connection; + private static async ValueTask OpenConnectionWithNotification(SqlServerConnection connection, + SqlDriverConfiguration configuration, bool isAsync, CancellationToken token = default) + { + var accessors = configuration.DbConnectionAccessors; + var initSql = configuration.ConnectionInitializationSql; + + if (!isAsync) { + SqlHelper.NotifyConnectionOpening(accessors, connection); + try { + connection.Open(); + if (!string.IsNullOrEmpty(initSql)) { + SqlHelper.NotifyConnectionInitializing(accessors, connection, initSql); + SqlHelper.ExecuteInitializationSql(connection, initSql); + } + SqlHelper.NotifyConnectionOpened(accessors, connection); + } + catch (Exception ex) { + SqlHelper.NotifyConnectionOpeningFailed(accessors, connection, ex); + throw; + } } - catch (Exception exception) { - var connectionString = connection.ConnectionString; + else { + await SqlHelper.NotifyConnectionOpeningAsync(accessors, connection, false, token); try { - connection.Close(); - connection.Dispose(); + await connection.OpenAsync(token); + if (!string.IsNullOrEmpty(initSql)) { + await SqlHelper.NotifyConnectionInitializingAsync(accessors, connection, initSql, false, token); + await SqlHelper.ExecuteInitializationSqlAsync(connection, initSql, token); + } + await SqlHelper.NotifyConnectionOpenedAsync(accessors, connection, false, token); } - catch { - // ignored + catch (Exception ex) { + await SqlHelper.NotifyConnectionOpeningFailedAsync(accessors, connection, ex, false, token); + throw; } + } + } - if (InternalHelpers.ShouldRetryOn(exception)) { - var (isReconnected, newConnection) = TryReconnect(connectionString, query); - if (isReconnected) { - return newConnection; + private static async ValueTask EnsureConnectionIsAliveFast(SqlServerConnection connection, + string query, bool isAsync, CancellationToken token = default) + { + if (!isAsync) { + try { + connection.Open(); + + using (var command = connection.CreateCommand()) { + command.CommandText = query; + _ = command.ExecuteNonQuery(); } + + return connection; + } + catch (Exception exception) { + try { + connection.Close(); + connection.Dispose(); + } + catch { + // ignored + } + + if (InternalHelpers.ShouldRetryOn(exception)) { + var (isReconnected, newConnection) = + TryReconnectFast(connection.ConnectionString, query, isAsync).GetAwaiter().GetResult(); + if (isReconnected) + return newConnection; + } + throw; + } + } + else { + try { + await connection.OpenAsync(token).ConfigureAwait(false); + + var command = connection.CreateCommand(); + await using (command.ConfigureAwait(false)) { + command.CommandText = query; + _ = await command.ExecuteNonQueryAsync(token).ConfigureAwait(false); + } + + return connection; + } + catch (Exception exception) { + try { + await connection.CloseAsync().ConfigureAwait(false); + await connection.DisposeAsync().ConfigureAwait(false); + } + catch { + // ignored + } + + if (InternalHelpers.ShouldRetryOn(exception)) { + var (isReconnected, newConnection) = + await TryReconnectFast(connection.ConnectionString, query, isAsync, token).ConfigureAwait(false); + if (isReconnected) { + return newConnection; + } + } + throw; } - throw; } } - private static async Task EnsureConnectionIsAliveAsync( - SqlServerConnection connection, string query, CancellationToken token) + private static async ValueTask EnsureConnectionIsAliveWithNotification(SqlServerConnection connection, + string query, IReadOnlyCollection connectionAccessos, bool isAsync, CancellationToken token = default) { - try { - await connection.OpenAsync(token).ConfigureAwait(false); - var command = connection.CreateCommand(); - await using (command.ConfigureAwait(false)) { - command.CommandText = query; - await command.ExecuteNonQueryAsync(token).ConfigureAwait(false); + if (!isAsync) { + SqlHelper.NotifyConnectionOpening(connectionAccessos, connection); + try { + connection.Open(); + + SqlHelper.NotifyConnectionInitializing(connectionAccessos, connection, query); + + using (var command = connection.CreateCommand()) { + command.CommandText = query; + _ = command.ExecuteNonQuery(); + } + + SqlHelper.NotifyConnectionOpened(connectionAccessos, connection); + return connection; } + catch (Exception exception) { + var retryToConnect = InternalHelpers.ShouldRetryOn(exception); + if (!retryToConnect) + SqlHelper.NotifyConnectionOpeningFailed(connectionAccessos, connection, exception); + try { + connection.Close(); + connection.Dispose(); + } + catch { + // ignored + } - return connection; + if (retryToConnect) { + var (isReconnected, newConnection) = TryReconnectWithNotification(connection.ConnectionString, query, connectionAccessos, isAsync) + .GetAwaiter().GetResult(); + if (isReconnected) { + return newConnection; + } + } + throw; + } } - catch (Exception exception) { - var connectionString = connection.ConnectionString; + else { + await SqlHelper.NotifyConnectionOpeningAsync(connectionAccessos, connection, false, token).ConfigureAwait(false); + try { - await connection.CloseAsync().ConfigureAwait(false); - await connection.DisposeAsync().ConfigureAwait(false); - } - catch { - // ignored + await connection.OpenAsync(token).ConfigureAwait(false); + + await SqlHelper.NotifyConnectionInitializingAsync(connectionAccessos, connection, query, false, token).ConfigureAwait(false); + + var command = connection.CreateCommand(); + await using (command.ConfigureAwait(false)) { + command.CommandText = query; + _ = await command.ExecuteNonQueryAsync(token).ConfigureAwait(false); + } + + await SqlHelper.NotifyConnectionOpenedAsync(connectionAccessos, connection, false, token).ConfigureAwait(false); + return connection; } + catch (Exception exception) { + var retryToConnect = InternalHelpers.ShouldRetryOn(exception); + if (!retryToConnect) { + await SqlHelper.NotifyConnectionOpeningFailedAsync(connectionAccessos, connection, exception, false, token).ConfigureAwait(false); + } - if (InternalHelpers.ShouldRetryOn(exception)) { - var (isReconnected, newConnection) = - await TryReconnectAsync(connectionString, query, token).ConfigureAwait(false); - if (isReconnected) { - return newConnection; + var connectionString = connection.ConnectionString; + try { + await connection.CloseAsync().ConfigureAwait(false); + await connection.DisposeAsync().ConfigureAwait(false); + } + catch { + // ignored } + + if (retryToConnect) { + var (isReconnected, newConnection) = + await TryReconnectWithNotification(connectionString, query, connectionAccessos, isAsync, token).ConfigureAwait(false); + if (isReconnected) { + return newConnection; + } + } + throw; } - throw; } } - private static (bool isReconnected, SqlServerConnection connection) TryReconnect( - string connectionString, string query) + private static async Task<(bool isReconnected, SqlServerConnection connection)> TryReconnectFast( + string connectionString, string query, bool isAsync, CancellationToken token = default) { - try { - var connection = new SqlServerConnection(connectionString); - connection.Open(); - using (var command = connection.CreateCommand()) { - command.CommandText = query; - command.ExecuteNonQuery(); + var connection = new SqlServerConnection(connectionString); + if (!isAsync) { + try { + connection.Open(); + + using (var command = connection.CreateCommand()) { + command.CommandText = query; + _ = command.ExecuteNonQuery(); + } + + return (true, connection); + } + catch { + connection.Dispose(); + return (false, null); } - return (true, connection); } - catch { - return (false, null); + else { + try { + await connection.OpenAsync(token).ConfigureAwait(false); + + var command = connection.CreateCommand(); + await using (command.ConfigureAwait(false)) { + command.CommandText = query; + _ = await command.ExecuteNonQueryAsync(token).ConfigureAwait(false); + } + + return (true, connection); + } + catch { + await connection.DisposeAsync(); + return (false, null); + } } } - private static async Task<(bool isReconnected, SqlServerConnection connection)> TryReconnectAsync( - string connectionString, string query, CancellationToken token) + private static async Task<(bool isReconnected, SqlServerConnection connection)> TryReconnectWithNotification( + string connectionString, string query, IReadOnlyCollection connectionAccessors, + bool isAsync, CancellationToken token = default) { - try { - var connection = new SqlServerConnection(connectionString); - await connection.OpenAsync(token).ConfigureAwait(false); - var command = connection.CreateCommand(); - await using (command.ConfigureAwait(false)) { - command.CommandText = query; - await command.ExecuteNonQueryAsync(token).ConfigureAwait(false); + var connection = new SqlServerConnection(connectionString); + if (!isAsync) { + SqlHelper.NotifyConnectionOpening(connectionAccessors, connection, true); + + try { + connection.Open(); + SqlHelper.NotifyConnectionInitializing(connectionAccessors, connection, query, true); + + using (var command = connection.CreateCommand()) { + command.CommandText = query; + _ = command.ExecuteNonQuery(); + } + + SqlHelper.NotifyConnectionOpened(connectionAccessors, connection, true); + return (true, connection); + } + catch (Exception exception) { + SqlHelper.NotifyConnectionOpeningFailed(connectionAccessors, connection, exception, true); + connection.Dispose(); + return (false, null); } - return (true, connection); } - catch { - return (false, null); + else { + await SqlHelper.NotifyConnectionOpeningAsync(connectionAccessors, connection, true, token).ConfigureAwait(false); + + try { + await connection.OpenAsync(token).ConfigureAwait(false); + + await SqlHelper.NotifyConnectionInitializingAsync(connectionAccessors, connection, query, true, token).ConfigureAwait(false); + + var command = connection.CreateCommand(); + await using (command.ConfigureAwait(false)) { + command.CommandText = query; + _ = await command.ExecuteNonQueryAsync(token).ConfigureAwait(false); + } + + await SqlHelper.NotifyConnectionOpenedAsync(connectionAccessors, connection, true, token).ConfigureAwait(false); + return (true, connection); + } + catch (Exception exception) { + await SqlHelper.NotifyConnectionOpeningFailedAsync(connectionAccessors, connection, exception, true, token).ConfigureAwait(false); + await connection.DisposeAsync(); + return (false, null); + } } } diff --git a/Orm/Xtensive.Orm.Sqlite/Sql.Drivers.Sqlite/DriverFactory.cs b/Orm/Xtensive.Orm.Sqlite/Sql.Drivers.Sqlite/DriverFactory.cs index 7978498454..a97e12838c 100644 --- a/Orm/Xtensive.Orm.Sqlite/Sql.Drivers.Sqlite/DriverFactory.cs +++ b/Orm/Xtensive.Orm.Sqlite/Sql.Drivers.Sqlite/DriverFactory.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2011-2020 Xtensive LLC. +// Copyright (C) 2011-2021 Xtensive LLC. // This code is distributed under MIT license terms. // See the License.txt file in the project root for more information. // Created by: Malisa Ncube @@ -43,8 +43,10 @@ private static string GetDataSource(string connectionString) protected override SqlDriver CreateDriver(string connectionString, SqlDriverConfiguration configuration) { using var connection = new SQLiteConnection(connectionString); - connection.Open(); - SqlHelper.ExecuteInitializationSql(connection, configuration); + if (configuration.DbConnectionAccessors.Count > 0) + OpenConnectionWithNotification(connection, configuration, false).GetAwaiter().GetResult(); + else + OpenConnectionFast(connection, configuration, false).GetAwaiter().GetResult(); var defaultSchema = GetDefaultSchema(connection); var version = new Version(connection.ServerVersion ?? string.Empty); return CreateDriverInstance(connectionString, version, defaultSchema); @@ -56,8 +58,10 @@ protected override async Task CreateDriverAsync( { var connection = new SQLiteConnection(connectionString); await using (connection.ConfigureAwait(false)) { - await connection.OpenAsync(token).ConfigureAwait(false); - await SqlHelper.ExecuteInitializationSqlAsync(connection, configuration, token).ConfigureAwait(false); + if (configuration.DbConnectionAccessors.Count > 0) + await OpenConnectionWithNotification(connection, configuration, true, token).ConfigureAwait(false); + else + await OpenConnectionFast(connection, configuration, true, token).ConfigureAwait(false); var defaultSchema = await GetDefaultSchemaAsync(connection, token: token).ConfigureAwait(false); var version = new Version(connection.ServerVersion ?? string.Empty); return CreateDriverInstance(connectionString, version, defaultSchema); @@ -103,5 +107,63 @@ protected override DefaultSchemaInfo ReadDefaultSchema(DbConnection connection, protected override Task ReadDefaultSchemaAsync( DbConnection connection, DbTransaction transaction, CancellationToken token) => Task.FromResult(new DefaultSchemaInfo(GetDataSource(connection.ConnectionString), Extractor.DefaultSchemaName)); + + private async ValueTask OpenConnectionFast(SQLiteConnection connection, + SqlDriverConfiguration configuration, + bool isAsync, + CancellationToken cancellationToken = default) + { + if (!isAsync) { + connection.Open(); + SqlHelper.ExecuteInitializationSql(connection, configuration); + } + else { + await connection.OpenAsync().ConfigureAwait(false); + await SqlHelper.ExecuteInitializationSqlAsync(connection, configuration, cancellationToken).ConfigureAwait(false); + } + } + + private async ValueTask OpenConnectionWithNotification(SQLiteConnection connection, + SqlDriverConfiguration configuration, + bool isAsync, + CancellationToken cancellationToken = default) + { + var accessors = configuration.DbConnectionAccessors; + if (!isAsync) { + SqlHelper.NotifyConnectionOpening(accessors, connection); + try { + connection.Open(); + if (!string.IsNullOrEmpty(configuration.ConnectionInitializationSql)) { + SqlHelper.NotifyConnectionInitializing(accessors, connection, configuration.ConnectionInitializationSql); + } + + SqlHelper.ExecuteInitializationSql(connection, configuration); + SqlHelper.NotifyConnectionOpened(accessors, connection); + } + catch (Exception ex) { + SqlHelper.NotifyConnectionOpeningFailed(accessors, connection, ex); + throw; + } + } + else { + await SqlHelper.NotifyConnectionOpeningAsync(accessors, connection, false, cancellationToken).ConfigureAwait(false); + try { + await connection.OpenAsync(cancellationToken).ConfigureAwait(false); + + if (!string.IsNullOrEmpty(configuration.ConnectionInitializationSql)) { + await SqlHelper.NotifyConnectionInitializingAsync(accessors, + connection, configuration.ConnectionInitializationSql, false, cancellationToken) + .ConfigureAwait(false); + } + + await SqlHelper.ExecuteInitializationSqlAsync(connection, configuration, cancellationToken).ConfigureAwait(false); + await SqlHelper.NotifyConnectionOpenedAsync(accessors, connection, false, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) { + await SqlHelper.NotifyConnectionOpeningFailedAsync(accessors, connection, ex, false, cancellationToken).ConfigureAwait(false); + throw; + } + } + } } } \ No newline at end of file diff --git a/Orm/Xtensive.Orm.Tests.Sql/DriverFactoryTest.cs b/Orm/Xtensive.Orm.Tests.Sql/DriverFactoryTest.cs index 493576c3b2..75b32fbabb 100644 --- a/Orm/Xtensive.Orm.Tests.Sql/DriverFactoryTest.cs +++ b/Orm/Xtensive.Orm.Tests.Sql/DriverFactoryTest.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2003-2010 Xtensive LLC. +// Copyright (C) 2009-2021 Xtensive LLC. // This code is distributed under MIT license terms. // See the License.txt file in the project root for more information. @@ -8,6 +8,77 @@ using Xtensive.Orm; using Xtensive.Orm.Building.Builders; using Xtensive.Sql; +using Xtensive.Orm.Tests.Sql.DriverFactoryTestTypes; +using System.Threading.Tasks; +using System.Threading; + +namespace Xtensive.Orm.Tests.Sql.DriverFactoryTestTypes +{ + public class TestConnectionAccessor : DbConnectionAccessor + { + public int OpeningCounter = 0; + public int OpeningAsyncCounter = 0; + + public int OpeningInitCounter = 0; + public int OpeningInitAsyncCounter = 0; + + public int OpenedCounter = 0; + public int OpenedAsyncCounter = 0; + + public int OpeningFailedCounter = 0; + public int OpeningFailedAsyncCounter = 0; + + public override void ConnectionOpening(ConnectionEventData eventData) + { + OpeningCounter++; + } + + public override Task ConnectionOpeningAsync(ConnectionEventData eventData, CancellationToken cancellationToken) + { + OpeningAsyncCounter++; + return base.ConnectionOpeningAsync(eventData, cancellationToken); + } + + public override void ConnectionInitialization(ConnectionInitEventData eventData) + { + OpeningInitCounter++; + } + + public override Task ConnectionInitializationAsync(ConnectionInitEventData eventData, CancellationToken cancellationToken) + { + OpeningInitAsyncCounter++; + return base.ConnectionInitializationAsync(eventData, cancellationToken); + } + + public override void ConnectionOpened(ConnectionEventData eventData) + { + OpenedCounter++; + } + + public override Task ConnectionOpenedAsync(ConnectionEventData eventData, CancellationToken cancellationToken) + { + OpenedAsyncCounter++; + return base.ConnectionOpenedAsync(eventData, cancellationToken); + } + + public override void ConnectionOpeningFailed(ConnectionErrorEventData eventData) + { + OpeningFailedCounter++; + } + + public override Task ConnectionOpeningFailedAsync(ConnectionErrorEventData eventData, CancellationToken cancellationToken) + { + OpeningFailedAsyncCounter++; + return base.ConnectionOpeningFailedAsync(eventData, cancellationToken); + } + } + + public static class StaticCounter + { + public static int OpeningReached; + public static int OpenedReached; + } +} namespace Xtensive.Orm.Tests.Sql { @@ -95,6 +166,187 @@ public void SqlServerConnectionCheckTest() Assert.That(GetCheckConnectionIsAliveFlag(driver), Is.False); } + [Test] + public void ConnectionAccessorTest() + { + var accessorInstance = new TestConnectionAccessor(); + var accessorsArray = new[] { accessorInstance }; + var descriptor = ProviderDescriptor.Get(provider); + var factory = (SqlDriverFactory) Activator.CreateInstance(descriptor.DriverFactory); + + Assert.That(accessorInstance.OpeningCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningAsyncCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningInitAsyncCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpenedCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpenedAsyncCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningFailedCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningFailedAsyncCounter, Is.EqualTo(0)); + + var configuration = new SqlDriverConfiguration(accessorsArray); + _ = factory.GetDriver(new ConnectionInfo(Url), configuration); + Assert.That(accessorInstance.OpeningCounter, Is.EqualTo(1)); + Assert.That(accessorInstance.OpeningAsyncCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningInitAsyncCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpenedCounter, Is.EqualTo(1)); + Assert.That(accessorInstance.OpenedAsyncCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningFailedCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningFailedAsyncCounter, Is.EqualTo(0)); + + configuration = new SqlDriverConfiguration(accessorsArray) { EnsureConnectionIsAlive = true }; + _ = factory.GetDriver(new ConnectionInfo(Url), configuration); + Assert.That(accessorInstance.OpeningCounter, Is.EqualTo(2)); + Assert.That(accessorInstance.OpeningAsyncCounter, Is.EqualTo(0)); + + if (provider == WellKnown.Provider.SqlServer) { + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(1)); + Assert.That(accessorInstance.OpeningInitAsyncCounter, Is.EqualTo(0)); + } + else { + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningInitAsyncCounter, Is.EqualTo(0)); + } + + Assert.That(accessorInstance.OpenedCounter, Is.EqualTo(2)); + Assert.That(accessorInstance.OpenedAsyncCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningFailedCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningFailedAsyncCounter, Is.EqualTo(0)); + + configuration = new SqlDriverConfiguration(accessorsArray) { ConnectionInitializationSql = InitQueryPerProvider(provider) }; + _ = factory.GetDriver(new ConnectionInfo(Url), configuration); + Assert.That(accessorInstance.OpeningCounter, Is.EqualTo(3)); + Assert.That(accessorInstance.OpeningAsyncCounter, Is.EqualTo(0)); + + if (provider == WellKnown.Provider.SqlServer) { + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(2)); + Assert.That(accessorInstance.OpeningInitAsyncCounter, Is.EqualTo(0)); + } + else { + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(1)); + Assert.That(accessorInstance.OpeningInitAsyncCounter, Is.EqualTo(0)); + } + + Assert.That(accessorInstance.OpenedCounter, Is.EqualTo(3)); + Assert.That(accessorInstance.OpenedAsyncCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningFailedCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningFailedAsyncCounter, Is.EqualTo(0)); + + configuration = new SqlDriverConfiguration(accessorsArray) { ConnectionInitializationSql = "dummy string to trigger error" }; + try { + _ = factory.GetDriver(new ConnectionInfo(Url), configuration); + } + catch { + //skip it + } + Assert.That(accessorInstance.OpeningCounter, Is.EqualTo(4)); + Assert.That(accessorInstance.OpeningAsyncCounter, Is.EqualTo(0)); + + if (provider == WellKnown.Provider.SqlServer) { + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(3)); + Assert.That(accessorInstance.OpeningInitAsyncCounter, Is.EqualTo(0)); + } + else { + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(2)); + Assert.That(accessorInstance.OpeningInitAsyncCounter, Is.EqualTo(0)); + } + + Assert.That(accessorInstance.OpenedCounter, Is.EqualTo(3)); + Assert.That(accessorInstance.OpenedAsyncCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningFailedCounter, Is.EqualTo(1)); + Assert.That(accessorInstance.OpeningFailedAsyncCounter, Is.EqualTo(0)); + } + + [Test] + public async Task ConnectionAccessorAsyncTest() + { + var accessorInstance = new TestConnectionAccessor(); + var accessorsArray = new[] { accessorInstance }; + var descriptor = ProviderDescriptor.Get(provider); + var factory = (SqlDriverFactory) Activator.CreateInstance(descriptor.DriverFactory); + + Assert.That(accessorInstance.OpeningCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningAsyncCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningInitAsyncCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpenedCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpenedAsyncCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningFailedCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningFailedAsyncCounter, Is.EqualTo(0)); + + var configuration = new SqlDriverConfiguration(accessorsArray); + _ = await factory.GetDriverAsync(new ConnectionInfo(Url), configuration, CancellationToken.None); + Assert.That(accessorInstance.OpeningCounter, Is.EqualTo(1)); + Assert.That(accessorInstance.OpeningAsyncCounter, Is.EqualTo(1)); + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningInitAsyncCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpenedCounter, Is.EqualTo(1)); + Assert.That(accessorInstance.OpenedAsyncCounter, Is.EqualTo(1)); + Assert.That(accessorInstance.OpeningFailedCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningFailedAsyncCounter, Is.EqualTo(0)); + + configuration = new SqlDriverConfiguration(accessorsArray) { EnsureConnectionIsAlive = true }; + _ = await factory.GetDriverAsync(new ConnectionInfo(Url), configuration, CancellationToken.None); + Assert.That(accessorInstance.OpeningCounter, Is.EqualTo(2)); + Assert.That(accessorInstance.OpeningAsyncCounter, Is.EqualTo(2)); + + if (provider == WellKnown.Provider.SqlServer) { + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(1)); + Assert.That(accessorInstance.OpeningInitAsyncCounter, Is.EqualTo(1)); + } + else { + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningInitAsyncCounter, Is.EqualTo(0)); + } + + Assert.That(accessorInstance.OpenedCounter, Is.EqualTo(2)); + Assert.That(accessorInstance.OpenedAsyncCounter, Is.EqualTo(2)); + Assert.That(accessorInstance.OpeningFailedCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningFailedAsyncCounter, Is.EqualTo(0)); + + configuration = new SqlDriverConfiguration(accessorsArray) { ConnectionInitializationSql = InitQueryPerProvider(provider) }; + _ = await factory.GetDriverAsync(new ConnectionInfo(Url), configuration, CancellationToken.None); + Assert.That(accessorInstance.OpeningCounter, Is.EqualTo(3)); + Assert.That(accessorInstance.OpeningAsyncCounter, Is.EqualTo(3)); + if (provider == WellKnown.Provider.SqlServer) { + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(2)); + Assert.That(accessorInstance.OpeningInitAsyncCounter, Is.EqualTo(2)); + } + else { + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(1)); + Assert.That(accessorInstance.OpeningInitAsyncCounter, Is.EqualTo(1)); + } + + Assert.That(accessorInstance.OpenedCounter, Is.EqualTo(3)); + Assert.That(accessorInstance.OpenedAsyncCounter, Is.EqualTo(3)); + Assert.That(accessorInstance.OpeningFailedCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningFailedAsyncCounter, Is.EqualTo(0)); + + configuration = new SqlDriverConfiguration(accessorsArray) { ConnectionInitializationSql = "dummy string to trigger error" }; + try { + _ = await factory.GetDriverAsync(new ConnectionInfo(Url), configuration, CancellationToken.None); + } + catch { + //skip it + } + + Assert.That(accessorInstance.OpeningCounter, Is.EqualTo(4)); + Assert.That(accessorInstance.OpeningAsyncCounter, Is.EqualTo(4)); + + if (provider == WellKnown.Provider.SqlServer) { + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(3)); + Assert.That(accessorInstance.OpeningInitAsyncCounter, Is.EqualTo(3)); + } + else { + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(2)); + Assert.That(accessorInstance.OpeningInitAsyncCounter, Is.EqualTo(2)); + } + + Assert.That(accessorInstance.OpenedCounter, Is.EqualTo(3)); + Assert.That(accessorInstance.OpenedAsyncCounter, Is.EqualTo(3)); + Assert.That(accessorInstance.OpeningFailedCounter, Is.EqualTo(1)); + Assert.That(accessorInstance.OpeningFailedAsyncCounter, Is.EqualTo(1)); + } private static void TestProvider(string providerName, string connectionString, string connectionUrl) { @@ -109,5 +361,18 @@ private static bool GetCheckConnectionIsAliveFlag(SqlDriver driver) return (bool) type.GetField(fieldName, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic) .GetValue(driver); } + + private static string InitQueryPerProvider(string currentProvider) + { + switch (currentProvider) { + case WellKnown.Provider.Firebird: return "select current_timestamp from RDB$DATABASE;"; + case WellKnown.Provider.MySql: return "SELECT 0"; + case WellKnown.Provider.Oracle: return "select current_timestamp from DUAL"; + case WellKnown.Provider.PostgreSql: return "SELECT 0"; + case WellKnown.Provider.SqlServer: return "SELECT 0"; + case WellKnown.Provider.Sqlite: return "SELECT 0"; + default: throw new ArgumentOutOfRangeException(currentProvider); + } + } } } \ No newline at end of file diff --git a/Orm/Xtensive.Orm.Tests/Storage/ConnectionAccessorTest.cs b/Orm/Xtensive.Orm.Tests/Storage/ConnectionAccessorTest.cs new file mode 100644 index 0000000000..e2f7cd34db --- /dev/null +++ b/Orm/Xtensive.Orm.Tests/Storage/ConnectionAccessorTest.cs @@ -0,0 +1,367 @@ +// Copyright (C) 2021 Xtensive LLC. +// This code is distributed under MIT license terms. +// See the License.txt file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using NUnit.Framework; +using Xtensive.Core; +using Xtensive.Orm.Providers; +using Xtensive.Sql; +using Xtensive.Orm.Tests.Storage.ConnectionAccessorsModel; +using System.Threading.Tasks; + +namespace Xtensive.Orm.Tests.Storage.ConnectionAccessorsModel +{ + public class MyConnectionAccessor : DbConnectionAccessor + { + private Guid instanceMarker; + + public readonly Guid UniqueInstanceIdentifier; + + public int ConnectionOpeningCounter; + public int ConnectionInitializationCounter; + public int ConnectionOpenedCounter; + public int ConnectionOpeningFailedCounter; + + public override void ConnectionOpening(ConnectionEventData eventData) + { + instanceMarker = UniqueInstanceIdentifier; + ConnectionOpeningCounter++; + } + + public override void ConnectionInitialization(ConnectionInitEventData eventData) + { + ConnectionInitializationCounter++; + if (instanceMarker != UniqueInstanceIdentifier) { + throw new Exception("Not the same instance"); + } + } + + public override void ConnectionOpened(ConnectionEventData eventData) + { + ConnectionOpenedCounter++; + if (instanceMarker != UniqueInstanceIdentifier) { + throw new Exception("Not the same instance"); + } + } + + public override void ConnectionOpeningFailed(ConnectionErrorEventData eventData) + { + ConnectionOpeningFailedCounter++; + if (instanceMarker != UniqueInstanceIdentifier) { + throw new Exception("Not the same instance"); + } + } + + public MyConnectionAccessor() + { + UniqueInstanceIdentifier = Guid.NewGuid(); + } + } + + public class NoDefaultConstructorAccessor : DbConnectionAccessor + { +#pragma warning disable IDE0060 // Remove unused parameter + public NoDefaultConstructorAccessor(int dummyParameter) +#pragma warning restore IDE0060 // Remove unused parameter + { + } + } + + public class NonPublicDefaultConstructorAccessor : DbConnectionAccessor + { + private NonPublicDefaultConstructorAccessor() + { + } + } + + #region Performance Test accessors + + public class PerfAccessor1 : DbConnectionAccessor { } + public class PerfAccessor2 : DbConnectionAccessor { } + public class PerfAccessor3 : DbConnectionAccessor { } + public class PerfAccessor4 : DbConnectionAccessor { } + public class PerfAccessor5 : DbConnectionAccessor { } + public class PerfAccessor6 : DbConnectionAccessor { } + public class PerfAccessor7 : DbConnectionAccessor { } + public class PerfAccessor8 : DbConnectionAccessor { } + public class PerfAccessor9 : DbConnectionAccessor { } + public class PerfAccessor10 : DbConnectionAccessor { } + public class PerfAccessor11 : DbConnectionAccessor { } + public class PerfAccessor12 : DbConnectionAccessor { } + public class PerfAccessor13 : DbConnectionAccessor { } + public class PerfAccessor14 : DbConnectionAccessor { } + public class PerfAccessor15 : DbConnectionAccessor { } + public class PerfAccessor16 : DbConnectionAccessor { } + public class PerfAccessor17 : DbConnectionAccessor { } + public class PerfAccessor18 : DbConnectionAccessor { } + public class PerfAccessor19 : DbConnectionAccessor { } + public class PerfAccessor20 : DbConnectionAccessor { } + public class PerfAccessor21 : DbConnectionAccessor { } + public class PerfAccessor22 : DbConnectionAccessor { } + public class PerfAccessor23 : DbConnectionAccessor { } + public class PerfAccessor24 : DbConnectionAccessor { } + public class PerfAccessor25 : DbConnectionAccessor { } + + #endregion + + public static class StaticCounter + { + public static int OpeningReached; + public static int OpenedReached; + } + + public class DummyEntity : Entity + { + [Field, Key] + public int Id { get; private set; } + + [Field] + public int Value { get; set; } + + public DummyEntity(Session session) + : base(session) + { + } + } +} + +namespace Xtensive.Orm.Tests.Storage +{ + [TestFixture] + public class ConnectionAccessorTest + { + [Test] + public void DomainRegistryTest() + { + var domainConfig = DomainConfigurationFactory.Create(); + domainConfig.Types.Register(typeof(DummyEntity)); + domainConfig.Types.Register(typeof(MyConnectionAccessor)); + + Assert.That(domainConfig.Types.DbConnectionAccessors.Count(), Is.EqualTo(1)); + } + + [Test] + public void NoDefaultConstructorTest() + { + var domainConfig = DomainConfigurationFactory.Create(); + domainConfig.UpgradeMode = DomainUpgradeMode.Recreate; + domainConfig.Types.Register(typeof(DummyEntity)); + domainConfig.Types.Register(typeof(NoDefaultConstructorAccessor)); + + Domain domain = null; + _ = Assert.Throws(() => domain = Domain.Build(domainConfig)); + domain.DisposeSafely(); + } + + [Test] + public void NoDefaultConstructorAsyncTest() + { + var domainConfig = DomainConfigurationFactory.Create(); + domainConfig.UpgradeMode = DomainUpgradeMode.Recreate; + domainConfig.Types.Register(typeof(DummyEntity)); + domainConfig.Types.Register(typeof(NoDefaultConstructorAccessor)); + + Domain domain = null; + _ = Assert.ThrowsAsync(async () => domain = await Domain.BuildAsync(domainConfig)); + domain.DisposeSafely(); + } + + [Test] + public void NonPublicDefaultConstructorTest() + { + var domainConfig = DomainConfigurationFactory.Create(); + domainConfig.UpgradeMode = DomainUpgradeMode.Recreate; + domainConfig.Types.Register(typeof(DummyEntity)); + domainConfig.Types.Register(typeof(NonPublicDefaultConstructorAccessor)); + + using var domain = Domain.Build(domainConfig); + } + + [Test] + public async Task NonPublicDefaultConstructorAsyncTest() + { + var domainConfig = DomainConfigurationFactory.Create(); + domainConfig.UpgradeMode = DomainUpgradeMode.Recreate; + domainConfig.Types.Register(typeof(DummyEntity)); + domainConfig.Types.Register(typeof(NonPublicDefaultConstructorAccessor)); + + await using var domain = await Domain.BuildAsync(domainConfig); + } + + [Test] + public void SessionConnectionAccessorsTest() + { + var domainConfig = DomainConfigurationFactory.Create(); + domainConfig.UpgradeMode = DomainUpgradeMode.Recreate; + domainConfig.Types.Register(typeof(DummyEntity)); + domainConfig.Types.Register(typeof(MyConnectionAccessor)); + + Guid? first = null; + using (var domain = Domain.Build(domainConfig)) + using (var session = domain.OpenSession()) { + var nativeHandler = (SqlSessionHandler) session.Handler; + var extension = nativeHandler.Connection.Extensions.Get(); + var accessorInstance = (MyConnectionAccessor)extension.Accessors.First(); + Assert.That(accessorInstance.ConnectionOpeningCounter, Is.Not.EqualTo(0)); + Assert.That(accessorInstance.ConnectionOpenedCounter, Is.Not.EqualTo(0)); + first = accessorInstance.UniqueInstanceIdentifier; + } + + Guid? second = null; + using (var domain = Domain.Build(domainConfig)) + using (var session = domain.OpenSession()) { + var nativeHandler = (SqlSessionHandler) session.Handler; + var extension = nativeHandler.Connection.Extensions.Get(); + var accessorInstance = (MyConnectionAccessor) extension.Accessors.First(); + Assert.That(accessorInstance.ConnectionOpeningCounter, Is.Not.EqualTo(0)); + Assert.That(accessorInstance.ConnectionOpenedCounter, Is.Not.EqualTo(0)); + second = accessorInstance.UniqueInstanceIdentifier; + } + + Assert.That(first != null && second != null && first != second, Is.True); + } + + [Test] + public async Task SessionConnectionAccessorsAsyncTest() + { + var domainConfig = DomainConfigurationFactory.Create(); + domainConfig.UpgradeMode = DomainUpgradeMode.Recreate; + domainConfig.Types.Register(typeof(DummyEntity)); + domainConfig.Types.Register(typeof(MyConnectionAccessor)); + + Guid? first = null; + await using (var domain = await Domain.BuildAsync(domainConfig)) + await using (var session = await domain.OpenSessionAsync()) { + var nativeHandler = (SqlSessionHandler) session.Handler; + var extension = nativeHandler.Connection.Extensions.Get(); + var accessorInstance = (MyConnectionAccessor) extension.Accessors.First(); + Assert.That(accessorInstance.ConnectionOpeningCounter, Is.Not.EqualTo(0)); + Assert.That(accessorInstance.ConnectionOpenedCounter, Is.Not.EqualTo(0)); + first = accessorInstance.UniqueInstanceIdentifier; + } + + Guid? second = null; + await using (var domain = await Domain.BuildAsync(domainConfig)) + await using (var session = await domain.OpenSessionAsync()) { + var nativeHandler = (SqlSessionHandler) session.Handler; + var extension = nativeHandler.Connection.Extensions.Get(); + var accessorInstance = (MyConnectionAccessor) extension.Accessors.First(); + Assert.That(accessorInstance.ConnectionOpeningCounter, Is.Not.EqualTo(0)); + Assert.That(accessorInstance.ConnectionOpenedCounter, Is.Not.EqualTo(0)); + second = accessorInstance.UniqueInstanceIdentifier; + } + + Assert.That(first != null && second != null && first != second, Is.True); + } + + [Test] + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + public void ConnectionExtensionExistanceTest(int includeHandlersCount) + { + var domainConfig = DomainConfigurationFactory.Create(); + domainConfig.UpgradeMode = DomainUpgradeMode.Recreate; + + foreach (var accessor in GetAccessors(includeHandlersCount)) { + domainConfig.Types.Register(accessor); + } + + using (var domain = Domain.Build(domainConfig)) + using (var session = domain.OpenSession()) { + var nativeHandler = (SqlSessionHandler) session.Handler; + var extensions = nativeHandler.Connection.Extensions; + if (includeHandlersCount > 0) { + Assert.That(extensions.Count, Is.EqualTo(1)); + var extension = extensions.Get(); + Assert.That(extension, Is.Not.Null); + Assert.That(extension.Accessors.Count, Is.EqualTo(includeHandlersCount)); + } + else { + Assert.That(extensions.Count, Is.EqualTo(0)); + } + } + } + + [Test] + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + public async Task ConnectionExtensionExistanceAsyncTest(int amoundOtAccessors) + { + var domainConfig = DomainConfigurationFactory.Create(); + domainConfig.UpgradeMode = DomainUpgradeMode.Recreate; + + foreach (var accessor in GetAccessors(amoundOtAccessors)) { + domainConfig.Types.Register(accessor); + } + + await using (var domain = await Domain.BuildAsync(domainConfig)) + await using (var session = await domain.OpenSessionAsync()) { + var nativeHandler = (SqlSessionHandler) session.Handler; + var extensions = nativeHandler.Connection.Extensions; + if (amoundOtAccessors > 0) { + Assert.That(extensions.Count, Is.EqualTo(1)); + var extension = extensions.Get(); + Assert.That(extension, Is.Not.Null); + Assert.That(extension.Accessors.Count, Is.EqualTo(amoundOtAccessors)); + } + else { + Assert.That(extensions.Count, Is.EqualTo(0)); + } + } + } + + [Explicit] + [TestCase(0)] + [TestCase(5)] + [TestCase(10)] + [TestCase(15)] + [TestCase(20)] + [TestCase(25)] + public void SessionOpeningPerformanceTest(int amountOfAccessors) + { + var domainConfig = DomainConfigurationFactory.Create(); + domainConfig.UpgradeMode = DomainUpgradeMode.Recreate; + + foreach (var accessor in GetAccessors(amountOfAccessors)) { + domainConfig.Types.Register(accessor); + } + + var watch = new Stopwatch(); + using (var domain = Domain.Build(domainConfig)) { + watch.Start(); + for (var i = 0; i < 1000000; i++) { + domain.OpenSession().Dispose(); + } + watch.Stop(); + } + Console.WriteLine(watch.ElapsedTicks / 1000000); + } + + private IEnumerable GetAccessors(int neededCount) + { + if (neededCount > 25) { + throw new Exception(); + } + + var all = new Type[] { + typeof(PerfAccessor1), typeof(PerfAccessor2), typeof(PerfAccessor3), typeof(PerfAccessor4), + typeof(PerfAccessor5), typeof(PerfAccessor6), typeof(PerfAccessor7), typeof(PerfAccessor8), + typeof(PerfAccessor9), typeof(PerfAccessor10), typeof(PerfAccessor11), typeof(PerfAccessor12), + typeof(PerfAccessor13), typeof(PerfAccessor14), typeof(PerfAccessor15), typeof(PerfAccessor16), + typeof(PerfAccessor17), typeof(PerfAccessor18), typeof(PerfAccessor19), typeof(PerfAccessor20), + typeof(PerfAccessor21), typeof(PerfAccessor22), typeof(PerfAccessor23), typeof(PerfAccessor24), + typeof(PerfAccessor25) + }; + for (var i = 0; i < neededCount; i++) { + yield return all[i]; + } + } + } +} diff --git a/Orm/Xtensive.Orm/Orm/Configuration/DomainTypeRegistry.cs b/Orm/Xtensive.Orm/Orm/Configuration/DomainTypeRegistry.cs index 247fb20b55..2287ae88db 100644 --- a/Orm/Xtensive.Orm/Orm/Configuration/DomainTypeRegistry.cs +++ b/Orm/Xtensive.Orm/Orm/Configuration/DomainTypeRegistry.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2010-2020 Xtensive LLC. +// Copyright (C) 2010-2021 Xtensive LLC. // This code is distributed under MIT license terms. // See the License.txt file in the project root for more information. // Created by: Alex Yakunin @@ -26,7 +26,11 @@ public class DomainTypeRegistry : TypeRegistry private readonly static Type iModuleType = typeof(IModule); private readonly static Type iUpgradeHandlerType = typeof(IUpgradeHandler); private readonly static Type keyGeneratorType = typeof(KeyGenerator); - private static readonly Type ifulltextCatalogNameBuilder = typeof(IFullTextCatalogNameBuilder); + private readonly static Type ifulltextCatalogNameBuilder = typeof(IFullTextCatalogNameBuilder); + private readonly static Type iDbConnectionAccessorType = typeof(IDbConnectionAccessor); + + private Type[] connectionAccessors; + /// /// Gets all the registered persistent types. @@ -72,6 +76,27 @@ public class DomainTypeRegistry : TypeRegistry /// public IEnumerable FullTextCatalogResolvers => this.Where(IsFullTextCatalogNameBuilder); + /// + /// Gets all the registered implementations. + /// + public IEnumerable DbConnectionAccessors + { + get { + // a lot of access to this property. better to have items cached; + if (IsLocked) { + if(connectionAccessors == null) { + var container = new List(10);// not so many accessors expected + foreach (var type in this.Where(IsConnectionAccessor)) + container.Add(type); + connectionAccessors = container.Count == 0 ? Array.Empty() : container.ToArray(); + } + return connectionAccessors; + } + // if instance is not locked then there is a chance of new accessors appeared + return this.Where(IsConnectionAccessor); + } + } + #region IsXxx method group /// @@ -89,7 +114,8 @@ public static bool IsInterestingType(Type type) => IsUpgradeHandler(type) || IsKeyGenerator(type) || IsCompilerContainer(type) || - IsFullTextCatalogNameBuilder(type); + IsFullTextCatalogNameBuilder(type) || + IsConnectionAccessor(type); /// /// Determines whether a @@ -205,6 +231,21 @@ public static bool IsFullTextCatalogNameBuilder(Type type) return ifulltextCatalogNameBuilder.IsAssignableFrom(type) && ifulltextCatalogNameBuilder != type; } + /// + /// Determines whether the is + /// a connection accessor. + /// + /// The type to check. + /// Check result. + public static bool IsConnectionAccessor(Type type) + { + if (type.IsAbstract) { + return false; + } + + return iDbConnectionAccessorType.IsAssignableFrom(type) && iDbConnectionAccessorType != type; + } + #endregion #region ICloneable members diff --git a/Orm/Xtensive.Orm/Orm/ConnectionErrorEventData.cs b/Orm/Xtensive.Orm/Orm/ConnectionErrorEventData.cs new file mode 100644 index 0000000000..1e8ff71210 --- /dev/null +++ b/Orm/Xtensive.Orm/Orm/ConnectionErrorEventData.cs @@ -0,0 +1,28 @@ +// Copyright (C) 2021 Xtensive LLC. +// This code is distributed under MIT license terms. +// See the License.txt file in the project root for more information. + +using System; +using System.Data.Common; +using Xtensive.Core; + +namespace Xtensive.Orm +{ + /// + /// Extended with error happend during connection opening, restoration or initialization. + /// + public class ConnectionErrorEventData : ConnectionEventData + { + /// + /// The exception appeared. + /// + public Exception Exception { get; } + + public ConnectionErrorEventData(Exception exception, DbConnection connection, bool reconnect = false) + : base(connection, reconnect) + { + ArgumentValidator.EnsureArgumentNotNull(exception, nameof(exception)); + Exception = exception; + } + } +} diff --git a/Orm/Xtensive.Orm/Orm/ConnectionEventData.cs b/Orm/Xtensive.Orm/Orm/ConnectionEventData.cs new file mode 100644 index 0000000000..3230ca6867 --- /dev/null +++ b/Orm/Xtensive.Orm/Orm/ConnectionEventData.cs @@ -0,0 +1,32 @@ +// Copyright (C) 2021 Xtensive LLC. +// This code is distributed under MIT license terms. +// See the License.txt file in the project root for more information. + +using System.Data.Common; +using Xtensive.Core; + +namespace Xtensive.Orm +{ + /// + /// Contains general data for methods. + /// + public class ConnectionEventData + { + /// + /// The connection for which event triggered. + /// + public DbConnection Connection { get; } + + /// + /// Indicates whether event happened during an attempt to restore connection. + /// + public bool Reconnect { get; } + + public ConnectionEventData(DbConnection connection, bool reconnect = false) + { + ArgumentValidator.EnsureArgumentNotNull(connection, nameof(connection)); + Connection = connection; + Reconnect = reconnect; + } + } +} diff --git a/Orm/Xtensive.Orm/Orm/ConnectionInitEventData.cs b/Orm/Xtensive.Orm/Orm/ConnectionInitEventData.cs new file mode 100644 index 0000000000..6825bbaa38 --- /dev/null +++ b/Orm/Xtensive.Orm/Orm/ConnectionInitEventData.cs @@ -0,0 +1,27 @@ +// Copyright (C) 2021 Xtensive LLC. +// This code is distributed under MIT license terms. +// See the License.txt file in the project root for more information. + +using System.Data.Common; +using Xtensive.Core; + +namespace Xtensive.Orm +{ + /// + /// Extended with connection initialization script + /// + public class ConnectionInitEventData : ConnectionEventData + { + /// + /// Gets the script which will be used for connection initializatin + /// + public string InitializationScript { get; } + + public ConnectionInitEventData(string initializationScript, DbConnection connection, bool reconnect = false) + : base(connection, reconnect) + { + ArgumentValidator.EnsureArgumentNotNullOrEmpty(initializationScript, nameof(initializationScript)); + InitializationScript = initializationScript; + } + } +} diff --git a/Orm/Xtensive.Orm/Orm/Interfaces/DbConnectionAccessor.cs b/Orm/Xtensive.Orm/Orm/Interfaces/DbConnectionAccessor.cs new file mode 100644 index 0000000000..9c306daf49 --- /dev/null +++ b/Orm/Xtensive.Orm/Orm/Interfaces/DbConnectionAccessor.cs @@ -0,0 +1,63 @@ +// Copyright (C) 2021 Xtensive LLC. +// This code is distributed under MIT license terms. +// See the License.txt file in the project root for more information. + +using System.Threading; +using System.Threading.Tasks; + +namespace Xtensive.Orm +{ + /// + /// Base type for native database connection accessors to be inherited from. + /// + public abstract class DbConnectionAccessor : IDbConnectionAccessor + { + /// + public virtual void ConnectionOpening(ConnectionEventData eventData) + { + } + + /// + public virtual Task ConnectionOpeningAsync(ConnectionEventData eventData, CancellationToken cancellationToken) + { + ConnectionOpening(eventData); + return Task.CompletedTask; + } + + /// + public virtual void ConnectionInitialization(ConnectionInitEventData eventData) + { + } + + /// + public virtual Task ConnectionInitializationAsync(ConnectionInitEventData eventData, CancellationToken cancellationToken) + { + ConnectionInitialization(eventData); + return Task.CompletedTask; + } + + /// + public virtual void ConnectionOpened(ConnectionEventData eventData) + { + } + + /// + public virtual Task ConnectionOpenedAsync(ConnectionEventData eventData, CancellationToken cancellationToken) + { + ConnectionOpened(eventData); + return Task.CompletedTask; + } + + /// + public virtual void ConnectionOpeningFailed(ConnectionErrorEventData eventData) + { + } + + /// + public virtual Task ConnectionOpeningFailedAsync(ConnectionErrorEventData eventData, CancellationToken cancellationToken) + { + ConnectionOpeningFailed(eventData); + return Task.CompletedTask; + } + } +} diff --git a/Orm/Xtensive.Orm/Orm/Interfaces/IDbConnectionAccessor.cs b/Orm/Xtensive.Orm/Orm/Interfaces/IDbConnectionAccessor.cs new file mode 100644 index 0000000000..6a7231aa87 --- /dev/null +++ b/Orm/Xtensive.Orm/Orm/Interfaces/IDbConnectionAccessor.cs @@ -0,0 +1,73 @@ +// Copyright (C) 2021 Xtensive LLC. +// This code is distributed under MIT license terms. +// See the License.txt file in the project root for more information. + +using System.Threading; +using System.Threading.Tasks; + +namespace Xtensive.Orm +{ + /// + /// Offers event-like methods to access native database connection on different stages. + /// + public interface IDbConnectionAccessor + { + /// + /// Executes before connection opening. + /// + /// Information connected with this event. + void ConnectionOpening(ConnectionEventData eventData); + + /// + /// Executes before connection opening. + /// + /// Information connected with this event. + /// Cancellation token. + /// Task performing operation. + Task ConnectionOpeningAsync(ConnectionEventData eventData, CancellationToken cancellationToken); + + /// + /// Executes when connection is already opened but initialization script + /// hasn't been executed yet. + /// + /// Information connected with this event. + void ConnectionInitialization(ConnectionInitEventData eventData); + + /// + /// Executes when connection is already opened but initialization script + /// hasn't been executed yet. + /// + /// Information connected with this event. + /// Cancellation token. + /// Task performing operation. + Task ConnectionInitializationAsync(ConnectionInitEventData eventData, CancellationToken cancellationToken); + + /// + /// Executes when connection is successfully opened and initialized. + /// + /// Information connected with this event. + void ConnectionOpened(ConnectionEventData eventData); + + /// + /// Executes when connection is successfully opened and initialized. + /// + /// Information connected with this event. + /// Cancellation token. + /// Task performing operation. + Task ConnectionOpenedAsync(ConnectionEventData eventData, CancellationToken cancellationToken); + + /// + /// Executes if an error appeared on either connection opening or connection initialization. + /// + /// Information connected with this event. + void ConnectionOpeningFailed(ConnectionErrorEventData eventData); + + /// + /// Executes if an error appeared on either connection opening or connection initialization. + /// + /// Information connected with this event. + /// Cancellation token. + /// Task performing operation. + Task ConnectionOpeningFailedAsync(ConnectionErrorEventData eventData, CancellationToken cancellationToken); + } +} diff --git a/Orm/Xtensive.Orm/Orm/Providers/StorageDriver.Operations.cs b/Orm/Xtensive.Orm/Orm/Providers/StorageDriver.Operations.cs index 3d6eecce26..f57b21d3fa 100644 --- a/Orm/Xtensive.Orm/Orm/Providers/StorageDriver.Operations.cs +++ b/Orm/Xtensive.Orm/Orm/Providers/StorageDriver.Operations.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2009-2020 Xtensive LLC. +// Copyright (C) 2009-2021 Xtensive LLC. // This code is distributed under MIT license terms. // See the License.txt file in the project root for more information. // Created by: Denis Krjuchkov @@ -14,7 +14,7 @@ namespace Xtensive.Orm.Providers { - partial class StorageDriver + public partial class StorageDriver { private sealed class InitializationSqlExtension { @@ -51,6 +51,11 @@ public SqlConnection CreateConnection(Session session) throw ExceptionBuilder.BuildException(exception); } + if (connectionAccessorFactories != null) { + connection.AssignConnectionAccessors( + CreateConnectionAccessorsFast(configuration.Types.DbConnectionAccessors)); + } + var sessionConfiguration = GetConfiguration(session); connection.CommandTimeout = sessionConfiguration.DefaultCommandTimeout; var connectionInfo = GetConnectionInfo(session) ?? sessionConfiguration.ConnectionInfo; @@ -71,12 +76,11 @@ public void OpenConnection(Session session, SqlConnection connection) SqlLog.Info(Strings.LogSessionXOpeningConnectionY, session.ToStringSafely(), connection.ConnectionInfo); } - var extension = connection.Extensions.Get(); + var script = connection.Extensions.Get()?.Script; - var script = extension?.Script; try { if (!string.IsNullOrEmpty(script)) { - connection.OpenAndInitialize(extension.Script); + connection.OpenAndInitialize(script); } else { connection.Open(); @@ -97,11 +101,11 @@ public async Task OpenConnectionAsync(Session session, SqlConnection connection, SqlLog.Info(Strings.LogSessionXOpeningConnectionY, session.ToStringSafely(), connection.ConnectionInfo); } - var extension = connection.Extensions.Get(); + var script = connection.Extensions.Get()?.Script; try { - if (!string.IsNullOrEmpty(extension?.Script)) { - await connection.OpenAndInitializeAsync(extension.Script, cancellationToken).ConfigureAwait(false); + if (!string.IsNullOrEmpty(script)) { + await connection.OpenAndInitializeAsync(script, cancellationToken).ConfigureAwait(false); } else { await connection.OpenAsync(cancellationToken).ConfigureAwait(false); diff --git a/Orm/Xtensive.Orm/Orm/Providers/StorageDriver.cs b/Orm/Xtensive.Orm/Orm/Providers/StorageDriver.cs index 57f4d10ff1..31591b4983 100644 --- a/Orm/Xtensive.Orm/Orm/Providers/StorageDriver.cs +++ b/Orm/Xtensive.Orm/Orm/Providers/StorageDriver.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2009-2020 Xtensive LLC. +// Copyright (C) 2009-2021 Xtensive LLC. // This code is distributed under MIT license terms. // See the License.txt file in the project root for more information. // Created by: Denis Krjuchkov @@ -7,9 +7,12 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; +using System.Reflection; using Xtensive.Core; +using Xtensive.Linq; using Xtensive.Orm.Logging; using Xtensive.Orm.Configuration; using Xtensive.Orm.Model; @@ -26,6 +29,9 @@ namespace Xtensive.Orm.Providers /// public sealed partial class StorageDriver { + private static readonly MethodInfo FactoryCreatorMethod = typeof(StorageDriver) + .GetMethod(nameof(CreateNewAccessor), BindingFlags.Static | BindingFlags.NonPublic); + private readonly DomainConfiguration configuration; private readonly SqlDriver underlyingDriver; private readonly SqlTranslator translator; @@ -33,6 +39,8 @@ public sealed partial class StorageDriver private readonly bool isLoggingEnabled; private readonly bool hasSavepoints; + private readonly IReadOnlyDictionary> connectionAccessorFactories; + public ProviderInfo ProviderInfo { get; private set; } public StorageExceptionBuilder ExceptionBuilder { get; private set; } @@ -97,7 +105,7 @@ public DbDataReaderAccessor GetDataReaderAccessor(TupleDescriptor descriptor) public StorageDriver CreateNew(Domain domain) { ArgumentValidator.EnsureArgumentNotNull(domain, "domain"); - return new StorageDriver(underlyingDriver, ProviderInfo, domain.Configuration, GetModelProvider(domain)); + return new StorageDriver(underlyingDriver, ProviderInfo, domain.Configuration, GetModelProvider(domain), connectionAccessorFactories); } private static DomainModel GetNullModel() @@ -151,6 +159,61 @@ private void FixExtractionResultSqlServerFamily(SqlExtractionResult result) } } + private IReadOnlyCollection CreateConnectionAccessorsFast(IEnumerable connectionAccessorTypes) + { + if (connectionAccessorFactories == null) + return Array.Empty(); + var instances = new List(connectionAccessorFactories.Count); + foreach (var type in connectionAccessorTypes) { + if (connectionAccessorFactories.TryGetValue(type, out var factory)) { + instances.Add(factory()); + } + } + return instances.ToArray(); + } + + private static IReadOnlyCollection CreateConnectionAccessors(IEnumerable connectionAccessorTypes, + out IReadOnlyDictionary> factories) + { + factories = null; + + List instances; + Dictionary> factoriesLocal; + + if(connectionAccessorTypes is IReadOnlyCollection asCollection) { + if (asCollection.Count == 0) + return Array.Empty(); + instances = new List(asCollection.Count); + factoriesLocal = new Dictionary>(asCollection.Count); + } + else { + if (connectionAccessorTypes.Any()) + return Array.Empty(); + instances = new List(); + factoriesLocal = new Dictionary>(); + } + + foreach (var type in connectionAccessorTypes) { + var ctor = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null); + if (ctor == null) { + throw new NotSupportedException(string.Format(Strings.ExConnectionAccessorXHasNoParameterlessConstructor, type)); + } + + var accessorFactory = (Func) FactoryCreatorMethod.MakeGenericMethod(type).Invoke(null, null); + instances.Add(accessorFactory()); + factoriesLocal[type] = accessorFactory; + } + factories = factoriesLocal; + return instances.ToArray(); + } + + private static Func CreateNewAccessor() where T : IDbConnectionAccessor + { + return FastExpression.Lambda>( + Expression.Convert(Expression.New(typeof(T)), typeof(IDbConnectionAccessor))) + .Compile(); + } + // Constructors public static StorageDriver Create(SqlDriverFactory driverFactory, DomainConfiguration configuration) @@ -158,7 +221,8 @@ public static StorageDriver Create(SqlDriverFactory driverFactory, DomainConfigu ArgumentValidator.EnsureArgumentNotNull(driverFactory, nameof(driverFactory)); ArgumentValidator.EnsureArgumentNotNull(configuration, nameof(configuration)); - var driverConfiguration = new SqlDriverConfiguration { + var accessors = CreateConnectionAccessors(configuration.Types.DbConnectionAccessors, out var factories); + var driverConfiguration = new SqlDriverConfiguration(accessors) { ForcedServerVersion = configuration.ForcedServerVersion, ConnectionInitializationSql = configuration.ConnectionInitializationSql, EnsureConnectionIsAlive = configuration.EnsureConnectionIsAlive, @@ -167,7 +231,7 @@ public static StorageDriver Create(SqlDriverFactory driverFactory, DomainConfigu var driver = driverFactory.GetDriver(configuration.ConnectionInfo, driverConfiguration); var providerInfo = ProviderInfoBuilder.Build(configuration.ConnectionInfo.Provider, driver); - return new StorageDriver(driver, providerInfo, configuration, GetNullModel); + return new StorageDriver(driver, providerInfo, configuration, GetNullModel, factories); } public static async Task CreateAsync( @@ -176,7 +240,8 @@ public static async Task CreateAsync( ArgumentValidator.EnsureArgumentNotNull(driverFactory, nameof(driverFactory)); ArgumentValidator.EnsureArgumentNotNull(configuration, nameof(configuration)); - var driverConfiguration = new SqlDriverConfiguration { + var accessors = CreateConnectionAccessors(configuration.Types.DbConnectionAccessors, out var factories); + var driverConfiguration = new SqlDriverConfiguration(accessors) { ForcedServerVersion = configuration.ForcedServerVersion, ConnectionInitializationSql = configuration.ConnectionInitializationSql, EnsureConnectionIsAlive = configuration.EnsureConnectionIsAlive, @@ -186,11 +251,14 @@ public static async Task CreateAsync( .ConfigureAwait(false); var providerInfo = ProviderInfoBuilder.Build(configuration.ConnectionInfo.Provider, driver); - return new StorageDriver(driver, providerInfo, configuration, GetNullModel); + return new StorageDriver(driver, providerInfo, configuration, GetNullModel, factories); } - private StorageDriver( - SqlDriver driver, ProviderInfo providerInfo, DomainConfiguration configuration, Func modelProvider) + private StorageDriver(SqlDriver driver, + ProviderInfo providerInfo, + DomainConfiguration configuration, + Func modelProvider, + IReadOnlyDictionary> factoryCache) { underlyingDriver = driver; ProviderInfo = providerInfo; @@ -201,6 +269,7 @@ private StorageDriver( hasSavepoints = underlyingDriver.ServerInfo.ServerFeatures.Supports(ServerFeatures.Savepoints); isLoggingEnabled = SqlLog.IsLogged(LogLevel.Info); // Just to cache this value ServerInfo = underlyingDriver.ServerInfo; + connectionAccessorFactories = factoryCache; } } } \ No newline at end of file diff --git a/Orm/Xtensive.Orm/Sql/DbConnectionAccessorExtension.cs b/Orm/Xtensive.Orm/Sql/DbConnectionAccessorExtension.cs new file mode 100644 index 0000000000..1cc9e25a91 --- /dev/null +++ b/Orm/Xtensive.Orm/Sql/DbConnectionAccessorExtension.cs @@ -0,0 +1,25 @@ +// Copyright (C) 2021 Xtensive LLC. +// This code is distributed under MIT license terms. +// See the License.txt file in the project root for more information. + +using System.Collections.Generic; +using Xtensive.Orm; + +namespace Xtensive.Sql +{ + /// + /// Wrapper to pass s to connection. + /// + public sealed class DbConnectionAccessorExtension + { + /// + /// Collection of instances. + /// + public IReadOnlyCollection Accessors { get; } + + internal DbConnectionAccessorExtension(IReadOnlyCollection connectionAccessors) + { + Accessors = connectionAccessors; + } + } +} diff --git a/Orm/Xtensive.Orm/Sql/SqlConnection.cs b/Orm/Xtensive.Orm/Sql/SqlConnection.cs index 8dab85a83d..b1f0c9b3b8 100644 --- a/Orm/Xtensive.Orm/Sql/SqlConnection.cs +++ b/Orm/Xtensive.Orm/Sql/SqlConnection.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2009-2020 Xtensive LLC. +// Copyright (C) 2009-2021 Xtensive LLC. // This code is distributed under MIT license terms. // See the License.txt file in the project root for more information. @@ -166,7 +166,22 @@ public virtual IBinaryLargeObject CreateBinaryLargeObject() => public virtual void Open() { EnsureIsNotDisposed(); - UnderlyingConnection.Open(); + var connectionAccessorEx = Extensions.Get(); + if (connectionAccessorEx == null) { + UnderlyingConnection.Open(); + } + else { + var accessors = connectionAccessorEx.Accessors; + SqlHelper.NotifyConnectionOpening(accessors, UnderlyingConnection); + try { + UnderlyingConnection.Open(); + SqlHelper.NotifyConnectionOpened(accessors, UnderlyingConnection); + } + catch (Exception ex) { + SqlHelper.NotifyConnectionOpeningFailed(accessors, UnderlyingConnection, ex); + throw; + } + } } /// @@ -176,14 +191,37 @@ public virtual void Open() public virtual void OpenAndInitialize(string initializationScript) { EnsureIsNotDisposed(); - UnderlyingConnection.Open(); - if (string.IsNullOrEmpty(initializationScript)) { - return; - } + var connectionAccessorEx = Extensions.Get(); + if (connectionAccessorEx == null) { + UnderlyingConnection.Open(); + if (string.IsNullOrEmpty(initializationScript)) { + return; + } - using var command = UnderlyingConnection.CreateCommand(); - command.CommandText = initializationScript; - command.ExecuteNonQuery(); + using var command = UnderlyingConnection.CreateCommand(); + command.CommandText = initializationScript; + _ = command.ExecuteNonQuery(); + } + else { + var accessors = connectionAccessorEx.Accessors; + SqlHelper.NotifyConnectionOpening(accessors, UnderlyingConnection); + try { + UnderlyingConnection.Open(); + if (string.IsNullOrEmpty(initializationScript)) { + SqlHelper.NotifyConnectionOpened(accessors, UnderlyingConnection); + return; + } + + SqlHelper.NotifyConnectionInitializing(accessors, UnderlyingConnection, initializationScript); + using var command = UnderlyingConnection.CreateCommand(); + command.CommandText = initializationScript; + _ = command.ExecuteNonQuery(); + } + catch (Exception ex) { + SqlHelper.NotifyConnectionOpeningFailed(accessors, UnderlyingConnection, ex); + throw; + } + } } /// @@ -193,11 +231,26 @@ public virtual void OpenAndInitialize(string initializationScript) /// to ensure that all asynchronous operations have completed. /// Token to control cancellation. /// Awaitable task. - public virtual Task OpenAsync(CancellationToken cancellationToken) + public virtual async Task OpenAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); EnsureIsNotDisposed(); - return UnderlyingConnection.OpenAsync(cancellationToken); + var connectionAccessorEx = Extensions.Get(); + if (connectionAccessorEx == null) { + await UnderlyingConnection.OpenAsync(cancellationToken).ConfigureAwait(false); + } + else { + var accessors = connectionAccessorEx.Accessors; + await SqlHelper.NotifyConnectionOpeningAsync(accessors, UnderlyingConnection, false, cancellationToken); + try { + await UnderlyingConnection.OpenAsync(cancellationToken); + await SqlHelper.NotifyConnectionOpenedAsync(accessors, UnderlyingConnection, false, cancellationToken); + } + catch (Exception ex) { + await SqlHelper.NotifyConnectionOpeningFailedAsync(accessors, UnderlyingConnection, ex, false, cancellationToken); + throw; + } + } } /// @@ -212,21 +265,52 @@ public virtual async Task OpenAndInitializeAsync(string initializationScript, Ca { token.ThrowIfCancellationRequested(); EnsureIsNotDisposed(); - await UnderlyingConnection.OpenAsync(token).ConfigureAwait(false); - if (string.IsNullOrEmpty(initializationScript)) { - return; - } + var connectionAccessorEx = Extensions.Get(); + if (connectionAccessorEx == null) { + await UnderlyingConnection.OpenAsync(token).ConfigureAwait(false); + if (string.IsNullOrEmpty(initializationScript)) { + return; + } - try { - var command = UnderlyingConnection.CreateCommand(); - await using (command.ConfigureAwait(false)) { - command.CommandText = initializationScript; - await command.ExecuteNonQueryAsync(token).ConfigureAwait(false); + try { + var command = UnderlyingConnection.CreateCommand(); + await using (command.ConfigureAwait(false)) { + command.CommandText = initializationScript; + _ = await command.ExecuteNonQueryAsync(token).ConfigureAwait(false); + } + } + catch (OperationCanceledException) { + await UnderlyingConnection.CloseAsync().ConfigureAwait(false); + throw; } } - catch (OperationCanceledException) { - await UnderlyingConnection.CloseAsync().ConfigureAwait(false); - throw; + else { + var accessors = connectionAccessorEx.Accessors; + await SqlHelper.NotifyConnectionOpeningAsync(accessors, UnderlyingConnection, false, token); + await UnderlyingConnection.OpenAsync(token).ConfigureAwait(false); + if (string.IsNullOrEmpty(initializationScript)) { + await SqlHelper.NotifyConnectionOpenedAsync(accessors, UnderlyingConnection, false, token); + return; + } + + try { + await SqlHelper.NotifyConnectionInitializingAsync(accessors, UnderlyingConnection, initializationScript, false, token); + var command = UnderlyingConnection.CreateCommand(); + await using (command.ConfigureAwait(false)) { + command.CommandText = initializationScript; + _ = await command.ExecuteNonQueryAsync(token).ConfigureAwait(false); + } + await SqlHelper.NotifyConnectionOpenedAsync(accessors, UnderlyingConnection, false, token); + } + catch (OperationCanceledException ex) { + await SqlHelper.NotifyConnectionOpeningFailedAsync(accessors, UnderlyingConnection, ex, false, token); + await UnderlyingConnection.CloseAsync().ConfigureAwait(false); + throw; + } + catch (Exception ex) { + await SqlHelper.NotifyConnectionOpeningFailedAsync(accessors, UnderlyingConnection, ex, false, token); + throw; + } } } diff --git a/Orm/Xtensive.Orm/Sql/SqlDriver.cs b/Orm/Xtensive.Orm/Sql/SqlDriver.cs index 8711470403..107319c921 100644 --- a/Orm/Xtensive.Orm/Sql/SqlDriver.cs +++ b/Orm/Xtensive.Orm/Sql/SqlDriver.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2009-2020 Xtensive LLC. +// Copyright (C) 2009-2021 Xtensive LLC. // This code is distributed under MIT license terms. // See the License.txt file in the project root for more information. diff --git a/Orm/Xtensive.Orm/Sql/SqlDriverConfiguration.cs b/Orm/Xtensive.Orm/Sql/SqlDriverConfiguration.cs index 9dcebec5af..7a7f0b5a9b 100644 --- a/Orm/Xtensive.Orm/Sql/SqlDriverConfiguration.cs +++ b/Orm/Xtensive.Orm/Sql/SqlDriverConfiguration.cs @@ -1,9 +1,14 @@ -// Copyright (C) 2003-2012 Xtensive LLC. -// All rights reserved. -// For conditions of distribution and use, see license. +// Copyright (C) 2012-2021 Xtensive LLC. +// This code is distributed under MIT license terms. +// See the License.txt file in the project root for more information. // Created by: Denis Krjuchkov // Created: 2012.12.27 +using System; +using System.Collections.Generic; +using Xtensive.Core; +using Xtensive.Orm; + namespace Xtensive.Sql { /// @@ -26,13 +31,23 @@ public sealed class SqlDriverConfiguration /// public bool EnsureConnectionIsAlive { get; set; } + /// + /// Gets connection accessors that should be notified about connection events. + /// + public IReadOnlyCollection DbConnectionAccessors { get; private set; } + /// /// Clones this instance. /// /// Clone of this instance. public SqlDriverConfiguration Clone() { - return new SqlDriverConfiguration { + // no deep cloning + var accessors = (DbConnectionAccessors.Count == 0) + ? Array.Empty() + : DbConnectionAccessors.ToArray(DbConnectionAccessors.Count); + + return new SqlDriverConfiguration(accessors) { ForcedServerVersion = ForcedServerVersion, ConnectionInitializationSql = ConnectionInitializationSql, EnsureConnectionIsAlive = EnsureConnectionIsAlive @@ -44,6 +59,15 @@ public SqlDriverConfiguration Clone() /// public SqlDriverConfiguration() { + DbConnectionAccessors = Array.Empty(); + } + + /// + /// Creates new instance of this type. + /// + public SqlDriverConfiguration(IReadOnlyCollection connectionAccessors) + { + DbConnectionAccessors = connectionAccessors; } } } \ No newline at end of file diff --git a/Orm/Xtensive.Orm/Sql/SqlExtensions.cs b/Orm/Xtensive.Orm/Sql/SqlExtensions.cs index 5d656e584f..bcf5516b96 100644 --- a/Orm/Xtensive.Orm/Sql/SqlExtensions.cs +++ b/Orm/Xtensive.Orm/Sql/SqlExtensions.cs @@ -1,10 +1,11 @@ -// Copyright (C) 2003-2010 Xtensive LLC. -// All rights reserved. -// For conditions of distribution and use, see license. +// Copyright (C) 2009-2021 Xtensive LLC. +// This code is distributed under MIT license terms. +// See the License.txt file in the project root for more information. // Created by: Denis Krjuchkov // Created: 2009.07.30 using System; +using System.Collections.Generic; using Xtensive.Core; using Xtensive.Orm; using Xtensive.Sql.Dml; @@ -49,5 +50,16 @@ public static string GetSchema(this UrlInfo url, string defaultValue) var result = resource.Substring(position + 1).TryCutSuffix(SchemaSeparatorString); return string.IsNullOrEmpty(result) ? defaultValue : result; } + + /// + /// Assigns connection accessors to so they will have access. + /// + /// The connection to assign accessors. + /// The accessors. + public static void AssignConnectionAccessors(this SqlConnection connection, + IReadOnlyCollection connectionAccessors) + { + connection.Extensions.Set(new DbConnectionAccessorExtension(connectionAccessors)); + } } } \ No newline at end of file diff --git a/Orm/Xtensive.Orm/Sql/SqlHelper.cs b/Orm/Xtensive.Orm/Sql/SqlHelper.cs index f27601527b..b3ce61a215 100644 --- a/Orm/Xtensive.Orm/Sql/SqlHelper.cs +++ b/Orm/Xtensive.Orm/Sql/SqlHelper.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2009-2020 Xtensive LLC. +// Copyright (C) 2009-2021 Xtensive LLC. // This code is distributed under MIT license terms. // See the License.txt file in the project root for more information. // Created by: Denis Krjuchkov @@ -8,14 +8,9 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; -using System.Data.SqlClient; -using System.Linq; using System.Runtime.CompilerServices; -using System.Text; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; -using Xtensive.Collections; using Xtensive.Core; using Xtensive.Orm; using Xtensive.Sql.Dml; @@ -406,6 +401,21 @@ public static void ExecuteInitializationSql(DbConnection connection, SqlDriverCo command.ExecuteNonQuery(); } + /// + /// Executes (if any). + /// + /// Connection to initialize. + /// Sql expression. + public static void ExecuteInitializationSql(DbConnection connection, string initializationSql) + { + if (string.IsNullOrEmpty(initializationSql)) { + return; + } + using var command = connection.CreateCommand(); + command.CommandText = initializationSql; + _ = command.ExecuteNonQuery(); + } + /// /// Executes (if any). /// @@ -428,6 +438,28 @@ public static async Task ExecuteInitializationSqlAsync( } } + /// + /// Executes (if any). + /// + /// Multiple active operations are not supported. Use + /// to ensure that all asynchronous operations have completed. + /// Connection to initialize. + /// Sql expression. + /// The token to cancel async operation if needed. + public static async Task ExecuteInitializationSqlAsync( + DbConnection connection, string initializationSql, CancellationToken token) + { + if (string.IsNullOrEmpty(initializationSql)) { + return; + } + + var command = connection.CreateCommand(); + await using (command.ConfigureAwait(false)) { + command.CommandText = initializationSql; + _ = await command.ExecuteNonQueryAsync(token).ConfigureAwait(false); + } + } + /// /// Reduces the isolation level to the most commonly supported ones. /// @@ -490,5 +522,159 @@ public static NotSupportedException NotSupported(ServerFeatures feature) { return NotSupported(feature.ToString()); } + + #region Notifications + + /// + /// Notifies all the that + /// is about to be opened. + /// + /// The accessors that should be notified. + /// The connection that is opening. + /// if event happened on attemp to restore connection, otherwise . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void NotifyConnectionOpening( + IEnumerable connectionAccessors, DbConnection connection, bool reconnect = false) + { + foreach (var accessor in connectionAccessors) { + accessor.ConnectionOpening(new ConnectionEventData(connection, reconnect)); + } + } + + /// + /// Notifies all the that + /// is about to be opened. + /// + /// The accessors that should be notified. + /// The connection that is opening. + /// if event happened on attemp to restore connection, otherwise . + /// Cancellation token. + /// Task performing operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task NotifyConnectionOpeningAsync( + IEnumerable connectionAccessors, DbConnection connection, bool reconnect = false, CancellationToken token = default) + { + foreach (var accessor in connectionAccessors) { + await accessor.ConnectionOpeningAsync( + new ConnectionEventData(connection, reconnect), token) + .ConfigureAwait(false); + } + } + + /// + /// Notifies all the that + /// opened connection is about to be initialized with . + /// + /// The accessors that should be notified. + /// Opened but not initialized connection + /// The script that will run to initialize connection + /// if event happened on attemp to restore connection, otherwise . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void NotifyConnectionInitializing( + IEnumerable connectionAccessors, DbConnection connection, string initializationScript, bool reconnect = false) + { + foreach (var accessor in connectionAccessors) { + accessor.ConnectionInitialization(new ConnectionInitEventData(initializationScript, connection, reconnect)); + } + } + + /// + /// Notifies all the that + /// opened connection is about to be initialized with . + /// + /// The accessors that should be notified. + /// Opened but not initialized connection + /// The script that will run to initialize connection + /// if event happened on attemp to restore connection, otherwise . + /// Cancellation token. + /// Task performing operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task NotifyConnectionInitializingAsync( + IEnumerable connectionAccessors, DbConnection connection, string initializationScript, + bool reconnect = false, CancellationToken token = default) + { + foreach (var accessor in connectionAccessors) { + await accessor.ConnectionInitializationAsync( + new ConnectionInitEventData(initializationScript, connection, reconnect), token) + .ConfigureAwait(false); + } + } + + /// + /// Notifies all the about + /// successful connection opening. + /// + /// The accessors that should be notified. + /// The connection that is completely opened and initialized. + /// if event happened on attemp to restore connection, otherwise . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void NotifyConnectionOpened( + IEnumerable connectionAccessors, DbConnection connection, bool reconnect = false) + { + foreach (var accessor in connectionAccessors) { + accessor.ConnectionOpened(new ConnectionEventData(connection, reconnect)); + } + } + + /// + /// Notifies all the about + /// successful connection opening. + /// + /// The accessors that should be notified. + /// The connection that is completely opened and initialized. + /// if event happened on attemp to restore connection, otherwise . + /// Cancellation token. + /// Task performing operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task NotifyConnectionOpenedAsync( + IEnumerable connectionAccessors, DbConnection connection, bool reconnect = false, CancellationToken token = default) + { + foreach (var accessor in connectionAccessors) { + await accessor.ConnectionOpenedAsync( + new ConnectionEventData(connection, reconnect), token) + .ConfigureAwait(false); + } + } + + /// + /// Notifies all the about + /// connection opening failure. + /// + /// The accessors that should be notified. + /// Connection that failed to be opened or properly initialized. + /// The exception which appeared. + /// if event happened on attemp to restore connection, otherwise . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void NotifyConnectionOpeningFailed( + IEnumerable connectionAccessors, DbConnection connection, Exception exception, bool reconnect = false) + { + foreach (var accessor in connectionAccessors) { + accessor.ConnectionOpeningFailed(new ConnectionErrorEventData(exception, connection, reconnect)); + } + } + + /// + /// Notifies all the about + /// connection opening failure. + /// + /// The accessors that should be notified. + /// Connection that failed to be opened or properly initialized. + /// The exception which appeared. + /// if event happened on attemp to restore connection, otherwise . + /// Cancellation token. + /// Task performing operation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static async Task NotifyConnectionOpeningFailedAsync( + IEnumerable connectionAccessors, DbConnection connection, Exception exception, + bool reconnect = false, CancellationToken token = default) + { + foreach (var accessor in connectionAccessors) { + await accessor.ConnectionOpeningFailedAsync( + new ConnectionErrorEventData(exception, connection, reconnect), token) + .ConfigureAwait(false); + } + } + + #endregion } } diff --git a/Orm/Xtensive.Orm/Strings.Designer.cs b/Orm/Xtensive.Orm/Strings.Designer.cs index 34c0659ebd..2644a4da82 100644 --- a/Orm/Xtensive.Orm/Strings.Designer.cs +++ b/Orm/Xtensive.Orm/Strings.Designer.cs @@ -1524,6 +1524,15 @@ internal static string ExConfigurationWithXNameAlreadyRegistered { } } + /// + /// Looks up a localized string similar to Connection accessor '{0}' has no parameterless constructor.. + /// + internal static string ExConnectionAccessorXHasNoParameterlessConstructor { + get { + return ResourceManager.GetString("ExConnectionAccessorXHasNoParameterlessConstructor", resourceCulture); + } + } + /// /// Looks up a localized string similar to ConnectionInfo is missing. If you are using configuration file you should specify either 'connectionUrl' element or 'connectionString' and 'provider' elements. /// diff --git a/Orm/Xtensive.Orm/Strings.resx b/Orm/Xtensive.Orm/Strings.resx index add3536034..e001aecdff 100644 --- a/Orm/Xtensive.Orm/Strings.resx +++ b/Orm/Xtensive.Orm/Strings.resx @@ -3470,4 +3470,7 @@ Error: {1} Can't modify Active or Disposed scope. - + + Connection accessor '{0}' has no parameterless constructor. + + \ No newline at end of file