Kann ich einen Verweis auf eine ausstehende Transaktion von einem SQLConnection -Objekt erhalten?
-
03-07-2019 - |
Frage
Angenommen, jemand (außer mir) schreibt den folgenden Code und kompiliert ihn in eine Versammlung:
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();
}
}
Der Anruf bei InvokeonUpdate (IDBConnection Conn) ruft an einen Ereignishandler, den ich implementieren und registrieren kann. In diesem Handler werde ich also einen Hinweis auf das IdbConnection -Objekt haben, aber ich werde keinen Bezug auf die anhängige Transaktion haben. Gibt es eine Möglichkeit, wie ich die Transaktion erhalten kann? In meinem Onupdate -Handler möchte ich etwas Ähnliches wie folgt ausführen:
private void MyOnUpdateHandler(IDbConnection conn)
{
var cmd = conn.CreateCommand();
cmd.CommandText = someSQLString;
cmd.CommandType = CommandType.Text;
cmd.ExecuteNonQuery();
}
Der Aufruf an cmd.executenonQuery () wirft jedoch eine InvalidoperationException aus, die sich darüber beschwert
"ExecutenOnQuery erfordert, dass der Befehl eine Transaktion abhält, wenn die dem Befehl zugewiesene Verbindung in einer ausstehenden lokalen Transaktion liegt. Die Transaktionseigenschaft des Befehls wurde nicht initialisiert".
Kann ich meine SQLCommand -CMD in irgendeiner Weise mit der ausstehenden Transaktion einbeziehen? Kann ich einen Verweis auf die anhängige Transaktion aus dem IdbConnection -Objekt abrufen (ich würde gerne bei Bedarf Reflexion verwenden)?
Lösung
Wow, ich habe das zuerst nicht geglaubt. Ich bin überrascht, dass ich das überrascht habe CreateCommand()
Gibt dem Befehl keine Transaktion bei der Verwendung lokaler SQL -Servertransaktionen und der Transaktion ist nicht auf der SqlConnection
Objekt. Eigentlich beim Nachdenken über SqlConnection
Die aktuelle Transaktion wird in diesem Objekt nicht einmal gespeichert. Im Bearbeitungsbrüll habe ich Ihnen einige Hinweise gegeben, um das Objekt über einige ihrer internen Klassen aufzuspüren.
Ich weiß, dass Sie die Methode nicht ändern können, aber könnten Sie ein Transactionscope um die Methodenleiste verwenden? Also, wenn du hast:
public static void CallingFooBar()
{
using (var ts=new TransactionScope())
{
var foo=new Foo();
foo.Bar();
ts.Complete();
}
}
Dies wird funktionieren, ich habe mit ähnlichem Code mit Ihrem getestet und sobald ich den Wrapper hinzufüge, funktioniert alles einwandfrei, wenn Sie dies natürlich tun können. Wie ausgedrückt, achten Sie auf, wenn mehr eine Verbindung innerhalb der geöffnet ist TransactionScope
Sie werden zu einer verteilten Transaktion eskaliert. Wenn Ihr System nicht dafür konfiguriert ist, erhalten Sie einen Fehler.
Die Einstellung mit dem DTC ist auch mehrmals langsamer als eine lokale Transaktion.
Bearbeiten
Wenn Sie wirklich versuchen möchten, Reflection zu verwenden, verfügt SQLConnection über eine SQlinternalConnection. Dies hat wiederum eine Eigenschaft von verfügbarenInternaltransaction, die eine SQLINTENTELTRANTRANSACTIONACE zurückgibt. Dies hat eine Eigenschaft von übergeordnetem, die die benötigten SQLTransaction zurückgibt.
Andere Tipps
Falls jemand an dem Reflexionscode interessiert ist, um dies zu erreichen, heißt es hier:
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;
}
Anmerkungen:
- Die Typen sind intern und die Eigenschaften privat, sodass Sie keine Dynamik verwenden können
- Interne Typen verhindern auch, dass Sie die Zwischentypen deklarieren, wie ich es beim ersten ConnectionInfo getan habe. Ich muss Gettype für die Objekte verwenden
Für jeden, der sich für die C# -Version der Dekorateurin interessiert, die Denis in VB.NET gemacht hat, ist es hier:
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
}
}
Dem Befehlsobjekt kann nur ein Transaktionsobjekt mit einem seiner Konstruktoren zugewiesen werden. Sie können sich für den .NET 2.0 -Ansatz entscheiden und ein Transactionscope -Objekt verwenden, das in der definiert ist System.Transactions
Namespace (hat eine spezielle Baugruppe).
using System.Transactions;
class Foo
{
void Bar()
{
using (TransactionScope scope = new TransactionScope())
{
// Data access
// ...
scope.Complete()
}
}
}
Der Ansatz des Systems.Transactions verwendet in Verbindung mit SQL Server 2005, einem leichten Transaktionskoordinator (LTM). Achten Sie darauf, dass Sie nicht mehrere Verbindungsobjekte in Ihrem Transaktionsbereich verwenden, da die Transaktion als verteilt angesehen wird. Diese ressourcenintensivere Version der Transaktion wird dann von DTC bearbeitet.
Ich bin ein großer Befürworter von einfachem. (Entschuldigung für VB.NET -Code, ich schreibe dies gerade in VB.NET) Etwas wie folgt:
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
Sie würden dies also als:
Dim myConn as new MyConnection(new SqlConnection(...))
Und dann können Sie die Transaktion jederzeit mit verwenden:
myConn.Transaction
Falls jeder mit diesem Problem auf .NET 4.5 konfrontiert war, können Sie verwenden Transaction.Current
in System.Transactions
.