Parte II .NET y COM+: Preparando nuestro componente transaccional y comprobando su funcionamiento

Construyendo el componente

En esta parte construiremos el componente de .NET con soporte de transacciones COM+. El código funciona tanto para BTS 2004 como para BTS 2006, solo tiene que ser recompilado. Pueden bajar el código fuente en el post.

Como saben todavía en el .NET Framework 2.0.se emplea el modelo de transacciones de COM+, lo cual representa una capa de interoperabilidad, pero próximamente tendremos la siguiente versión de COM+ para el manejo de transacciones distribuidas es Windows Communication Foundation WCF (Indigo) y no solo para reemplazar COM+, sino para proveer soporte en otros modelos de comunicación existentes como Remoting y para modelos transaccionales que no existían como en los Web Services (recordando que los Web Services no son transaccionales en la actualidad por lo que se aplican las transacciones por compensación).

Para la aplicación necesitaremos de una base de datos en SQL como se muestra en este diagrama y también anexo el script.

Ahora en este caso emplearemos una operación de inserción a una base de datos para mostrar la transaccionalidad. Para bases de datos, ADO.NET ofrece 2 formas para hacer las transacciones:

1. DbConnection.BeginTransaction. Esta forma ofrece la ventaja de ejecutar por medio de la misma conexión "n" comandos que participarán en la transacción. Todos los proveedores de ADO.NET ofrecen este mecanismo (Oracle, Informix, etc) un ejemplo es:

