c# - ¿Cómo deshacerse de TransactionScope en cancelable async/await?
.net database (3)
El problema surge del hecho de que estaba creando un prototipo del código en una aplicación de consola, que no reflejé en la pregunta.
La forma en que async / await continúa ejecutando el código después de await
depende de la presencia de SynchronizationContext.Current
, y la aplicación de consola no tiene una por defecto, lo que significa que la continuación se ejecuta utilizando el TaskScheduler
actual, que es un ThreadPool
, por lo que ( ¿potencialmente? ) se ejecuta en un hilo diferente.
Por lo tanto, uno simplemente necesita tener un SynchronizationContext
que asegure que TransactionScope
está dispuesto en el mismo hilo que se creó. Las aplicaciones WinForms y WPF lo tendrán de forma predeterminada, mientras que las aplicaciones de consola pueden usar una personalizada o tomar DispatcherSynchronizationContext
de WPF.
Aquí hay dos grandes publicaciones de blog que explican la mecánica en detalle:
Aplicaciones Await, SynchronizationContext y Console
Await, SynchronizationContext y Console Apps: Part 2
Estoy tratando de usar la nueva función async / await para trabajar asincrónicamente con un DB. Como algunas de las solicitudes pueden ser largas, quiero poder cancelarlas. El problema al que me estoy enfrentando es que TransactionScope
aparentemente tiene una afinidad de hilos, y parece que al cancelar la tarea, su Dispose()
se ejecuta en un hilo equivocado.
Específicamente, cuando llamo a .TestTx()
obtengo la siguiente AggregateException
contiene InvalidOperationException
en task.Wait ()
:
"A TransactionScope must be disposed on the same thread that it was created."
Aquí está el código:
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 ();
// ...
//}
}
}
}
ACTUALIZADO: la parte comentada es para mostrar que hay algo que hacer, de forma asincrónica, con la conexión una vez que está abierta, pero ese código no es necesario para reproducir el problema.
En .NET Framework 4.5.1, hay un conjunto de nuevos constructores para TransactionScope que toman un parámetro TransactionScopeAsyncFlowOption .
De acuerdo con MSDN, permite el flujo de transacciones a través de continuaciones de subprocesos.
Según entiendo, está destinado a permitir que escribas 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();
}
Todavía no lo he probado, así que no sé si funcionará.
Sí, tienes que mantener tu transactioncope en un solo hilo. Dado que está creando el scope de transacciones antes de la acción asíncrona y lo usa en la acción asíncrona, el scope de transacciones no se usa en un solo subproceso. El TransactionScope no fue diseñado para ser utilizado de esa manera.
Una solución simple, creo, sería mover la creación del objeto TransactionScope y el objeto Connection a la acción asíncrona.
ACTUALIZAR
Dado que la acción asíncrona está dentro del objeto SqlConnection, no podemos alterar eso. Lo que podemos hacer es enlistar la conexión en el alcance de la transacción . Yo crearía el objeto de conexión de manera asíncrona, y luego crearía el alcance de la transacción, y alistaría la transacción.
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.
// ...
}