diff --git a/ChangeLog/6.0.7_dev.txt b/ChangeLog/6.0.7_dev.txt index 2f5be49ea7..3c42620964 100644 --- a/ChangeLog/6.0.7_dev.txt +++ b/ChangeLog/6.0.7_dev.txt @@ -1,3 +1,4 @@ [main] Fixed issue of actual NULL constant being treated as a caching value [main] Fixed rare case of infinite loop on batching commands -[main] Improved VS compatibility by not processing design-time builds \ No newline at end of file +[main] Improved VS compatibility by not processing design-time builds +[main] Introduced IDbConnectionAccessor that gives access to certain stages of connection opening \ No newline at end of file diff --git a/Orm/Xtensive.Orm.Firebird/Sql.Drivers.Firebird/DriverFactory.cs b/Orm/Xtensive.Orm.Firebird/Sql.Drivers.Firebird/DriverFactory.cs index ebef162ec7..375e7bac1a 100644 --- a/Orm/Xtensive.Orm.Firebird/Sql.Drivers.Firebird/DriverFactory.cs +++ b/Orm/Xtensive.Orm.Firebird/Sql.Drivers.Firebird/DriverFactory.cs @@ -1,6 +1,6 @@ -// Copyright (C) 2003-2010 Xtensive LLC. -// All rights reserved. -// For conditions of distribution and use, see license. +// 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 // Created: 2011.01.08 @@ -34,24 +34,36 @@ public class DriverFactory : SqlDriverFactory protected override SqlDriver CreateDriver(string connectionString, SqlDriverConfiguration configuration) { using (var connection = new FbConnection(connectionString)) { - connection.Open(); - SqlHelper.ExecuteInitializationSql(connection, configuration); - var dataSource = new FbConnectionStringBuilder(connectionString).DataSource; + if (configuration.DbConnectionAccessors.Count > 0) + OpenConnectionWithNotification(connection, configuration); + else + OpenConnectionFast(connection, configuration); var defaultSchema = GetDefaultSchema(connection); - var coreServerInfo = new CoreServerInfo { - ServerVersion = GetVersionFromServerVersionString(connection.ServerVersion), - ConnectionString = connectionString, - MultipleActiveResultSets = true, - DatabaseName = defaultSchema.Database, - DefaultSchemaName = defaultSchema.Schema, - }; - - if (Int32.Parse(coreServerInfo.ServerVersion.Major.ToString() + coreServerInfo.ServerVersion.Minor.ToString()) < 25) - throw new NotSupportedException(Strings.ExFirebirdBelow25IsNotSupported); - if (coreServerInfo.ServerVersion.Major==2 && coreServerInfo.ServerVersion.Minor==5) - return new v2_5.Driver(coreServerInfo); - return null; + return CreateDriverInstance( + connectionString, GetVersionFromServerVersionString(connection.ServerVersion), defaultSchema); + } + } + + private static SqlDriver CreateDriverInstance( + string connectionString, Version version, DefaultSchemaInfo defaultSchema) + { + var coreServerInfo = new CoreServerInfo { + ServerVersion = version, + ConnectionString = connectionString, + MultipleActiveResultSets = true, + DatabaseName = defaultSchema.Database, + DefaultSchemaName = defaultSchema.Schema, + }; + + if (coreServerInfo.ServerVersion < new Version(2, 5)) { + throw new NotSupportedException(Strings.ExFirebirdBelow25IsNotSupported); } + + if (coreServerInfo.ServerVersion.Major == 2 && coreServerInfo.ServerVersion.Minor == 5) { + return new v2_5.Driver(coreServerInfo); + } + + return null; } /// @@ -94,6 +106,29 @@ protected override DefaultSchemaInfo ReadDefaultSchema(DbConnection connection, return SqlHelper.ReadDatabaseAndSchema(DatabaseAndSchemaQuery, connection, transaction); } + private void OpenConnectionFast(FbConnection connection, SqlDriverConfiguration configuration) + { + connection.Open(); + SqlHelper.ExecuteInitializationSql(connection, configuration); + } + + private void OpenConnectionWithNotification(FbConnection connection, SqlDriverConfiguration configuration) + { + var accessors = configuration.DbConnectionAccessors; + 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; + } + } + private 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 0ed434f692..a7adc6adcf 100644 --- a/Orm/Xtensive.Orm.MySql/Sql.Drivers.MySql/DriverFactory.cs +++ b/Orm/Xtensive.Orm.MySql/Sql.Drivers.MySql/DriverFactory.cs @@ -1,6 +1,6 @@ -// Copyright (C) 2003-2010 Xtensive LLC. -// All rights reserved. -// For conditions of distribution and use, see license. +// 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 // Created: 2011.02.25 @@ -66,8 +66,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); + else + OpenConnectionFast(connection, configuration); var versionString = string.IsNullOrEmpty(configuration.ForcedServerVersion) ? connection.ServerVersion : configuration.ForcedServerVersion; @@ -76,32 +78,60 @@ protected override SqlDriver CreateDriver(string connectionString, SqlDriverConf var builder = new MySqlConnectionStringBuilder(connectionString); var dataSource = string.Format(DataSourceFormat, builder.Server, builder.Port, builder.Database); var defaultSchema = GetDefaultSchema(connection); - var coreServerInfo = new CoreServerInfo { - ServerVersion = version, - ConnectionString = connectionString, - MultipleActiveResultSets = false, - DatabaseName = defaultSchema.Database, - DefaultSchemaName = defaultSchema.Schema, - }; - - if (version.Major < 5) - throw new NotSupportedException(Strings.ExMySqlBelow50IsNotSupported); - if (version.Major==5 && version.Minor==0) - return new v5_0.Driver(coreServerInfo); - if (version.Major==5 && version.Minor==1) - return new v5_1.Driver(coreServerInfo); - if (version.Major==5 && version.Minor==5) - return new v5_5.Driver(coreServerInfo); - if (version.Major==5 && version.Minor==6) - return new v5_6.Driver(coreServerInfo); - return new v5_6.Driver(coreServerInfo); + return CreateDriverInstance(connectionString, version, defaultSchema); } } + private static SqlDriver CreateDriverInstance(string connectionString, Version version, DefaultSchemaInfo defaultSchema) + { + var coreServerInfo = new CoreServerInfo { + ServerVersion = version, + ConnectionString = connectionString, + MultipleActiveResultSets = false, + DatabaseName = defaultSchema.Database, + DefaultSchemaName = defaultSchema.Schema, + }; + + if (version.Major < 5) { + throw new NotSupportedException(Strings.ExMySqlBelow50IsNotSupported); + } + + return version.Major switch { + 5 when version.Minor == 0 => new v5_0.Driver(coreServerInfo), + 5 when version.Minor == 1 => new v5_1.Driver(coreServerInfo), + 5 when version.Minor == 5 => new v5_5.Driver(coreServerInfo), + 5 when version.Minor == 6 => new v5_6.Driver(coreServerInfo), + _ => new v5_6.Driver(coreServerInfo) + }; + } + /// protected override DefaultSchemaInfo ReadDefaultSchema(DbConnection connection, DbTransaction transaction) { return SqlHelper.ReadDatabaseAndSchema(DatabaseAndSchemaQuery, connection, transaction); } + + private void OpenConnectionFast(MySqlConnection connection, SqlDriverConfiguration configuration) + { + connection.Open(); + SqlHelper.ExecuteInitializationSql(connection, configuration); + } + + private void OpenConnectionWithNotification(MySqlConnection connection, SqlDriverConfiguration configuration) + { + var accessors = configuration.DbConnectionAccessors; + 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; + } + } } } \ 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 56de8f860f..f316d27726 100644 --- a/Orm/Xtensive.Orm.Oracle/Sql.Drivers.Oracle/DriverFactory.cs +++ b/Orm/Xtensive.Orm.Oracle/Sql.Drivers.Oracle/DriverFactory.cs @@ -1,6 +1,6 @@ -// 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.16 @@ -66,34 +66,66 @@ 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); + else + OpenConnectionFast(connection, configuration); var version = string.IsNullOrEmpty(configuration.ForcedServerVersion) ? ParseVersion(connection.ServerVersion) : new Version(configuration.ForcedServerVersion); var dataSource = new OracleConnectionStringBuilder(connectionString).DataSource; var defaultSchema = GetDefaultSchema(connection); - var coreServerInfo = new CoreServerInfo { - ServerVersion = version, - ConnectionString = connectionString, - MultipleActiveResultSets = true, - DatabaseName = defaultSchema.Database, - DefaultSchemaName = defaultSchema.Schema, - }; - if (version.Major < 9 || version.Major==9 && version.Minor < 2) - throw new NotSupportedException(Strings.ExOracleBelow9i2IsNotSupported); - if (version.Major==9) - return new v09.Driver(coreServerInfo); - if (version.Major==10) - return new v10.Driver(coreServerInfo); - return new v11.Driver(coreServerInfo); + return CreateDriverInstance(connectionString, version, defaultSchema); } } + private static SqlDriver CreateDriverInstance(string connectionString, Version version, DefaultSchemaInfo defaultSchema) + { + var coreServerInfo = new CoreServerInfo { + ServerVersion = version, + ConnectionString = connectionString, + MultipleActiveResultSets = true, + DatabaseName = defaultSchema.Database, + DefaultSchemaName = defaultSchema.Schema, + }; + if (version.Major < 9 || (version.Major == 9 && version.Minor < 2)) { + throw new NotSupportedException(Strings.ExOracleBelow9i2IsNotSupported); + } + + return version.Major switch { + 9 => new v09.Driver(coreServerInfo), + 10 => new v10.Driver(coreServerInfo), + _ => new v11.Driver(coreServerInfo) + }; + } + /// protected override DefaultSchemaInfo ReadDefaultSchema(DbConnection connection, DbTransaction transaction) { return SqlHelper.ReadDatabaseAndSchema(DatabaseAndSchemaQuery, connection, transaction); } + + private void OpenConnectionFast(OracleConnection connection, SqlDriverConfiguration configuration) + { + connection.Open(); + SqlHelper.ExecuteInitializationSql(connection, configuration); + } + + private void OpenConnectionWithNotification(OracleConnection connection, SqlDriverConfiguration configuration) + { + var accessors = configuration.DbConnectionAccessors; + 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; + } + } } } \ 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 68ae309abd..0b857669f8 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 @@ -60,45 +60,76 @@ 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); + else + OpenConnectionFast(connection, configuration); var version = string.IsNullOrEmpty(configuration.ForcedServerVersion) ? connection.PostgreSqlVersion : new Version(configuration.ForcedServerVersion); var builder = new NpgsqlConnectionStringBuilder(connectionString); var dataSource = string.Format(DataSourceFormat, builder.Host, builder.Port, builder.Database); var defaultSchema = GetDefaultSchema(connection); - var coreServerInfo = new CoreServerInfo { - ServerVersion = version, - ConnectionString = connectionString, - MultipleActiveResultSets = false, - DatabaseName = defaultSchema.Database, - DefaultSchemaName = defaultSchema.Schema, - }; - - if (version.Major < 8 || version.Major==8 && version.Minor < 3) { - throw new NotSupportedException(Strings.ExPostgreSqlBelow83IsNotSupported); - } - - // We support 8.3, 8.4 and any 9.0+ - - if (version.Major == 8) { - return version.Minor == 3 - ? new v8_3.Driver(coreServerInfo) - : new v8_4.Driver(coreServerInfo); - } - - if (version.Major == 9) { - return version.Minor == 0 - ? new v9_0.Driver(coreServerInfo) - : new v9_1.Driver(coreServerInfo); - } - return new v10_0.Driver(coreServerInfo); + return CreateDriverInstance(connectionString, version, defaultSchema); + } + } + + private static SqlDriver CreateDriverInstance( + string connectionString, Version version, DefaultSchemaInfo defaultSchema) + { + var coreServerInfo = new CoreServerInfo { + ServerVersion = version, + ConnectionString = connectionString, + MultipleActiveResultSets = false, + DatabaseName = defaultSchema.Database, + DefaultSchemaName = defaultSchema.Schema, + }; + + if (version.Major < 8 || (version.Major == 8 && version.Minor < 3)) { + throw new NotSupportedException(Strings.ExPostgreSqlBelow83IsNotSupported); + } + + // We support 8.3, 8.4 and any 9.0+ + + if (version.Major == 8) { + return version.Minor == 3 + ? new v8_3.Driver(coreServerInfo) + : new v8_4.Driver(coreServerInfo); + } + + if (version.Major == 9) { + return version.Minor == 0 + ? new v9_0.Driver(coreServerInfo) + : new v9_1.Driver(coreServerInfo); } + return new v10_0.Driver(coreServerInfo); } /// protected override DefaultSchemaInfo ReadDefaultSchema(DbConnection connection, DbTransaction transaction) => SqlHelper.ReadDatabaseAndSchema(DatabaseAndSchemaQuery, connection, transaction); + + private void OpenConnectionFast(NpgsqlConnection connection, SqlDriverConfiguration configuration) + { + connection.Open(); + SqlHelper.ExecuteInitializationSql(connection, configuration); + } + + private void OpenConnectionWithNotification(NpgsqlConnection connection, SqlDriverConfiguration configuration) + { + var accessors = configuration.DbConnectionAccessors; + 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; + } + } } } \ No newline at end of file diff --git a/Orm/Xtensive.Orm.SqlServer/Sql.Drivers.SqlServer/Connection.cs b/Orm/Xtensive.Orm.SqlServer/Sql.Drivers.SqlServer/Connection.cs index 1fb0abf897..b25df18b53 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 @@ -38,20 +38,35 @@ public override DbParameter CreateParameter() /// public override void Open() { - if (!checkConnectionIsAlive) + if (!checkConnectionIsAlive) { base.Open(); - else - OpenWithCheck(DefaultCheckConnectionQuery); + } + else { + var connectionAccessorEx = Extensions.Get(); + if (connectionAccessorEx == null) { + OpenWithCheckFast(DefaultCheckConnectionQuery); + } + else { + OpenWithCheckAndNotification(DefaultCheckConnectionQuery, connectionAccessorEx); + } + } } /// public override Task OpenAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - if (!checkConnectionIsAlive) + if (!checkConnectionIsAlive) { return base.OpenAsync(cancellationToken); - else - return OpenWithCheckAsync(DefaultCheckConnectionQuery, cancellationToken); + } + + var connectionAccessorEx = Extensions.Get(); + if (connectionAccessorEx == null) { + return OpenWithCheckFastAsync(DefaultCheckConnectionQuery, cancellationToken); + } + else { + return OpenWithCheckAndNotificationAsync(DefaultCheckConnectionQuery, connectionAccessorEx, cancellationToken); + } } /// @@ -65,7 +80,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); + } } /// @@ -77,7 +98,10 @@ public override Task OpenAndInitializeAsync(string initializationScript, Cancell var script = string.IsNullOrEmpty(initializationScript.Trim()) ? DefaultCheckConnectionQuery : initializationScript; - return OpenWithCheckAsync(script, cancellationToken); + var connectionAccessorEx = Extensions.Get(); + return connectionAccessorEx == null + ? OpenWithCheckFastAsync(script, cancellationToken) + : OpenWithCheckAndNotificationAsync(script, connectionAccessorEx, cancellationToken); } /// @@ -131,16 +155,16 @@ protected override void ClearActiveTransaction() activeTransaction = null; } - private void OpenWithCheck(string checkQueryString) + private void OpenWithCheckFast(string checkQueryString) { - bool connectionChecked = false; - bool restoreTriggered = false; + var connectionChecked = false; + var restoreTriggered = false; while (!connectionChecked) { base.Open(); try { using (var command = underlyingConnection.CreateCommand()) { command.CommandText = checkQueryString; - command.ExecuteNonQuery(); + _ = command.ExecuteNonQuery(); } connectionChecked = true; } @@ -160,16 +184,57 @@ private void OpenWithCheck(string checkQueryString) restoreTriggered = true; continue; } - else - throw; + + throw; } } } - private async Task OpenWithCheckAsync(string checkQueryString, CancellationToken cancellationToken) + private void OpenWithCheckAndNotification(string checkQueryString, DbConnectionAccessorExtension connectionAccessorEx) { - bool connectionChecked = false; - bool restoreTriggered = false; + 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(); @@ -177,11 +242,60 @@ private async Task OpenWithCheckAsync(string checkQueryString, CancellationToken try { using (var command = underlyingConnection.CreateCommand()) { command.CommandText = checkQueryString; - await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + _ = 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(); + + SqlHelper.NotifyConnectionOpening(accessors, UnderlyingConnection, !connectionChecked && !restoreTriggered); + + await underlyingConnection.OpenAsync(cancellationToken).ConfigureAwait(false); + try { + SqlHelper.NotifyConnectionInitializing(accessors, + UnderlyingConnection, checkQueryString, !connectionChecked && !restoreTriggered); + + using (var command = underlyingConnection.CreateCommand()) { + command.CommandText = checkQueryString; + _ = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); } 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; @@ -197,8 +311,8 @@ private async Task OpenWithCheckAsync(string checkQueryString, CancellationToken restoreTriggered = true; continue; } - else - throw; + + throw; } } } diff --git a/Orm/Xtensive.Orm.SqlServer/Sql.Drivers.SqlServer/DriverFactory.cs b/Orm/Xtensive.Orm.SqlServer/Sql.Drivers.SqlServer/DriverFactory.cs index 512ec74bff..e9833aa2e6 100644 --- a/Orm/Xtensive.Orm.SqlServer/Sql.Drivers.SqlServer/DriverFactory.cs +++ b/Orm/Xtensive.Orm.SqlServer/Sql.Drivers.SqlServer/DriverFactory.cs @@ -27,6 +27,7 @@ public class DriverFactory : SqlDriverFactory private const string DatabaseAndSchemaQuery = "SELECT DB_NAME(), COALESCE(SCHEMA_NAME(), 'dbo')"; + private const string LangIdQuery = "SELECT @@LANGID"; private const string MessagesQuery = @"Declare @MSGLANGID int; Select @MSGLANGID = msglangid FROM [master].[sys].[syslanguages] lang WHERE lang.langid = @@LANGID; @@ -34,11 +35,15 @@ public class DriverFactory : SqlDriverFactory FROM [master].[sys].[sysmessages] msg WHERE msg.msglangid = @MSGLANGID AND msg.error IN ( 2627, 2601, 515, 547 )"; + private const string VersionQuery = "SELECT @@VERSION"; + + private const string ForcedAzureVersion = "12.0.0.0"; + private static ErrorMessageParser CreateMessageParser(SqlServerConnection connection) { bool isEnglish; using (var command = connection.CreateCommand()) { - command.CommandText = "SELECT @@LANGID"; + command.CommandText = LangIdQuery; isEnglish = command.ExecuteScalar().ToString()=="0"; } var templates = new Dictionary(); @@ -54,7 +59,7 @@ private static ErrorMessageParser CreateMessageParser(SqlServerConnection connec private static bool IsAzure(SqlServerConnection connection) { using (var command = connection.CreateCommand()) { - command.CommandText = "SELECT @@VERSION"; + command.CommandText = VersionQuery; return ((string) command.ExecuteScalar()).IndexOf("Azure", StringComparison.Ordinal) >= 0; } } @@ -94,116 +99,219 @@ protected override string BuildConnectionString(UrlInfo url) protected override SqlDriver CreateDriver(string connectionString, SqlDriverConfiguration configuration) { var isPooingOn = !IsPoolingOff(connectionString); - configuration.EnsureConnectionIsAlive = isPooingOn && configuration.EnsureConnectionIsAlive; + configuration.EnsureConnectionIsAlive &= isPooingOn; - using (var connection = CreateAndOpenConnection(connectionString, configuration)) { - string versionString; - bool isAzure; + using var connection = CreateAndOpenConnection(connectionString, configuration); + var isEnsureAlive = configuration.EnsureConnectionIsAlive; + var forcedServerVersion = configuration.ForcedServerVersion; + var isForcedVersion = !string.IsNullOrEmpty(forcedServerVersion); + var isForcedAzure = isForcedVersion && forcedServerVersion.Equals("azure", StringComparison.OrdinalIgnoreCase); + var isAzure = isForcedAzure || (!isForcedVersion && IsAzure(connection)); + var parser = isAzure ? new ErrorMessageParser() : CreateMessageParser(connection); + + var versionString = isForcedVersion + ? isForcedAzure ? ForcedAzureVersion : forcedServerVersion + : connection.ServerVersion ?? string.Empty; + var version = new Version(versionString); + var defaultSchema = GetDefaultSchema(connection); - var forcedServerVersion = configuration.ForcedServerVersion; - if (string.IsNullOrEmpty(forcedServerVersion)) { - versionString = connection.ServerVersion; - isAzure = IsAzure(connection); - } - else if (forcedServerVersion.Equals("azure", StringComparison.OrdinalIgnoreCase)) { - versionString = "12.0.0.0"; - isAzure = true; - } - else { - versionString = forcedServerVersion; - isAzure = false; - } + return CreateDriverInstance(connectionString, isAzure, version, defaultSchema, parser, isEnsureAlive); + } - var builder = new SqlConnectionStringBuilder(connectionString); - var version = new Version(versionString); - var defaultSchema = GetDefaultSchema(connection); - var coreServerInfo = new CoreServerInfo { - ServerVersion = version, - ConnectionString = connectionString, - MultipleActiveResultSets = builder.MultipleActiveResultSets, - DatabaseName = defaultSchema.Database, - DefaultSchemaName = defaultSchema.Schema, - }; - if (isAzure) - return new Azure.Driver(coreServerInfo, new ErrorMessageParser(), configuration.EnsureConnectionIsAlive); - if (version.Major < 9) - throw new NotSupportedException(Strings.ExSqlServerBelow2005IsNotSupported); - var parser = CreateMessageParser(connection); - if (version.Major==9) - return new v09.Driver(coreServerInfo, parser, configuration.EnsureConnectionIsAlive); - if (version.Major==10) - return new v10.Driver(coreServerInfo, parser, configuration.EnsureConnectionIsAlive); - if (version.Major==11) - return new v11.Driver(coreServerInfo, parser, configuration.EnsureConnectionIsAlive); - if (version.Major==12) - return new v12.Driver(coreServerInfo, parser, configuration.EnsureConnectionIsAlive); - if (version.Major==13) - return new v13.Driver(coreServerInfo, parser, configuration.EnsureConnectionIsAlive); - return new v13.Driver(coreServerInfo, parser, configuration.EnsureConnectionIsAlive); + private static SqlDriver CreateDriverInstance(string connectionString, bool isAzure, Version version, + DefaultSchemaInfo defaultSchema, ErrorMessageParser parser, bool isEnsureAlive) + { + var builder = new SqlConnectionStringBuilder(connectionString); + var coreServerInfo = new CoreServerInfo { + ServerVersion = version, + ConnectionString = connectionString, + MultipleActiveResultSets = builder.MultipleActiveResultSets, + DatabaseName = defaultSchema.Database, + DefaultSchemaName = defaultSchema.Schema, + }; + if (isAzure) { + return new Azure.Driver(coreServerInfo, parser, isEnsureAlive); } + + if (version.Major < 9) { + throw new NotSupportedException(Strings.ExSqlServerBelow2005IsNotSupported); + } + return version.Major switch { + 9 => new v09.Driver(coreServerInfo, parser, isEnsureAlive), + 10 => new v10.Driver(coreServerInfo, parser, isEnsureAlive), + 11 => new v11.Driver(coreServerInfo, parser, isEnsureAlive), + 12 => new v12.Driver(coreServerInfo, parser, isEnsureAlive), + 13 => new v13.Driver(coreServerInfo, parser, isEnsureAlive), + _ => new v13.Driver(coreServerInfo, parser, isEnsureAlive) + }; } /// - protected override DefaultSchemaInfo ReadDefaultSchema(DbConnection connection, DbTransaction transaction) - { - return SqlHelper.ReadDatabaseAndSchema(DatabaseAndSchemaQuery, connection, transaction); - } + protected override DefaultSchemaInfo ReadDefaultSchema(DbConnection connection, DbTransaction transaction) => + SqlHelper.ReadDatabaseAndSchema(DatabaseAndSchemaQuery, connection, transaction); private 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); + else + OpenConnectionWithNotification(connection, configuration); return connection; } - var testQuery = (string.IsNullOrEmpty(configuration.ConnectionInitializationSql)) + var testQuery = string.IsNullOrEmpty(initScript) ? CheckConnectionQuery - : configuration.ConnectionInitializationSql; + : initScript; + if (configuration.DbConnectionAccessors.Count == 0) + return EnsureConnectionIsAliveFast(connection, testQuery); + else + return EnsureConnectionIsAliveWithNotification(connection, testQuery, configuration.DbConnectionAccessors); + } + + private static void OpenConnectionFast(SqlServerConnection connection, string sqlScript) + { connection.Open(); - EnsureConnectionIsAlive(ref connection, testQuery); - return connection; + SqlHelper.ExecuteInitializationSql(connection, sqlScript); } - private void EnsureConnectionIsAlive(ref SqlServerConnection connection, string query) + private static void OpenConnectionWithNotification(SqlServerConnection connection, + SqlDriverConfiguration configuration) { + var accessors = configuration.DbConnectionAccessors; + var initSql = configuration.ConnectionInitializationSql; + + 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; + } + } + + private static SqlServerConnection EnsureConnectionIsAliveFast(SqlServerConnection connection, string query) + { + try { + connection.Open(); + using (var command = connection.CreateCommand()) { command.CommandText = query; - command.ExecuteNonQuery(); + _ = command.ExecuteNonQuery(); } + + return connection; } catch (Exception exception) { + try { + connection.Close(); + connection.Dispose(); + } + catch { + // ignored + } + if (InternalHelpers.ShouldRetryOn(exception)) { - if (!TryReconnect(ref connection, query)) - throw; + var (isReconnected, newConnection) = TryReconnectFast(connection.ConnectionString, query); + if (isReconnected) { + return newConnection; + } } - else - throw; + throw; } } - private static bool TryReconnect(ref SqlServerConnection connection, string query) + private static SqlServerConnection EnsureConnectionIsAliveWithNotification(SqlServerConnection connection, + string query, IReadOnlyCollection connectionAccessors) { + SqlHelper.NotifyConnectionOpening(connectionAccessors, connection); try { - var newConnection = new SqlServerConnection(connection.ConnectionString); + connection.Open(); + + SqlHelper.NotifyConnectionInitializing(connectionAccessors, connection, query); + + using (var command = connection.CreateCommand()) { + command.CommandText = query; + _ = command.ExecuteNonQuery(); + } + + SqlHelper.NotifyConnectionOpened(connectionAccessors, connection); + return connection; + } + catch (Exception exception) { + var retryToConnect = InternalHelpers.ShouldRetryOn(exception); + if (!retryToConnect) + SqlHelper.NotifyConnectionOpeningFailed(connectionAccessors, connection, exception); try { connection.Close(); connection.Dispose(); } - catch { } + catch { + // ignored + } + + if (retryToConnect) { + var (isReconnected, newConnection) = TryReconnectWithNotification(connection.ConnectionString, query, connectionAccessors); + if (isReconnected) { + return newConnection; + } + } + throw; + } + } + + private static (bool isReconnected, SqlServerConnection connection) TryReconnectFast( + string connectionString, string query) + { + var connection = new SqlServerConnection(connectionString); - connection = newConnection; + try { + connection.Open(); + + using (var command = connection.CreateCommand()) { + command.CommandText = query; + _ = command.ExecuteNonQuery(); + } + + return (true, connection); + } + catch { + connection.Dispose(); + return (false, null); + } + } + + private static (bool isReconnected, SqlServerConnection connection) TryReconnectWithNotification( + string connectionString, string query, IReadOnlyCollection connectionAccessors) + { + var connection = new SqlServerConnection(connectionString); + + SqlHelper.NotifyConnectionOpening(connectionAccessors, connection, true); + try { connection.Open(); + SqlHelper.NotifyConnectionInitializing(connectionAccessors, connection, query, true); + using (var command = connection.CreateCommand()) { command.CommandText = query; - command.ExecuteNonQuery(); + _ = command.ExecuteNonQuery(); } - return true; + + SqlHelper.NotifyConnectionOpened(connectionAccessors, connection, true); + return (true, connection); } - catch (Exception) { - return false; + catch (Exception exception) { + SqlHelper.NotifyConnectionOpeningFailed(connectionAccessors, connection, exception, true); + connection.Dispose(); + 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 e617f92645..9393507190 100644 --- a/Orm/Xtensive.Orm.Sqlite/Sql.Drivers.Sqlite/DriverFactory.cs +++ b/Orm/Xtensive.Orm.Sqlite/Sql.Drivers.Sqlite/DriverFactory.cs @@ -1,6 +1,6 @@ -// Copyright (C) 2003-2010 Xtensive LLC. -// All rights reserved. -// For conditions of distribution and use, see license. +// 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 // Created: 2011.04.29 @@ -42,22 +42,32 @@ protected override SqlDriver CreateDriver(string connectionString, SqlDriverConf private SqlDriver DoCreateDriver(string connectionString, SqlDriverConfiguration configuration) { using (var connection = new SQLiteConnection(connectionString)) { - connection.Open(); - SqlHelper.ExecuteInitializationSql(connection, configuration); + if (configuration.DbConnectionAccessors.Count > 0) + OpenConnectionWithNotification(connection, configuration); + else + OpenConnectionFast(connection, configuration); var version = new Version(connection.ServerVersion); var defaultSchema = GetDefaultSchema(connection); - var coreServerInfo = new CoreServerInfo { - ServerVersion = version, - ConnectionString = connectionString, - MultipleActiveResultSets = false, - DatabaseName = defaultSchema.Database, - DefaultSchemaName = defaultSchema.Schema, - }; + return CreateDriverInstance(connectionString, version, defaultSchema); + } + } - if (version.Major < 3) - throw new NotSupportedException(Strings.ExSqlLiteServerBelow3IsNotSupported); - return new v3.Driver(coreServerInfo); + private static SqlDriver CreateDriverInstance(string connectionString, Version version, + DefaultSchemaInfo defaultSchema) + { + var coreServerInfo = new CoreServerInfo { + ServerVersion = version, + ConnectionString = connectionString, + MultipleActiveResultSets = false, + DatabaseName = defaultSchema.Database, + DefaultSchemaName = defaultSchema.Schema, + }; + + if (version.Major < 3) { + throw new NotSupportedException(Strings.ExSqlLiteServerBelow3IsNotSupported); } + + return new v3.Driver(coreServerInfo); } /// @@ -77,5 +87,28 @@ protected override DefaultSchemaInfo ReadDefaultSchema(DbConnection connection, { return new DefaultSchemaInfo(GetDataSource(connection.ConnectionString), Extractor.DefaultSchemaName); } + + private void OpenConnectionFast(SQLiteConnection connection, SqlDriverConfiguration configuration) + { + connection.Open(); + SqlHelper.ExecuteInitializationSql(connection, configuration); + } + + private void OpenConnectionWithNotification(SQLiteConnection connection, SqlDriverConfiguration configuration) + { + var accessors = configuration.DbConnectionAccessors; + 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; + } + } } } \ 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..bb800ce5df 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,44 @@ using Xtensive.Orm; using Xtensive.Orm.Building.Builders; using Xtensive.Sql; +using Xtensive.Orm.Tests.Sql.DriverFactoryTestTypes; + +namespace Xtensive.Orm.Tests.Sql.DriverFactoryTestTypes +{ + public class TestConnectionAccessor : DbConnectionAccessor + { + public int OpeningCounter = 0; + public int OpenedCounter = 0; + public int OpeningInitCounter = 0; + public int OpeningFailedCounter = 0; + + public override void ConnectionOpening(ConnectionEventData eventData) + { + OpeningCounter++; + } + + public override void ConnectionOpened(ConnectionEventData eventData) + { + OpenedCounter++; + } + + public override void ConnectionInitialization(ConnectionInitEventData eventData) + { + OpeningInitCounter++; + } + + public override void ConnectionOpeningFailed(ConnectionErrorEventData eventData) + { + OpeningFailedCounter++; + } + } + + public static class StaticCounter + { + public static int OpeningReached; + public static int OpenedReached; + } +} namespace Xtensive.Orm.Tests.Sql { @@ -95,6 +133,61 @@ 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.OpeningInitCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpenedCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpeningFailedCounter, Is.EqualTo(0)); + + var configuration = new SqlDriverConfiguration(accessorsArray); + _ = factory.GetDriver(new ConnectionInfo(Url), configuration); + Assert.That(accessorInstance.OpeningCounter, Is.EqualTo(1)); + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpenedCounter, Is.EqualTo(1)); + Assert.That(accessorInstance.OpeningFailedCounter, Is.EqualTo(0)); + + configuration = new SqlDriverConfiguration(accessorsArray) { EnsureConnectionIsAlive = true }; + _ = factory.GetDriver(new ConnectionInfo(Url), configuration); + Assert.That(accessorInstance.OpeningCounter, Is.EqualTo(2)); + if (provider == WellKnown.Provider.SqlServer) + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(1)); + else + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(0)); + Assert.That(accessorInstance.OpenedCounter, Is.EqualTo(2)); + Assert.That(accessorInstance.OpeningFailedCounter, Is.EqualTo(0)); + + configuration = new SqlDriverConfiguration(accessorsArray) { ConnectionInitializationSql = InitQueryPerProvider(provider) }; + _ = factory.GetDriver(new ConnectionInfo(Url), configuration); + Assert.That(accessorInstance.OpeningCounter, Is.EqualTo(3)); + if (provider == WellKnown.Provider.SqlServer) + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(2)); + else + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(1)); + Assert.That(accessorInstance.OpenedCounter, Is.EqualTo(3)); + Assert.That(accessorInstance.OpeningFailedCounter, 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)); + if (provider == WellKnown.Provider.SqlServer) + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(3)); + else + Assert.That(accessorInstance.OpeningInitCounter, Is.EqualTo(2)); + Assert.That(accessorInstance.OpenedCounter, Is.EqualTo(3)); + Assert.That(accessorInstance.OpeningFailedCounter, Is.EqualTo(1)); + } private static void TestProvider(string providerName, string connectionString, string connectionUrl) { @@ -109,5 +202,25 @@ 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..e00d8e947e --- /dev/null +++ b/Orm/Xtensive.Orm.Tests/Storage/ConnectionAccessorTest.cs @@ -0,0 +1,281 @@ +// 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 PerfCheckAccessor1 : DbConnectionAccessor { } + public class PerfCheckAccessor2 : DbConnectionAccessor { } + public class PerfCheckAccessor3 : DbConnectionAccessor { } + public class PerfCheckAccessor4 : DbConnectionAccessor { } + public class PerfCheckAccessor5 : DbConnectionAccessor { } + public class PerfCheckAccessor6 : DbConnectionAccessor { } + public class PerfCheckAccessor7 : DbConnectionAccessor { } + public class PerfCheckAccessor8 : DbConnectionAccessor { } + public class PerfCheckAccessor9 : DbConnectionAccessor { } + public class PerfCheckAccessor10 : DbConnectionAccessor { } + public class PerfCheckAccessor11 : DbConnectionAccessor { } + public class PerfCheckAccessor12 : DbConnectionAccessor { } + public class PerfCheckAccessor13 : DbConnectionAccessor { } + public class PerfCheckAccessor14 : DbConnectionAccessor { } + public class PerfCheckAccessor15 : DbConnectionAccessor { } + public class PerfCheckAccessor16 : DbConnectionAccessor { } + public class PerfCheckAccessor17 : DbConnectionAccessor { } + public class PerfCheckAccessor18 : DbConnectionAccessor { } + public class PerfCheckAccessor19 : DbConnectionAccessor { } + public class PerfCheckAccessor20 : DbConnectionAccessor { } + public class PerfCheckAccessor21 : DbConnectionAccessor { } + public class PerfCheckAccessor22 : DbConnectionAccessor { } + public class PerfCheckAccessor23 : DbConnectionAccessor { } + public class PerfCheckAccessor24 : DbConnectionAccessor { } + public class PerfCheckAccessor25 : 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 sealed 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 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 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] + [TestCase(0)] + [TestCase(1)] + [TestCase(2)] + public void ConnectionExtensionExistanceTest(int amountOfAccessors) + { + var domainConfig = DomainConfigurationFactory.Create(); + domainConfig.UpgradeMode = DomainUpgradeMode.Recreate; + + foreach (var accessor in GetAccessors(amountOfAccessors)) { + 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 (amountOfAccessors > 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(amountOfAccessors)); + } + 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(PerfCheckAccessor1), typeof(PerfCheckAccessor2), typeof(PerfCheckAccessor3), typeof(PerfCheckAccessor4), + typeof(PerfCheckAccessor5), typeof(PerfCheckAccessor6), typeof(PerfCheckAccessor7), typeof(PerfCheckAccessor8), + typeof(PerfCheckAccessor9), typeof(PerfCheckAccessor10), typeof(PerfCheckAccessor11), typeof(PerfCheckAccessor12), + typeof(PerfCheckAccessor13), typeof(PerfCheckAccessor14), typeof(PerfCheckAccessor15), typeof(PerfCheckAccessor16), + typeof(PerfCheckAccessor17), typeof(PerfCheckAccessor18), typeof(PerfCheckAccessor19), typeof(PerfCheckAccessor20), + typeof(PerfCheckAccessor21), typeof(PerfCheckAccessor22), typeof(PerfCheckAccessor23), typeof(PerfCheckAccessor24), + typeof(PerfCheckAccessor25) + }; + 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 5594bb7548..8a4fe8934c 100644 --- a/Orm/Xtensive.Orm/Orm/Configuration/DomainTypeRegistry.cs +++ b/Orm/Xtensive.Orm/Orm/Configuration/DomainTypeRegistry.cs @@ -26,7 +26,10 @@ 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. @@ -100,6 +103,27 @@ public IEnumerable FullTextCatalogResolvers get { return 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(IsDbConnectionAccessor)) + 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(IsDbConnectionAccessor); + } + } + #region IsXxx method group /// @@ -119,7 +143,8 @@ public static bool IsInterestingType(Type type) IsUpgradeHandler(type) || IsKeyGenerator(type) || IsCompilerContainer(type) || - IsFullTextCatalogNameBuilder(type); + IsFullTextCatalogNameBuilder(type) || + IsDbConnectionAccessor(type); } /// @@ -238,6 +263,21 @@ public static bool IsFullTextCatalogNameBuilder(Type type) return false; } + /// + /// Determines whether the is + /// a database connection accessor. + /// + /// The type to check. + /// Check result. + public static bool IsDbConnectionAccessor(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..d5670d7ed1 --- /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; + } + } +} \ No newline at end of file diff --git a/Orm/Xtensive.Orm/Orm/ConnectionEventData.cs b/Orm/Xtensive.Orm/Orm/ConnectionEventData.cs new file mode 100644 index 0000000000..6ab3d75ff5 --- /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; + } + } +} \ No newline at end of file diff --git a/Orm/Xtensive.Orm/Orm/ConnectionInitEventData.cs b/Orm/Xtensive.Orm/Orm/ConnectionInitEventData.cs new file mode 100644 index 0000000000..d14f825c11 --- /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; + } + } +} \ No newline at end of file diff --git a/Orm/Xtensive.Orm/Orm/Interfaces/DbConnectionAccessor.cs b/Orm/Xtensive.Orm/Orm/Interfaces/DbConnectionAccessor.cs new file mode 100644 index 0000000000..c5806ca096 --- /dev/null +++ b/Orm/Xtensive.Orm/Orm/Interfaces/DbConnectionAccessor.cs @@ -0,0 +1,35 @@ +// 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 void ConnectionInitialization(ConnectionInitEventData eventData) + { + } + + /// + public virtual void ConnectionOpened(ConnectionEventData eventData) + { + } + + /// + public virtual void ConnectionOpeningFailed(ConnectionErrorEventData eventData) + { + } + } +} \ No newline at end of file diff --git a/Orm/Xtensive.Orm/Orm/Interfaces/IDbConnectionAccessor.cs b/Orm/Xtensive.Orm/Orm/Interfaces/IDbConnectionAccessor.cs new file mode 100644 index 0000000000..fe82974d60 --- /dev/null +++ b/Orm/Xtensive.Orm/Orm/Interfaces/IDbConnectionAccessor.cs @@ -0,0 +1,40 @@ + // 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 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 successfully opened and initialized. + /// + /// Information connected with this event. + void ConnectionOpened(ConnectionEventData eventData); + + /// + /// Executes if an error appeared on either connection opening or connection initialization. + /// + /// Information connected with this event. + void ConnectionOpeningFailed(ConnectionErrorEventData eventData); + } +} \ No newline at end of file diff --git a/Orm/Xtensive.Orm/Orm/Providers/StorageDriver.Operations.cs b/Orm/Xtensive.Orm/Orm/Providers/StorageDriver.Operations.cs index b63607ee50..cbef6e3bd8 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 { @@ -50,6 +50,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; @@ -69,14 +74,14 @@ public void OpenConnection(Session session, SqlConnection connection) if (isLoggingEnabled) SqlLog.Info(Strings.LogSessionXOpeningConnectionY, session.ToStringSafely(), connection.ConnectionInfo); - var extension = connection.Extensions.Get(); + var script = connection.Extensions.Get()?.Script; try { - if (extension == null || string.IsNullOrEmpty(extension.Script)) { - connection.Open(); + if (!string.IsNullOrEmpty(script)) { + connection.OpenAndInitialize(script); } else { - connection.OpenAndInitialize(extension.Script); + connection.Open(); } } catch (Exception exception) { @@ -94,13 +99,15 @@ public async Task OpenConnectionAsync(Session session, SqlConnection connection, if (isLoggingEnabled) 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); - else + if (!string.IsNullOrEmpty(script)) { + await connection.OpenAndInitializeAsync(script, cancellationToken).ConfigureAwait(false); + } + else { await connection.OpenAsync(cancellationToken).ConfigureAwait(false); + } } catch (OperationCanceledException) { throw; diff --git a/Orm/Xtensive.Orm/Orm/Providers/StorageDriver.cs b/Orm/Xtensive.Orm/Orm/Providers/StorageDriver.cs index 3fd4b3188c..9ecd310414 100644 --- a/Orm/Xtensive.Orm/Orm/Providers/StorageDriver.cs +++ b/Orm/Xtensive.Orm/Orm/Providers/StorageDriver.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2003-2010 Xtensive LLC. +// Copyright (C) 2009-2021 Xtensive LLC. // All rights reserved. // For conditions of distribution and use, see license. // Created by: Denis Krjuchkov @@ -7,7 +7,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; +using System.Reflection; using Xtensive.Core; +using Xtensive.Linq; using Xtensive.Orm.Logging; using Xtensive.Orm.Configuration; using Xtensive.Orm.Model; @@ -24,6 +27,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; @@ -31,6 +37,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; } @@ -86,7 +94,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() @@ -140,6 +148,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) @@ -147,7 +210,8 @@ public static StorageDriver Create(SqlDriverFactory driverFactory, DomainConfigu ArgumentValidator.EnsureArgumentNotNull(driverFactory, "driverFactory"); ArgumentValidator.EnsureArgumentNotNull(configuration, "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, @@ -156,11 +220,14 @@ 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); } - 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; @@ -171,6 +238,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..bf881f246d --- /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; + } + } +} \ No newline at end of file diff --git a/Orm/Xtensive.Orm/Sql/SqlConnection.cs b/Orm/Xtensive.Orm/Sql/SqlConnection.cs index 074e12e4b4..286bb83f7c 100644 --- a/Orm/Xtensive.Orm/Sql/SqlConnection.cs +++ b/Orm/Xtensive.Orm/Sql/SqlConnection.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. @@ -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; + } + } } /// @@ -175,15 +190,37 @@ public virtual void Open() /// Initialization script. public virtual void OpenAndInitialize(string initializationScript) { - 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()) { + 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; + } + } } /// 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..42092ff731 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,17 @@ 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. + /// to database connection on certain operations. + /// + /// 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 dcc10e826b..ae5bd74056 100644 --- a/Orm/Xtensive.Orm/Sql/SqlHelper.cs +++ b/Orm/Xtensive.Orm/Sql/SqlHelper.cs @@ -1,4 +1,4 @@ -// Copyright (C) 2003-2010 Xtensive LLC. +// Copyright (C) 2009-2021 Xtensive LLC. // All rights reserved. // For conditions of distribution and use, see license. // Created by: Denis Krjuchkov @@ -371,6 +371,21 @@ public static void ExecuteInitializationSql(DbConnection connection, SqlDriverCo } } + /// + /// 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(); + } + /// /// Reduces the isolation level to the most commonly supported ones. /// @@ -433,5 +448,75 @@ 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 + /// 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 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 connectionAccessor, DbConnection connection, bool reconnect = false) + { + foreach (var accessor in connectionAccessor) { + accessor.ConnectionOpened(new ConnectionEventData(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 . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void NotifyConnectionOpeningFailed( + IEnumerable connnectionAccessors, DbConnection connection, Exception exception, bool reconnect = false) + { + foreach (var accessor in connnectionAccessors) { + accessor.ConnectionOpeningFailed(new ConnectionErrorEventData(exception, connection, reconnect)); + } + } + + #endregion } } diff --git a/Orm/Xtensive.Orm/Strings.Designer.cs b/Orm/Xtensive.Orm/Strings.Designer.cs index 6a87df93bf..1904947164 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 226c2e0c07..bf6ede1988 100644 --- a/Orm/Xtensive.Orm/Strings.resx +++ b/Orm/Xtensive.Orm/Strings.resx @@ -3467,4 +3467,7 @@ Error: {1} Can't modify Active or Disposed scope. + + Connection accessor '{0}' has no parameterless constructor. + \ No newline at end of file