使用async / await进行TransactionScope登记

时间:2017-02-07 22:01:45

标签: c# .net sql-server async-await

原始问题

对于访问数据库的集成测试,我一直在NUnit TransactionScope方法中设置SetUp并在TearDown中回滚。当我将测试切换为对所有内容使用异步时,更改不会被回滚。我将SetUpasync Task切换为void,它开始按预期运行。

从案例

派生的简短问题

将TransactionScope与async / await一起使用时,是否需要在与TransactionScope相同的线程上创建SqlConnection,以便传播到所有后续的异步操作?

长问题

.NET将TransactionScopeAsyncFlowOption添加到TransactionScope,并将其描述为控制“与事务范围关联的环境事务是否将跨越线程延续”

根据我看到的行为,看起来你仍然需要在TransactionScope的根线程上实例化你的SqlConnections,否则命令不会在环境事务中自动登记。这有机械意义,我只是在文档中找不到它。所以我想我想知道是否有人对此主题有更多了解?

这是测试用例(使用NUnit和Dapper),当我试图弄清楚我的具体问题是怎么回事时,这是一个超时bc表被一个事务锁定我的第二个连接没有被列入在(我认为?)

NUnit相关的附注:如果您正在测试异步代码并且想要在TransactionScope中运行所有内容,请不要使[SetUp]方法成为异步任务。如果这样做,它可能会在与您的实际测试方法不同的线程上运行,并且您的连接将不会在事务中登记。

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 个答案:

答案 0 :(得分:0)

基于对我的回答的评论,特别是Panagiotis Kanavos&#39;回答:

transactionscope的async选项使TransactionScope通过同步上下文从任务传递到任务。我的问题中的测试代码在与使用Task.StartNew打开TransactionScope的不同线程上打开了连接,因此没有传递上下文,没有要传播的事务。