Ejecutar acciones al finalizar la transacción: TransactionCompleted vs. EnlistVolatile

Hace unas semanas me encontré con la necesidad de realizar una acción al final de una transacción, pero sólo si dicha transacción había sido completada.
Imaginemos que queremos enviar un mail de aviso, sólo en caso de que la transacción se complete correctamente. Si se hace rollback de la transacción, el mail no se ha de enviar. Veamos cómo hacerlo.

La clase Transaction proporciona un evento TransactionCompleted que nos puede servir perfectamente para esto, y se puede programar de una forma muy sencilla. Veamos el código de ejemplo:

        static void Main(string[] args)
        {
            using (var tx = new TransactionScope())
            {
                Transaction.Current.TransactionCompleted +=
                    (sender, e) =>
                    {
                        if (e.Transaction.TransactionInformation.Status == TransactionStatus.Committed)
                        {
                            Console.WriteLine("TransactionCompleted Event");
                        }
                    };
                // do stuff
                tx.Complete();
            }
            Console.ReadLine();
        }
 

Este código es súper simple, pero tiene un par de problemas:

  • Según la información sobre el evento disponible en la MSDN, suscribirse a este evento puede afectar negativamente al rendimiento de la transacción.
  • Y lo más importante en el escenario en el que me encontraba: Hay un bug, solucionado en la versión 4 del framework, que impide consultar el estado de la transacción en el manejador del evento. Si accedemos a la transacción (para consultar e.Transaction.TransactionInformation.Status) salta una excepción de tipo ObjectDisposedException 😦

Como alternativa, podemos implementar un administrador de recursos volátil y enlistarlo en la transacción. Esto suena complicado, pero es bastante sencillo. Tan sólo hay que crear una clase que implemente la interfaz IEnlistmentNotification.

    public class MyTxCommittedHandler : IEnlistmentNotification
    {
        public MyTxCommittedHandler() { }
        public void Commit(Enlistment enlistment)
        {
            Console.WriteLine("EnlistVolatile MyTxCommittedHandler.Commit");
            enlistment.Done();
        }

        public void InDoubt(Enlistment enlistment) { enlistment.Done(); }
        public void Prepare(PreparingEnlistment preparingEnlistment) { preparingEnlistment.Prepared(); }
        public void Rollback(Enlistment enlistment) { enlistment.Done(); }
    }

    class Program
    {
        static void Main(string[] args)
        {
            using (var tx = new TransactionScope())
            {
                Transaction.Current.EnlistVolatile(new MyTxCommittedHandler(), EnlistmentOptions.None);
                // do stuff
                tx.Complete();
            }
        }
    }

Con esto funciona pero, ¿no podríamos hacerlo reutilizable? Vamos a crear un extension method para Transaction que admita como parámetro la acción a ejecutar:

    public static class TransactionExtensions
    {
        private class OnTxCommittedWrapper : IEnlistmentNotification
        {
            Action _txCommittedAction;
            public OnTxCommittedWrapper(Action aTxCommittedAction)
            {
                _txCommittedAction = aTxCommittedAction;
            }

            public void Commit(Enlistment enlistment)
            {
                _txCommittedAction();
                enlistment.Done();
            }

            public void InDoubt(Enlistment enlistment) { enlistment.Done(); }
            public void Prepare(PreparingEnlistment preparingEnlistment) { preparingEnlistment.Prepared(); }
            public void Rollback(Enlistment enlistment) { enlistment.Done(); }
        }

        public static void OnTransactionCommitted(this Transaction aTransaction, Action aTxCommittedAction)
        {
            var lWrapper = new OnTxCommittedWrapper(aTxCommittedAction);
            aTransaction.EnlistVolatile(lWrapper, EnlistmentOptions.EnlistDuringPrepareRequired);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            using (var tx = new TransactionScope())
            {
                Transaction.Current.OnTransactionCommitted(() =>
                {
                    Console.WriteLine("EnlistVolatile OnTxCommittedWrapper.Commit");
                });
                // do stuff
                tx.Complete();
            }
            Console.ReadLine();
        }
    }

De esta forma, nos queda disponible un método Transaction.OnTransactionCommitted, reutilizable y muy cómodo de utilizar.

Entonces ¿cuál es la mejor solución? Dejando de lado el tema del posible impacto negativo en el rendimiento, del cual no he encontrado datos y tampoco he medido personalmente, me inclinaría por el uso del evento TransactionCompleted… siempre que estés trabajando con el framework 4.0 y no te afecte el bug, claro😉

Hasta la próxima, happy coding!

Esta entrada fue publicada en Dev y etiquetada , . Guarda el enlace permanente.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s