Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 05137b9

Browse files
authored
Throw exception on enlisting SqlConnection in transaction (#19968) (#20116)
* Adding the Enlist Keyword to Connection String * adding error message for Ambient enlistment * adding tests * Skip Netfx tests and address PR feedback
1 parent 525bf83 commit 05137b9

File tree

9 files changed

+168
-8
lines changed

9 files changed

+168
-8
lines changed

src/System.Data.SqlClient/src/Resources/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,4 +1099,7 @@
10991099
<data name="MDF_UnableToBuildCollection" xml:space="preserve">
11001100
<value>Unable to build schema collection '{0}';</value>
11011101
</data>
1102+
<data name="AmbientTransactionsNotSupported" xml:space="preserve">
1103+
<value>Enlisting in Ambient transactions is not supported.</value>
1104+
</data>
11021105
</root>

src/System.Data.SqlClient/src/System/Data/Common/AdapterUtil.SqlClient.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Globalization;
88
using System.IO;
99
using System.Runtime.CompilerServices;
10+
using System.Transactions;
1011

1112
namespace System
1213
{
@@ -671,6 +672,11 @@ internal static string MachineName()
671672
return Environment.MachineName;
672673
}
673674

675+
internal static Transaction GetCurrentTransaction()
676+
{
677+
return Transaction.Current;
678+
}
679+
674680
internal static bool IsDirection(DbParameter value, ParameterDirection condition)
675681
{
676682
#if DEBUG
@@ -733,6 +739,10 @@ internal static void IsNullOrSqlType(object value, out bool isNull, out bool isS
733739
}
734740
}
735741

742+
internal static Exception AmbientTransactionIsNotSupported()
743+
{
744+
return new NotSupportedException(SR.AmbientTransactionsNotSupported);
745+
}
736746

737747
private static Version s_systemDataVersion;
738748

src/System.Data.SqlClient/src/System/Data/Common/DbConnectionStringCommon.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ internal static class DbConnectionStringDefaults
238238
internal const string CurrentLanguage = "";
239239
internal const string DataSource = "";
240240
internal const bool Encrypt = false;
241+
internal const bool Enlist = true;
241242
internal const string FailoverPartner = "";
242243
internal const string InitialCatalog = "";
243244
internal const bool IntegratedSecurity = false;

src/System.Data.SqlClient/src/System/Data/SqlClient/SqlConnection.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
using System.Threading;
1010
using System.Threading.Tasks;
1111
using System.Diagnostics.CodeAnalysis;
12-
13-
12+
using System.Transactions;
1413

1514
namespace System.Data.SqlClient
1615
{
@@ -608,6 +607,11 @@ override public void Open()
608607
}
609608
}
610609

