Implementar uma transação implícita usando o escopo da transação

O TransactionScope classe fornece uma maneira simples para marcar um bloco de código como participar de uma transação, sem a necessidade de interagir com a própria transação. Selecione um escopo de transação e gerenciar a transação ambiente automaticamente. Devido à sua facilidade de uso e a eficiência, é recomendável que você use o TransactionScope classe ao desenvolver um aplicativo de transação.

Além disso, você não precisa conseguir recursos explicitamente com a transação. Qualquer System.Transactions Gerenciador de recursos (como o SQL Server 2005) pode detectar a existência de uma transação ambiente criada pelo escopo e inscrever-se automaticamente.

Criar um escopo de transação

O exemplo a seguir mostra um uso simple do 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

O escopo da transação é iniciado depois que você cria um objeto TransactionScope. Conforme ilustrado no exemplo de código, é recomendável que você crie escopos com uma instrução using. A instrução using está disponível em C# e no Visual Basic, e funciona como um bloco tryfinally para garantir que o escopo é descartado corretamente.

Quando você cria uma instância TransactionScope, o Gerenciador de transações determina qual transação para participar. Uma vez determinado, o escopo sempre participa dessa transação. A decisão se baseia em dois fatores: se houver uma transação de ambiente e o valor de TransactionScopeOption parâmetro no construtor. A transação ambiente é a transação na qual o código é executado. Você pode obter uma referência para a transação de ambiente chamando estático Transaction.Current propriedade o Transaction classe. Para obter mais informações sobre como esse parâmetro é usado, confira a seção Como gerenciar o fluxo da transação usando TransactionScopeOption deste tópico.

Concluindo um escopo de transação

Quando seu aplicativo conclui todo o trabalho que deseja executar em uma transação, você deve chamar o TransactionScope.Complete método apenas uma vez para informar o Gerenciador de transações que é aceitável para confirmar a transação. É muito bom colocar a chamada para Complete como a última instrução no bloco using.

Falha ao chamar esse método anula a transação, porque o gerenciador de transação interpretará isso como uma falha do sistema ou equivalente a uma exceção gerada dentro do escopo da transação. No entanto, chamar este método não garante que a transação será ser confirmada. É simplesmente uma maneira de informar o Gerenciador de transações do seu status. Depois de chamar o Complete método, você não pode acessar a transação de ambiente usando o Current propriedade e tentar fazer isso resultará em uma exceção é lançada.

Se o objeto TransactionScope criou a transação inicialmente, o trabalho real de confirmação da transação, o gerenciador de transação após a última linha de código no bloco using. Se ele não criou a transação, a confirmação ocorre sempre que Commit é chamado pelo proprietário do CommittableTransaction objeto. Nesse momento o gerenciador de transação chama os gerentes de recurso e os informa de que devem confirmar ou reverter, dependendo de se o método Complete foi chamado no objeto TransactionScope.

A instrução using garante que o método Dispose do objeto TransactionScope seja chamado mesmo se ocorrer uma exceção. O Dispose método marca o final do escopo da transação. Exceções que ocorrem depois de chamar esse método não podem afetar a transação. Esse método também restaura a transação de ambiente para ele estado anterior.

Um TransactionAbortedException será lançada se o escopo cria a transação e a transação for anulada. Um TransactionInDoubtException será lançada se o Gerenciador de transações não pode chegar a uma decisão de confirmação. Nenhuma exceção é gerada se a transação for confirmada.

Revertendo uma transação

Se você quiser reverter uma transação, você não deve chamar o Complete método dentro do escopo da transação. Por exemplo, você pode lançar uma exceção dentro do escopo. A transação na qual participa também será revertida.

Gerenciando o fluxo de transações usando TransactionScopeOption

Escopo da transação pode ser aninhado, chamando um método que usa um TransactionScope de dentro de um método que usa seu próprio escopo, como é o caso com o RootMethod método no exemplo a seguir,

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

O escopo da transação mais alto é conhecido como o escopo raiz.

O TransactionScope classe fornece vários construtores sobrecarregados que aceitam uma enumeração do tipo TransactionScopeOption, que define o comportamento transacional do escopo.

Um TransactionScope objeto tem três opções:

  • Una a transação de ambiente ou criar um novo caso não exista.

  • Ser um novo escopo de raiz, ou seja, iniciar uma nova transação e ter essa transação com a nova transação ambiente dentro de seu próprio escopo.

  • Não fazer parte de uma transação, todo. Como resultado, há uma transação de ambiente.

Se o escopo é instanciado com Requirede uma transação de ambiente estiver presente, o escopo une a transação. Se, por outro lado, não há nenhuma transação de ambiente, o escopo cria uma nova transação e se tornar o escopo raiz. Esse é o valor padrão. Quando Required é usado, o código dentro do escopo não precisa ter um comportamento diferente seja a raiz ou apenas associando a transação de ambiente. Ele deve operar identicamente em ambos os casos.

Se o escopo é instanciado com RequiresNew, é sempre o escopo raiz. Ele inicia uma nova transação, e sua transação se torna a nova transação ambiente dentro do escopo.

Se o escopo é instanciado com Suppress, ele nunca faz parte de uma transação, independentemente de se uma transação de ambiente está presente. Um escopo instanciado com esse valor sempre tem null como sua transação de ambiente.

As opções acima são resumidas na tabela a seguir.

TransactionScopeOption Transação de ambiente O escopo faz parte do
Obrigatório No Nova transação (será a raiz)
Requer novo No Nova transação (será a raiz)
Suprimir No Nenhuma transação
Obrigatório Sim Transação de ambiente
Requer novo Sim Nova transação (será a raiz)
Suprimir Sim Nenhuma transação

