我遇到了一个关于TransactionScope和异步/同步SQL调用的奇怪情况,很难理解。希望对这些操作有更深入了解的人能够解决这个问题。
情况: 我有一个NUnit测试固件,在[SetUp]期间创建TransactionScope,并在[TearDown]处释放它,以便每个测试都在相同的数据上运行。我有一系列的测试,其中会在数据库上启动异步操作,然后执行同步操作。第一个测试成功完成。第二个测试失败并显示"There is already an open DataReader associated with this Command which must be closed first."。
- 如果完全注释掉TransactionScope,则所有测试都通过。
- 我尝试了各种不同的TransactionScope选项和Complete/Dispose,但仍然出现相同的问题。
- 我正在使用Resharper测试运行器运行NUnit测试,.NET 4.5.1。
- 我知道“正确”的答案可能是“使一切都异步等待”。不幸的是,这对我来说不是一种选择。
- 我不想启用MARS,因为这个问题只在测试中出现。
- 由于潜在的死锁问题,我不想使用GetAwaiter().GetResult()。
在我的看法中,一旦调用TransactionScope.Dispose/Complete,自动SQLConnection池就会失去跟踪哪些连接有打开的DataReaders。它将同一个SqlConnection分配给两个同时运行的操作,并且第二个操作失败。
我主要的问题是“是什么导致了这种行为(具体来说是什么)?”
我的次要问题是“是否有任何安全的解决方法可以解决这个问题?”
下面的可复制代码打印出客户端连接Id。在我的机器上,第二个测试用例中的ASYNC和SYNC调用的ClientConnectionId始终相同。
可复制代码:
[TestFixture]
public class DataReaderTests
{
private TransactionScope _scope;
private string _connString = @"my connection string";
[SetUp]
public void Setup()
{
var options = new TransactionOptions()
{
IsolationLevel = IsolationLevel.ReadCommitted,
Timeout = TimeSpan.FromMinutes(1)
};
_scope = new TransactionScope(TransactionScopeOption.RequiresNew, options, TransactionScopeAsyncFlowOption.Enabled);
}
[Test]
[TestCase("First")]
[TestCase("Second")]
public void Test(string name)
{
DoAsyncThing().ConfigureAwait(false);
using (var conn = new SqlConnection(_connString))
{
try
{
conn.Open();
Console.WriteLine("SYNC: " + conn.ClientConnectionId);
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "SELECT 1";
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
int id = reader.GetInt32(0);
}
}
}
}
catch (TransactionAbortedException tax)
{
Console.WriteLine("ERROR: " + ((SqlException)tax.InnerException.InnerException).ClientConnectionId);
throw;
}
}
}
private async Task DoAsyncThing()
{
using (var connection = new SqlConnection(_connString))
{
await connection.OpenAsync();
Console.WriteLine("ASYNC: " + connection.ClientConnectionId);
using (var cmd = connection.CreateCommand())
{
cmd.CommandText = "WAITFOR DELAY '00:02';";
await cmd.ExecuteNonQueryAsync();
Console.WriteLine("ASYNC COMPLETE");
}
}
}
[TearDown]
public void Teardown()
{
_scope.Dispose();
}
}`