Domanda

Supponiamo che qualcuno (diverso da me) scriva il seguente codice e lo compili in un assembly:

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 chiamata a InvokeOnUpdate (IDbConnection conn) chiama un gestore di eventi che posso implementare e registrare. Pertanto, in questo gestore avrò un riferimento all'oggetto IDbConnection, ma non avrò un riferimento alla transazione in sospeso. Esiste un modo per ottenere una sospensione della transazione? Nel mio gestore OnUpdate voglio eseguire qualcosa di simile al seguente:

private void MyOnUpdateHandler(IDbConnection conn) 
{
    var cmd = conn.CreateCommand();
    cmd.CommandText = someSQLString;
    cmd.CommandType = CommandType.Text;

    cmd.ExecuteNonQuery();
}

Tuttavia, la chiamata a cmd.ExecuteNonQuery () genera un InvalidOperationException lamentandosi che

  

" ExecuteNonQuery richiede il comando   avere una transazione quando il   la connessione assegnata al comando è   in una transazione locale in sospeso. Il   Proprietà di transazione del comando   non è stato inizializzato " ;.

Posso in qualche modo arruolare il mio cmd SqlCommand con la transazione in sospeso? Posso recuperare un riferimento alla transazione in sospeso dall'oggetto IDbConnection (sarei felice di usare la riflessione se necessario)?

È stato utile?

Soluzione

Wow, all'inizio non ci credevo. Sono sorpreso che CreateCommand () non dia il comando è transazione quando si utilizzano transazioni locali di SQL Server e che la transazione non è esposta sull'oggetto SqlConnection . In realtà, riflettendo su SqlConnection la transazione corrente non viene nemmeno memorizzata in quell'oggetto. Nel seguito di modifica, ti ho dato alcuni suggerimenti per rintracciare l'oggetto tramite alcune delle loro classi interne.

So che non puoi modificare il metodo ma potresti usare un TransactionScope attorno alla barra del metodo? Quindi se hai:

public static void CallingFooBar()
{
   using (var ts=new TransactionScope())
   {
      var foo=new Foo();
      foo.Bar();
      ts.Complete();
   }
}

Funzionerà, ho provato usando un codice simile al tuo e una volta aggiunto il wrapper tutto funzionerà bene se puoi farlo ovviamente. Come sottolineato, se viene aperta più di una connessione all'interno del TransactionScope verrai convertito in una Transazione distribuita che, a meno che il tuo sistema non sia configurato per loro, visualizzerà un errore.

Anche l'arruolamento con il DTC è più volte più lento di una transazione locale.

Modifica

se vuoi davvero provare a usare la riflessione, SqlConnection ha una SqlInternalConnection che a sua volta ha una proprietà di AvailableInternalTransaction che restituisce una SqlInternalTransaction, questa ha una proprietà di Parent che restituisce la SqlTransaction di cui avresti bisogno.

Altri suggerimenti

Nel caso in cui qualcuno sia interessato al codice di riflessione per farlo, eccolo qui:

    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;
    }

Note:

  • I tipi sono interni e le proprietà private quindi non è possibile utilizzare la dinamica
  • i tipi interni ti impediscono anche di dichiarare i tipi intermedi come ho fatto con il primo ConnectionInfo. Devo usare GetType sugli oggetti

Per chiunque sia interessato alla versione C # della classe decoratrice che Denis ha realizzato in VB.NET, eccolo qui:

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
    }
}

All'oggetto comando può essere assegnato un oggetto transazione solo usando uno dei suoi costruttori. Puoi scegliere l'approccio .NET 2.0 e utilizzare un oggetto TransactionScope che è definito nello spazio dei nomi System.Transactions (ha un assembly dedicato).

   using System.Transactions;

    class Foo
    {   
        void Bar()
        {
            using (TransactionScope scope = new TransactionScope())
            {
                // Data access
                // ...
                scope.Complete()
            }
        }
    }

L'approccio System.Transactions utilizza in combinazione con SQL Server 2005 un leggero coordinatore delle transazioni (LTM). Fare attenzione a non utilizzare più oggetti di connessione nell'ambito della transazione o la transazione verrà promossa come viene vista come distribuita. Questa versione più intensiva delle risorse della transazione verrà quindi gestita da DTC.

Sono un grande sostenitore del semplice, quindi che ne dite di scrivere un wrapper su IDBConnection (DELEGATE PATTERN) che espone Transaction. (Mi dispiace per il codice VB.NET, sto scrivendo questo in VB.NET in questo momento) Qualcosa del genere:

  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

Quindi dovresti creare un'istanza come:

Dim myConn as new MyConnection(new SqlConnection(...))

e quindi puoi ottenere la transazione in qualsiasi momento utilizzando:

 myConn.Transaction

Nel caso in cui qualcuno abbia riscontrato questo problema su .Net 4.5 è possibile utilizzare Transaction.Current in System.Transactions .

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top