Faça o TransactionScope funcionar com async / await

114

Estou tentando integrar async/ awaitem nosso barramento de serviço. Implementei um com SingleThreadSynchronizationContextbase neste exemplo http://blogs.msdn.com/b/pfxteam/archive/2012/01/20/10259049.aspx .

E ele funciona muito bem, exceto por uma coisa: TransactionScope. Aguardo coisas dentro do TransactionScopee quebre o TransactionScope.

TransactionScopenão parece funcionar bem com o async/ await, certamente porque ele armazena coisas no thread usando ThreadStaticAttribute. Eu recebo esta exceção:

"TransactionScope aninhado incorretamente.".

Tentei salvar os TransactionScopedados antes de enfileirar a tarefa e restaurá-la antes de executá-la, mas parece que nada mudou. E o TransactionScopecódigo é uma bagunça, então é muito difícil entender o que está acontecendo lá.

Existe uma maneira de fazer funcionar? Existe alguma alternativa para TransactionScope?

Yann
fonte
Aqui está um código muito simples para reproduzir um erro de TransactionScope pastebin.com/Eh1dxG4a, exceto que a exceção aqui é Transação Abortada
Yann
Você pode nit apenas usar uma transação SQL regular? Ou você está abrangendo vários recursos?
Marc Gravell
Estou abrangendo vários recursos
Yann
Parece que você precisará passar o escopo para seu método assíncrono ou fornecer uma maneira de recuperá-lo de algum tipo de contexto comum que é identificado com sua unidade de trabalho.
Bertrand Le Roy
Você precisará de um tópico separado com o seu próprio SingleThreadSynchronizationContextpara cada nível superior TransactionScope.
Stephen Cleary

Respostas:

161

No .NET Framework 4.5.1, há um conjunto de novos construtores paraTransactionScope que recebam um TransactionScopeAsyncFlowOptionparâmetro.

De acordo com o MSDN, ele permite o fluxo de transações entre continuações de thread.

Meu entendimento é que isso permite que você escreva um código como este:

// 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();
}
ZunTzu
fonte
10

Um pouco tarde para uma resposta, mas eu estava tendo o mesmo problema com MVC4 e atualizei meu projeto de 4.5 para 4.5.1 clicando com o botão direito do mouse em projeto ir para propriedades. Selecione a guia do aplicativo, altere a estrutura de destino para 4.5.1 e use a transação como segue.

using (AccountServiceClient client = new AccountServiceClient())
using (TransactionScope scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
}
Atul Chaudhary
fonte
2
Como isso difere da resposta aceita?
Liam
6

Você pode usar DependentTransaction criado pelo método Transaction.DependentClone () :

static void Main(string[] args)
{
  // ...

  for (int i = 0; i < 10; i++)
  {

    var dtx = Transaction.Current.DependentClone(
        DependentCloneOption.BlockCommitUntilComplete);

    tasks[i] = TestStuff(dtx);
  }

  //...
}


static async Task TestStuff(DependentTransaction dtx)
{
    using (var ts = new TransactionScope(dtx))
    {
        // do transactional stuff

        ts.Complete();
    }
    dtx.Complete();
}

Gerenciando simultaneidade com DependentTransaction

http://adamprescott.net/2012/10/04/transactionscope-in-multi-threaded-applications/

maximpa
fonte
2
A tarefa filho de exemplo de Adam Prescott não foi marcada como assíncrona. Se você substituir "do transactional stuff" por algo como await Task.Delay(500)este padrão, também falhará TransactionScope nested incorrectlyporque o TransactionScope mais externo (não mostrado no exemplo acima) sai do escopo antes que a tarefa filho seja concluída corretamente. Substitua awaitpor Task.Wait()e funciona, mas você perdeu os benefícios de async.
mdisibio
Essa é a maneira mais difícil de resolver o problema. TransactionScope é para esconder todo esse encanamento.
Eniola