Condividi tramite


Implementazione di una transazione implicita utilizzando l'ambito di transazione

La TransactionScope classe fornisce un modo semplice per contrassegnare un blocco di codice come partecipante a una transazione, senza che sia necessario interagire con la transazione stessa. Un ambito di transazione può selezionare e gestire automaticamente la transazione ambientale. Grazie alla facilità d'uso e all'efficienza, è consigliabile usare la classe TransactionScope quando si sviluppa un'applicazione di transazione.

Inoltre, non è necessario integrare le risorse in modo esplicito con la transazione. Qualsiasi System.Transactions gestore delle risorse, ad esempio SQL Server 2005, può rilevare l'esistenza di una transazione ambientale creata dall'ambito ed eseguire automaticamente l'integrazione.

Creazione di un ambito di transazione

L'esempio seguente illustra un utilizzo semplice della TransactionScope classe .

// This function takes arguments for 2 connection strings and commands to create a transaction
// involving two SQL Servers. It returns a value > 0 if the transaction is committed, 0 if the
// transaction is rolled back. To test this code, you can connect to two different databases
// on the same server by altering the connection string, or to another 3rd party RDBMS by
// altering the code in the connection2 code block.
static public int CreateTransactionScope(
    string connectString1, string connectString2,
    string commandText1, string commandText2)
{
    // Initialize the return value to zero and create a StringWriter to display results.
    int returnValue = 0;
    System.IO.StringWriter writer = new System.IO.StringWriter();

    try
    {
        // Create the TransactionScope to execute the commands, guaranteeing
        // that both commands can commit or roll back as a single unit of work.
        using (TransactionScope scope = new TransactionScope())
        {
            using (SqlConnection connection1 = new SqlConnection(connectString1))
            {
                // Opening the connection automatically enlists it in the
                // TransactionScope as a lightweight transaction.
                connection1.Open();

                // Create the SqlCommand object and execute the first command.
                SqlCommand command1 = new SqlCommand(commandText1, connection1);
                returnValue = command1.ExecuteNonQuery();
                writer.WriteLine("Rows to be affected by command1: {0}", returnValue);

                // If you get here, this means that command1 succeeded. By nesting
                // the using block for connection2 inside that of connection1, you
                // conserve server and network resources as connection2 is opened
                // only when there is a chance that the transaction can commit.
                using (SqlConnection connection2 = new SqlConnection(connectString2))
                {
                    // The transaction is escalated to a full distributed
                    // transaction when connection2 is opened.
                    connection2.Open();

                    // Execute the second command in the second database.
                    returnValue = 0;
                    SqlCommand command2 = new SqlCommand(commandText2, connection2);
                    returnValue = command2.ExecuteNonQuery();
                    writer.WriteLine("Rows to be affected by command2: {0}", returnValue);
                }
            }

            // The Complete method commits the transaction. If an exception has been thrown,
            // Complete is not  called and the transaction is rolled back.
            scope.Complete();
        }
    }
    catch (TransactionAbortedException ex)
    {
        writer.WriteLine("TransactionAbortedException Message: {0}", ex.Message);
    }

    // Display messages.
    Console.WriteLine(writer.ToString());

    return returnValue;
}
'  This function takes arguments for 2 connection strings and commands to create a transaction
'  involving two SQL Servers. It returns a value > 0 if the transaction is committed, 0 if the
'  transaction is rolled back. To test this code, you can connect to two different databases
'  on the same server by altering the connection string, or to another 3rd party RDBMS
'  by altering the code in the connection2 code block.
Public Function CreateTransactionScope( _
  ByVal connectString1 As String, ByVal connectString2 As String, _
  ByVal commandText1 As String, ByVal commandText2 As String) As Integer

    ' Initialize the return value to zero and create a StringWriter to display results.
    Dim returnValue As Integer = 0
    Dim writer As System.IO.StringWriter = New System.IO.StringWriter

    Try
        ' Create the TransactionScope to execute the commands, guaranteeing
        '  that both commands can commit or roll back as a single unit of work.
        Using scope As New TransactionScope()
            Using connection1 As New SqlConnection(connectString1)
                ' Opening the connection automatically enlists it in the
                ' TransactionScope as a lightweight transaction.
                connection1.Open()

                ' Create the SqlCommand object and execute the first command.
                Dim command1 As SqlCommand = New SqlCommand(commandText1, connection1)
                returnValue = command1.ExecuteNonQuery()
                writer.WriteLine("Rows to be affected by command1: {0}", returnValue)

                ' If you get here, this means that command1 succeeded. By nesting
                ' the using block for connection2 inside that of connection1, you
                ' conserve server and network resources as connection2 is opened
                ' only when there is a chance that the transaction can commit.
                Using connection2 As New SqlConnection(connectString2)
                    ' The transaction is escalated to a full distributed
                    ' transaction when connection2 is opened.
                    connection2.Open()

                    ' Execute the second command in the second database.
                    returnValue = 0
                    Dim command2 As SqlCommand = New SqlCommand(commandText2, connection2)
                    returnValue = command2.ExecuteNonQuery()
                    writer.WriteLine("Rows to be affected by command2: {0}", returnValue)
                End Using
            End Using

            ' The Complete method commits the transaction. If an exception has been thrown,
            ' Complete is called and the transaction is rolled back.
            scope.Complete()
        End Using
    Catch ex As TransactionAbortedException
        writer.WriteLine("TransactionAbortedException Message: {0}", ex.Message)
    End Try

    ' Display messages.
    Console.WriteLine(writer.ToString())

    Return returnValue
