Могу ли я получить ссылку на ожидающую транзакцию из объекта SqlConnection?
Предположим, что кто-то (кроме меня) записывает следующий код и компилирует его в сборку:
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 требует команды иметь транзакцию, когда соединение, назначенное команде, является в ожидающей локальной транзакции. Свойство транзакции команды не был инициализирован".
Могу ли я каким-либо образом заручиться моей командой SqlCommand с ожидающей транзакцией? Могу ли я получить ссылку на ожидающую транзакцию из объекта IDbConnection (я был бы рад использовать отражение при необходимости)?
Ответы
Ответ 1
Ничего себе, я сначала не верил в это. Я удивлен, что CreateCommand() не передает команду транзакции при использовании локальных транзакций SQL Server и что транзакция не отображается в объекте SqlConnection. Фактически, когда отражается на SqlConnection, текущая транзакция даже не сохраняется в этом объекте. В следующем редакторе я дал вам некоторые подсказки для отслеживания объекта через некоторые из их внутренних классов.
Я знаю, что вы не можете изменить метод, но можете ли вы использовать TransactionScope вокруг панели методов? Поэтому, если у вас есть:
public static void CallingFooBar()
{
using (var ts=new TransactionScope())
{
var foo=new Foo();
foo.Bar();
ts.Complete();
}
}
Это сработает, я проверил с помощью simillar-кода на ваш, и как только я добавлю обертку, все отлично работает, если вы можете это сделать, конечно. Как указано, не забудьте, если в TransactionScope откроется более одного соединения, вы будете перенаправлены на распределенный транзакт, который, если ваша система не настроена для них, вы получите сообщение об ошибке.
Включение с помощью DTC также в несколько раз медленнее локальной транзакции.
Изменить
Если вы действительно хотите попробовать и использовать отражение, SqlConnection имеет SqlInternalConnection, это, в свою очередь, имеет свойство AvailableInternalTransaction, которое возвращает SqlInternalTransaction, у этого есть свойство Parent, которое возвращает требуемое SqlTransaction.
Ответ 2
Если кто-то интересуется кодом отражения, чтобы выполнить это, вот он:
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 для объектов
Ответ 3
Для любого, кто интересуется версией класса С# класса декоратора, созданной Денисом в 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 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
}
}
Ответ 4
Объекту команды может быть назначен объект транзакции с использованием одного из его конструкторов. Вы можете использовать подход .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.
Ответ 5
Я большой сторонник простого, так как насчет написания обертки над IDBConnection (DELEGATE PATTERN), который предоставляет транзакцию. (Извините за код 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
Ответ 6
Если кто-то столкнулся с этой проблемой на .Net 4.5, вы можете использовать Transaction.Current
в System.Transactions
.