Pregunta

Supongamos que alguien (aparte de mí) escribe el siguiente código y lo compila en un ensamblaje:

using (SqlConnection conn = new SqlConnection(connString)) 
{
    conn.Open();
    using (var transaction = conn.BeginTransaction())
    {
        /* Update something in the database */
        /* Then call any registered OnUpdate handlers */
        InvokeOnUpdate(conn);

        transaction.Commit();
    }
}

La llamada a InvokeOnUpdate (IDbConnection conn) llama a un controlador de eventos que puedo implementar y registrar. Por lo tanto, en este controlador tendré una referencia al objeto IDbConnection, pero no tendré una referencia a la transacción pendiente. ¿Hay alguna forma en que pueda obtener la transacción? En mi controlador OnUpdate quiero ejecutar algo similar a lo siguiente:

private void MyOnUpdateHandler(IDbConnection conn) 
{
    var cmd = conn.CreateCommand();
    cmd.CommandText = someSQLString;
    cmd.CommandType = CommandType.Text;

    cmd.ExecuteNonQuery();
}

Sin embargo, la llamada a cmd.ExecuteNonQuery () arroja una InvalidOperationException quejándose de que

  

" ExecuteNonQuery requiere el comando   tener una transacción cuando el   La conexión asignada al comando es   En una transacción local pendiente. los   Propiedad de transacción del comando   no se ha inicializado " ;.

¿Puedo de alguna manera alistar mi cmd SqlCommand con la transacción pendiente? ¿Puedo recuperar una referencia a la transacción pendiente desde el objeto IDbConnection (estaría feliz de usar la reflexión si es necesario)?

¿Fue útil?

Solución

Wow, no creía esto al principio. Me sorprende que CreateCommand () no le dé el comando a su transacción cuando usa transacciones locales de SQL Server, y que la transacción no está expuesta en el objeto SqlConnection . En realidad, al reflexionar sobre SqlConnection , la transacción actual ni siquiera se almacena en ese objeto. En la edición a continuación, te di algunas pistas para rastrear el objeto a través de algunas de sus clases internas.

Sé que no puede modificar el método pero, ¿podría usar un TransactionScope alrededor de la barra de métodos? Entonces, si tienes:

public static void CallingFooBar()
{
   using (var ts=new TransactionScope())
   {
      var foo=new Foo();
      foo.Bar();
      ts.Complete();
   }
}

Esto funcionará, probé usando un código similar al tuyo y una vez que agregué la envoltura, todo funcionará bien si puedes hacerlo, por supuesto. Como se señaló, tenga en cuenta que si se abre más de una conexión dentro del TransactionScope , pasará a una Transacción distribuida que, a menos que su sistema esté configurado para ellos, recibirá un error.

La inscripción con el DTC también es varias veces más lenta que una transacción local.

Editar

si realmente quieres probar y usar la reflexión, SqlConnection tiene una SqlInternalConnection que a su vez tiene una Propiedad de AvailableInternalTransaction que devuelve una SqlInternalTransaction, esta tiene una propiedad de Parent que devuelve la SqlTransaction que necesitarías.

Otros consejos

En caso de que alguien esté interesado en el código de reflexión para lograr esto, aquí va:

    private static readonly PropertyInfo ConnectionInfo = typeof(SqlConnection).GetProperty("InnerConnection", BindingFlags.NonPublic | BindingFlags.Instance);
    private static SqlTransaction GetTransaction(IDbConnection conn) {
        var internalConn = ConnectionInfo.GetValue(conn, null);
        var currentTransactionProperty = internalConn.GetType().GetProperty("CurrentTransaction", BindingFlags.NonPublic | BindingFlags.Instance);
        var currentTransaction = currentTransactionProperty.GetValue(internalConn, null);
        var realTransactionProperty = currentTransaction.GetType().GetProperty("Parent", BindingFlags.NonPublic | BindingFlags.Instance);
        var realTransaction = realTransactionProperty.GetValue(currentTransaction, null);
        return (SqlTransaction) realTransaction;
    }

