Puis-je obtenir une référence à une transaction en attente à partir d'un objet SqlConnection?

StackOverflow https://stackoverflow.com/questions/417024

Question

Supposons que quelqu'un (autre que moi) écrit le code suivant et le compile dans un assemblage:

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

L'appel à InvokeOnUpdate (IDbConnection conn) appelle un gestionnaire d'événements que je peux implémenter et enregistrer. Ainsi, dans ce gestionnaire, j'aurai une référence à l'objet IDbConnection, mais je n'aurai pas de référence à la transaction en attente. Existe-t-il un moyen de mettre la main sur la transaction? Dans mon gestionnaire OnUpdate, je souhaite exécuter quelque chose de similaire au suivant:

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

    cmd.ExecuteNonQuery();
}

Cependant, l'appel à cmd.ExecuteNonQuery () lève une exception InvalidOperationException en se plaignant que

  

" ExecuteNonQuery requiert la commande   d'avoir une transaction lorsque le   connexion affectée à la commande est   dans une transaction locale en attente. le   Propriété de transaction de la commande   n'a pas été initialisé ".

Puis-je, de quelque manière que ce soit, inscrire ma cmd SqlCommand dans la transaction en attente? Puis-je récupérer une référence à la transaction en attente à partir de l'objet IDbConnection (je serais heureux d'utiliser la réflexion si nécessaire)?

Était-ce utile?

La solution

Wow, je n'y croyais pas au début. Je suis surpris que CreateCommand () ne donne pas à la commande sa transaction lors de l'utilisation de transactions SQL Server locales et que la transaction n'est pas exposée sur l'objet SqlConnection . En fait, lorsqu’on réfléchit sur SqlConnection , la transaction en cours n’est même pas stockée dans cet objet. Dans l'édition ci-dessous, je vous ai donné quelques astuces pour retrouver l'objet via certaines de leurs classes internes.

Je sais que vous ne pouvez pas modifier la méthode, mais pourriez-vous utiliser un TransactionScope autour de la barre de méthode? Donc, si vous avez:

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

Cela fonctionnera, j'ai testé en utilisant un code similaire au vôtre et une fois que j'ai ajouté le wrapper, tout fonctionne correctement si vous pouvez le faire, bien sûr. Comme indiqué, soyez attentif si plus d'une connexion est ouverte dans TransactionScope , vous serez transféré en une transaction distribuée qui, à moins que votre système ne soit configuré pour eux, vous obtiendrez une erreur.

L'inscription au DTC est également plusieurs fois plus lente qu'une transaction locale.

Modifier

Si vous voulez vraiment essayer d’utiliser la réflexion, SqlConnection a un SqlInternalConnection qui a à son tour une propriété de AvailableInternalTransaction qui retourne un SqlInternalTransaction, cela a une propriété de Parent qui retourne le SqlTransaction dont vous auriez besoin.

Autres conseils

Au cas où le code de réflexion vous intéresserait, le voici:

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

Notes:

  • Les types sont internes et les propriétés privées, vous ne pouvez donc pas utiliser dynamique
  • Les types internes vous empêchent également de déclarer les types intermédiaires comme je l'avais fait avec le premier ConnectionInfo. Je dois utiliser GetType sur les objets

Pour tous ceux qui sont intéressés par la version C # de la classe de décorateur que Denis a créée dans VB.NET, la voici:

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
    }
}

L'objet de commande ne peut être affecté à un objet de transaction qu'à l'aide de l'un de ses constructeurs. Vous pouvez choisir l’approche .NET 2.0 et utiliser un objet TransactionScope défini dans l’espace de nom System.Transactions (avec un assembly dédié).

   using System.Transactions;

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

L'approche System.Transactions utilise conjointement avec SQL Server 2005 un coordinateur de transaction léger (LTM). Veillez à ne pas utiliser plusieurs objets de connexion dans l'étendue de votre transaction, sinon la transaction sera promue car elle est considérée comme distribuée. Cette version de la transaction, plus gourmande en ressources, sera ensuite gérée par DTC.

Je suis un grand partisan du simple, alors pourquoi ne pas écrire un wrapper sur IDBConnection (DELEGATE PATTERN) qui expose Transaction. (Désolé pour le code VB.NET, j'écris ceci actuellement dans VB.NET) Quelque chose comme ça:

  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

Vous pouvez donc instancier ceci en tant que:

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

et vous pouvez alors obtenir la transaction à tout moment en utilisant:

 myConn.Transaction

Si quelqu'un rencontrait ce problème sous .Net 4.5, vous pouvez utiliser Transaction.Current . dans System.Transactions .

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top