Pergunta

Suponha que alguém (além de mim) escreva o seguinte código e o compilie em uma montagem:

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();
    }
}

A chamada para InvokeonUpdate (IDBConnection Conn) chama para um manipulador de eventos que eu possa implementar e registrar. Assim, neste manipulador, terei uma referência ao objeto IDBConnection, mas não terei uma referência à transação pendente. Existe alguma maneira de conseguir a transação? No meu manipulador OnUpdate, quero executar algo semelhante ao seguinte:

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

    cmd.ExecuteNonQuery();
}

No entanto, o chamado para cmd.executenonQuery () lança um invalidoperationException reclamando que

"ExecutenonQuery exige que o comando tenha uma transação quando a conexão atribuída ao comando estiver em uma transação local pendente. A propriedade de transação do comando não foi inicializada".

De alguma forma, posso contratar meu sqlcmand cmd com a transação pendente? Posso recuperar uma referência à transação pendente do objeto IDBConnection (ficaria feliz em usar a reflexão, se necessário)?

Foi útil?

Solução

Uau, eu não acreditei nisso a princípio. Estou surpreso que CreateCommand() não fornece o comando sua transação ao usar transações locais do SQL Server e que a transação não é exposta no SqlConnection objeto. Na verdade, ao refletir sobre SqlConnection A transação atual nem é armazenada nesse objeto. Na edição abaixo, dei -lhe algumas dicas para rastrear o objeto através de algumas de suas classes internas.

Eu sei que você não pode modificar o método, mas você poderia usar um transactionscope em torno da barra de métodos? Então, se você tem:

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

Isso funcionará, testei usando código semelhante ao seu e, uma vez que eu adicionar, tudo funcionará bem, se você puder fazer isso, é claro. Como apontado, cuidado se mais, uma conexão é aberta dentro do TransactionScope Você será escalado para uma transação distribuída que, a menos que seu sistema esteja configurado para eles, você receberá um erro.

O recrutamento com o DTC também é várias vezes mais lento que uma transação local.

Editar

Se você realmente deseja tentar usar a reflexão, o SQLConnection possui um SQLinernalConnection, por sua vez, possui uma propriedade de Disponível na Transação que retorna uma SqlinernalTransaction, isso possui uma propriedade de pai que retorna a SQLTransact necessária.

Outras dicas

Caso alguém esteja interessado no código de reflexão para conseguir isso, aqui vai:

    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:

  • Os tipos são internos e as propriedades privadas, para que você não possa usar dinâmico
  • Os tipos internos também impedem que você declare os tipos intermediários, como eu fiz com o primeiro ConnectionInfo. Tenho que usar gettype nos objetos

Para quem está interessado na versão C# da classe Decorator que Denis fez em VB.net, aqui 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
    }
}

O objeto de comando pode receber apenas um objeto de transação usando um de seus construtores. Você pode optar pela abordagem .NET 2.0 e usar um objeto TransactionsCope que é definido no System.Transactions espaço para nome (possui uma montagem dedicada).

   using System.Transactions;

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

A abordagem System.Transactions usa em conjunto com o SQL Server 2005, um Coordenador de Transações Luzes (LTM). Cuidado para não usar vários objetos de conexão no seu escopo de transação ou a transação será promovida, conforme visto como distribuído. Esta versão mais intensiva de recursos da transação será tratada pelo DTC.

Eu sou um grande defensor do simples, e quanto a escrever um invólucro sobre o IDBConnection (padrão de delegado) que expõe a transação. (Desculpe pelo código VB.NET, estou escrevendo isso em vb.net agora) algo assim:

  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

Então você instanciaria isso como:

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

E então você pode obter a transação a qualquer momento usando:

 myConn.Transaction

No caso de alguém enfrentar esse problema no .NET 4.5, você pode usar Transaction.Currentdentro System.Transactions.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top