610+
public override void EnlistTransaction(Transaction transaction)
611+
{
612+
throw ADP.AmbientTransactionIsNotSupported();
613+
}
614+
611615
internal void RegisterWaitingForReconnect(Task waitingTask)
612616
{
613617
if (((SqlConnectionString)ConnectionOptions).MARS)
@@ -1002,6 +1006,13 @@ private void PrepareStatisticsForNewConnection()
10021006
private bool TryOpen(TaskCompletionSource<DbConnectionInternal> retry)
10031007
{
10041008
SqlConnectionString connectionOptions = (SqlConnectionString)ConnectionOptions;
1009+
1010+
// Fail Fast in case an application is trying to enlist the SqlConnection in a Transaction Scope.
1011+
if (connectionOptions.Enlist && ADP.GetCurrentTransaction() != null)
1012+
{
1013+
throw ADP.AmbientTransactionIsNotSupported();
1014+
}
1015+
10051016
_applyTransientFaultHandling = (retry == null && connectionOptions != null && connectionOptions.ConnectRetryCount > 0);
10061017

10071018
if (ForceNewConnection)

src/System.Data.SqlClient/src/System/Data/SqlClient/SqlConnectionFactory.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ override protected DbConnectionInternal CreateConnection(DbConnectionOptions opt
9494
// NOTE: Cloning connection option opt to set 'UserInstance=True' and 'Enlist=False'
9595
// This first connection is established to SqlExpress to get the instance name
9696
// of the UserInstance.
97-
SqlConnectionString sseopt = new SqlConnectionString(opt, opt.DataSource, true /* user instance=true */);
97+
SqlConnectionString sseopt = new SqlConnectionString(opt, opt.DataSource, userInstance: true, setEnlistValue: false);
9898
sseConnection = new SqlInternalConnectionTds(identity, sseopt, null, false, applyTransientFaultHandling: applyTransientFaultHandling);
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;
@@ -129,7 +129,7 @@ override protected DbConnectionInternal CreateConnection(DbConnectionOptions opt
129129
// NOTE: Here connection option opt is cloned to set 'instanceName=<UserInstanceName>' that was
130130
// retrieved from the previous SSE connection. For this UserInstance connection 'Enlist=True'.
131131
// options immutable - stored in global hash - don't modify
132-
opt = new SqlConnectionString(opt, instanceName, false /* user instance=false */);
132+
opt = new SqlConnectionString(opt, instanceName, userInstance: false, setEnlistValue: null);
133133
poolGroupProviderInfo = null; // null so we do not pass to constructor below...
134134
}
135135
result = new SqlInternalConnectionTds(identity, opt, poolGroupProviderInfo, redirectedUserInstance, userOpt, recoverySessionData, applyTransientFaultHandling: applyTransientFaultHandling);

src/System.Data.SqlClient/src/System/Data/SqlClient/SqlConnectionString.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ internal static class DEFAULT
2929
internal const string Current_Language = "";
3030
internal const string Data_Source = "";
3131
internal const bool Encrypt = false;
32+
internal const bool Enlist = true;
3233
internal const string FailoverPartner = "";
3334
internal const string Initial_Catalog = "";
3435
internal const bool Integrated_Security = false;
@@ -159,6 +160,7 @@ internal static class TYPESYSTEMVERSION
159160

160161
private readonly bool _encrypt;
161162
private readonly bool _trustServerCertificate;
163+
private readonly bool _enlist;
162164
private readonly bool _mars;
163165
private readonly bool _persistSecurityInfo;
164166
private readonly bool _pooling;
@@ -193,7 +195,6 @@ internal SqlConnectionString(string connectionString) : base(connectionString, G
193195
ThrowUnsupportedIfKeywordSet(KEY.AsynchronousProcessing);
194196
ThrowUnsupportedIfKeywordSet(KEY.Connection_Reset);
195197
ThrowUnsupportedIfKeywordSet(KEY.Context_Connection);
196-
ThrowUnsupportedIfKeywordSet(KEY.Enlist);
197198
ThrowUnsupportedIfKeywordSet(KEY.TransactionBinding);
198199

199200
// Network Library has its own special error message
@@ -204,6 +205,7 @@ internal SqlConnectionString(string connectionString) : base(connectionString, G
204205

205206
_integratedSecurity = ConvertValueToIntegratedSecurity();
206207
_encrypt = ConvertValueToBoolean(KEY.Encrypt, DEFAULT.Encrypt);
208+
_enlist = ConvertValueToBoolean(KEY.Enlist, DEFAULT.Enlist);
207209
_mars = ConvertValueToBoolean(KEY.MARS, DEFAULT.MARS);
208210
_persistSecurityInfo = ConvertValueToBoolean(KEY.Persist_Security_Info, DEFAULT.Persist_Security_Info);
209211
_pooling = ConvertValueToBoolean(KEY.Pooling, DEFAULT.Pooling);
@@ -357,11 +359,20 @@ internal SqlConnectionString(string connectionString) : base(connectionString, G
357359

358360
// This c-tor is used to create SSE and user instance connection strings when user instance is set to true
359361
// BUG (VSTFDevDiv) 479687: Using TransactionScope with Linq2SQL against user instances fails with "connection has been broken" message
360-
internal SqlConnectionString(SqlConnectionString connectionOptions, string dataSource, bool userInstance) : base(connectionOptions)
362+
internal SqlConnectionString(SqlConnectionString connectionOptions, string dataSource, bool userInstance, bool? setEnlistValue) : base(connectionOptions)
361363
{
362364
_integratedSecurity = connectionOptions._integratedSecurity;
363365
_encrypt = connectionOptions._encrypt;
364366

367+
if (setEnlistValue.HasValue)
368+
{
369+
_enlist = setEnlistValue.Value;
370+
}
371+
else
372+
{
373+
_enlist = connectionOptions._enlist;
374+
}
375+
365376
_mars = connectionOptions._mars;
366377
_persistSecurityInfo = connectionOptions._persistSecurityInfo;
367378
_pooling = connectionOptions._pooling;
@@ -402,6 +413,7 @@ internal SqlConnectionString(SqlConnectionString connectionOptions, string dataS
402413
// internal bool EnableUdtDownload { get { return _enableUdtDownload;} }
403414
internal bool Encrypt { get { return _encrypt; } }
404415
internal bool TrustServerCertificate { get { return _trustServerCertificate; } }
416+
internal bool Enlist { get { return _enlist; } }
405417
internal bool MARS { get { return _mars; } }
406418
internal bool MultiSubnetFailover { get { return _multiSubnetFailover; } }
407419

src/System.Data.SqlClient/src/System/Data/SqlClient/SqlConnectionStringBuilder.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ private enum Keywords
3131
UserID,
3232
Password,
3333

34+
Enlist,
3435
Pooling,
3536
MinPoolSize,
3637
MaxPoolSize,
@@ -65,7 +66,7 @@ private enum Keywords
6566
}
6667

6768
internal const int KeywordsCount = (int)Keywords.KeywordsCount;
68-
internal const int DeprecatedKeywordsCount = 6;
69+
internal const int DeprecatedKeywordsCount = 5;
6970

7071
private static readonly string[] s_validKeywords = CreateValidKeywords();
7172
private static readonly Dictionary<string, Keywords> s_keywords = CreateKeywordsDictionary();
@@ -93,6 +94,7 @@ private enum Keywords
9394

9495
private bool _encrypt = DbConnectionStringDefaults.Encrypt;
9596
private bool _trustServerCertificate = DbConnectionStringDefaults.TrustServerCertificate;
97+
private bool _enlist = DbConnectionStringDefaults.Enlist;
9698
private bool _integratedSecurity = DbConnectionStringDefaults.IntegratedSecurity;
9799
private bool _multipleActiveResultSets = DbConnectionStringDefaults.MultipleActiveResultSets;
98100
private bool _multiSubnetFailover = DbConnectionStringDefaults.MultiSubnetFailover;
@@ -111,6 +113,7 @@ private static string[] CreateValidKeywords()
111113
validKeywords[(int)Keywords.CurrentLanguage] = DbConnectionStringKeywords.CurrentLanguage;
112114
validKeywords[(int)Keywords.DataSource] = DbConnectionStringKeywords.DataSource;
113115
validKeywords[(int)Keywords.Encrypt] = DbConnectionStringKeywords.Encrypt;
116+
validKeywords[(int)Keywords.Enlist] = DbConnectionStringKeywords.Enlist;
114117
validKeywords[(int)Keywords.FailoverPartner] = DbConnectionStringKeywords.FailoverPartner;
115118
validKeywords[(int)Keywords.InitialCatalog] = DbConnectionStringKeywords.InitialCatalog;
116119
validKeywords[(int)Keywords.IntegratedSecurity] = DbConnectionStringKeywords.IntegratedSecurity;
@@ -145,6 +148,7 @@ private static Dictionary<string, Keywords> CreateKeywordsDictionary()
145148
hash.Add(DbConnectionStringKeywords.CurrentLanguage, Keywords.CurrentLanguage);
146149
hash.Add(DbConnectionStringKeywords.DataSource, Keywords.DataSource);
147150
hash.Add(DbConnectionStringKeywords.Encrypt, Keywords.Encrypt);
151+
hash.Add(DbConnectionStringKeywords.Enlist, Keywords.Enlist);
148152
hash.Add(DbConnectionStringKeywords.FailoverPartner, Keywords.FailoverPartner);
149153
hash.Add(DbConnectionStringKeywords.InitialCatalog, Keywords.InitialCatalog);
150154
hash.Add(DbConnectionStringKeywords.IntegratedSecurity, Keywords.IntegratedSecurity);
@@ -238,6 +242,7 @@ public override object this[string keyword]
238242

239243
case Keywords.Encrypt: Encrypt = ConvertToBoolean(value); break;
240244
case Keywords.TrustServerCertificate: TrustServerCertificate = ConvertToBoolean(value); break;
245+
case Keywords.Enlist: Enlist = ConvertToBoolean(value); break;
241246
case Keywords.MultipleActiveResultSets: MultipleActiveResultSets = ConvertToBoolean(value); break;
242247
case Keywords.MultiSubnetFailover: MultiSubnetFailover = ConvertToBoolean(value); break;
243248
case Keywords.PersistSecurityInfo: PersistSecurityInfo = ConvertToBoolean(value); break;
@@ -348,6 +353,16 @@ public bool TrustServerCertificate
348353
}
349354
}
350355

356+
public bool Enlist
357+
{
358+
get { return _enlist; }
359+
set
360+
{
361+
SetValue(DbConnectionStringKeywords.Enlist, value);
362+
_enlist = value;
363+
}
364+
}
365+
351366
public string FailoverPartner
352367
{
353368
get { return _failoverPartner; }
@@ -649,6 +664,7 @@ private object GetAt(Keywords index)
649664
case Keywords.CurrentLanguage: return CurrentLanguage;
650665
case Keywords.DataSource: return DataSource;
651666
case Keywords.Encrypt: return Encrypt;
667+
case Keywords.Enlist: return Enlist;
652668
case Keywords.FailoverPartner: return FailoverPartner;
653669
case Keywords.InitialCatalog: return InitialCatalog;
654670
case Keywords.IntegratedSecurity: return IntegratedSecurity;
@@ -729,6 +745,9 @@ private void Reset(Keywords index)
729745
case Keywords.Encrypt:
730746
_encrypt = DbConnectionStringDefaults.Encrypt;
731747
break;
748+
case Keywords.Enlist:
749+
_enlist = DbConnectionStringDefaults.Enlist;
750+
break;
732751
case Keywords.FailoverPartner:
733752
_failoverPartner = DbConnectionStringDefaults.FailoverPartner;
734753
break;
@@ -840,7 +859,6 @@ public override bool TryGetValue(string keyword, out object value)
840859
DbConnectionStringKeywords.AsynchronousProcessing,
841860
DbConnectionStringKeywords.ConnectionReset,
842861
DbConnectionStringKeywords.ContextConnection,
843-
DbConnectionStringKeywords.Enlist,
844862
DbConnectionStringKeywords.TransactionBinding,
845863

846864
DbConnectionStringSynonyms.Async
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Threading.Tasks;
6+
using System.Transactions;
7+
using Xunit;
8+
9+
namespace System.Data.SqlClient.Tests
10+
{
11+
public class AmbientTransactionFailureTest
12+
{
13+
14+
private static readonly string s_servername = Guid.NewGuid().ToString();
15+
private static readonly string s_connectionStringWithEnlistAsDefault = $"Data Source={s_servername}; Integrated Security=true; Connect Timeout=1;";
16+
private static readonly string s_connectionStringWithEnlistOff = $"Data Source={s_servername}; Integrated Security=true; Connect Timeout=1;Enlist=False";
17+
18+
private static Action<string> ConnectToServer = (connectionString) =>
19+
{
20+
using (SqlConnection connection = new SqlConnection(connectionString))
21+
{
22+
connection.Open();
23+
}
24+
};
25+
26+
private static Action<string> ConnectToServerTask = (connectionString) =>
27+
{
28+
using (SqlConnection connection = new SqlConnection(connectionString))
29+
{
30+
connection.OpenAsync();
31+
}
32+
};
33+
34+
private static Func<string, Task> ConnectToServerInTransactionScopeTask = (connectionString) =>
35+
{
36+
return Task.Run(() =>
37+
{
38+
using (TransactionScope scope = new TransactionScope())
39+
{
40+
ConnectToServerTask(connectionString);
41+
}
42+
});
43+
};
44+
45+
private static Action<string> ConnectToServerInTransactionScope = (connectionString) =>
46+
{
47+
using (TransactionScope scope = new TransactionScope())
48+
{
49+
ConnectToServer(connectionString);
50+
}
51+
};
52+
53+
private static Action<string> EnlistConnectionInTransaction = (connectionString) =>
54+
{
55+
using (TransactionScope scope = new TransactionScope())
56+
{
57+
SqlConnection connection = new SqlConnection(connectionString);
58+
connection.EnlistTransaction(Transaction.Current);
59+
}
60+
};
61+
62+
public static readonly object[][] ExceptionTestDataForSqlException =
63+
{
64+
new object[] { ConnectToServerInTransactionScope, s_connectionStringWithEnlistOff },
65+
new object[] { ConnectToServer, s_connectionStringWithEnlistAsDefault }
66+
};
67+
68+
public static readonly object[][] ExceptionTestDataForNotSupportedException =
69+
{
70+
new object[] { ConnectToServerInTransactionScope, s_connectionStringWithEnlistAsDefault },
71+
new object[] { EnlistConnectionInTransaction, s_connectionStringWithEnlistAsDefault },
72+
new object[] { EnlistConnectionInTransaction, s_connectionStringWithEnlistOff }
73+
};
74+
75+
[Theory]
76+
[MemberData(nameof(ExceptionTestDataForSqlException))]
77+
public void TestSqlException(Action<string> connectAction, string connectionString)
78+
{
79+
Assert.Throws<SqlException>(() =>
80+
{
81+
connectAction(connectionString);
82+
});
83+
}
84+
85+
[Theory]
86+
[MemberData(nameof(ExceptionTestDataForNotSupportedException))]
87+
[SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, ".NET Framework doesn't throw the Not Supported Exception")]
88+
public void TestNotSupportedException(Action<string> connectAction, string connectionString)
89+
{
90+
Assert.Throws<NotSupportedException>(() =>
91+
{
92+
connectAction(connectionString);
93+
});
94+
}
95+
96+
[Fact]
97+
[SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, ".NET Framework doesn't throw the Not Supported Exception")]
98+
public void TestNotSupportedExceptionForTransactionScopeAsync()
99+
{
100+
Assert.ThrowsAsync<NotSupportedException>(() => ConnectToServerInTransactionScopeTask(s_connectionStringWithEnlistAsDefault));
101+
}
102+
103+
}
104+
}

src/System.Data.SqlClient/tests/FunctionalTests/System.Data.SqlClient.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<Compile Include="BaseProviderAsyncTest\MockConnection.cs" />
1616
<Compile Include="BaseProviderAsyncTest\MockDataReader.cs" />
1717
<Compile Include="DiagnosticTest.cs" />
18+
<Compile Include="AmbientTransactionFailureTest.cs" />
1819
<Compile Include="ExceptionTest.cs" />
1920
<Compile Include="FakeDiagnosticListenerObserver.cs" />
2021
<Compile Include="SqlBulkCopyColumnMappingCollectionTest.cs" />

0 commit comments

Comments
 (0)