From 2a1ecc3fba740bc5d863f6adf50b42298cbf6a5a Mon Sep 17 00:00:00 2001 From: David Engel Date: Tue, 7 Apr 2020 16:49:10 -0700 Subject: [PATCH] Add SqlConnectionOverrides for a fast Open() option. --- .../SqlConnection.xml | 40 +++++++++++++++++++ .../SqlConnectionOverrides.xml | 17 ++++++++ .../netcore/ref/Microsoft.Data.SqlClient.cs | 10 +++++ .../Microsoft/Data/SqlClient/SqlConnection.cs | 15 +++++-- .../src/Microsoft/Data/SqlClient/TdsEnums.cs | 10 +++++ .../netfx/ref/Microsoft.Data.SqlClient.cs | 11 +++++ .../Microsoft/Data/SqlClient/SqlConnection.cs | 26 +++++++++--- .../src/Microsoft/Data/SqlClient/TdsEnums.cs | 10 +++++ .../SQL/ConnectivityTests/ConnectivityTest.cs | 33 +++++++++++++++ 9 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 doc/snippets/Microsoft.Data.SqlClient/SqlConnectionOverrides.xml diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml index 6eddd8df55..d878ed20cd 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml @@ -928,6 +928,46 @@ GO The tag in the app.config file has invalid or unknown elements. There are two entries with the same name in the section. + + Options to override default connection open behavior. + + Opens a database connection with the property settings specified by the . + + + + draws an open connection from the connection pool if one is available. Otherwise, it establishes a new connection to an instance of SQL Server. If overrides are specified, the first open attempt will apply the specified overrides to the open action. + +> [!NOTE] +> If the goes out of scope, it is not closed. Therefore, you must explicitly close the connection by calling . + +> [!NOTE] +> If you specify a port number other than 1433 when you are trying to connect to an instance of SQL Server and using a protocol other than TCP/IP, the method fails. To specify a port number other than 1433, include "server=machinename,port number" in the connection string, and use the TCP/IP protocol. + +> [!NOTE] +> The .NET Framework Data Provider for SQL Server requires the Security permission with "Allows calls to unmanaged assemblies" enabled ( with set to `UnmanagedCode`) to open a with SQL Debugging enabled. + +## Examples + The following example creates a , opens it, and displays some of its properties. The connection is automatically closed at the end of the `using` block. + + [!code-csharp[SqlConnection_Open Example#1](~/../sqlclient/doc/samples/SqlConnection_Open.cs#1)] + +]]> + + + Cannot open a connection without specifying a data source or server. + + or + + The connection is already open. + + A connection-level error occurred while opening the connection. If the property contains the value 18487 or 18488, this indicates that the specified password has expired or must be reset. See the method for more information. + + The tag in the app.config file has invalid or unknown elements. + There are two entries with the same name in the section. + The cancellation instruction. An asynchronous version of , which opens a database connection with the property settings specified by the . The cancellation token can be used to request that the operation be abandoned before the connection timeout elapses. Exceptions will be propagated via the returned Task. If the connection timeout time elapses without successfully connecting, the returned Task will be marked as faulted with an Exception. The implementation returns a Task without blocking the calling thread for both pooled and non-pooled connections. diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlConnectionOverrides.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlConnectionOverrides.xml new file mode 100644 index 0000000000..cf6c2afb93 --- /dev/null +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlConnectionOverrides.xml @@ -0,0 +1,17 @@ + + + + + Specifies a value for Overrides. + + + + No overrides. + 0 + + + Disable transient fault handling during the initial SqlConnection Open attempt. + 1 + + + diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs index 60fb125690..e037728eee 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs @@ -591,6 +591,8 @@ public event Microsoft.Data.SqlClient.SqlInfoMessageEventHandler InfoMessage { a public override System.Data.DataTable GetSchema(string collectionName, string[] restrictionValues) { throw null; } /// public override void Open() { } + /// + public void Open(SqlConnectionOverrides overrides) { } /// public override System.Threading.Tasks.Task OpenAsync(System.Threading.CancellationToken cancellationToken) { throw null; } /// @@ -598,6 +600,14 @@ public void ResetStatistics() { } /// public System.Collections.IDictionary RetrieveStatistics() { throw null; } } + /// + public enum SqlConnectionOverrides + { + /// + None = 0, + /// + OpenWithoutRetry = 1, + } /// [System.ComponentModel.DefaultPropertyAttribute("DataSource")] public sealed partial class SqlConnectionStringBuilder : System.Data.Common.DbConnectionStringBuilder diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs index 4f847bc1a7..201763a227 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -998,6 +998,12 @@ private void DisposeMe(bool disposing) /// public override void Open() + { + Open(SqlConnectionOverrides.None); + } + + /// + public void Open(SqlConnectionOverrides overrides) { long scopeID = SqlClientEventSource.Log.ScopeEnterEvent(" ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); SqlClientEventSource.Log.CorrelationTraceEvent(" ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); @@ -1014,7 +1020,7 @@ public override void Open() { statistics = SqlStatistics.StartTimer(Statistics); - if (!TryOpen(null)) + if (!TryOpen(null, overrides)) { throw ADP.InternalError(ADP.InternalErrorCode.SynchronousConnectReturnedPending); } @@ -1469,7 +1475,7 @@ private void PrepareStatisticsForNewConnection() } } - private bool TryOpen(TaskCompletionSource retry) + private bool TryOpen(TaskCompletionSource retry, SqlConnectionOverrides overrides = SqlConnectionOverrides.None) { SqlConnectionString connectionOptions = (SqlConnectionString)ConnectionOptions; @@ -1489,7 +1495,7 @@ private bool TryOpen(TaskCompletionSource retry) } } - _applyTransientFaultHandling = (retry == null && connectionOptions != null && connectionOptions.ConnectRetryCount > 0); + _applyTransientFaultHandling = (!overrides.HasFlag(SqlConnectionOverrides.OpenWithoutRetry) && retry == null && connectionOptions != null && connectionOptions.ConnectRetryCount > 0); if (connectionOptions != null && (connectionOptions.Authentication == SqlAuthenticationMethod.SqlPassword || connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryPassword) && @@ -1515,6 +1521,9 @@ private bool TryOpen(TaskCompletionSource retry) } // does not require GC.KeepAlive(this) because of ReRegisterForFinalize below. + // Set future transient fault handling based on connection options + _applyTransientFaultHandling = (retry == null && connectionOptions != null && connectionOptions.ConnectRetryCount > 0); + var tdsInnerConnection = (SqlInternalConnectionTds)InnerConnection; Debug.Assert(tdsInnerConnection.Parser != null, "Where's the parser?"); diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs index 262b8bc2ca..3d735d21e7 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -1078,6 +1078,16 @@ public enum SqlConnectionColumnEncryptionSetting Enabled, } + /// + [Flags] + public enum SqlConnectionOverrides + { + /// + None = 0, + /// + OpenWithoutRetry = 1, + } + /// public enum SqlCommandColumnEncryptionSetting { diff --git a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs index 44cc4bb3e1..c6154c3ad4 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs @@ -777,6 +777,8 @@ public override void EnlistTransaction(System.Transactions.Transaction transacti public override System.Data.DataTable GetSchema(string collectionName, string[] restrictionValues) { throw null; } /// public override void Open() { } + /// + public void Open(SqlConnectionOverrides overrides) { } /// public override System.Threading.Tasks.Task OpenAsync(System.Threading.CancellationToken cancellationToken) { throw null; } /// @@ -813,6 +815,15 @@ public enum SqlConnectionAttestationProtocol HGS = 3 } + /// + public enum SqlConnectionOverrides + { + /// + None = 0, + /// + OpenWithoutRetry = 1, + } + /// [System.ComponentModel.DefaultPropertyAttribute("DataSource")] public sealed partial class SqlConnectionStringBuilder : System.Data.Common.DbConnectionStringBuilder diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs index ed014abc6a..9ecacf253c 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs @@ -1396,6 +1396,12 @@ public void EnlistDistributedTransaction(System.EnterpriseServices.ITransaction /// override public void Open() + { + Open(SqlConnectionOverrides.None); + } + + /// + public void Open(SqlConnectionOverrides overrides) { long scopeID = SqlClientEventSource.Log.ScopeEnterEvent(" ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); SqlClientEventSource.Log.CorrelationTraceEvent(" ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); @@ -1420,7 +1426,7 @@ override public void Open() { statistics = SqlStatistics.StartTimer(Statistics); - if (!TryOpen(null)) + if (!TryOpen(null, overrides)) { throw ADP.InternalError(ADP.InternalErrorCode.SynchronousConnectReturnedPending); } @@ -1814,10 +1820,13 @@ internal void Retry(Task retryTask) } } - private bool TryOpen(TaskCompletionSource retry) + private bool TryOpen(TaskCompletionSource retry, SqlConnectionOverrides overrides = SqlConnectionOverrides.None) { SqlConnectionString connectionOptions = (SqlConnectionString)ConnectionOptions; - _applyTransientFaultHandling = (retry == null && connectionOptions != null && connectionOptions.ConnectRetryCount > 0); + + bool result = false; + + _applyTransientFaultHandling = (!overrides.HasFlag(SqlConnectionOverrides.OpenWithoutRetry) && retry == null && connectionOptions != null && connectionOptions.ConnectRetryCount > 0); if (connectionOptions != null && (connectionOptions.Authentication == SqlAuthenticationMethod.SqlPassword || connectionOptions.Authentication == SqlAuthenticationMethod.ActiveDirectoryPassword) && @@ -1833,13 +1842,13 @@ private bool TryOpen(TaskCompletionSource retry) { if (_impersonateIdentity.User == identity.User) { - return TryOpenInner(retry); + result = TryOpenInner(retry); } else { using (WindowsImpersonationContext context = _impersonateIdentity.Impersonate()) { - return TryOpenInner(retry); + result = TryOpenInner(retry); } } } @@ -1854,8 +1863,13 @@ private bool TryOpen(TaskCompletionSource retry) { _lastIdentity = null; } - return TryOpenInner(retry); + result = TryOpenInner(retry); } + + // Set future transient fault handling based on connection options + _applyTransientFaultHandling = (retry == null && connectionOptions != null && connectionOptions.ConnectRetryCount > 0); + + return result; } private bool TryOpenInner(TaskCompletionSource retry) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs index afcb377d03..204397478d 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsEnums.cs @@ -1023,6 +1023,16 @@ public enum SqlConnectionColumnEncryptionSetting Enabled, } + /// + [Flags] + public enum SqlConnectionOverrides + { + /// + None = 0, + /// + OpenWithoutRetry = 1, + } + /// public enum SqlCommandColumnEncryptionSetting { diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs index 7616a0cb4d..6c049b422d 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs @@ -304,5 +304,38 @@ public static void ConnectionStringPersistentInfoTest() Assert.True(connectionStringBuilder.Password != string.Empty, "Password must persist according to set the PersistSecurityInfo by true!"); } } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void ConnectionOpenDisableRetry() + { + using (SqlConnection sqlConnection = new SqlConnection((new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString) { InitialCatalog = "DoesNotExist0982532435423", Pooling = false }).ConnectionString)) + { + TimeSpan duration; + DateTime start = DateTime.Now; + try + { + sqlConnection.Open(SqlConnectionOverrides.OpenWithoutRetry); + Assert.True(false, "Connection succeeded to database that should not exist."); + } + catch (SqlException) + { + duration = DateTime.Now - start; + Assert.True(duration.TotalSeconds < 2, $"Connection Open() without retries took longer than expected. Expected < 2 sec. Took {duration.TotalSeconds} sec."); + } + + start = DateTime.Now; + try + { + sqlConnection.Open(); + Assert.True(false, "Connection succeeded to database that should not exist."); + } + catch (SqlException) + { + duration = DateTime.Now - start; + //Should not fail fast due to transient fault handling when DB does not exist + Assert.True(duration.TotalSeconds > 5, $"Connection Open() with retries took less time than expected. Expect > 5 sec with transient fault handling. Took {duration.TotalSeconds} sec."); + } + } + } } }