c# - 使用异步/等待的 TransactionScope 登记

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

原始问题

对于命中数据库的集成测试,我一直在 NUnit SetUp 方法中设置一个 TransactionScope 并在 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);
        }
    }
}

最佳答案

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

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

关于c# - 使用异步/等待的 TransactionScope 登记,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42100847/

相关文章:

c# - 列表框转换为数组 int

sql-server - SSMS T-SQL 设置重复行号的列

c# - 如何从 MahApps.Metro 模板中删除窗口命令?

c# - 如何以通过 AAD 进行身份验证的 ASP.NET MVC 站点中的登录用户身份查询 Dynamics 365 CRM?

c# - 如何获取控制台应用程序的屏幕大小?

c# - C# 中隐藏的枚举值

c# - WPF 将图像添加到 Canvas 而不将它们添加为资源

c# - WCF的DataContractSerilaizer线程安全吗?

sql-server - Powershell参数路径的值为NULL

sql - 将列添加到 SQL 查询结果