Skip to content

Commit 59dcb78

Browse files
Add support for SqlConnectionOverrides for OpenAsync() API (#2433)
1 parent a6f9ce3 commit 59dcb78

File tree

8 files changed

+105
-29
lines changed

8 files changed

+105
-29
lines changed

doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2853,6 +2853,39 @@
28532853
Any error returned by SQL Server that occurred while opening the connection.
28542854
</exception>
28552855
</OpenAsync>
2856+
<OpenAsyncWithOverrides>
2857+
<param name="overrides">
2858+
Options to override default connection open behavior.
2859+
</param>
2860+
<param name="cancellationToken">
2861+
The cancellation instruction.
2862+
</param>
2863+
<summary>
2864+
An asynchronous version of <see cref="M:Microsoft.Data.SqlClient.SqlConnection.Open" />, which opens a database connection with the property settings specified by the <see cref="P:Microsoft.Data.SqlClient.SqlConnection.ConnectionString" />. 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.
2865+
</summary>
2866+
<returns>
2867+
A task representing the asynchronous operation.
2868+
</returns>
2869+
<remarks>
2870+
<para>
2871+
After calling <see cref="M:Microsoft.Data.SqlClient.SqlConnection.OpenAsync" />, <see cref="P:Microsoft.Data.SqlClient.SqlConnection.State" /> must return <see cref="F:System.Data.ConnectionState.Connecting" /> until the returned <see cref="T:System.Threading.Tasks.Task" /> is completed. Then, if the connection was successful, <see cref="P:Microsoft.Data.SqlClient.SqlConnection.State" /> must return <see cref="F:System.Data.ConnectionState.Open" />. If the connection fails, <see cref="P:Microsoft.Data.SqlClient.SqlConnection.State" /> must return <see cref="F:System.Data.ConnectionState.Closed" />.
2872+
</para>
2873+
<para>
2874+
A call to <see cref="M:Microsoft.Data.SqlClient.SqlConnection.Close" /> will attempt to cancel or close the corresponding <see cref="M:Microsoft.Data.SqlClient.SqlConnection.OpenAsync" /> call. For more information about asynchronous programming in the .NET Framework Data Provider for SQL Server, see <see href="/sql/connect/ado-net/asynchronous-programming">Asynchronous Programming</see>.
2875+
</para>
2876+
</remarks>
2877+
<exception cref="T:System.InvalidOperationException">
2878+
<para>
2879+
Calling <see cref="M:Microsoft.Data.SqlClient.SqlConnection.OpenAsync(Microsoft.Data.SqlClient.SqlConnectionOverrides, System.Threading.CancellationToken)" /> more than once for the same instance before task completion.
2880+
</para>
2881+
<para>
2882+
A connection was not available from the connection pool before the connection time out elapsed.
2883+
</para>
2884+
</exception>
2885+
<exception cref="T:Microsoft.Data.SqlClient.SqlException">
2886+
Any error returned by SQL Server that occurred while opening the connection.
2887+
</exception>
2888+
</OpenAsyncWithOverrides>
28562889
<PacketSize>
28572890
<summary>
28582891
Gets the size (in bytes) of network packets used to communicate with an instance of SQL Server.

src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,8 @@ public override void Open() { }
975975
public void Open(SqlConnectionOverrides overrides) { }
976976
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/OpenAsync/*'/>
977977
public override System.Threading.Tasks.Task OpenAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
978+
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/OpenAsyncWithOverrides/*'/>
979+
public System.Threading.Tasks.Task OpenAsync(Microsoft.Data.SqlClient.SqlConnectionOverrides overrides, System.Threading.CancellationToken cancellationToken) { throw null; }
978980
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/ResetStatistics/*'/>
979981
public void ResetStatistics() { }
980982
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/RetrieveStatistics/*'/>

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnection.cs

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1662,16 +1662,20 @@ private void CancelOpenAndWait()
16621662
Debug.Assert(_currentCompletion == null, "After waiting for an async call to complete, there should be no completion source");
16631663
}
16641664

1665-
private Task InternalOpenWithRetryAsync(CancellationToken cancellationToken)
1666-
=> RetryLogicProvider.ExecuteAsync(this, () => InternalOpenAsync(cancellationToken), cancellationToken);
1667-
16681665
/// <include file='../../../../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/OpenAsync/*' />
1669-
public override Task OpenAsync(CancellationToken cancellationToken)
1666+
public override Task OpenAsync(CancellationToken cancellationToken)
1667+
=> OpenAsync(SqlConnectionOverrides.None, cancellationToken);
1668+
1669+
/// <include file='../../../../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/OpenAsyncWithOverrides/*' />
1670+
public Task OpenAsync(SqlConnectionOverrides overrides, CancellationToken cancellationToken)
16701671
=> IsProviderRetriable ?
1671-
InternalOpenWithRetryAsync(cancellationToken) :
1672-
InternalOpenAsync(cancellationToken);
1672+
InternalOpenWithRetryAsync(overrides, cancellationToken) :
1673+
InternalOpenAsync(overrides, cancellationToken);
16731674

1674-
private Task InternalOpenAsync(CancellationToken cancellationToken)
1675+
private Task InternalOpenWithRetryAsync(SqlConnectionOverrides overrides, CancellationToken cancellationToken)
1676+
=> RetryLogicProvider.ExecuteAsync(this, () => InternalOpenAsync(overrides, cancellationToken), cancellationToken);
1677+
1678+
private Task InternalOpenAsync(SqlConnectionOverrides overrides, CancellationToken cancellationToken)
16751679
{
16761680
long scopeID = SqlClientEventSource.Log.TryPoolerScopeEnterEvent("SqlConnection.InternalOpenAsync | API | Object Id {0}", ObjectID);
16771681
SqlClientEventSource.Log.TryCorrelationTraceEvent("SqlConnection.InternalOpenAsync | API | Correlation | Object Id {0}, Activity Id {1}", ObjectID, ActivityCorrelator.Current);
@@ -1710,7 +1714,7 @@ private Task InternalOpenAsync(CancellationToken cancellationToken)
17101714

17111715
try
17121716
{
1713-
completed = TryOpen(completion);
1717+
completed = TryOpen(completion, overrides);
17141718
}
17151719
catch (Exception e)
17161720
{
@@ -1730,7 +1734,7 @@ private Task InternalOpenAsync(CancellationToken cancellationToken)
17301734
{
17311735
registration = cancellationToken.Register(s_openAsyncCancel, completion);
17321736
}
1733-
OpenAsyncRetry retry = new OpenAsyncRetry(this, completion, result, registration);
1737+
OpenAsyncRetry retry = new OpenAsyncRetry(this, completion, result, overrides, registration);
17341738
_currentCompletion = new Tuple<TaskCompletionSource<DbConnectionInternal>, Task>(completion, result.Task);
17351739
completion.Task.ContinueWith(retry.Retry, TaskScheduler.Default);
17361740
return result.Task;
@@ -1805,13 +1809,15 @@ private class OpenAsyncRetry
18051809
private SqlConnection _parent;
18061810
private TaskCompletionSource<DbConnectionInternal> _retry;
18071811
private TaskCompletionSource<object> _result;
1812+
private SqlConnectionOverrides _overrides;
18081813
private CancellationTokenRegistration _registration;
18091814

1810-
public OpenAsyncRetry(SqlConnection parent, TaskCompletionSource<DbConnectionInternal> retry, TaskCompletionSource<object> result, CancellationTokenRegistration registration)
1815+
public OpenAsyncRetry(SqlConnection parent, TaskCompletionSource<DbConnectionInternal> retry, TaskCompletionSource<object> result, SqlConnectionOverrides overrides, CancellationTokenRegistration registration)
18111816
{
18121817
_parent = parent;
18131818
_retry = retry;
18141819
_result = result;
1820+
_overrides = overrides;
18151821
_registration = registration;
18161822
SqlClientEventSource.Log.TryTraceEvent("SqlConnection.OpenAsyncRetry | Info | Object Id {0}", _parent?.ObjectID);
18171823
}
@@ -1846,7 +1852,7 @@ internal void Retry(Task<DbConnectionInternal> retryTask)
18461852
// protect continuation from races with close and cancel
18471853
lock (_parent.InnerConnection)
18481854
{
1849-
result = _parent.TryOpen(_retry);
1855+
result = _parent.TryOpen(_retry, _overrides);
18501856
}
18511857
if (result)
18521858
{
@@ -1968,9 +1974,6 @@ private bool TryOpen(TaskCompletionSource<DbConnectionInternal> retry, SqlConnec
19681974
}
19691975
// does not require GC.KeepAlive(this) because of ReRegisterForFinalize below.
19701976

1971-
// Set future transient fault handling based on connection options
1972-
_applyTransientFaultHandling = connectionOptions != null && connectionOptions.ConnectRetryCount > 0;
1973-
19741977
var tdsInnerConnection = (SqlInternalConnectionTds)InnerConnection;
19751978

19761979
Debug.Assert(tdsInnerConnection.Parser != null, "Where's the parser?");

src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ override protected DbConnectionInternal CreateConnection(DbConnectionOptions opt
9999
// NOTE: Retrieve <UserInstanceName> here. This user instance name will be used below to connect to the Sql Express User Instance.
100100
instanceName = sseConnection.InstanceName;
101101

102+
// Set future transient fault handling based on connection options
103+
sqlOwningConnection._applyTransientFaultHandling = opt != null && opt.ConnectRetryCount > 0;
104+
102105
if (!instanceName.StartsWith("\\\\.\\", StringComparison.Ordinal))
103106
{
104107
throw SQL.NonLocalSSEInstance();

src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,8 @@ public override void Open() { }
885885
public void Open(SqlConnectionOverrides overrides) { }
886886
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/OpenAsync/*'/>
887887
public override System.Threading.Tasks.Task OpenAsync(System.Threading.CancellationToken cancellationToken) { throw null; }
888+
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/OpenAsyncWithOverrides/*'/>
889+
public System.Threading.Tasks.Task OpenAsync(Microsoft.Data.SqlClient.SqlConnectionOverrides overrides, System.Threading.CancellationToken cancellationToken) { throw null; }
888890
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/RegisterColumnEncryptionKeyStoreProviders/*'/>
889891
public static void RegisterColumnEncryptionKeyStoreProviders(System.Collections.Generic.IDictionary<string, Microsoft.Data.SqlClient.SqlColumnEncryptionKeyStoreProvider> customProviders) { }
890892
/// <include file='../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/RegisterColumnEncryptionKeyStoreProvidersOnConnection/*' />

src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnection.cs

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1922,16 +1922,20 @@ void CancelOpenAndWait()
19221922
Debug.Assert(_currentCompletion == null, "After waiting for an async call to complete, there should be no completion source");
19231923
}
19241924

1925-
private Task InternalOpenWithRetryAsync(CancellationToken cancellationToken)
1926-
=> RetryLogicProvider.ExecuteAsync(this, () => InternalOpenAsync(cancellationToken), cancellationToken);
1927-
19281925
/// <include file='../../../../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/OpenAsync/*' />
19291926
public override Task OpenAsync(CancellationToken cancellationToken)
1927+
=> OpenAsync(SqlConnectionOverrides.None, cancellationToken);
1928+
1929+
/// <include file='../../../../../../../doc/snippets/Microsoft.Data.SqlClient/SqlConnection.xml' path='docs/members[@name="SqlConnection"]/OpenAsyncWithOverrides/*' />
1930+
public Task OpenAsync(SqlConnectionOverrides overrides, CancellationToken cancellationToken)
19301931
=> IsProviderRetriable ?
1931-
InternalOpenWithRetryAsync(cancellationToken) :
1932-
InternalOpenAsync(cancellationToken);
1932+
InternalOpenWithRetryAsync(overrides, cancellationToken) :
1933+
InternalOpenAsync(overrides, cancellationToken);
19331934

1934-
private Task InternalOpenAsync(CancellationToken cancellationToken)
1935+
private Task InternalOpenWithRetryAsync(SqlConnectionOverrides overrides, CancellationToken cancellationToken)
1936+
=> RetryLogicProvider.ExecuteAsync(this, () => InternalOpenAsync(overrides, cancellationToken), cancellationToken);
1937+
1938+
private Task InternalOpenAsync(SqlConnectionOverrides overrides, CancellationToken cancellationToken)
19351939
{
19361940
long scopeID = SqlClientEventSource.Log.TryPoolerScopeEnterEvent("<sc.SqlConnection.OpenAsync|API> {0}", ObjectID);
19371941
SqlClientEventSource.Log.TryCorrelationTraceEvent("<sc.SqlConnection.OpenAsync|API|Correlation> ObjectID {0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current);
@@ -1977,7 +1981,7 @@ private Task InternalOpenAsync(CancellationToken cancellationToken)
19771981

19781982
try
19791983
{
1980-
completed = TryOpen(completion);
1984+
completed = TryOpen(completion, overrides);
19811985
}
19821986
catch (Exception e)
19831987
{
@@ -1996,7 +2000,7 @@ private Task InternalOpenAsync(CancellationToken cancellationToken)
19962000
{
19972001
registration = cancellationToken.Register(() => completion.TrySetCanceled());
19982002
}
1999-
OpenAsyncRetry retry = new OpenAsyncRetry(this, completion, result, registration);
2003+
OpenAsyncRetry retry = new OpenAsyncRetry(this, completion, result, overrides, registration);
20002004
_currentCompletion = new Tuple<TaskCompletionSource<DbConnectionInternal>, Task>(completion, result.Task);
20012005
completion.Task.ContinueWith(retry.Retry, TaskScheduler.Default);
20022006
return result.Task;
@@ -2020,13 +2024,15 @@ private class OpenAsyncRetry
20202024
SqlConnection _parent;
20212025
TaskCompletionSource<DbConnectionInternal> _retry;
20222026
TaskCompletionSource<object> _result;
2027+
SqlConnectionOverrides _overrides;
20232028
CancellationTokenRegistration _registration;
20242029

2025-
public OpenAsyncRetry(SqlConnection parent, TaskCompletionSource<DbConnectionInternal> retry, TaskCompletionSource<object> result, CancellationTokenRegistration registration)
2030+
public OpenAsyncRetry(SqlConnection parent, TaskCompletionSource<DbConnectionInternal> retry, TaskCompletionSource<object> result, SqlConnectionOverrides overrides, CancellationTokenRegistration registration)
20262031
{
20272032
_parent = parent;
20282033
_retry = retry;
20292034
_result = result;
2035+
_overrides = overrides;
20302036
_registration = registration;
20312037
}
20322038

@@ -2062,7 +2068,7 @@ internal void Retry(Task<DbConnectionInternal> retryTask)
20622068
// protect continuation from races with close and cancel
20632069
lock (_parent.InnerConnection)
20642070
{
2065-
result = _parent.TryOpen(_retry);
2071+
result = _parent.TryOpen(_retry, _overrides);
20662072
}
20672073
if (result)
20682074
{
@@ -2139,9 +2145,6 @@ private bool TryOpen(TaskCompletionSource<DbConnectionInternal> retry, SqlConnec
21392145
result = TryOpenInner(retry);
21402146
}
21412147

2142-
// Set future transient fault handling based on connection options
2143-
_applyTransientFaultHandling = connectionOptions != null && connectionOptions.ConnectRetryCount > 0;
2144-
21452148
return result;
21462149
}
21472150

src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlConnectionFactory.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ override protected DbConnectionInternal CreateConnection(DbConnectionOptions opt
108108
// NOTE: Retrieve <UserInstanceName> here. This user instance name will be used below to connect to the Sql Express User Instance.
109109
instanceName = sseConnection.InstanceName;
110110

111+
// Set future transient fault handling based on connection options
112+
sqlOwningConnection._applyTransientFaultHandling = opt != null && opt.ConnectRetryCount > 0;
113+
111114
if (!instanceName.StartsWith("\\\\.\\", StringComparison.Ordinal))
112115
{
113116
throw SQL.NonLocalSSEInstance();

src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/ConnectivityTests/ConnectivityTest.cs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,8 @@ public static void ConnectionOpenDisableRetry()
369369
{
370370
InitialCatalog = "DoesNotExist0982532435423",
371371
Pooling = false,
372-
ConnectTimeout = 15
372+
ConnectTimeout = 15,
373+
ConnectRetryCount = 3
373374
};
374375
using SqlConnection sqlConnection = new(connectionStringBuilder.ConnectionString);
375376
Stopwatch timer = new();
@@ -384,7 +385,33 @@ public static void ConnectionOpenDisableRetry()
384385
Assert.Throws<SqlException>(() => sqlConnection.Open());
385386
timer.Stop();
386387
duration = timer.Elapsed;
387-
Assert.True(duration.Seconds > 5, $"Connection Open() with retries took less time than expected. Expect > 5 sec with transient fault handling. Took {duration.Seconds} sec."); // sqlConnection.Open();
388+
Assert.True(duration.Seconds > 5, $"Connection Open() with retries took less time than expected. Expect > 5 sec with transient fault handling. Took {duration.Seconds} sec."); // sqlConnection.Open();
389+
}
390+
391+
[ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer), nameof(DataTestUtility.TcpConnectionStringDoesNotUseAadAuth))]
392+
public static async Task ConnectionOpenAsyncDisableRetry()
393+
{
394+
SqlConnectionStringBuilder connectionStringBuilder = new(DataTestUtility.TCPConnectionString)
395+
{
396+
InitialCatalog = DataTestUtility.GetUniqueNameForSqlServer("DoesNotExist", false),
397+
Pooling = false,
398+
ConnectTimeout = 15,
399+
ConnectRetryCount = 3
400+
};
401+
using SqlConnection sqlConnection = new(connectionStringBuilder.ConnectionString);
402+
Stopwatch timer = new();
403+
404+
timer.Start();
405+
await Assert.ThrowsAsync<SqlException>(async () => await sqlConnection.OpenAsync(SqlConnectionOverrides.OpenWithoutRetry, CancellationToken.None));
406+
timer.Stop();
407+
TimeSpan duration = timer.Elapsed;
408+
Assert.True(duration.Seconds < 2, $"Connection OpenAsync() without retries took longer than expected. Expected < 2 sec. Took {duration.Seconds} sec.");
409+
410+
timer.Restart();
411+
await Assert.ThrowsAsync<SqlException>(async () => await sqlConnection.OpenAsync(CancellationToken.None));
412+
timer.Stop();
413+
duration = timer.Elapsed;
414+
Assert.True(duration.Seconds > 5, $"Connection OpenAsync() with retries took less time than expected. Expect > 5 sec with transient fault handling. Took {duration.Seconds} sec.");
388415
}
389416

390417
[PlatformSpecific(TestPlatforms.Windows)]

0 commit comments

Comments
 (0)