c# - ejemplo - sqlconnection conn=new sqlconnection
¿Puedo obtener una referencia a una transacción pendiente desde un objeto SqlConnection? (6)
Guau, no creí esto al principio. Me sorprende que CreateCommand () no dé al comando su transacción cuando usa transacciones locales de SQL Server, y que la transacción no está expuesta en el objeto SqlConnection. En realidad, al reflexionar sobre SqlConnection, la transacción actual ni siquiera está almacenada en ese objeto. En la edición de abajo, te di algunos consejos para rastrear el objeto a través de algunas de sus clases internas.
Sé que no puede modificar el método, pero ¿podría usar un TransactionScope en la barra de métodos? Entonces si tienes:
public static void CallingFooBar()
{
using (var ts=new TransactionScope())
{
var foo=new Foo();
foo.Bar();
ts.Complete();
}
}
Esto funcionará, probé usando el código similar al tuyo y una vez que agregué el envoltorio todo funciona bien si puedes hacer esto por supuesto. Como se indicó, tenga cuidado de que si se abre más de una conexión dentro de TransactionScope, se le escalará a una transacción distribuida, a menos que su sistema esté configurado para ellos, obtendrá un error.
Alistarse con el DTC también es varias veces más lento que una transacción local.
Editar
si realmente quieres probar y usar el reflejo, SqlConnection tiene una SqlInternalConnection que a su vez tiene una Propiedad de AvailableInternalTransaction que devuelve un SqlInternalTransaction, esto tiene una propiedad de Parent que devuelve el SqlTransaction que necesitarías.
Supongamos que alguien (que no sea yo) escribe el siguiente código y lo compila en un conjunto:
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();
}
}
La llamada a InvokeOnUpdate (IDbConnection conn) llama a un controlador de eventos que puedo implementar y registrar. Por lo tanto, en este controlador tendré una referencia al objeto IDbConnection, pero no tendré una referencia a la transacción pendiente. ¿Hay alguna manera en que pueda obtener la transacción? En mi controlador OnUpdate quiero ejecutar algo similar a lo siguiente:
private void MyOnUpdateHandler(IDbConnection conn)
{
var cmd = conn.CreateCommand();
cmd.CommandText = someSQLString;
cmd.CommandType = CommandType.Text;
cmd.ExecuteNonQuery();
}
Sin embargo, la llamada a cmd.ExecuteNonQuery () arroja una InvalidOperationException quejándose de que
"ExecuteNonQuery requiere que el comando tenga una transacción cuando la conexión asignada al comando se encuentra en una transacción local pendiente. La propiedad de Transacción del comando no se ha inicializado".
¿Puedo de alguna manera enlistar mi cmd SqlCommand con la transacción pendiente? ¿Puedo recuperar una referencia a la transacción pendiente desde el objeto IDbConnection (me gustaría usar el reflejo si es necesario)?
Soy un gran defensor de lo simple, entonces, ¿qué tal escribir un contenedor sobre IDBConnection (DELEGATE PATTERN) que expone Transaction? (Lo siento por el código VB.NET, estoy escribiendo esto en VB.NET en este momento) Algo así:
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
Entonces crearías esto como:
Dim myConn as new MyConnection(new SqlConnection(...))
y luego puede obtener la transacción en cualquier momento usando:
myConn.Transaction
El objeto de comando solo puede asignarse a un objeto de transacción utilizando uno de sus constructores. Puede elegir el enfoque .NET 2.0 y usar un objeto TransactionScope que se define en el System.Transactions
nombres System.Transactions
(tiene un ensamblado dedicado).
using System.Transactions;
class Foo
{
void Bar()
{
using (TransactionScope scope = new TransactionScope())
{
// Data access
// ...
scope.Complete()
}
}
}
El enfoque System.Transactions utiliza, junto con SQL Server 2005, un coordinador de transacciones ligeras (LTM). Tenga cuidado de no utilizar múltiples objetos de conexión en su alcance de transacción o la transacción se promocionará ya que se ve como distribuida. DTC manejará esta versión más intensiva de recursos de la transacción.
Para cualquiera que esté interesado en la versión C # de la clase de decorador que hizo Denis en VB.NET, aquí 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
}
}
En caso de que alguien esté interesado en el código de reflexión para lograr esto, aquí va:
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:
- Los tipos son internos y las propiedades son privadas, por lo que no puede usar dinámicas
- los tipos internos también le impiden declarar los tipos intermedios como lo hice con el primer ConnectionInfo. Tengo que usar GetType en los objetos
En caso de que alguien haya enfrentado este problema en .Net 4.5, puede usar Transaction.Current
en System.Transactions
.