Notas:

  • Los tipos son internos y las propiedades privadas, por lo que no puede usar dinámico
  • los tipos internos también le impiden declarar los tipos intermedios como lo hice con la primera ConnectionInfo. Tengo que usar GetType en los objetos

Para cualquiera que esté interesado en la versión C # de la clase de decorador que Denis creó en VB.NET, aquí está:

using System;
using System.Collections.Generic;
using System.Text;
using System.Data;

namespace DataAccessLayer
{
    /// <summary>
    /// Decorator for the connection class, exposing additional info like it's transaction.
    /// </summary>
    public class ConnectionWithExtraInfo : IDbConnection
    {
        private IDbConnection connection = null;
        private IDbTransaction transaction = null;

        public IDbConnection Connection
        {
            get { return connection; }
        }

        public IDbTransaction Transaction
        {
            get { return transaction; }
        }

        public ConnectionWithExtraInfo(IDbConnection connection)
        {
            this.connection = connection;
        }

        #region IDbConnection Members

        public IDbTransaction BeginTransaction(IsolationLevel il)
        {
            transaction = connection.BeginTransaction(il);
            return transaction;
        }

        public IDbTransaction BeginTransaction()
        {
            transaction = connection.BeginTransaction();
            return transaction;
        }

        public void ChangeDatabase(string databaseName)
        {
            connection.ChangeDatabase(databaseName);
        }

        public void Close()
        {
            connection.Close();
        }

        public string ConnectionString
        {
            get 
            {
                return connection.ConnectionString; 
            }
            set 
            {
                connection.ConnectionString = value;
            }
        }

        public int ConnectionTimeout
        {
            get { return connection.ConnectionTimeout; }
        }

        public IDbCommand CreateCommand()
        {
            return connection.CreateCommand();
        }

        public string Database
        {
            get { return connection.Database; }
        }

        public void Open()
        {
            connection.Open();
        }

        public ConnectionState State
        {
            get { return connection.State; }
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            connection.Dispose();
        }

        #endregion
    }
}

Al objeto de comando solo se le puede asignar un objeto de transacción usando uno de sus constructores. Puede optar por el enfoque de .NET 2.0 y usar un objeto TransactionScope que se define en el espacio de nombres System.Transactions (tiene un ensamblaje dedicado).

   using System.Transactions;

    class Foo
    {   
        void Bar()
        {
            using (TransactionScope scope = new TransactionScope())
            {
                // Data access
                // ...
                scope.Complete()
            }
        }
    }

El enfoque de System.Transactions utiliza, junto con SQL Server 2005, un coordinador de transacciones ligero (LTM). Tenga cuidado de no usar múltiples objetos de conexión en el alcance de su transacción o la transacción se promocionará ya que se ve como distribuida. Esta versión más intensiva en recursos de la transacción será manejada por DTC.

Soy un gran defensor de lo simple, entonces, ¿qué hay de escribir un contenedor sobre IDBConnection (DELEGATE PATTERN) que expone Transaction. (Lo siento por el código VB.NET, estoy escribiendo esto en VB.NET en este momento) Algo así:

  Public class MyConnection
      Implements IDbConnection

      Private itsConnection as IDbConnection
      Private itsTransaction as IDbTransaction

      Public Sub New(ByVal conn as IDbConnection)
         itsConnection = conn
      End Sub

      //...  'All the implementations would look like
      Public Sub Dispose() Implements IDbConnection.Dispose
         itsConnection.Dispose()
      End Sub
      //...

      //     'Except BeginTransaction which would look like
       Public Overridable Function BeginTransaction() As IDbTransaction Implements IDbConnection.BeginTransaction
         itsTransaction = itsConnection.BeginTransaction()
         Return itsTransaction
       End Function  


      // 'Now you can create a property and use it everywhere without any hacks
       Public ReadOnly Property Transaction
          Get
              return itsTransaction
          End Get
       End Property

    End Class

Así que crearías una instancia de esto como:

Dim myConn as new MyConnection(new SqlConnection(...))

y luego puede obtener la transacción en cualquier momento utilizando:

 myConn.Transaction

En caso de que alguien enfrente este problema en .Net 4.5, puede usar Transaction.Current en System.Transactions .

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top