Могу ли я получить ссылку на ожидающую транзакцию из объекта SqlConnection? - PullRequest
37 голосов
/ 06 января 2009

Предположим, кто-то (кроме меня) пишет следующий код и компилирует его в сборку:

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

Вызов InvokeOnUpdate (IDbConnection conn) вызывает обработчик событий, который я могу реализовать и зарегистрировать. Таким образом, в этом обработчике у меня будет ссылка на объект IDbConnection, но у меня не будет ссылки на ожидающую транзакцию. Есть ли способ, которым я могу получить транзакцию? В моем обработчике OnUpdate я хочу выполнить что-то похожее на следующее:

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

    cmd.ExecuteNonQuery();
}

Однако вызов cmd.ExecuteNonQuery () вызывает исключение InvalidOperationException, сообщающее, что

"ExecuteNonQuery требует команды иметь сделку, когда соединение, назначенное команде в ожидающей локальной транзакции. Свойство транзакции команды не был инициализирован ".

Можно ли каким-либо образом подключить мой cmd SqlCommand к ожидающей транзакции? Могу ли я получить ссылку на ожидающую транзакцию из объекта IDbConnection (я был бы рад использовать отражение при необходимости)?

Ответы [ 6 ]

17 голосов
/ 05 сентября 2012

В случае, если кто-то заинтересован в коде отражения, чтобы выполнить это, вот оно:

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

Примечания:

  • Типы являются внутренними, а свойства частными, поэтому вы не можете использовать динамические
  • внутренние типы также не позволяют вам объявлять промежуточные типы, как я делал с первым ConnectionInfo. Должен использовать GetType на объектах
9 голосов
/ 06 января 2009

Ух ты, я сначала не поверил. Я удивлен, что CreateCommand() не дает команду, что это транзакция при использовании локальных транзакций SQL Server, и что транзакция не отображается в объекте SqlConnection. На самом деле при отражении SqlConnection текущая транзакция даже не сохраняется в этом объекте. В редактируемой ниже я дал вам несколько советов, чтобы отследить объект через некоторые из их внутренних классов.

Я знаю, что вы не можете изменить метод, но не могли бы вы использовать TransactionScope вокруг панели методов? Так что если у вас есть:

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

Это будет работать, я тестировал, используя код, подобный вашему, и как только я добавляю оболочку, все работает нормально, если вы, конечно, можете сделать это. Как указывалось выше, следите за тем, чтобы в TransactionScope было открыто более одного соединения, и вы перейдете к распределенной транзакции, которая, если ваша система не настроена для них, приведет к ошибке.

Регистрация с помощью кода DTC также в несколько раз медленнее, чем локальной транзакции.

Редактировать

Если вы действительно хотите попробовать и использовать рефлексию, у SqlConnection есть SqlInternalConnection, который в свою очередь имеет свойство AvailableInternalTransaction, которое возвращает SqlInternalTransaction, у него есть свойство Parent, которое возвращает необходимую вам SqlTransaction.

4 голосов
/ 22 августа 2012

Для всех, кто интересуется версией C # класса декоратора, которую Денис сделал в VB.NET, вот она:

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
    }
}
3 голосов
/ 06 января 2009

Командный объект может быть назначен только объекту транзакции, используя один из его конструкторов. Вы можете перейти на подход .NET 2.0 и использовать объект TransactionScope, который определен в пространстве имен System.Transactions (имеет выделенную сборку).

   using System.Transactions;

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

Подход System.Transactions использует в сочетании с SQL Server 2005 облегченный координатор транзакций (LTM). Будьте осторожны, чтобы не использовать несколько объектов подключения в своей области транзакции, иначе транзакция будет продвигаться, поскольку она рассматривается как распределенная. Эта более ресурсоемкая версия транзакции будет обработана DTC.

0 голосов
/ 02 июля 2015

Если кто-то сталкивался с этой проблемой в .Net 4.5, вы можете использовать Transaction.Current в System.Transactions.

0 голосов
/ 20 декабря 2010

Я большой сторонник простого, так как насчет написания оболочки над IDBConnection (DELEGATE PATTERN), которая выставляет Transaction. (Извините за код VB.NET, я сейчас пишу это в VB.NET) Примерно так:

  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

Итак, вы бы создали это как:

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

и затем вы можете получить транзакцию в любое время, используя:

 myConn.Transaction
...