End Function

L'ambito della transazione viene avviato una volta creato un nuovo oggetto TransactionScope. Come illustrato nell'esempio di codice, è consigliabile creare ambiti con un'istruzione using . L'istruzione using è disponibile sia in C# che in Visual Basic e funziona come un tryblocco ...finally per assicurarsi che l'ambito venga eliminato correttamente.

Quando si crea un'istanza di TransactionScope, il gestore delle transazioni determina in quale transazione partecipare. Una volta determinato, l'ambito partecipa sempre a tale transazione. La decisione si basa su due fattori: se è presente una transazione ambientale e il valore del parametro TransactionScopeOption nel costruttore. La transazione di ambiente è la transazione all'interno della quale viene eseguito il codice. È possibile ottenere un riferimento alla transazione di ambiente chiamando la proprietà statica Transaction.Current della Transaction classe . Per altre informazioni sull'uso di questo parametro, vedere la sezione Gestione del flusso delle transazioni tramite TransactionScopeOption di questo argomento.

Completamento di un ambito di transazione

Quando l'applicazione completa tutto il lavoro che vuole eseguire in una transazione, è necessario chiamare il TransactionScope.Complete metodo una sola volta per informare il gestore transazioni che è accettabile eseguire il commit della transazione. È consigliabile inserire la chiamata a Complete come ultima istruzione nel using blocco.

Se non si chiama questo metodo, la transazione viene interrotta perché la gestione transazioni interpreta questa operazione come errore di sistema o equivalente a un'eccezione generata nell'ambito della transazione. Tuttavia, chiamare questo metodo non garantisce il completamento della transazione. Si tratta semplicemente di informare il gestore delle transazioni riguardo al proprio stato. Dopo aver chiamato il Complete metodo , non è più possibile accedere alla transazione di ambiente usando la Current proprietà e il tentativo di farlo comporterà la generazione di un'eccezione.

Se l'oggetto TransactionScope ha creato inizialmente la transazione, il lavoro effettivo di commit della transazione da parte del gestore transazioni si verifica dopo l'ultima riga di codice nel using blocco. Se la transazione non è stata creata, il commit viene eseguito ogni volta che Commit viene chiamato dal proprietario dell'oggetto CommittableTransaction . A quel punto il gestore delle transazioni chiama i gestori risorse e li informa di eseguire il commit o il rollback, in base al fatto che il Complete metodo sia stato chiamato sull'oggetto TransactionScope .

L'istruzione using garantisce che il Dispose metodo dell'oggetto TransactionScope venga chiamato anche se si verifica un'eccezione. Il Dispose metodo contrassegna la fine dell'ambito della transazione. Le eccezioni che si verificano dopo la chiamata a questo metodo potrebbero non influire sulla transazione. Questo metodo ripristina anche lo stato precedente della transazione di ambiente.

Viene TransactionAbortedException generata un'eccezione se l'ambito crea la transazione e la transazione viene abortita. Viene generata un'eccezione TransactionInDoubtException se il gestore delle transazioni non riesce a raggiungere una decisione di Commit. Non viene generata alcuna eccezione se la transazione viene eseguita.

Rollback di una transazione

Se si vuole eseguire il rollback di una transazione, non è consigliabile chiamare il Complete metodo all'interno dell'ambito della transazione. Ad esempio, è possibile generare un'eccezione all'interno dell'ambito. Verrà eseguito il rollback della transazione in cui partecipa.

Gestione del flusso delle transazioni tramite TransactionScopeOption

L'ambito della transazione può essere annidato chiamando un metodo che usa un TransactionScope dall'interno di un metodo che usa il proprio ambito, come nel caso del RootMethod nell'esempio seguente,

void RootMethod()
{
    using(TransactionScope scope = new TransactionScope())
    {
        /* Perform transactional work here */
        SomeMethod();
        scope.Complete();
    }
}

