原始问题
对于需要访问数据库的集成测试,我一直在NUnit的SetUp
方法中设置TransactionScope
,并在TearDown
中回滚。但是当我将测试改为使用async时,更改没有被回滚。 我将我的SetUp
从async Task
改为void
,然后它开始按预期运行。
基于案例的简短问题
在使用async/await和TransactionScope时,是否需要在与TransactionScope相同的线程上创建SqlConnection以便传播到所有后续的异步操作?
长问题
.NET添加了TransactionScopeAsyncFlowOption到TransactionScope,并将其描述为控制“与事务范围关联的环境事务是否会在线程继续执行时流动”
根据我看到的行为,似乎仍然需要在TransactionScope的根线程上实例化您的SqlConnection,否则命令不会自动注册在环境事务中。这在机械上是有道理的,只是我找不到文档中的任何信息。所以我想知道是否有人对此主题有更多了解?
这是我在尝试弄清楚我的具体问题时得出的测试用例(使用NUnit和Dapper),我的问题是由于我的第二个连接没有注册在事务中,因此表被锁定而导致超时。
与NUnit相关的附注:如果您正在测试异步代码并且想要在TransactionScope中运行所有内容,请不要使您的[SetUp]方法成为async Task。如果这样做,它可能会在与实际测试方法不同的线程上运行,并且您的连接将不会在事务中注册。
public class SqlConnectionTimeout
{
public string DatabaseName = "AsyncDeadlock_TestCase";
public string ConnectionString = "";
[Test, Explicit]
public void _RecreateDatabase()
{
using (var connection = IntegrationTestDatabase.RecreateDatabase(DatabaseName))
{
connection.Execute(@"
CREATE TABLE [dbo].[example](
[id] [int] IDENTITY(1,1) NOT NULL,
[number] [int] NOT NULL,
CONSTRAINT [PK_example] PRIMARY KEY CLUSTERED
(
[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY];
CREATE TABLE [dbo].[exampleTwo](
[id] [int] IDENTITY(1,1) NOT NULL,
[number] [int] NOT NULL,
CONSTRAINT [PK_exampleTwo] PRIMARY KEY CLUSTERED
(
[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY];
");
}
}
[Test]
public async Task Timeout()
{
TransactionScope transaction = null;
SqlConnection firstConnection = null;
Task.Factory.StartNew(() =>
{
transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
firstConnection = new SqlConnection(ConnectionString);
firstConnection.Open();
}).Wait();
using (transaction)
{
using (firstConnection)
{
using (var secondConnection = new SqlConnection(ConnectionString))
{
await secondConnection.OpenAsync();
await firstConnection.ExecuteAsync("INSERT INTO example (number) VALUES (100);");
Assert.ThrowsAsync<SqlException>(async () => await secondConnection.QueryAsync<int>(
new CommandDefinition("SELECT * FROM example", commandTimeout: 1)
));
}
}
}
}
[Test]
public async Task NoTimeout()
{
TransactionScope transaction = null;
SqlConnection firstConnection = null;
SqlConnection secondConnection = null;
Task.Factory.StartNew(() =>
{
transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
firstConnection = new SqlConnection(ConnectionString);
firstConnection.Open();
secondConnection = new SqlConnection(ConnectionString);
secondConnection.Open();
}).Wait();
using (transaction)
{
using (firstConnection)
{
using (secondConnection )
{
await firstConnection.ExecuteAsync("INSERT INTO example (number) VALUES (100);");
await secondConnection.QueryAsync<int>(
new CommandDefinition("SELECT * FROM example", commandTimeout: 1)
);
}
}
}
// verify that my connections correctly enlisted in the transaction
// and rolled back my insert
using (var thirdConnection = new SqlConnection(ConnectionString))
{
thirdConnection.Open();
var count = await thirdConnection.ExecuteScalarAsync("SELECT COUNT(*) FROM example");
Assert.AreEqual(0, count);
}
}
}
await
没有将TransactionScope
保留到同步上下文中。但是,测试代码使用Task.StartNew
在不同的线程上打开连接。没有上下文需要保留。你为什么不使用SqlConnection.OpenAsync()
呢? - Panagiotis KanavosTransactionScope
的块和后来的using
块链接 - 因此,在这里不应该期望此选项产生任何影响。换句话说,您在StartNew
lambda中创建了一个环境事务,但周围的代码中从未存在过环境事务。" - Damien_The_Unbeliever