web-dev-qa-db-fra.com

Comment se débarrasser de TransactionScope en async/wait annulable?

J'essaie d'utiliser la nouvelle fonctionnalité async/wait pour travailler de manière asynchrone avec une base de données. Certaines demandes pouvant être longues, je souhaite pouvoir les annuler. Le problème que je rencontre est que TransactionScope a apparemment une affinité de fil, et il semble que lors de l'annulation de la tâche, sa Dispose() soit exécutée sur un mauvais fil.

Plus précisément, lorsque j'appelle .TestTx(), la AggregateException suivante contenant InvalidOperationException sur task.Wait ():

"A TransactionScope must be disposed on the same thread that it was created."

Voici le code:

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 ();
            //  ...
            //}
        }
    }
}

MISE À JOUR: la partie commentée consiste à montrer qu'il y a quelque chose à faire - de manière asynchrone - avec la connexion une fois ouverte, mais ce code n'est pas nécessaire pour reproduire le problème.

46
chase

Le problème provient du fait que je prototypais le code dans une application console, ce que je n’ai pas reflété dans la question.

La façon dont async/wait continue à exécuter le code après await dépend de la présence de SynchronizationContext.Current, et l'application console n'en possède pas par défaut, ce qui signifie que la suite est exécutée à l'aide de la TaskScheduler actuelle, qui est une ThreadPool, potentiellement?) s'exécute sur un autre thread.

Ainsi, il suffit simplement d'avoir une variable SynchronizationContext qui garantira que TransactionScope est disposé sur le même thread qu'il a été créé. Les applications WinForms et WPF l'auront par défaut, alors que les applications console pourront en utiliser une personnalisée ou emprunter DispatcherSynchronizationContext à WPF.

Voici deux excellents articles de blog qui expliquent les mécanismes en détail:
Applications Await, SynchronizationContext et Console
Applications Await, SynchronizationContext et Console: 2ème partie

4
chase

Dans .NET Framework 4.5.1, il existe un ensemble de nouveaux constructeurs pour TransactionScope qui prennent un paramètre TransactionScopeAsyncFlowOption.

Selon le MSDN, il active le flux de transaction entre les continuations de threads.

D'après ce que j'ai compris, cela a pour but de 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();
}

Je n'ai pas encore essayé, donc je ne sais pas si cela fonctionnera.

92
ZunTzu

Je sais qu'il s'agit d'un ancien thread, mais si quelqu'un a rencontré le problème System.InvalidOperationException: un TransactionScope doit être disposé sur le même thread que celui où il a été créé.

La solution consiste à effectuer une mise à niveau vers .net 4.5.1 au minimum et à utiliser une transaction comme celle-ci:

using (var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
   //Run some code here, like calling an async method
   await someAsnycMethod();
   transaction.Complete();
} 

La transaction est maintenant partagée entre les méthodes. Regardez le lien ci-dessous. Il fournit un exemple simple et plus de détails

Pour plus de détails, jetez un œil à This

0
Terry Slack

Oui, vous devez conserver vos transactions sur un seul fil. Puisque vous créez le transactioncope avant l'action asynchrone et que vous l'utilisez dans l'action asynchrone, le transactioncope n'est pas utilisé dans un seul thread. Le TransactionScope n'a pas été conçu pour être utilisé comme ça.

Je pense qu'une solution simple serait de déplacer la création de l'objet TransactionScope et de l'objet Connection dans l'action asynchrone.

METTRE À JOUR

Etant donné que l'action asynchrone se trouve à l'intérieur de l'objet SqlConnection, nous ne pouvons pas modifier cela . Ce que nous pouvons faire, c'est inscrire la connexion dans la portée de la transaction . Je créerais l'objet de connexion de manière asynchrone, puis créer l'étendue de la transaction et inscrire la transaction.

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.
    // ...
}
0
Maarten