void SomeMethod()
{
    using(TransactionScope scope = new TransactionScope())
    {
        /* Perform transactional work here */
        scope.Complete();
    }
}

L'ambito della transazione più alto viene definito ambito radice.

La TransactionScope classe fornisce diversi costruttori di overload che accettano un'enumerazione del tipo TransactionScopeOption, che definisce il comportamento transazionale dell'ambito.

Un TransactionScope oggetto ha tre opzioni:

  • Aderire alla transazione ambientale o crearne una nuova se non esiste.

  • Essere un nuovo ambito radice, ovvero avviare una nuova transazione e avere tale transazione come nuova transazione di ambiente all'interno del proprio ambito.

  • Non prendere parte a una transazione. Di conseguenza, non esiste alcuna transazione ambientale.

Se l'ambito viene istanziato con Required e c'è una transazione ambientale presente, l'ambito entra a far parte di quella transazione. Se invece non esiste alcuna transazione di ambiente, l'ambito crea una nuova transazione e diventa l'ambito radice. Questo è il valore predefinito. Quando Required viene usato, il codice all'interno dell'ambito non deve comportarsi in modo diverso, indipendentemente dal fatto che si tratti della radice o semplicemente dell'unione della transazione ambientale. Deve funzionare in modo identico in entrambi i casi.

Se l'ambito viene istanziato con RequiresNew, è sempre l'ambito radice. Avvia una nuova transazione e questa diventa la nuova transazione ambientale all'interno dell'ambito.

Se viene istanziato un ambito con Suppress, non partecipa mai a una transazione, indipendentemente dalla presenza di una transazione di ambiente. Un ambito di cui è stata creata un'istanza con questo valore ha sempre null come transazione ambientale.

Le opzioni precedenti sono riepilogate nella tabella seguente.

OpzioneAmbitoTransazione Transazione ambientale L'ambito è coinvolto in
Obbligatorio NO Nuova transazione (sarà la radice)
Richiede qualcosa di nuovo NO Nuova transazione (sarà la radice)
Reprimere NO Nessuna transazione
Obbligatorio Transazione ambientale
Richiede qualcosa di nuovo Nuova transazione (sarà la radice)
Reprimere Nessuna transazione

Quando un TransactionScope oggetto si unisce a una transazione ambientale esistente, la distruzione dell'oggetto ambito potrebbe non terminare la transazione, a meno che l'ambito non interrompa la transazione. Se la transazione di ambiente è stata creata da un ambito radice, Commit viene chiamato sulla transazione solo quando l'ambito radice viene eliminato. Se la transazione è stata creata manualmente, la transazione termina quando viene interrotta o confermata dal creatore.

Nell'esempio seguente viene mostrato un oggetto TransactionScope che crea tre oggetti ambito annidati, ciascuno istanziato con un valore diverso TransactionScopeOption.

using(TransactionScope scope1 = new TransactionScope())
//Default is Required
{
    using(TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Required))
    {
        //...
    }

    using(TransactionScope scope3 = new TransactionScope(TransactionScopeOption.RequiresNew))
    {
        //...  
    }
  
    using(TransactionScope scope4 = new TransactionScope(TransactionScopeOption.Suppress))
    {
        //...  
    }
}

L'esempio mostra un blocco di codice senza alcuna transazione di ambiente che crea un nuovo ambito (scope1) con Required. L'ambito scope1 è un ambito radice poiché crea una nuova transazione (Transazione A) e rende Transazione A la transazione corrente. Scope1 crea quindi altri tre oggetti, ognuno con un valore diverso TransactionScopeOption . Ad esempio, scope2 viene creato con Requirede poiché è presente una transazione di ambiente, unisce la prima transazione creata da scope1. Si noti che scope3 è l'ambito radice di una nuova transazione e che scope4 non ha alcuna transazione di ambiente.

Anche se il valore predefinito e più comunemente usato di TransactionScopeOption è Required, ognuno degli altri valori ha lo scopo univoco.

Codice non transazionale all'interno di un ambito di transazione

Suppress è utile quando si desidera mantenere le operazioni eseguite dalla sezione del codice e non si vuole interrompere la transazione di ambiente se le operazioni hanno esito negativo. Ad esempio, quando si desidera eseguire operazioni di registrazione o controllo o quando si desidera pubblicare eventi agli abbonati indipendentemente dal fatto che le transazioni ambientali eseguano commit o interruzione. Questo valore consente di avere una sezione di codice non transazionale all'interno di un ambito di transazione, come illustrato nell'esempio seguente.