private static void ExecuteSqlTransaction(string connectionString){ // El using nos ayuda a no tener que hacer un connection.Close() using (SqlConnection connection = new SqlConnection(connectionString)) { connection.Open(); SqlCommand command = connection.CreateCommand(); SqlTransaction transaction; //La conexión ya debe estar abierta para crear la transacción transaction = connection.BeginTransaction("SampleTransaction"); command.Connection = connection; command.Transaction = transaction; try { command.CommandText = "Insert into Region (R_ID, R_Description) VALUES (100, 'Description')"; command.ExecuteNonQuery(); command.CommandText = "Insert into Region (R_ID, R_Description) VALUES (101, 'Description')"; command.ExecuteNonQuery(); transaction.Commit(); Console.WriteLine("Both records are written to database."); } catch (Exception ex) { Console.WriteLine("Commit Exception Type: {0}", ex.GetType()); Console.WriteLine(" Message: {0}", ex.Message); try { transaction.Rollback(); } catch (Exception ex2) { Console.WriteLine("Rollback Exception Type: {0}", ex2.GetType()); Console.WriteLine(" Message: {0}", ex2.Message); } } }}

Este mecanismo funciona bien si los comando se ejecutan sobre la misma conexión, pero si se requiere que la transacción opere sobre mas de 1 conexión, entonces ya se requiere de otro método transaccional.

2. COM+. El cual abarcamos en este artículo, pero como el mundo COM+ es muy grande, solo será lo necesario para nuestro componente.

Partamos de un class library para poder ejecutar una inserción de un registro a una base de datos:

using System;using System.Data;using System.Data.SqlClient;using System.Diagnostics;using System.Xml;namespace EnterpriseComponents.DB{ public class TxComponent { public void InsertRows(string DBConnString,int ID) { SqlConnection objConn = new SqlConnection(); SqlCommand objComm = new SqlCommand("insert into monitor values (@ID,@D)", objConn); SqlParameterCollection Pams = objComm.Parameters; objConn.ConnectionString = DBConnString; Pams.Add("ID", SqlDbType.Int, 4).Value = ID; Pams.Add("D", SqlDbType.DateTime).Value = DateTime.Now.ToString("yyyy-MM-dd"); try { objConn.Open(); objComm.ExecuteNonQuery(); } catch (Exception ex) { EventLog.WriteEntry("TestsCOM+inBTS", string.Format("Error actualizando {0}.", ex.Message)); } finally { if (objConn.State != ConnectionState.Closed) objConn.Close(); } } }}

Se requiere añadir las referencias para todo el soporte de COM+, para esto es necesario que por medio de Add Reference agreguen las librerías de System.EnterpriseServices y System.Runtime.InteropServices; asi como añadirlas a su código por medio de la directiva using.

Básicamente necesitamos 3 elementos para habilitar nuestro componente como transaccional en COM+:

1.

El atributo Transaction. Este atributo se aplica a nivel clase y tiene algunos valores por default que les recomiendo revisen (ver ayuda). Dentro de las propiedades de este atributo, value que es el miembro por default, representa TransacionOption que indica como se comportará la transacción.

Member name Description
Disabled Ignora cualquier transacción del contexto actual.
NotSupported Crea el componente en un contexto donde no exista una transacción.
Required Comparte la transacción en case de que exista una, pero si no existe la crea.
RequiresNew Obliga la creación de una nueva transacción, independientemente si el contexto tiene uno o no.
Supported Si existe una transacción en el contexto la comparte.

En nuestra clase emplearemos TransactionOption.Required, por lo que el código es:

[Transaction(TransactionOption.Required)]public class TxComponent 

o

[Transaction]public class TxComponent 

2. Heredar de la clase ServicedComponent. Nuestra clase transaccional debe heredar de ServicedComponent para poder contener las propiedades y métodos de soporte para COM+. Dentro de las características que podemos resaltar es el método Dispose. Este método es importante debido a la administración de recursos que tiene .NET en especial cuando son recursos del mundo COM. Nuestro código es:

[Transaction(TransactionOption.Required)]public class TxComponent : ServicedComponent

3. Exponer el componente a COM. Para habilitar el componente en COM empleamos el atributo ComVisible(true). Este atributo se puede añadir a nivel assembly o a nivel clase. La diferencia radica en que si se hace a nivel clase, se generarán los metadatos para las clases marcadas con el atributo para el archivo de tipos (tbl) y si es a nivel assembly se generará información para todo el assembly. Listo las opciones para usar el atributo:
a. A nivel de assembly. Normalmente este atributo lo pueden localizar en el archivo de propiedades como se muestra aquí. Para acceder a la pantalla de propiedades tiene que dar click derecho en el proyecto, entrar a Properties. En la sección de de Application, el botón de Assembly Information, y marcando la opción de Make assembly COM-visible, como se muestran en esta figura.

Lo pueden modificar por medio del editor o empleando la pantalla de propiedades, ya que la pantalla de propiedades afecta los valores del archivo de propiedades AssemblyInfo.cs.

b. A nivel clase. Permite de manera selectiva indicar cuales clases estarán disponibles para COM.

[

Transaction(...), ComVisible(true) ]public class TxComponent : ServicedComponent{...}

Controlando las transacciones: Commit y Rollback

Las transacciones cuentan con 2 procesos que controlan el estado, estas son:

  • Commit. Esta instrucción le indica a la transacción que todos los cambios deben ser guardados hasta el punto donde se invoca.
  • Rollback. Esta instrucción le indica a la transacción que todos los cambios hechos hasta el punto donde se invoca deben ser eliminados.

Este proceso debe controlarse dentro del código invocando métodos según la lógica que tengamos implementada, los nombres de los métodos pueden variar. La estructura mas común es:

try
{
   Componente1.Ejecuta();
   Componente2.Ejecuta();
   .....
   ComponenteN.Ejecuta();
   Commit(); //Solo es un ejemplo
}
catch (Exception ex)
{
   ...
   Rollback(); //Solo es un ejemplo
}

El bloque try-catch controla toda la transacción y los componentes "Component1..N" participa en ella, claro si y solo si son transaccionales. En el caso de que algun componete no sea transaccional no hay ningun problema. Este prodecimiento es manual, ya que nosotros estamos controlando la transacción al momento de invocar los métodos.

Existe otro mecanismo el cual dejamos que el sistema controle la transacción por nosotros. Para esto, los componentes emplean un mecanismo denominado votación para poderle indicar al sistema si debe invocar el Commit o el Rollback al final de la operación. Existen 2 formas de realizar esta votación:

1. Manualmente. Invocando SetComplete y SetAbort del objeto ContextUtil

try{ objConn.Open(); objComm.ExecuteNonQuery(); ContextUtil .SetComplete(); }catch (Exception ex){ EventLog.WriteEntry("TestsCOM+inBTS", string.Format("Error {0}.", ex.Message)); ContextUtil .SetAbort(); }finally{ if (objConn.State != ConnectionState.Closed) objConn.Close();}

2. Automáticamente. Empleando el atributo AutoComplete en el método.

[AutoComplete ] public void InsertRows(string DBConnString,int ID)La mecánica que tiene el atributo es que si el método no arroja ninguna excepción, se vota a favor de la transacción de manera automática, de lo contrario se vota en contra. Debido a esta mecánica, dentro del componente estoy incluyendo una variable de tipo excepción que permita cerrar la conexión a la BD y posteriormente arrojar la excepción original, de lo contrario podríamos dejar la conexión abierta. Hay varias formas para poder manejar esto, pero les proponga una:

catch (Exception ex){   EventLog.WriteEntry("TestsCOM+inBTS", string.Format("Error {0}.", ex.Message));   bubble = ex;}finally{   if (objConn.State != ConnectionState.Closed)      objConn.Close();}if (bubble != null)   throw bubble;

Todos los componentes transaccionales que participen en la operación deben votar a favor, para que el sistema invoque el Commit, de lo contrario se invocará el Rollback.

Finalmente nuestro código quede de la siguiente forma:

using System;using System.Data;using System.Data.SqlClient;using System.Diagnostics;using System.Xml;using System.EnterpriseServices;using System.Runtime.InteropServices;using System.Runtime.Serialization;

[assembly: ApplicationName("BTSMyAppTX")]

namespace

EnterpriseComponents.DB
{
[ Transaction(TransactionOption.Required), ComVisible(true )]
   public class TxComponent : ServicedComponent
   {
[AutoComplete]
      public void InsertRows(string DBConnString,int ID)
      {
Exception bubble = null;
SqlConnection objConn = new SqlConnection();
SqlCommand objComm = new SqlCommand("insert into monitor values (@ID,@D)", objConn);
SqlParameterCollection Pams = objComm.Parameters;
         
objConn.ConnectionString = DBConnString;
Pams.Add("ID", SqlDbType.Int, 4).Value = ID;
Pams.Add("D", SqlDbType.DateTime).Value = DateTime.Now.ToString("yyyy-MM-dd");
         try
         {
            objConn.Open();
            objComm.ExecuteNonQuery();
         }
catch (Exception ex)
         {
EventLog.WriteEntry("TestsCOM+inBTS", string.Format("Error actualizando {0}.", ex.Message));
            bubble = ex;
         }
         finally
         {
if (objConn.State != ConnectionState.Closed)
               objConn.Close();
         }
if (bubble != null)
throw bubble;
      }
   }
}

Registro del componente en COM+

El componetne debe ser instalado en COM+, por lo que se deben realizar los siguientes pasos:

  1. Firmar el componente. Obtienen una llave por medio de la herramienta Strong Name Tool y registrarla en la propiedades del assembly. Para aquellos que no les gusta tener que abrir el command prompt para ejecutar el comando, pueden programar la generación de la llave por medio de External Tools del menu te Tools (ver imagen).
  2. Proporcionar una identidad al componente. Por medio de atributos a nivel assembly se debe dar un nombre y/o un GUID

               [assembly: ApplicationName("BTSMyAppTX")]
               [assembly: ApplicationID("4fb2d46f-efc8-4643-bcd0-6e5bfa6a174c")]

Ahora necesitamos registrar el componente. La manera mas rápida es empleando la herramienta .NET Services Installation Tool:

               regsvcs C:\Tests\Transaccionts\EntServComponent\bin\Debug\EntServComponent.dll

si desean desinstalar el componente solo tiene que añadir el parámetro /u

               regsvcs /u C:\Tests\Transaccionts\EntServComponent\bin\Debug\EntServComponent.dll

Este comando también lo pueden programa en el menu de External Tools.

Una vez que queda instalado el componente en COM+, lo puede revisar en Component Services. Click en Start, luego en Administrative Tools y luego en Component Services (ver imagen)

Errores comunes al ejecutar regsvcs :

Error:
The following installation error occurred:
1: Invalid ServicedComponent-derived classes were found in the assembly.
(Classes must be public, concrete, have a public default constructor, and meet all other ComVisibility requirements)
EnterpriseComponents.DB.TxComponent: Unspecified error
Posible Solución:
Esto es porque no se a establecido el atributo de ComVisible(true), ya sea a nivel assembly o a nivel clase.

Error:
Warning: No ServicedComponent-derived classes were found in the assembly.
Posible Solución:
Esto se arregla haciendo que la clase que queremos que sea transaccional herede de ServicedComponent


Probando el componente

Ahora en .NET Framework 2.0 tenemos nuevas características para el soporte de transacciones. Por medio de la clase TranactionScope podemos iniciar la transacción y realizar el control manual. Si desean hacer la prueba para .NET Framework 1.1, pueden usar este ejemplo How to: Use the BYOT (Bring Your Own Transaction) Feature of COM+.

Vamos a hacer la prueba por medio de una Winform. Añadiendo un botón con el siguiente código:

private void button2_Click(object sender, EventArgs e)
{
   string DBConnString = "server=.;initial catalog=poolingdata;User Id=sa;pwd=password";
   TxComponent T1 = new TxComponent();
   TxComponent T2 = new TxComponent();
   TxComponent T3 = new TxComponent();
   using (TransactionScope tx = new TransactionScope())
   {
      T1.InsertRows(DBConnString,1);
      T2.InsertRows(DBConnString,2);
      T3.InsertRows(DBConnString,3);
      tx.Complete();
   }
}

Si ejecutan el codigo, realizara la inserción de 3 registros en nuestra base de datos y se completará la transaccion pues no hay ningún error (la tabla debe estar limpia de registros), por lo que pueden ejecutar un query como SELECT * FROM monitor para poder ver los registros.

En Component Services hay un monitor de transacciones que pueden emplear para ver el comportamiento de nuestro código. Si colocamos un breakpoint en el código podremos ver que el monitor incrementa los contadores de las transacciones en la máquina (ver imagen)

Como podrán ver, después de dar click en el botón y terminar el proceso, el contador de transacciones finalizadas exitosamente (Commited) y el total se incrementa en 1.

Si desean establecer el contador de transacciones en ceros, tiene que reiniciar el servicio de MSDTC y una forma de hacerlo es dando click en My Computer y click en Stop MS DTC y nuevamente click derecho y Start MS DTC (ver imagen)

Ahora, sin borrar los registros que acabamos de insertar, modificamos el código de tal forma que podamos generar un error de llave única duplicada al momento de intentar insertar in ID que ya se encuentra en la base de datos. El código queda como sigue:

using (TransactionScope tx = new TransactionScope())
{
   T1.InsertRows(DBConnString,4);
   T2.InsertRows(DBConnString,2);
   T3.InsertRows(DBConnString,3);
   tx.Complete();
}

Colocando un breakpoint en la segunda inserción y ejecutamos la aplicación. Lo que sucederá es que se ejecutará la primera inserción (ID = 4); en este momento pueden ejecutar un query de la forma SELECT * FROM monitor, pero de forma extraña el query analyzer se quedara ejecutando la instrucción sin regresar resultados. Esto es una característica que tienen las transacciones, al menos dentro de las bases de datos: los bloqueos de información cuando hay transacciones.

Estos bloqueos tiene una razón, la cual es asegurar la consistencia de la información. Con un ejemplo podemos verlo mejor. Supongamos que exactamente en el instante en el que se inserto el registro con el ID = 4, otro sistema intenta leer información de esa misma tabla. Si dejamos que lea todos los registros que existen, esto implica el registro con ID = 4 entonces esa otra aplicación tendrá como resultado 4 registros. Pero lo que sigue es que al momento que ejecutemos la siguiente instrucción de inserción con el ID = 2, generaremos un error pues el ID = 2 ya se encuentra en la base de datos, por lo que la transacción no se completará y el registro de la tabla con el ID = 4 desaparecerá, causando que la otra aplicación que ya había leído ese registro con ID = 4 tengo lo que se llama una lectura fantasma.

Regresando al breakpoint de la segunda inserción en el que estábamos, para poder hacer una consulta a la tabla sin que en query analyzer se bloquee, podemos usar la sentencia SELECT * FROM monitor with (READPAST ) . Esta opción establece que deseamos hacer la lectura de toda la información que no este bloqueada por una transacción como lo es el registro con el ID = 4 y así podremos ver los 3 registros que ya habíamos insertado.

NOTA. Necesitan tener cuidado mientras hacen sus pruebas, porque si dejan pasar mucho tiempo en el breakpoint, las transacciónes tiene un timeout y podría ser que se venza por lo que ya no haya transacción activa y se genere otro tipo de error. Este timeout es configurable.

Si de todas formas deseamos hacer una lectura fantasma, esto es mientras estamos en el breakpoint de la segunda inserción deseamos ver los registros de la tabla (incluyendo el registro con ID = 4) podemos emplear la sentencia SELECT * FROM monitor with (NOLOCK ) , de esta forma podemos ver los 4 registros que hasta ese punto hemos insertado

Si continuamos con la ejecución de la WinForm, se generará una excepción en la segunda inserción, por lo que la transaccion terminará. Regresamos a revisar los registros en la tabla y verán que solo tenemos 3 registros, pues aun que insertamos el registro con ID = 4, al momento de fallar la transacción ese registro deja de existir. En este momento el contador de transacciones fallidas (Aborted) y el total se incrementa en 1 como se muestra en la figura (ver imagen)

Algunas referencias útiles:

Y hasta aquí concluimos la contrucción y prueba del componente.

Ver: