C# 如何在可取消异步/等待中处理TransactionScope?

C# 如何在可取消异步/等待中处理TransactionScope?,c#,.net,database,transactions,async-await,C#,.net,Database,Transactions,Async Await,我正在尝试使用新的async/await特性异步处理DB。由于有些请求可能很长,我希望能够取消它们。我遇到的问题是,TransactionScope显然有一个线程关联,而且在取消任务时,它的Dispose()似乎在错误的线程上运行 具体地说,当调用.TestTx()时,我在task.Wait()上得到以下aggregateeexception包含InvalidOperationException: 代码如下: public void TestTx () { var cancellatio

我正在尝试使用新的async/await特性异步处理DB。由于有些请求可能很长,我希望能够取消它们。我遇到的问题是,
TransactionScope
显然有一个线程关联,而且在取消任务时,它的
Dispose()
似乎在错误的线程上运行

具体地说,当调用
.TestTx()
时,我在
task.Wait()
上得到以下
aggregateeexception
包含
InvalidOperationException

代码如下:

public void TestTx () {
    var cancellation = new CancellationTokenSource ();
    var task = TestTxAsync ( cancellation.Token );
    cancellation.Cancel ();
    task.Wait ();
}

private async Task TestTxAsync ( CancellationToken cancellationToken ) {
    using ( var scope = new TransactionScope () ) {
        using ( var connection = new SqlConnection ( m_ConnectionString ) ) {
            await connection.OpenAsync ( cancellationToken );
            //using ( var command = new SqlCommand ( ... , connection ) ) {
            //  await command.ExecuteReaderAsync ();
            //  ...
            //}
        }
    }
}

更新:注释掉的部分是为了显示,一旦连接打开,就需要异步地处理连接,但不需要代码来重现问题。

是的,您必须将transactionscope保持在单个线程上。由于您在异步操作之前创建transactionscope,并在异步操作中使用它,因此transactionscope不会在单个线程中使用。TransactionScope不是设计用来这样使用的

我认为一个简单的解决方案是将TransactionScope对象和Connection对象的创建转移到异步操作中

更新

由于异步操作位于SqlConnection对象内部,因此我们无法更改它。 我们所能做的就是。我将以异步方式创建连接对象,然后创建事务范围,并登记事务

SqlConnection connection = null;
// TODO: Get the connection object in an async fashion
using (var scope = new TransactionScope()) {
    connection.EnlistTransaction(Transaction.Current);
    // ...
    // Do something with the connection/transaction.
    // Do not use async since the transactionscope cannot be used/disposed outside the 
    // thread where it was created.
    // ...
}

这个问题源于我在一个控制台应用程序中对代码进行原型化的事实,我没有在问题中反映这一点

async/await在
await
之后继续执行代码的方式取决于
SynchronizationContext的存在。当前的
和控制台应用程序在默认情况下没有同步上下文,这意味着使用当前的
TaskScheduler
执行继续,这是一个
线程池,因此(可能?)在不同的线程上执行

因此,我们只需要有一个
SynchronizationContext
,它将确保
TransactionScope
在创建它的同一个线程上被释放。WinForms和WPF应用程序将默认拥有它,而控制台应用程序可以使用自定义上下文,也可以从WPF借用
DispatchersSynchronizationContext

这里有两篇很好的博客文章详细解释了这些机制:


在.NET Framework 4.5.1中,有一组使用TransactionScopeAsyncFlowOption参数的

根据MSDN,它支持跨线程连续性的事务流

我的理解是,它允许您编写如下代码:

// transaction scope
using (var scope = new TransactionScope(... ,
  TransactionScopeAsyncFlowOption.Enabled))
{
  // connection
  using (var connection = new SqlConnection(_connectionString))
  {
    // open connection asynchronously
    await connection.OpenAsync();

    using (var command = connection.CreateCommand())
    {
      command.CommandText = ...;

      // run command asynchronously
      using (var dataReader = await command.ExecuteReaderAsync())
      {
        while (dataReader.Read())
        {
          ...
        }
      }
    }
  }
  scope.Complete();
}

我还没有尝试过它,所以我不知道它是否会工作。

我知道这是一个旧线程,但如果有人遇到了问题系统。InvalidOperationException:TransactionScope必须在创建它的同一个线程上处理

解决方案是至少升级到.net 4.5.1,并使用如下事务:

using (var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
   //Run some code here, like calling an async method
   await someAsnycMethod();
   transaction.Complete();
} 
现在,事务在方法之间共享。请查看下面的链接。它提供了一个简单的示例和更多详细信息


有关完整的详细信息,请查看针对.NET Framework 4.6+、.NET Core 2.1+或.NET Standard 2.0的

+

考虑使用,它将.NET Framework和.NET Core的System.Data.SqlClient组件集中在一起。 如果您想使用一些较新的SQL Server功能,也很有用

检查或从中拉出

添加包后添加using语句:

using Microsoft.Data.SqlClient;
使用C#8的示例:


你能详细说明你的第二点吗?将创建移动到哪里?然后你是建议只异步打开连接,并对实际工作负载使用阻塞调用吗?还是我又遗漏了什么?在你的问题中,目前你只异步获取连接。我已经按照你的例子,但是如果你更新你的que如果有一些实际的工作负载,那么我也可以更新我的答案。问题中的代码是我能给出的演示该问题的最小示例。我假设打开然后关闭连接显然不是目标。我现在更新了代码,以指示连接打开后存在一些异步工作负载。如果您无法升级到4.5.1怎么办?那么解决方案是什么?@JobaDiniz您仍然可以通过调用DbConnection.BeginTransaction来使用老式(非环境)ADO.NET事务,而不是TransactionScope。您救了我一天,thanx
using Microsoft.Data.SqlClient;
// transaction scope
using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);

// connection
await using var connection = new SqlConnection(_connectionString);

// open connection asynchronously
await connection.OpenAsync();
await using var command = connection.CreateCommand();
command.CommandText = "SELECT CategoryID, CategoryName FROM Categories;";

// run command asynchronously
await using var dataReader = await command.ExecuteReaderAsync();

while (dataReader.Read())
{
    Console.WriteLine("{0}\t{1}", dataReader.GetInt32(0), dataReader.GetString(1));
}

scope.Complete();