¿Puedo obtener una referencia a una transacción pendiente de un objeto SqlConnection?
-
03-07-2019 - |
Pregunta
Supongamos que alguien (aparte de mí) escribe el siguiente código y lo compila en un ensamblaje:
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 forma 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 el comando tener una transacción cuando el La conexión asignada al comando es En una transacción local pendiente. los Propiedad de transacción del comando no se ha inicializado " ;.
¿Puedo de alguna manera alistar mi cmd SqlCommand con la transacción pendiente? ¿Puedo recuperar una referencia a la transacción pendiente desde el objeto IDbConnection (estaría feliz de usar la reflexión si es necesario)?
Solución
Wow, no creía esto al principio. Me sorprende que CreateCommand ()
no le dé el comando a 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 se almacena en ese objeto. En la edición a continuación, te di algunas pistas 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 alrededor de 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 un código similar al tuyo y una vez que agregué la envoltura, todo funcionará bien si puedes hacerlo, por supuesto. Como se señaló, tenga en cuenta que si se abre más de una conexión dentro del TransactionScope
, pasará a una Transacción distribuida que, a menos que su sistema esté configurado para ellos, recibirá un error.
La inscripción con el DTC también es varias veces más lenta que una transacción local.
Editar
si realmente quieres probar y usar la reflexión, SqlConnection tiene una SqlInternalConnection que a su vez tiene una Propiedad de AvailableInternalTransaction que devuelve una SqlInternalTransaction, esta tiene una propiedad de Parent que devuelve la SqlTransaction que necesitarías.
Otros consejos
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 privadas, por lo que no puede usar dinámico
- los tipos internos también le impiden declarar los tipos intermedios como lo hice con la primera ConnectionInfo. Tengo que usar GetType en los objetos
Para cualquiera que esté interesado en la versión C # de la clase de decorador que Denis creó 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
}
}
Al objeto de comando solo se le puede asignar un objeto de transacción usando uno de sus constructores. Puede optar por el enfoque de .NET 2.0 y usar un objeto TransactionScope que se define en el espacio de nombres System.Transactions
(tiene un ensamblaje dedicado).
using System.Transactions;
class Foo
{
void Bar()
{
using (TransactionScope scope = new TransactionScope())
{
// Data access
// ...
scope.Complete()
}
}
}
El enfoque de System.Transactions utiliza, junto con SQL Server 2005, un coordinador de transacciones ligero (LTM). Tenga cuidado de no usar múltiples objetos de conexión en el alcance de su transacción o la transacción se promocionará ya que se ve como distribuida. Esta versión más intensiva en recursos de la transacción será manejada por DTC.
Soy un gran defensor de lo simple, entonces, ¿qué hay de 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
Así que crearías una instancia de esto como:
Dim myConn as new MyConnection(new SqlConnection(...))
y luego puede obtener la transacción en cualquier momento utilizando:
myConn.Transaction
En caso de que alguien enfrente este problema en .Net 4.5, puede usar Transaction.Current
en System.Transactions
.