using(TransactionScope scope1 = new TransactionScope())
{
    try
    {
        //Start of non-transactional section
        using(TransactionScope scope2 = new
            TransactionScope(TransactionScopeOption.Suppress))  
        {  
            //Do non-transactional work here  
        }  
        //Restores ambient transaction here
   }
   catch {}  
   //Rest of scope1
}

Voto all'interno di un ambito annidato

Anche se un ambito annidato può unire la transazione di ambiente dell'ambito radice, la chiamata Complete nell'ambito annidato non ha alcun effetto sull'ambito radice. Verrà eseguito il commit della transazione solo se tutti gli ambiti, dall'ambito radice all'ultimo ambito annidato, voteranno per eseguire il commit della transazione. La mancata chiamata Complete in un ambito annidato influirà sull'ambito radice, poiché la transazione ambientale verrà immediatamente abortita.

Impostazione del timeout di TransactionScope

Alcuni costruttori sovraccaricati di TransactionScope accettano un valore di tipo TimeSpan, usato per controllare il timeout della transazione. Un timeout impostato su zero indica un timeout infinito. Il timeout infinito è utile principalmente per il debug, quando si vuole isolare un problema nella logica di business eseguendo un'istruzione all'interno del codice e non si vuole che la transazione di cui si esegue il debug venga timeout durante il tentativo di individuare il problema. Prestare molta attenzione nell'usare il valore di timeout infinito in tutti gli altri casi, perché aggira le protezioni contro i deadlock delle transazioni.

In genere, il TransactionScope timeout viene impostato su valori diversi da quello predefinito in due casi. Il primo è durante lo sviluppo, quando si vuole testare il modo in cui l'applicazione gestisce le transazioni interrotte. Impostando il timeout su un valore ridotto (ad esempio un millisecondo), si verifica l'esito negativo della transazione e quindi si può osservare il codice di gestione degli errori. Il secondo caso in cui si imposta il valore come minore del timeout predefinito è quando si ritiene che l'ambito sia coinvolto in contesa di risorse, causando deadlock. In tal caso, si vuole interrompere la transazione il prima possibile e non attendere la scadenza del timeout predefinito.

Quando un ambito unisce una transazione di ambiente, ma specifica un timeout inferiore a quello su cui è impostata la transazione di ambiente, il nuovo timeout più breve viene applicato all'oggetto TransactionScope e l'ambito deve terminare entro il tempo annidato specificato oppure la transazione viene interrotta automaticamente. Se il timeout dell'ambito annidato è maggiore di quello della transazione ambientale, non ha alcun effetto.

Impostazione del livello di isolamento TransactionScope

Alcuni costruttori sovraccarichi di TransactionScope accettano una struttura di tipo TransactionOptions per specificare un livello di isolamento, oltre a un valore di timeout. Per impostazione predefinita, la transazione viene eseguita con il livello di isolamento impostato su Serializable. La selezione di un livello di isolamento diverso da Serializable viene comunemente usata per i sistemi a elevato utilizzo di lettura. Ciò richiede una conoscenza approfondita della teoria dell'elaborazione delle transazioni e della semantica della transazione stessa, dei problemi di concorrenza coinvolti e delle conseguenze per la coerenza del sistema.

Inoltre, non tutti i gestori di risorse supportano tutti i livelli di isolamento e possono scegliere di partecipare alla transazione a un livello superiore rispetto a quello configurato.

Ogni livello di isolamento oltre Serializable a è soggetto a incoerenza risultante da altre transazioni che accedono alle stesse informazioni. La differenza tra i vari livelli di isolamento sta nel modo in cui vengono utilizzati i blocchi di lettura e scrittura. Un blocco può essere mantenuto solo quando la transazione accede ai dati nel gestore risorse oppure può essere mantenuta fino a quando non viene eseguito il commit o l'interruzione della transazione. Il primo è migliore per la velocità effettiva, il secondo per la coerenza. I due tipi di blocchi e i due tipi di operazioni (lettura/scrittura) offrono quattro livelli di isolamento di base. Per altre informazioni, vedere IsolationLevel.

Quando si usano oggetti annidati TransactionScope, è necessario configurare tutti gli ambiti annidati in modo da usare esattamente lo stesso livello di isolamento se si vuole partecipare alla transazione ambientale. Se un oggetto annidato TransactionScope tenta di partecipare alla transazione ambientale ma specifica un livello di isolamento diverso, viene sollevata un'eccezione ArgumentException.

Interoperabilità con COM+

Quando si crea una nuova TransactionScope istanza, è possibile usare l'enumerazione EnterpriseServicesInteropOption in uno dei costruttori per specificare come interagire con COM+. Per altre informazioni, vedere Interoperabilità con Enterprise Services e transazioni COM+.

Vedere anche