Estoy haciendo algunas cosas dentro de un bloque de uso para un objeto TransactionScope. En algún momento quise llamar a algún código asíncrono disparando y olvidar (no quiero esperar el resultado, y no estoy interesado en lo que sucede durante esa llamada) y quería que ese código no fuera parte del transacción (usando la opción TransactionScopeOption.Suppress).

Así que inicialmente hice algo similar al methodFails que he comentado en el código a continuación. Me dio un buen "System.InvalidOperationException: 'TransactionScope anidado incorrectamente'" . Busqué en SO alguien que tuviera problemas similares y encontré esta Pregunta donde la respuesta de ZunTzu me dio la idea de method1 usando la opción TransactionScopeAsyncFlowOption.Enabled, que funciona como esperaba para methodFails pero sin excepción.

Entonces pensé en una alternativa que puse en method2 que consiste en poner el código asíncrono en un tercer método (method3) llamado disparando y olvidando mientras se mantiene la opción TransactionScopeOption.Suppress en el no asincrónico method2. Y este enfoque parece funcionar tan bien como method1 en mi programa de muestra.

Entonces, mi pregunta es: ¿qué enfoque es mejor, method1 o method2, o tal vez un tercero en el que no he pensado? Me inclino por method1 porque parece que "las personas que hacen la clase TransactionScope ponen ese TransactionScopeAsyncFlowOption allí por una razón". Pero el hecho de que TransactionScopeAsyncFlowOption.Enabled no sea el valor predeterminado para TransactionScope me hace pensar que tal vez haya un impacto en el rendimiento al habilitarlo, y disparar y olvidar puede ser un caso especial en el que puedo guardar ese impacto en el rendimiento.

El código de muestra:

    class Program
    {
        static void Main(string[] args)
        {
            using (TransactionScope scope1 = new TransactionScope())
            {
                // Do some stuff in scope1...

                // Start calls that could execute async code
                //Task a = methodFails(); // This commented method would launch exception: System.InvalidOperationException: 'TransactionScope nested incorrectly'
                Task b = method1(); // Fire and forget
                method2();

                // Rest of stuff in scope1 ...
            }
            Console.ReadLine();
        }

        static async Task methodFails()
        {
            //Start of non-transactional section 
            using (TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Suppress))
            {
                //Do non-transactional work here
                Console.WriteLine("Hello World 0.1!!");
                await Task.Delay(10000);
                Console.WriteLine("Hello World 0.2!!");
            }
            //Restores ambient transaction here
            Console.WriteLine("Hello World 0.3!!");
        }

        static async Task method1()
        {
            //Start of non-transactional section 
            using (TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled))
            {
                //Do non-transactional work here
                Console.WriteLine("Hello World 1.1!!");
                await Task.Delay(10000);
                Console.WriteLine("Hello World 1.2!!");
            }
            //Restores ambient transaction here
            Console.WriteLine("Hello World 1.3!!");
        }

        static void method2()
        {
            //Start of non-transactional section 
            using (TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Suppress))
            {
                //Do non-transactional work here
                Task ignored = method3(); // Fire and forget
            }
            //Restores ambient transaction here
            Console.WriteLine("Hello World 2.2!!");
        }

        static async Task method3()
        {
            //Do non-transactional work here
            Console.WriteLine("Hello World 2.1!!");
            await Task.Delay(10000);
            Console.WriteLine("Hello World 2.3!!");
        }
    }
3
Jorge Y. 14 feb. 2018 a las 10:59

2 respuestas

La mejor respuesta

Pero el hecho de que TransactionScopeAsyncFlowOption.Enabled no sea el valor predeterminado para TransactionScope me hace pensar que tal vez haya un impacto en el rendimiento al habilitarlo, y disparar y olvidar puede ser un caso especial en el que puedo guardar ese impacto en el rendimiento.

TransactionScopeAsyncFlowOption.Enabled se introdujo por motivos de compatibilidad con versiones anteriores cuando arreglaron un error. Curiosamente, no se beneficia de la corrección de errores a menos que "suscriba" estableciendo esta marca. Lo hicieron de esa manera para que la corrección de errores no rompiera ningún código existente que se basara en el comportamiento defectuoso.

En este artículo:

Es posible que no lo sepa, pero la versión 4.5.0 de .NET Framework contiene un error grave con respecto a System.Transactions.TransactionScope y cómo se comporta con async / await. Debido a este error, TransactionScope no puede fluir hacia sus continuaciones asincrónicas. Esto potencialmente cambia el contexto de subprocesos de la transacción, lo que provoca que se produzcan excepciones cuando se elimina el alcance de la transacción.

Este es un gran problema, ya que hace que escribir código asincrónico que involucre transacciones sea extremadamente propenso a errores.

La buena noticia es que, como parte de .NET Framework 4.5.1, Microsoft lanzó la solución para ese error de "continuación asincrónica". Lo que pasa es que los desarrolladores como nosotros ahora deben optar explícitamente para obtener este nuevo comportamiento. Echemos un vistazo a cómo hacerlo.

  • Un código asincrónico de empaquetado de TransactionScope debe especificar TransactionScopeAsyncFlowOption.Enabled en su constructor.
4
John Wu 14 feb. 2018 a las 08:38

Puede llamar a sus métodos asíncronos dentro de una llamada HostingEnvironment.QueueBackgroundWorkItem.

HostingEnvironment.QueueBackgroundWorkItem(async cancellationToken =>
{
    await LongRunningMethodAsync();
});

QueueBackgroundWorkItem se resume de la siguiente manera:

El método HostingEnvironment.QueueBackgroundWorkItem le permite programar pequeños elementos de trabajo en segundo plano. ASP.NET realiza un seguimiento de estos elementos y evita que IIS termine abruptamente el proceso de trabajo hasta que se hayan completado todos los elementos de trabajo en segundo plano.

3
Nicholas 25 feb. 2018 a las 06:03