Desenvolvimento - C#

WCF - Transações

As transações já existem há algum tempo, e a finalidade deste artigo é mostrar as alternativas que temos para incorporá-las dentro de serviços e clientes que fazem o uso do WCF como meio de comunicação.

por Israel Aéce



function doClick(index, numTabs, id) { document.all("tab" + id, index).className = "tab"; for (var i=1; i Uma necessidade existente em muitas aplicações é assegurar a consistência dos dados durante a sua manipulação. Ao executar uma tarefa, precisaremos garantir que, se algum problema ocorrer, os dados voltem ao seu estado inicial. Dentro da computação isso é garantido pelo uso de transações. As transações já existem há algum tempo, e a finalidade deste artigo é mostrar as alternativas que temos para incorporá-las dentro de serviços e clientes que fazem o uso do WCF como meio de comunicação.

Um exemplo clássico que demonstra a necessidade das transações é quando precisamos criar uma rotina de transferência de dinheiro entre duas contas. A transferência consiste basicamente em duas operações: sacar o dinheiro de uma conta e efetuar o depósito em outra. Caso algum problema ocorra entre o saque e o depósito e isso não estiver envolvido por uma transação, a conta de origem ficará sem o dinheiro e a conta destino não receberá a quantia, fazendo com que os dados envolvidos (saldo da conta de origem) fique em um estado inconsistente.

A definição de uma transação consiste em um conjunto de operações (muitas vezes complexas) que, caso alguma delas falhe, o processo como um todo deverá falhar, ou seja, uma operação atômica (ou tudo, ou nada). A atomicidade é uma das quatro características que toda transação deverá ter, a saber: Atomicidade, Consistência, Isolamento e Durabilidade (ACID). A consistência garante que, se alguma das operações que estiver envolvida em uma transação falhar, ela garantirá que os dados voltem ao seu estado inicial, ou seja, as mesmas informações antes da transação iniciar; já o isolamento garantirá que nenhuma outra entidade acessará os dados que estão sendo alterados durante a transação corrente, evitando que essas outras entidades acessem um valor que, talvez, não seja o valor final. Finalmente, e não menos importante, a durabilidade garante que uma vez que ela foi "comitada", a transação será efetivamente persistida, resistindo a possíveis falhas na aplicação/banco de dados.

Tradicionalmente, as transações foram associadas aos banco de dados, mas elas podem ser aplicadas em uma série de operações que envolvem mudanças em dados. Atualmente a necessidade das transações vai além de garantir a consistência de registros de um banco de dados ou qualquer outro repositório. Com os serviços cada vez mais em evidência e sendo desenvolvido por muitas empresas para disponibilizar alguma funcionalidade, a necessidade de envolver a chamada à esses serviços em uma única operação transacionada fez com que as transações fossem implementadas de forma a garantir a propagação da mesma de uma forma genérica para diferentes serviços, processos, organizações e plataformas.

Como falado acima, uma transação é um conjunto de operações que, na maioria das vezes, envolvem recursos transacionais, como é o caso de banco de dados e até mesmo o Message Queue. Esses recursos transacionais são (devem ser) capazes de efetuar o commit ou rollback nas possíveis mudanças que foram feitas nos dados. Envolver um destes recursos em uma transação é chamado de enlistment; há também alguns recursos que conseguem detectar que estão sendo acessados por uma transação e, automaticamente, ele fará parte da mesma. Essa técnica é conhecida como auto-enlistment.

Dentro de uma transação, há sempre a aplicação que a inicia, conhecida como "coordenador"; já os demais são referenciados como "participantes". A comunicação entre o coordenador e os demais participantes deve ser realizada de forma a garantir atomicidade quanto a consistência, como já falamos acima. Nesse contexto, isso é garantido através de um protocolo popularmente conhecido como two-phase commit protocol (2PC). Como o próprio nome diz, ele consiste basicamente em duas fases para garantir o commit ou rollback das informações, abstraindo toda a grande complexidade que existe por trás deste processo. Abaixo a descrição de cada uma dessas fases:

  • Fase 1: Preparação. Nesta fase, todos os envolvidos (participantes) na transação enviam uma notificação para o coordenador da mesma, informando que ele está preparado para efetuar o commit ou rollback (voto).

  • Fase 2: Commit ou Rollback. Ao coletar todos os votos (de commit ou rollback), o coordenador irá decidir o que deve ser feita. Se algum participante votou como rollback, então o coordenador irá notificar a todos os participantes para efetuar o rollback da transação; caso todos votem como commit, então o coordenador envia uma notificação para cada participante para efetivar as mudanças.

Observação: O estado dos dados entre a fase 1 e fase 2 é conhecido como in-doubt state. Como mencionado acima, o isolamento, uma das quatro características da transação, garantirá que essa informação não será acessada por nenhuma outra transação, fazendo com que não hajam inconsistências.

Os recursos transacionais estão divididos em duas categorias: duráveis (durable) e voláteis (volatile). Um recurso transacional durável é capaz de salvar a informação durante a fase 1 do protocolo two-phase commit e, mesmo que um problema ocorra na máquina e ela precisar ser reinicializada, ela poderá dar continuidade na transação (exemplo: Microsoft SQL Server). Já os recursos voláteis podem ser alistados para receberem notificações do protocolo two-phase commit, mas eles não resistirão aos possíveis problemas que possam acontecer, ou seja, eles não conseguem sobreviver à uma falha mais severa, como uma reinicialização do sistema. A criação de recursos voláteis está fora do escopo deste artigo mas, como referência, há classes e interfaces disponíveis dentro do Assembly System.Transactions que possibilitam a criação dos mesmos.

Ainda falando sobre as tecnologias que circundam as transações, temos os protocolos (aqui, a palavra protocolo se refere a forma de comunicação) que permitem que todo esse processo aconteça. Atualmente temos três diferentes protocolos, e cada um deles com uma finalidade diferente. Abaixo temos cada um deles com sua respectiva descrição:

  • Lightweight: Esse é o protocolo mais performático em relação aos outros dois, mas tem uma limitação de que não pode propagar o contexto da transação fora do domínio da aplicação (AppDomain).

  • OleTx: Ao contrário do Lightweight, o protocolo OleTx pode ser propagado através do domínio da aplicação (AppDomain), processos e máquinas. Por se tratar de um protocolo nativo do Windows, ele não pode ultrapassar firewalls ou mesmo interoperar com outras plataformas. Geralmente isso é utilizado em um ambiente de intranet e completamente homogêneo, onde essas "limitações" não são problemas.

  • WS-Atomic Transaction (WS-AT): Esse protocolo é similar ao OleTx, podendo propagar entre domínio da aplicação (AppDomain), processos e máquinas, gerenciando o two-phase commit. A vantagem deste protocolo em relação ao anterior é que ele é baseado em um padrão aberto, podendo ser implementado por qualquer plataforma. Além disso, ele pode ser utilizado sob HTTP e na internet, atravessando possíveis firewalls existentes dentro da infraestrutura.

Finalmente, temos os gerenciadores de transações (Transaction Managers). Vimos resumidamente como funciona o two-phase commit e, felizmente, tudo o que precisamos fazer é dizer se deu certo ou não e ele se encarregará do resto. Mas afinal, quem gerencia tudo isso? Toda essa responsabilidade fica à cargo dos Transaction Managers. São eles quem alistam os recursos transacionais (banco de dados, message queues) dentro do ambiente transacional e fazem o uso de um dos protocolos acima para a determinar o commit ou rollback. Atualmente temos três gerenciadores de transação, a saber:

  • Lightweight Transaction Manager (LTM): Este gerenciador somente é capaz de lidar com uma transação local, ou seja, dentro de um mesmo AppDomain. Ele utiliza o protocolo Lightweight para gerenciar o two-phase commit. Ele poderá gerenciar um único recurso durável e vários voláteis.

  • Kernel Transaction Manager (KTM): O KTM, por sua vez, permite gerenciar recursos transacionais a nível de kernel (KRM), como é o caso do sistema de arquivos (TXF) e também do registry (TXR), fazendo o uso do protocolo Lightweight. Assim como o anterior, o KTM pode gerenciar um único recurso KRM e vários recursos voláteis. Somente suportado no Windows Vista.

  • Distributed Transaction Coordinator (DTC): O DTC é o mais abrangente de todos. Ele é capaz de gerenciar uma transação sem um limite de escopo, ou seja, a transação poderá ser propagada entre vários AppDomains, diferentes processos e máquinas. Nativamente o DTC suporta o protocolo OleTx e, mais recentemente, ele foi adaptado para também suportar o protocolo WS-AT, pertindo à ele interoperabilidade com serviços e clientes não Microsoft.

Observação: Por padrão, o suporte ao protocolo WS-Atomic Transaction (WS-AT) no DTC vem desabilitado. Para que você consiga manipular essa configuração, primeiramente é necessário que você consiga visualizá-la e, para isso, é necessário executar a seguinte linha de comando (via prompt do Visual Studio .NET): regasm.exe /codebase wsatui.dll. Uma vez rodado este comando, você poderá ir até Control Panel, Administrative Tools, Component Services, Propriedades do Distributed Transaction Coordinator e verá a aba WS-AT a sua disposição, como é mostrado abaixo:

Figura 1 - Habilitando o WS-AT no DTC.

Transações no .NET 2.0

A partir da versão 2.0 do .NET Framework, a Microsoft introduziu um novo modelo de programação para suportar transações em código gerenciado. Trata-se do namespace System.Transactions (Assembly System.Transactions.dll). Este novo namespace traz vários tipos (classes, interfaces, etc.) para que você consiga manipular transações, alistar recursos duráveis ou voláteis e trabalhando com transações locais ou distribuídas.

Este novo modelo de programação permite ao desenvolvedor determinar uma seção (bloco) do código que será envolvido pela transação. Tudo o que você efetuar dentro deste bloco estará protegido por uma transação, ficando sob responsabilidade do desenvolvedor dizer se ela completou com sucesso para, posteriormente, ser "comitada". Esse namespace já suporta o protocolo Lightweight Transaction Manager (LTM), promovendo-o de forma transparente para OleTx quando, por alguma limitação, o LTM não puder ser utilizado.

Uma das principais classes utilizadas é a classe TransactionScope. Essa classe é a responsável por determinar um bloco transacional dentro da aplicação. Geralmente, essa classe é criada e envolvida por um bloco using e, dentro dele, fazemos as chamadas para os recursos (banco de dados, message queues, etc.) que farão parte da transação. Uma vez que todas as tarefas concluírem com sucesso, você deve chamar o método Complete da classe TransactionScope; caso contrário, você apenas não deverá chamar este método e, automaticamente, o rollback acontecerá. O trecho de código abaixo ilustra um exemplo de como utilizá-la:

TransactionOptions opts = new TransactionOptions();
opts.IsolationLevel = IsolationLevel.ReadCommitted;
opts.Timeout = TimeSpan.FromMinutes(1);

using (TransactionScope ts = 
    new TransactionScope(TransactionScopeOption.RequiresNew, opts))
{
    Message msg = RecuperarMensagem();
    if (msg != null)
        InserirMensagemNoBancoDeDados(msg);

    ts.Complete();
}
Dim opts As New TransactionOptions()
opts.IsolationLevel = IsolationLevel.ReadCommitted
opts.Timeout = TimeSpan.FromMinutes(1)

Using ts As New TransactionScope(TransactionScopeOption.RequiresNew, opts)
    Dim msg As Message = RecuperarMensagem()
    If Not IsNothing(msg) Then
        InserirMensagemNoBancoDeDados(msg)
    End If

    ts.Complete()
End Using
C# VB.NET

É importante notar que se por algum problema uma exceção ocorrer durante a execução do método RecuperarMensagem ou InserirMensagemNoBancoDeDados, o método Complete não será disparado e, conseqüentemente, será feito o rollback de forma automática (neste caso, a verificação é efetuada dentro do método Dispose, o qual sempre será invocado). Já se ambos os métodos não disparar nenhuma exceção, o método Complete será chamado, e todas as modificações serão efetivadas.

Os recursos fornecidos para manipular transações não param por aqui. Há várias funcionalidades a disposição, como o caso da criação e alistamento de recursos voláteis mas, infelizmente, estão fora do escopo deste artigo. Caso queira saber sobre mais detalhes sobre como funcionam as transações dentro do .NET, então você pode recorrer a documentação deste namespace direto do MSDN.

Utilizando transações no WCF

Como os serviços vem ganhando cada vez mais espaço e uma das principais necessidades que eles têm é justamente a necessidade de envolvê-lo em uma transação, fazendo com que ele seja um coordenador ou participante de um processo transacional. Felizmente, a Microsoft se preocupou com isso e disponibilizou através de classes e atributos uma série de funcionalidades dentro do WCF para manipular transações.

É importante dizer que o suporte e configuração das transações são características do binding, ou seja, será o binding que determinará o suporte ou não à transação, definindo sob qual dos protocolos (WS-AT ou OleTx) a transação será exposta. Opcionalmente, o binding também poderá definir um timeout e a propagação das mesmas do cliente para o serviço (será abordado mais tarde, ainda neste artigo). Assim como quase todas as funcionalidades no WCF, a configuração das transações pode ser feita de forma imperativa ou declarativa.

Como tudo se inicia pelo contrato, então é ele que vamos inicialmente criar. Ele terá dois simples métodos: Adicionar e Recuperar, onde o primeiro deles deverá ser envolvido por uma transação e o segundo não. Note que o código abaixo ilustra a estrutura do contrato, mas sem nenhuma configuração de transação; o WCF desacopla a configuração da transação da definição do contrato (salvo uma, que será abordada mais adiante), sendo o desenvolvedor obrigado a configurar isso na implementação, através de behaviors.

using System;
using System.ServiceModel;

[ServiceContract]
public interface IUsuarios
{
    [OperationContract]
    bool Adicionar(string nome);

    [OperationContract]
    string[] Recuperar();
}
Imports System
Imports System.ServiceModel

<ServiceContract()> _
Public Interface IUsuarios
    <OperationContract()>
    Function Adicionar(ByVal nome As String) As Boolean

    <OperationContract()>
    Function Recuperar(ByVal nome As String) As String()
End Interface
C# VB.NET

Uma vez implementado este contrato em uma classe que representará o serviço, o primeiro passo é determinar se uma operação deverá ou não executar em um ambiente transacionado que, por padrão, está desabilitado e, neste caso, mesmo que uma transação seja propagada do cliente para o serviço, ela será ignorada. Para habilitar o uso da transação em uma operação, basta recorrermos a propriedade TransactionScopeRequired do atributo OperationBehaviorAttribute, definindo-a como True, como é mostrado através do exemplo de código abaixo:

[ServiceBehavior(
    TransactionTimeout = "00:02:00"
    , TransactionIsolationLevel = IsolationLevel.ReadCommitted)]
public class ServicoDeUsuarios : IUsuarios
{
    [OperationBehavior(TransactionScopeRequired = true)]
    public bool Adicionar(string nome)
    {
        //implementação

        return true;
    }

    [OperationBehavior(TransactionScopeRequired = false)]
    public string[] Recuperar()
    {
        //implementação

        return null;
    }
}
Imports System
Imports System.ServiceModel
Imports System.Transactions

<ServiceBehavior( _
    TransactionTimeout:="00:02:00", _
    TransactionIsolationLevel:=IsolationLevel.ReadCommitted)> _
Public Class ServicoDeUsuarios
    Implements IUsuarios

    <OperationBehavior(TransactionScopeRequired:=True)> _
    Public Function Adicionar(ByVal nome As String) As Boolean _
        Implements IUsuarios.Adicionar

        "implementação

        Return True
    End Function

    <OperationBehavior(TransactionScopeRequired:=False)> _
    Public Function Recuperar(ByVal nome As String) As String() _
        Implements IUsuarios.Recuperar

        "implementação

        Return Nothing
    End Function
End Class
C# VB.NET

Opcionalmente, você poderá definir, através do atributo ServiceBehaviorAttribute, um timeout e o nível de isolamento. O timeout, especificado a partir da propriedade TransactionTimeout (Timespan), especificará o período entre a criação e a finalização (com commit ou rollback) da transação. Caso ela não seja completada até o tempo especificado nesta propriedade, a transação será automaticamente abortada. Já o nível de isolamento, definido através da propriedade TransactionIsolationLevel, receberá uma das opções especificadas no enumerador IsolationLevel, que está contido no namespace System.Transactions e, quando não especificado, a opção Serializable é utilizada. Como os dados que são modificados pela transação são considerados in-doubt, é através do nível de isolamento que determinará se essas mudanças poderão ou não ser acessadas antes da transação ser concluída. É importante dizer que ambas as propriedades afetam diretamente todas as operações que tiverem a propriedade TransactionScopeRequired definida como True. No código acima, apenas o método Adicionar será envolvido em um contexto transacional. Explicitamente definimos a propriedade TransactionScopeRequired para False no método Recuperar, mas lembre-se de que ocultando este atributo iremos obter o mesmo resultado, ou seja, a operação não será transacionada. Para sabermos se a transação foi ou não criada, podemos recorrer à classe Transaction, uma das principais classes do namespace System.Transactions. Essa classe fornece uma propriedade estática chamada Current, que retorna uma instância da classe Transaction, representando a transação corrente e, quando o retorno estiver vazio, então nenhuma transação foi criada.

Se o método suportar transação e ela estiver criada, automaticamente, qualquer recurso transacional que você acesse dentro desta operação, automaticamente será alistado (auto-enlistment) e, com isso, toda e qualquer manipulação será gerenciada pela transação. Isso quer dizer que não há necessidade de escrevermos código para criar a transação, pois o WCF já garante isso; independentemente da transação criada pelo WCF, você pode perfeitamente, dentro do método, criar um bloco transacional através da classe TransactionScope, que um dos seus construtores aceita uma das opções especificadas no enumerador TransactionScopeOption, que permite "interagir" com o ambiente transacionado existente. Os possíveis valores são:

  • Required: Uma transação é requerida. Caso a transação exista, o processo fará parte dela; do contrário, uma nova transação será criada.

  • RequiredNew: Uma nova transação é requerida. Independentemente se existe ou não uma transação, sempre será criada uma nova.

  • Suppress: Não é necessário uma transação. Independentemente se existe ou não uma transação, a tarefa não será envolvida em um ambiente transacionado.

O exemplo abaixo exibe como podemos proceder para suprimir uma transação dentro de um método que solicitou uma transação ao WCF. Notem que apenas uma parte do método será suprimido, não influenciando no voto da transação (commit ou rollback).

[ServiceBehavior(
    TransactionTimeout = "00:02:00"
    , TransactionIsolationLevel = IsolationLevel.ReadCommitted)]
public class ServicoDeUsuarios : IUsuarios
{
    [OperationBehavior(TransactionScopeRequired = true)]
    public bool Adicionar(string nome)
    {
        //operações

        using (TransactionScope scope = 
            new TransactionScope(TransactionScopeOption.Suppress))
        {
            //bloco não transacionado
        }

        return true;
    }
}
Imports System
Imports System.ServiceModel
Imports System.Transactions

<ServiceBehavior( _
    TransactionTimeout:="00:02:00", _
    TransactionIsolationLevel:=IsolationLevel.ReadCommitted)> _
Public Class ServicoDeUsuarios
    Implements IUsuarios

    <OperationBehavior(TransactionScopeRequired:=True)> _
    Public Function Adicionar(ByVal nome As String) As Boolean _
        Implements IUsuarios.Adicionar

        "operações

        Using scope As New TransactionScope(TransactionScopeOption.Suppress)
            "bloco não transacionado
        End Using

        Return True
    End Function
End Class
C# VB.NET

Efetuando o Voto

Uma vez que sabemos como configurar uma operação para suportar transações, precisamos agora saber como proceder para aplicar o voto, ou seja, dizer ao runtime se a transação foi completada com sucesso ou se alguma falha ocorreu. Há duas formas de efetuar o voto: declarativa (via atributos) ou imperativa (via código).

No modo declarativo devemos utilizar a propriedade TransactionAutoComplete, exposta também pelo atributo OperationBehaviorAttribute. Essa propriedade, do tipo booleana (o padrão é True), quando definida como True, determinará que, se nenhuma exceção não tratada ocorrer durante a execução da operação/método, a transação deverá ser marcada como "completada" quando o método retornar; se por algum motivo alguma exceção acontecer, a transação será abortada. Quando esta propriedade estiver definida como False, o comportamento é um pouco diferente, ou seja, a transação ficará vinculada à instância do serviço (*) e somente será marcada como "completada" se o cliente efetuar uma nova chamada para um método que também tenha esta propriedade definida como True ou quando invocamos o método SetTransactionComplete.

(*) Isso obrigará o serviço ser definido como PerSession.

A utilização do método estático SetTransactionComplete, definido na classe OperationContext, se faz necessária quando a propriedade TransactionAutoComplete estiver definida como False. A idéia aqui é permitir ao desenvolvedor determinar quando é o momento apropriado para marcar a transação como "completada". Isso dá uma flexibilidade maior, pois nem sempre será uma exceção que determinará se a transação deverá ou não ser abortada. O exemplo abaixo ilustra como podemos fazer o uso deste método:

[ServiceBehavior(
    TransactionTimeout = "00:02:00"
    , TransactionIsolationLevel = IsolationLevel.ReadCommitted)]
public class ServicoDeUsuarios : IUsuarios
{
    [OperationBehavior(TransactionScopeRequired = 
true, TransactionAutoComplete = false)]
    public bool Adicionar(string nome)
    {
        if (true)
        {
            AdicionarNoBancoDeDados(nome);
            if(NotificarGerentes(nome))
            {
                OperationContext.Current.SetTransactionComplete();
                return true;
            }
        }

        return false;
    }
}
Imports System
Imports System.ServiceModel
Imports System.Transactions

<ServiceBehavior( _
    TransactionTimeout:="00:02:00", _
    TransactionIsolationLevel:=IsolationLevel.ReadCommitted)> _
Public Class ServicoDeUsuarios
    Implements IUsuarios

    <OperationBehavior(TransactionScopeRequired:=True, TransactionAutoComplete:=False)> _
    Public Function Adicionar(ByVal nome As String) As Boolean _
        Implements IUsuarios.Adicionar

        If True Then
            AdicionarNoBancoDeDados(nome)
            If NotificarGerentes(nome) Then
                OperationContext.Current.SetTransactionComplete()
                Return True
            End If
        End If

        Return False
    End Function
End Class
C# VB.NET

Como podemos notar no código acima, a propriedade TransactionAutoComplete está definida como False e, neste caso, compete ao desenvolvedor determinar quando a transação deverá ser "completada". Já dentro do método, quando a inserção no banco de dados acontecer e, se o método NotificarGerentes for efetuado com sucesso, o método SetTransactionComplete será invocado. Caso a notificação para os gerentes não aconteça, o método SetTransactionComplete não será invocado e, quando o método Adicionar retornar, a transação será abortada.

Observação: Se ao chamar o método SetTransactionComplete e não houver um ambiente transacional, uma exceção do tipo InvalidOperationException será disparada.

Propagando as Transações

Pelo fato de todas as configurações serem server-side (via behaviors), elas afetam apenas a implementação e execução do serviço, e os clientes não sofrerão nenhuma mudança. Mas e, se por acaso, quisermos que o cliente também seja envolvido na transação? Para isso, o WCF fornece uma característica interessante chamada de propagação da transação.

A propagação da transação permitirá ao cliente do serviço encaminhar uma transação existente, fazendo com que a execução da operação também faça parte dela, ao invés do WCF criá-la. A configuração para suportar a propagação está definida em dois lugares: no contrato e no binding. A configuração do contrato irá garantir que o serviço seja exposto sob um binding que suporte essa característica, apenas isso. A configuração no contrato é definida através do atributo TransactionFlowAttribute que, em seu construtor, deve receber uma das três opções definidas pelo enumerador TransactionFlowOption:

  • Allowed: Esta opção permite à execução da operação fazer parte da transação do cliente, caso exista; do contrário, o WCF criará uma nova transação.

  • NotAllowed: Ao contrário da opção anterior, NotAllowed sempre rejeitará qualquer transação criada pelo cliente, fazendo com que o WCF sempre crie uma nova transação. Este é o valor padrão.

  • Mandatory: Como o próprio nome diz, esta opção obriga o cliente a criar uma transação e propagá-la para o serviço como parte da requisição. Caso isso não aconteça, uma exceção será disparada.

O código a seguir exibe o mesmo contrato criado acima (IUsuarios), mas com as devidas mudanças para configurar a propagação da transação do cliente para o serviço. Para fins de exemplos, vamos definir como Mandatory, obrigando o cliente sempre a criar a transação:

using System;
using System.ServiceModel;

[ServiceContract]
public interface IUsuarios
{
    [OperationContract]
    [TransactionFlow(TransactionFlowOption.Mandatory)]
    bool Adicionar(string nome);

    [OperationContract]
    string[] Recuperar();
}
Imports System
Imports System.ServiceModel

<ServiceContract()> _
Public Interface IUsuarios
    <OperationContract(), TransactionFlow(TransactionFlowOption.Mandatory)>
    Function Adicionar(ByVal nome As String) As Boolean

    <OperationContract()>
    Function Recuperar(ByVal nome As String) As String()
End Interface
C# VB.NET

Ao definir, no mínimo, uma das operações como Mandatory, durante a carga do serviço o WCF irá analisar se o binding está permitindo a propagação da transação. Caso não esteja, uma exceção do tipo InvalidOperationException será disparada, informando que isso não é permitido. Os bindings que suportam a propagação da transação fornecem uma propriedade chamada TransactionFlow, que recebe um valor booleano (que, por padrão, é False), indicando se isso será ou não permitido. Além disso, é importante dizer que, quando essa propriedade for definida como True, refletirá no documento WSDL do serviço.

Como estamos falando sobre bindings é importante mencionar que, além de determinar se podem ou não propagar as transações, podemos definir qual o protocolo que ele deverá utilizar. Obviamente que nem todos os protocolos poderão ser utilizados por todos bindings, justamente pelo fato do escopo de utilização de cada um. Bindings como NetTcpBinding e NetNamedPipeBinding suportam tanto o protocolo WS-AT ou OleTx; já os bindings de internet (WSHttpBinding, WSDualHttpBinding e WSFederationHttpBinding) apenas podem expor as transações através do protocolo WS-AT. Apenas para constar, o binding BasicHttpBinding não suporta transações.

Pelo fato dos bindings NetTcpBinding e NetNamedPipeBinding suportarem dois protocolos diferentes, eles expõem uma propriedade chamada TransactionProtocol, onde podemos definir qual dos protocolos utilizar. Para efetuar essa configuração, basta escolher uma das proriedades estáticas da classe TransactionProtocol, que retornará uma instância desta mesma classe, devidamente configurada para tal protocolo. O código abaixo ilustra a configuração de um host, exibindo a customização para o suporte a transações:

using (ServiceHost host = 
    new ServiceHost(typeof(ServicoDeUsuarios), 
	new Uri[] { new Uri("net.tcp://localhost:9393") }))
{
    NetTcpBinding tcp = new NetTcpBinding();
    tcp.TransactionFlow = true;
    tcp.TransactionProtocol = TransactionProtocol.WSAtomicTransactionOctober2004;

    host.AddServiceEndpoint(
        typeof(IUsuarios), 
        tcp, 
        "srv");

    host.AddServiceEndpoint(
        typeof(IMetadataExchange), 
        MetadataExchangeBindings.CreateMexTcpBinding(), 
        "mex");

    host.Open();
    Console.ReadLine();
}
Using host As New ServiceHost( _
    GetType(ServicoDeUsuarios), New Uri() {New Uri("net.tcp://localhost:9393")})

    Dim tcp As New NetTcpBinding()
    tcp.TransactionFlow = True
    tcp.TransactionProtocol = TransactionProtocol.WSAtomicTransactionOctober2004

    host.AddServiceEndpoint( _
        GetType(IUsuarios), _
        tcp, _
        "srv")

    host.AddServiceEndpoint( _
        GetType(IMetadataExchange), _
        MetadataExchangeBindings.CreateMexTcpBinding(), _
        "mex")

    host.Open()
    Console.ReadLine()
End Using
C# VB.NET

Com essa configuração, o cliente será obrigado a criar uma transação antes de efetivamente invocar a operação e, caso ele não informe, uma exceção do tipo ProtocolException será disparada. A configuração acima é feita de forma imperativa, mas é perfeitamente possível fazer a mesma configuração de forma declarativa. Quando o cliente também for .NET/WCF, podemos utilizar a classe TransactionScope para criar um bloco transacional e, com isso, ao efetuar a chamada de uma operação dentro do bloco transacional, automaticamente ela será propagada para o serviço. O exemplo abaixo ilustra superficialmente como podemos proceder para criar a transação do lado do cliente. Note que nenhuma configuração extra é necessária.

using System;
using System.Transactions;

using (UsuariosClient proxy = new UsuariosClient())
{
    using (TransactionScope scope = new TransactionScope())
    {
        Console.WriteLine(proxy.Adicionar("Israel"));
        scope.Complete();
    }
}
Imports System
Imports System.Transactions

Using proxy As New UsuariosClient()
    Using scope As New TransactionScope()
        Console.WriteLine(proxy.Adicionar("Israel"))
        scope.Complete()
    End Using
End Using
C# VB.NET

Observação: Quando o serviço for referenciado através do Visual Studio ou quando utilizamos o utilitário svcutil.exe para gerar o proxy, a propriedade TransactionFlow do binding já é configurada como True e o contrato já terá o atributo TransactionFlowAttribute definido, respeitando a mesma configuração do serviço, e tudo isso garantirá que o cliente propague (ou não) a transação para o serviço.

Transações e o Gerenciamento de Instâncias

Um dos grandes desafios das transações é justamente manter a consistência dos dados que ela manipula entre o ínicio e o fim da mesma. As informações manipuladas vão desde dados em banco de dados (o mais convencional) até variáveis de memória e, em ambos os casos, é necessário que eles fiquem em formato consistente.

A escolha do modo de gerenciamento de instâncias (discutido neste artigo) influencia drasticamente no comportamento das transações. O modo PerCall é o mais ideal para suportá-las, já que a transação será finalizada quando a instância do serviço for finalizada. No modo PerCall, para cada chamada de uma operação (transacionada ou não), uma nova instância será criada para serví-la, mantendo a consistência inicial das informações. Depois do retorno do método e antes da desativação da instância, o WCF irá finalizar a transação, efetuando o commint ou abort.

O gerenciamento das transações começa a ficar um pouco mais complicado quando o serviço é exposto através do modo PerSession (que é o padrão), fazendo com que uma instância sobreviva entre as chamadas. Uma vez que o cliente efetua a conexão com um serviço deste tipo, a instância viverá enquanto a instância do cliente (proxy) exista. Um cuidado extra deve ser tomado neste cenário, devido ao fato que isso poderá quebrar duas das características das transações: consistência e isolamento.

A consistência pode ser afetada pelo fato de que o método poderá alterar o estado interno dos membros da classe ou de qualquer outro recurso envolvido na transação e, caso a transação não seja explicitamente encerrada, os dados que estão em um formato in-doubt ficarão disponíveis, e qualquer outra transação poderá acessá-lo, quebrando assim, a segunda característica, o isolamento. Uma forma que temos para manter um serviço PerSession de forma a garantir que transações funcionem como deveriam, é definindo a propriedade ReleaseServiceInstanceOnTransactionComplete do atributo ServiceBehaviorAttribute para True (que já é o padrão), como é mostrado abaixo:

using System;
using System.ServiceModel;
using System.Transactions;

[ServiceBehavior(
    TransactionTimeout = "00:02:00"
    , TransactionIsolationLevel = IsolationLevel.ReadCommitted
    , ReleaseServiceInstanceOnTransactionComplete = true
    , InstanceContextMode = InstanceContextMode.PerSession
    , ConcurrencyMode = ConcurrencyMode.Single)]
public class ServicoDeUsuarios : IUsuarios
{
    [OperationBehavior(TransactionScopeRequired = true)]
    public bool Adicionar(string nome)
    {
        //implementação
    }
}
Imports System
Imports System.ServiceModel
Imports System.Transactions

<ServiceBehavior( _
    TransactionTimeout:="00:02:00", _
    TransactionIsolationLevel:=IsolationLevel.ReadCommitted, _
    ReleaseServiceInstanceOnTransactionComplete = True, _
    InstanceContextMode = InstanceContextMode.PerSession, _
    ConcurrencyMode = ConcurrencyMode.Single)> _
Public Class ServicoDeUsuarios
    Implements IUsuarios

    <OperationBehavior(TransactionScopeRequired:=True)> _
    Public Function Adicionar(ByVal nome As String) As Boolean _
        Implements IUsuarios.Adicionar

        "implementação
    End Function
End Class
C# VB.NET

A utilização da propriedade ReleaseServiceInstanceOnTransactionComplete obriga o serviço a ter, no mínimo, uma operação com a propriedade TransactionScopeRequired definida como True, e lembrando que você deverá votar se a transação deu certou ou não, utilizando as técnicas que já falamos acima. Isso fará com que o WCF descarte o objeto silenciosamente, sem refletir nada para o cliente. Essa funcionalidade é semelhante ao JIT Just-In-Time, fornecido pelo COM+. Outra consistência que será feita será em cima da modo de concorrência (já discutido neste artigo), nos obrigando a definí-lo como Single, para evitar o acesso multi-threading a partir de um mesmo cliente.

Ainda falando sobre o modo PerSession, o WCF fornece outro recurso para lidar com transações, que é completamente independente da propriedade ReleaseServiceInstanceOnTransactionComplete. Esse modo permitirá ao cliente criar uma transação para que a mesma dure enquanto a sessão estiver ativa, ou seja, a sessão será transacionada. A idéia aqui é a transação não ser finalizada dentro do serviço, já que o WCF descartaria a instância. Para evitar isso, podemos definir a propriedade TransactionAutoComplete para False e, através do atributo ServiceBehaviorAttribute, definirmos a propriedade TransactionAutoCompleteOnSessionClose como True que, ao finalizar a sessão, completará a transação. Apenas deve-se atentar ao timeout, pois a transação está sujeita a ser abortada por isso caso o mesmo seja excedido. O exemplo abaixo ilustra essa configuração:

using System;
using System.ServiceModel;

[ServiceBehavior(TransactionAutoCompleteOnSessionClose = true)]
public class ServicoDeUsuarios : IUsuarios
{
    [OperationBehavior(TransactionScopeRequired = true, 
TransactionAutoComplete = false)]
    public bool Adicionar(string nome)
    {
        //implementação
    }
}
Imports System
Imports System.ServiceModel

<ServiceBehavior(TransactionAutoCompleteOnSessionClose:=True)> _
Public Class ServicoDeUsuarios
    Implements IUsuarios

    <OperationBehavior(TransactionScopeRequired:=True, _
        TransactionAutoComplete:=False)> _
    Public Function Adicionar(ByVal nome As String) As Boolean _
        Implements IUsuarios.Adicionar

        "implementação
    End Function
End Class
C# VB.NET

Finalmente, o modo Single de gerenciamento de instância também possui suas peculiaridades. Serviços expostos sob este modo tem um comportamento parecido com o modo PerCall. Como falado acima, o valor padrão da propriedade ReleaseServiceInstanceOnTransactionComplete é True e, com isso, após uma transação, a instância do serviço será descartada para manter a consistência dos dados. Esse comportamento deverá fazer com que o serviço gerencie o estado a cada chamada e, tecnicamente falando, um identificador do cliente deverá ser passado para que o serviço consiga recuperar uma possível informação.

Conclusão: O artigo demonstrou as várias configurações suportadas pelo WCF para criar e gerenciar transações. Foi falado desde a necessidade das transações, passando pela infraestrutura necessária e, finalmente, falando sobre como configurá-la em serviços expostos via WCF. Notamos que o WCF desacopla totalmente a criação (e algumas vezes o gerenciamento) das transações da implementação do serviço, fazendo com que o desenvolvedor se concentre na regra do negócio, pois a transação será facilmente habilitada.
Israel Aéce

Israel Aéce - Especialista em tecnologias de desenvolvimento Microsoft, atua como desenvolvedor de aplicações para o mercado financeiro utilizando a plataforma .NET. Como instrutor Microsoft, leciona sobre o desenvolvimento de aplicações .NET. É palestrante em diversos eventos Microsoft no Brasil e autor de diversos artigos que podem ser lidos a partir de seu site http://www.israelaece.com/. Possui as seguintes credenciais: MVP (Connected System Developer), MCP, MCAD, MCTS (Web, Windows, Distributed, ASP.NET 3.5, ADO.NET 3.5, Windows Forms 3.5 e WCF), MCPD (Web, Windows, Enterprise, ASP.NET 3.5 e Windows 3.5) e MCT.