Posso obter uma referência a uma transação pendente de um objeto SQLConnection?
-
03-07-2019 - |
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)?
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.Current
dentro System.Transactions
.