Faire fonctionner TransactionScope avec async / await

114

J'essaye d'intégrer async/ awaitdans notre bus de service. J'ai implémenté un SingleThreadSynchronizationContextbasé sur cet exemple http://blogs.msdn.com/b/pfxteam/archive/2012/01/20/10259049.aspx .

Et cela fonctionne très bien, sauf pour une chose: TransactionScope. J'attends des trucs à l'intérieur TransactionScopeet ça casse le TransactionScope.

TransactionScopene semble pas jouer bien avec le async/ await, certainement parce qu'il stocke des choses dans le thread en utilisant ThreadStaticAttribute. J'obtiens cette exception:

"TransactionScope imbriqué incorrectement.".

J'ai essayé de sauvegarder les TransactionScopedonnées avant de mettre la tâche en file d'attente et de la restaurer avant de l'exécuter, mais cela ne semble rien changer. Et le TransactionScopecode est un gâchis, il est donc vraiment difficile de comprendre ce qui se passe là-bas.

Y a-t-il un moyen de le faire fonctionner? Y a-t-il une alternative à TransactionScope?

Yann
la source
Voici un code très simple pour reproduire une erreur TransactionScope pastebin.com/Eh1dxG4a sauf que l'exception ici est Transaction Aborted
Yann
Pouvez-vous simplement utiliser une transaction SQL standard? Ou utilisez-vous plusieurs ressources?
Marc Gravell
Je couvre plusieurs ressources
Yann
Il semble que vous deviez soit transmettre l'étendue à votre méthode asynchrone, soit lui donner un moyen de la récupérer à partir d'une sorte de contexte commun identifié avec votre unité de travail.
Bertrand Le Roy
Vous aurez besoin d'un thread distinct avec le sien SingleThreadSynchronizationContextpour chaque niveau supérieur TransactionScope.
Stephen Cleary

Réponses:

161

Dans .NET Framework 4.5.1, il existe un ensemble de nouveaux constructeursTransactionScope qui acceptent un TransactionScopeAsyncFlowOptionparamètre.

Selon le MSDN, il permet le flux de transaction entre les continuations de thread.

Je crois comprendre que cela est censé vous permettre d'écrire du code comme celui-ci:

// 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
la source
10

Un peu en retard pour une réponse mais j'avais le même problème avec MVC4 et j'ai mis à jour mon projet de 4.5 à 4.5.1 en faisant un clic droit sur le projet aller aux propriétés. Sélectionnez l'onglet de l'application, changez le cadre cible en 4.5.1 et utilisez la transaction comme suit.

using (AccountServiceClient client = new AccountServiceClient())
using (TransactionScope scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
}
Atul Chaudhary
la source
2
En quoi cela diffère-t-il de la réponse acceptée?
Liam
6

Vous pouvez utiliser DependentTransaction créé par la méthode 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();
}

Gestion de la concurrence avec DependentTransaction

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

maximpa
la source
2
L'exemple de tâche enfant d'Adam Prescott n'a pas été marqué comme asynchrone. Si vous remplacez «faire des trucs transactionnels» par quelque chose comme await Task.Delay(500)ce modèle échouera également TransactionScope nested incorrectlycar le TransactionScope le plus à l'extérieur (non illustré dans l'exemple ci-dessus) quitte la portée avant que la tâche enfant ne se termine correctement. Remplacez awaitpar Task.Wait()et cela fonctionne, mais vous avez perdu les avantages de async.
mdisibio
C'est une manière plus difficile de résoudre le problème. TransactionScope est de cacher toute cette plomberie.
Eniola