Quando um TransactionScope objeto ingressa em uma transação ambiente existente, descarte o objeto de escopo não pode terminar a transação, a menos que o escopo anula a transação. Se a transação ambiente criada por um escopo de raiz, somente quando o escopo raiz é descartado, não Commit chamado na transação. Se a transação foi criada manualmente, a transação termina quando ele é anulado ou confirmado pelo seu criador.

A exemplo a seguir mostra um TransactionScope que cria três objetos de escopo aninhado, cada instanciados com outro objeto TransactionScopeOption valor.

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))
    {
        //...  
    }
}

O exemplo mostra um bloco de código sem qualquer transação ambiente criando um novo escopo (scope1) com Required. O escopo scope1 é um escopo de raiz quando ele cria uma nova transação (uma transação) e faz com que a transação A transação de ambiente. Scope1 então cria mais três objetos, cada um com um valor TransactionScopeOption diferente. Por exemplo, scope2 é criado com Required, e como há uma transação de ambiente, ele adiciona a primeira transação criada pelo scope1. Observe que scope3 é o escopo da raiz de uma nova transação e que scope4 não tem nenhuma transação de ambiente.

Embora o padrão e mais comumente usado o valor de TransactionScopeOption é Required, cada um dos outros valores tem sua finalidade exclusiva.

Código não transacional dentro de um escopo de transação

Suppress é útil quando você quer preservar as operações executadas pela seção de código e não quer anular a transação de ambiente no caso de falha das operações. Por exemplo, quando você deseja executar um log ou operações de auditoria ou quando desejar publicar eventos para assinantes, independentemente de se a transação de ambiente confirma ou anula. Esse valor permite que você tenha uma seção de código não-transacional dentro de um escopo de transação, como mostrado no exemplo a seguir.

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
}

Votação dentro de um escopo aninhado

Embora um escopo aninhado pode unir a transação de ambiente do escopo raiz, chamar Complete no escopo aninhado não tem nenhum efeito sobre o escopo raiz. A transação só será confirmada se todos os escopos, desde o escopo raiz até o último escopo aninhado, votarem para confirmar a transação. Não chamar Complete em um escopo aninhado afetará o escopo raiz como a transação ambiente imediatamente será anulada.

Definir o tempo limite de TransactionScope

Alguns dos construtores sobrecarregados de TransactionScope aceitar um valor do tipo TimeSpan, que é usado para controlar o tempo limite da transação. Um tempo limite definido como zero significa um tempo limite infinito. Tempo limite infinito é útil principalmente para depuração, quando quiser isolar um problema em sua lógica de negócios, percorrendo seu código, e você não deseja que a transação que você depurar tempo limite durante a tentativa de localizar o problema. Ser extremamente cauteloso ao usar o valor de tempo limite infinito em todos os outros casos, porque ele substitui as proteções contra bloqueios de transação.

Você normalmente define o TransactionScope tempo limite para valores que não seja padrão nos dois casos. A primeira é durante o desenvolvimento, quando você deseja testar a maneira como o seu aplicativo manipula transações anuladas. Definindo o tempo limite como um valor pequeno (como um milissegundo), você fazer com que a transação falhar e, portanto, pode observar o código de tratamento de erros. O segundo caso em que você definir o valor como menor que o tempo limite padrão é quando você acreditar que o escopo está envolvido na contenção de recursos, resultando em bloqueios. Nesse caso, você deseja anular a transação assim que possível e aguarda o tempo limite padrão expirar.

Quando um escopo ingressa em uma transação de ambiente, mas Especifica um tempo limite menor do que a transação ambiente é definida como, o tempo limite de novo e mais curto é imposto no TransactionScope objeto e o escopo devem terminar no tempo aninhado especificado ou a transação é cancelada automaticamente. Se o tempo limite do escopo aninhado é maior do que a transação de ambiente, ele não tem efeito.

Definindo o nível de isolamento de TransactionScope

Alguns dos construtores sobrecarregados de TransactionScope aceitar uma estrutura do tipo TransactionOptions para especificar um nível de isolamento, além de um valor de tempo limite. Por padrão, a transação será executado com nível de isolamento definido como Serializable. Selecionando um nível de isolamento diferente de Serializable é normalmente usado para sistemas com uso intensivo de leitura. Isso requer uma compreensão sólida da teoria e a semântica da própria transação, os problemas de simultaneidade envolvidos e as conseqüências de consistência do sistema de processamento de transações.

Além disso, nem todos os gerenciadores de recursos oferecer suporte a todos os níveis de isolamento, e eles podem optar por fazer parte de transação em um nível mais alto que o configurado.

Cada nível de isolamento além Serializable é suscetível a inconsistência resultantes de outras transações que acessam as mesmas informações. É a diferença entre os diferentes níveis de isolamento da forma leitura e gravação bloqueios são usados. Um bloqueio pode ser mantido somente quando a transação acessa os dados no Gerenciador de recursos, ou pode ser mantida até que a transação é confirmada ou anulada. O primeiro é melhor taxa de transferência, o segundo para manter a consistência. Os dois tipos de bloqueios e os dois tipos de operações (leitura/gravação) oferecem quatro níveis de isolamento básico. Consulte IsolationLevel para obter mais informações.

Quando usando aninhadas TransactionScope objetos, todos os escopos aninhados devem ser configurados para usar exatamente o mesmo nível de isolamento se deseja unir a transação de ambiente. Se um aninhada TransactionScope objeto tentar unir a transação ambiente ainda Especifica um nível de isolamento diferente, um ArgumentException é lançada.

Interoperação com COM+

Quando você cria um novo TransactionScope instância, você pode usar o EnterpriseServicesInteropOption enumeração em um dos construtores para especificar como interagir com COM+. Para mais informações sobre isso, confira Interoperabilidade com os serviços corporativos e as transações COM+.

Confira também