使用async/await进行TransactionScope注册

4

原始问题

对于需要访问数据库的集成测试,我一直在NUnit的SetUp方法中设置TransactionScope,并在TearDown中回滚。但是当我将测试改为使用async时,更改没有被回滚。 我将我的SetUpasync 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);
        }
    }
}

1
请注意您正在使用的.NET运行时版本。 - Stephen Cleary
1
TransactionScope不会影响异步操作。这个选项是必需的,因为在之前的版本中,await没有将TransactionScope保留到同步上下文中。但是,测试代码使用Task.StartNew在不同的线程上打开连接。没有上下文需要保留。你为什么不使用SqlConnection.OpenAsync()呢? - Panagiotis Kanavos
1
"与事务范围相关联的环境事务是否会在线程连续中流动 - 是的,但您的代码没有使用线程连续将创建TransactionScope的块和后来的using块链接 - 因此,在这里不应该期望此选项产生任何影响。换句话说,您在StartNew lambda中创建了一个环境事务,但周围的代码中从未存在过环境事务。" - Damien_The_Unbeliever
哦,好的,那就有道理了。也许在我的实际情况中,我的程序之所以无法正常工作,是因为 NUnit 不是将设置/测试/清理作为线程继续进行,而是作为单独的任务或其他什么原因? - Joshua Evensen
@PanagiotisKanavos在问题中添加了我原始问题的解释。我从尝试简化的测试用例开始,这可能不是最清晰的起点。同时,我感觉你和Damien已经回答了我的问题。 - Joshua Evensen
显示剩余2条评论
1个回答

0

根据对我的回答的评论,特别是Panagiotis Kanavos的回答:

事务范围的异步选项会导致TransactionScope通过同步上下文从任务传递到任务。我问题中的测试代码使用Task.StartNew在不同的线程上打开连接,因此没有上下文可以传递,也没有事务可以传播。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接