Fundamentos

Aplique transações a serviços facilmente

Juval Lowy

Código disponível para download na MSDN Code Gallery
Navegue pelo código online

Sumário

Gerenciamento de estado e transações
Serviços transacionais por chamada
Gerenciamento de instância e transações
Serviços baseados em sessão e VRMs
Serviços duráveis transacionais
Comportamento transacional
Adicionando contexto à associação IPC
InProcFactory e transações

Um problema fundamental em programação é a recuperação de erro. Após um erro, o aplicativo deve ser restaurado para o estado em que estava antes de o erro ocorrer. Considere um aplicativo que tente executar uma operação que consiste em várias operações menores, possivelmente ao mesmo tempo, em que cada uma das operações individuais pode falhar ou ter êxito independentemente das outras. Um erro em qualquer uma das operações menores significa que o sistema está em um estado inconsistente.

Considere um aplicativo bancário, por exemplo, que transfira fundos entre duas contas, creditando em uma conta e debitando em outra. Debitar em uma conta com êxito, mas falhar ao creditar na outra é um estado inconsistente, porque os fundos não podem estar em dois lugares ao mesmo tempo. Do mesmo modo que falhar ao debitar, mas creditar com êxito resulta em um estado inconsistente, no qual o dinheiro não existe mais. É sempre necessário que o aplicativo se recupere do erro, restaurando o sistema ao estado original.

É mais fácil falar do que fazer por uma série de motivos. Primeiramente, em uma operação grande, o enorme número de permutações com êxito parcial e falha parcial saem de controle rapidamente. Isso resulta em um código frágil, caro para desenvolver e manter e que, muitas vezes, não funciona realmente, uma vez que os desenvolvedores costumam lidar somente com casos de recuperação simples, dos quais já estão cientes e sabem como manipular. Em segundo lugar, a operação composta pode ser parte de uma operação muito maior e, mesmo que o código seja executado perfeitamente, talvez seja necessário desfazê-lo se um erro imprevisto for encontrado. Isso implica uma grande união entre os que participam do gerenciamento e da coordenação das operações. Por fim, também é necessário isolar suas ações de quem mais for necessário para interagir com o sistema, porque se você se recuperar de um erro mais tarde, desfazendo algumas de suas ações, colocará implicitamente alguém em estado de erro.

Como se pode observar, é praticamente impossível escrever códigos de recuperação de erro robustos manualmente. Essa conclusão não é nova. Desde que softwares foram usados em contextos de negócios (nos anos 60), ficou claro que tinha que haver uma maneira melhor de gerenciar a recuperação. Há uma maneira melhor: transações. Uma transação é um conjunto de operações em que a falha em uma operação individual faz com que o conjunto inteiro falhe, como uma única operação atômica. Usando transações, não é necessário escrever uma lógica de recuperação, já que não há nada a recuperar. Ou todas as operações tiveram êxito e não há nada a recuperar ou todas falharam e não afetaram o estado do sistema, portanto, não há nada a recuperar.

Nas transações, é essencial usar gerenciadores de recurso transacionais. O gerenciador de recursos é capaz de reverter todas as alterações feitas durante a transação, caso ela seja anulada, e continuar as alterações, caso a transação seja confirmada. O gerenciador de recursos também fornece isolamento. Enquanto uma transação está em andamento, o gerenciador de recursos impede que todas as outras partes (além da transação) a acessem e observem as alterações, que ainda poderiam ser revertidas. Isso também significa que a transação nunca deveria acessar gerenciadores de não-recursos, já que qualquer alteração feita neles não será revertida se a transação for anulada e, portanto, a recuperação seria necessária.

Tradicionalmente, os gerenciadores de recursos eram recursos duráveis, como bancos de dados e filas de mensagens. No entanto, no artigo da edição de maio de 2005 da MSDN Magazine intitulado “Não é possível confirmar? Gerenciadores de recursos voláteis no.NET trazem transações para o Common Type”, apresentei minha técnica para implementar um gerenciador de recursos volátil (VRM) de finalidade geral, chamado Transacional<T>:

public class Transactional<T> : ...
{
   public Transactional(T value);
   public Transactional();
   public T Value
   {get;set;}
   /* Conversion operators to and from T */
}

Com a especificação de qualquer parâmetro de tipo serializável (como um inteiro ou uma cadeia de caracteres) como Transacional<T>, esse tipo é transformado em um gerenciador de recursos volátil completo que se inscreve automaticamente na transação de ambiente, confirma ou reverte as alterações de acordo com o resultado da transação e isola as alterações atuais de todas as outras transações.

A Figura 1 demonstra o uso do Transacional<T>. Como o escopo não foi concluído, a transação é anulada e os valores de número e cidade são revertidos para seu estado pré-transação.

Figura 1 Usando oTransacional<T>

Transactional<int> number = new Transactional<int>(3);
Transactional<string> city = new Transactional<string>("New York, ");

city.Value += "NY"; //Can use with or without transactions
using(TransactionScope scope = new TransactionScope())
{
   city.Value = "London, ";
   city.Value += "UK";
   number.Value = 4;
   number.Value++;
}
Debug.Assert(number == 3); //Conversion operators at work
Debug.Assert(city == "New York, NY");

Além do Transacional<T>, também forneci uma matriz transacional, bem como versões transacionais das coleções em System.Collections.Generic, como TransactionalDictionary<K,T>. Essas coleções podem se apresentar sob a forma de seus primos não-transacionais, entre outras, e são usadas exatamente da mesma maneira.

Gerenciamento de estado e transações

A única finalidade da programação transacional é deixar o sistema em um estado consistente. No caso do Windows Communication Foundation (WCF), o estado do sistema consiste nos gerenciadores de recursos e no estado da memória das instâncias de serviço. Embora os gerenciadores de recursos gerenciem automaticamente seu estado como produto do resultado da transação, esse não é o caso com objetos da memória ou variáveis estáticas.

A solução para esse problema de gerenciamento de estado é desenvolver um serviço com reconhecimento de estado, a fim de gerenciar o estado de maneira proativa. Entre as transações, o serviço deve armazenar seu estado em um gerenciador de recursos. No início de cada transação, o serviço deve recuperar seu estado no recurso e, ao fazê-lo, inscrever o recurso na transação. Ao final da transação, o serviço deve salvar seu estado novamente no gerenciador de recursos. Essa técnica permite uma recuperação automática de estado eficiente. Qualquer alteração no estado da instância será confirmada ou revertida como parte da transação.

Se a transação for confirmada, da próxima vez em que o serviço obtiver seu estado, este será pós-transação. Se a transação for anulada, da próxima vez, ela terá o estado pré-transação. De qualquer maneira, o serviço terá um estado consistente, pronto para ser acessado por uma nova transação.

Há dois problemas restantes ao escrever serviços transacionais. O primeiro é como o serviço pode saber quando as transações são iniciadas e encerradas para que possa obter e salvar seus estados. O serviço pode ser parte de uma transação muito maior que abrange vários serviços e máquinas. A qualquer momento entre as chamadas, a transação pode ser encerrada. Quem chamará o serviço, informando-o para salvar seu estado? O segundo problema tem a ver com o isolamento. Clientes diferentes podem chamar o serviço ao mesmo tempo, em transações diferentes. Como o serviço pode se isolar de uma alteração de transação em seu estado feita por outra transação? Se a outra transação acessasse seu estado e operasse com base em seus valores, ela seria inconsistente, caso a transação original fosse anulada e as alterações, revertidas.

A solução para os dois problemas é equacionar os limites de métodos com limites de transação. No início de cada chamada de método, o serviço deve ler seu estado do gerenciador de recursos e, ao final de cada chamada de método, o serviço deve salvar seu estado no gerenciador de recursos. Fazer isso garante que, se uma transação for encerrada entre chamadas de métodos, o estado do serviço persista ou seja revertido com ela. Além disso, ler e armazenar o estado no gerenciador de recursos em cada chamada de método aborda o desafio do isolamento, porque o serviço simplesmente permite que o gerenciador de recursos isole o acesso ao estado entre transações simultâneas.

Como o serviço equaciona os limites de método com os limites de transação, a instância de serviço deve também votar no resultado da transação ao final de cada chamada de método. Da perspectiva do serviço, a transação é concluída uma vez que o método seja retornado. No WCF, isso é feito automaticamente por meio da propriedade TransactionAutoComplete do atributo OperationBehavior. Quando essa propriedade é definida como true, se não houver nenhuma exceção não manipulada na operação, o WCF votará automaticamente em confirmar. Se houver uma exceção não manipulada, o WCF votará na anulação. Como o default de TransactionAutoComplete é true, qualquer método transacional usará o recurso autocompletar por padrão, da seguinte maneira:

//These two definitions are equivalent:
[OperationBehavior(TransactionScopeRequired = true,
                   TransactionAutoComplete = true)]   
public void MyMethod(...)
{...}

[OperationBehavior(TransactionScopeRequired = true)]   
public void MyMethod(...)
{...}

Para saber mais sobre programação transacional do WCF, consulte minha coluna Fundamentos “Propagação de transações do WCF” na edição de maio de 2007.

Serviços transacionais por chamada

No serviço por chamada, uma vez retornada a chamada, a instância é destruída. Portanto, o gerenciador de recursos usado para armazenar o estado entre chamadas deve estar fora do escopo da instância. O cliente e o serviço devem também concordar sobre quais operações são responsáveis pela criação ou remoção da instância do gerenciador de recursos.

Como pode haver várias instâncias do mesmo tipo de serviço acessando o mesmo gerenciador de recursos , cada operação deve conter algum parâmetro que permita que a instância de serviço localize seu estado no gerenciador de recursos e se ligue a ele. Chamo esse parâmetro de ID da instância. O cliente deve também chamar uma operação dedicada para remover o estado da instância do armazenamento. Observe que os requisitos comportamentais de um objeto transacional com reconhecimento de estado e um objeto por chamada são os mesmos: os dois recuperam e salvam seus estados nos limites de método. Com um serviço por chamada, qualquer gerenciador de recursos pode ser usado para armazenar o estado do serviço. É possível usar um banco de dados ou um VRM, conforme mostrado na Figura 2.

Figura 2 Serviço por chamada usando um VRM

[ServiceContract]
interface IMyCounter
{
   [OperationContract]
   [TransactionFlow(TransactionFlowOption.Allowed)]
   void Increment(string instanceId);

   [OperationContract]
   [TransactionFlow(TransactionFlowOption.Allowed)]
   void RemoveCounter(string instanceId);
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyService : IMyCounter
{
   static TransactionalDictionary<string,int> m_StateStore = 
                               new TransactionalDictionary<string,int>();

   [OperationBehavior(TransactionScopeRequired = true)]
   public void Increment(string instanceId)
   {
    if(m_StateStore.ContainsKey(instanceId) == false)
      {
         m_StateStore[instanceId] = 0;
      }
      m_StateStore[instanceId]++;
      Trace.WriteLine(m_StateStore[instanceId]); 
   }
   [OperationBehavior(TransactionScopeRequired = true)]
   public void RemoveCounter(string instanceId)
   {
    if(m_StateStore.ContainsKey(instanceId))
      {
         m_StateStore.Remove(instanceId);
      }
   }
}

//Client side:
MyCounterClient proxy = new MyCounterClient();

using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment("MyInstance");
   scope.Complete();
}    

//This transaction will abort since the scope is not completed 
using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment("MyInstance");
} 

using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment("MyInstance");
   proxy.RemoveCounter("MyInstance");
   scope.Complete();
}

proxy.Close();

//Traces:
1
2
2

Gerenciamento de instância e transações

O WCF força a instância de serviço a equacionar os limites de método com os limites de transação e a reconhecer o estado, o que significa limpar todos os estados de instância nos limites de método. Por padrão, uma vez concluída a transação, o WCF destrói a instância de serviço, garantindo que não haja sobras na memória que possam colocar em risco a consistência.

O ciclo de vida de qualquer serviço transacional é controlado pela propriedade ReleaseServiceInstanceOnTransactionComplete do atributo ServiceBehavior. Quando ReleaseServiceInstanceOnTransactionComplete estiver definido como true (o padrão), ele descartará a instância de serviço assim que o método concluir a transação, transformando efetivamente qualquer serviço do WCF em um serviço por chamada no que diz respeito à programação de instância.

Essa abordagem agressiva não surgiu com o WCF. Todos os modelos de programação transacionais distribuídos na plataforma Microsoft, desde o MTS até o COM+ e Enterprise Services, equacionaram um objeto transacional com um objeto por chamada. Os arquitetos dessas tecnologias simplesmente não confiavam que os desenvolvedores gerenciariam de maneira adequada o estado do objeto diante de transações, algo que é intrincado e um modelo de programação não intuitivo. A desvantagem principal é que todos os desenvolvedores que desejam desfrutar de transações devem adotar o incomum modelo de programação por chamada (consulte a Figura 2), embora a maioria dos desenvolvedores se sinta mais confortável com o familiar modelo de programação baseado em sessão dos objetos normais do Microsoft .NET Framework.

Pessoalmente, sempre acreditei que equacionar transações com criações de instância por chamada fosse um mal necessário e, ainda assim, isso é conceitualmente distorcido. O modo de criação de instância por chamada deve ser escolhido somente quando a escalabilidade for necessária e, de maneira ideal, as transações devem ser separadas do gerenciamento de instância de objeto e da relação do aplicativo com a escalabilidade.

Se o seu aplicativo exigir escalabilidade, escolher por chamada e usar transações funcionará muito bem em conjunto. No entanto, se a escalabilidade não for necessária (provavelmente o que ocorre com a maioria dos aplicativos), deve-se permitir que seus serviços sejam baseados em sessão, com monitoração de estado e transacionais. O restante desta coluna apresenta minha solução para o problema de ativar e preservar o modelo de programação baseado em seção ao mesmo tempo em que as transações são usadas com serviços comuns.

Serviços baseados em sessão e VRMs

O WCF permite manter a semântica da sessão com um serviço transacional, configurando-se ReleaseServiceInstanceOnTransactionComplete como false. Nesse caso, o WCF ficará fora do caminho e permitirá que o desenvolvedor do serviço se preocupe em gerenciar o estado da instância do serviço diante das transações. O serviço por sessão deve ainda equacionar os limites de método com os limites de transação, porque cada chamada de método pode estar em uma transação diferente e uma transação pode ser encerrada entre as chamadas de método na mesma sessão. Embora seja possível gerenciar esse estado manualmente, assim como no serviço por chamada (ou usar alguns recursos avançados do WCF fora do escopo dessa coluna), você poderia usar VRMs para os membros do serviço, conforme mostrado na Figura 3.

Figura 3 Usando VRMs por serviço transacional por sessão

[ServiceBehavior(ReleaseServiceInstanceOnTransactionComplete = false)]
class MyService : IMyContract
{
   Transactional<string> m_Text = new Transactional<string>("Some initial value");
   TransactionalArray<int> m_Numbers = new TransactionalArray<int>(3);

   [OperationBehavior(TransactionScopeRequired = true)]
   public void MyMethod()
   {
      m_Text.Value = "This value will roll back if the transaction aborts";

      //These will roll back if the transaction aborts
      m_Numbers[0] = 11;
      m_Numbers[1] = 22;
      m_Numbers[2] = 33;
   }
}

O uso de VRMs ativa um modelo de programação com monitoração de estado: a instância de serviço simplesmente acessa seu estado como se nenhuma transação estivesse envolvida. Qualquer alteração feita no estado da instância será confirmada ou revertida com a transação. No entanto, penso que a Figura 3 seja um modelo de programação especializado e, portanto, com suas próprias armadilhas. Ele exige familiaridade com VRM, definição meticulosa de membros, bem como a disciplina de sempre configurar todas as operações para exigir transações e desativar a liberação da instância após a conclusão.

Serviços transacionais duráveis

No artigo de outubro de 2008 desta coluna (“Gerenciamento de estado com serviços duráveis”), apresentei o suporte que o WCF oferece para serviços duráveis. Um serviço durável recupera seu estado do armazenamento configurado e o salva novamente no armazenamento em todas as operações. O armazenamento de estado pode ou não ser um gerenciador de recursos transacional.

Se o serviço for transacional, ele deve usar, é claro, somente o armazenamento transacional e inscrever-se em cada transação de operação. Dessa maneira, se uma transação for anulada, o armazenamento de estado será revertido para seu estado pré-transação. No entanto, o WCF não sabe se um serviço foi criado para propagar suas transações para o armazenamento de estado e, por padrão, ele não inscreverá o armazenamento na transação mesmo que o armazenamento seja um gerenciador de recursos transacional, como o SQL Server 2005 ou o SQL Server 2008. Para instruir o WCF a propagar a transação e inscrever o armazenamento subjacente, configure a propriedade SaveStateInOperationTransaction do atributo DurableService como true:

[Serializable]
[DurableService(SaveStateInOperationTransaction = true)]
class MyService: IMyContract
{...}

O padrão de SaveStateInOperationTransaction é false, portanto, o armazenamento de estado não participará da transação. Como somente um serviço transacional poderia se beneficiar de SaveStateInOperationTransaction ser definido como true, se for true, o WCF insistirá para que todas as operações no serviço tenham TransactionScopeRequired definido como true ou que tenham um fluxo de transações obrigatório. Se a operação for configurada com TransactionScopeRequired definido como true, a transação de ambiente da operação será a usada para inscrever o armazenamento.

Comportamento transacional

No caso do atributo DurableService, a palavra durável é um nome inadequado, já que não é indicado, necessariamente, um comportamento durável aqui. Isso significa que o WCF irá automaticamente tirar de série o estado do serviço de um armazenamento configurado e o serializará novamente em todas as operações. De maneira semelhante, o comportamento do provedor de persistência não significa necessariamente persistência, já que qualquer provedor que deriva da classe de provedor abstrata prescrita é suficiente.

O fato de a infraestrutura de serviço durável ser, na verdade, uma infraestrutura de serialização, me permitiu melhor utilizá-la como uma técnica para gerenciar o estado do serviço diante das transações, contando ainda com um gerenciador de recursos volátil, sem que fosse necessário que a instância de serviço fizesse algo a respeito. Isso otimiza ainda mais o modelo de programação transacional do WCF e gera o benefício do modelo de programação superior de transações para meros objetos e serviços comuns.

O primeiro passo foi definir duas fábricas de provedor transacionais na memória chamadas TransactionalMemoryProviderFactory e TransactionalInstanceProviderFactory. A TransactionalMemoryProviderFactory usa um TransactionalDictionary<ID,T> estático para armazenar as instâncias de serviço. O dicionário é compartilhado por todos os clientes e sessões. Enquanto o host estiver em execução, TransactionalMemoryProviderFactory permite que os clientes se conectem e desconectem do serviço. Ao usar a TransactionalMemoryProviderFactory, é recomendável designar uma operação de conclusão que remova o estado da instância do armazenamento usando a propriedade CompletesInstance do atributo DurableOperation.

TransactionalInstanceProviderFactory, por outro lado, une cada sessão a uma instância dedicada de Transacional<T>. Não há necessidade de uma operação de conclusão, já que o estado do serviço será coletado como lixo depois que a sessão for fechada.

Em seguida, defini o atributo TransactionalBehavior, mostrado na Figura 4. TransactionalBehavior é um atributo de comportamento de serviço que executa as configurações. Primeiro, ele injeta na descrição do serviço um atributo DurableService com SaveStateInOperationTransaction definido como true. Depois, ele adiciona o uso de TransactionalMemoryProviderFactory ou TransactionalInstanceProviderFactory ao comportamento persistente, de acordo com o valor da propriedade AutoCompleteInstance. Se AutoCompleteInstance estiver definido como true (o padrão), ele usará TransactionalInstanceProviderFactory. Por fim, se a propriedade TransactionRequiredAllOperations estiver definida como true (o padrão), TransactionalBehavior definirá TransactionScopeRequired como true em todos os comportamentos de operação de serviço, fornecendo a todas as operações, portanto, uma transação de ambiente. Quando estiver definido explicitamente como false, o desenvolvedor do serviço poderá escolher quais operações serão transacionais.

Figura 4 O atributo TransactionalBehavior

[AttributeUsage(AttributeTargets.Class)]
public class TransactionalBehaviorAttribute : Attribute,IServiceBehavior
{
   public bool TransactionRequiredAllOperations
   {get;set;}

   public bool AutoCompleteInstance
   {get;set;}

   public TransactionalBehaviorAttribute()
   {
      TransactionRequiredAllOperations = true;
      AutoCompleteInstance = true;   
   }
   void IServiceBehavior.Validate(ServiceDescription description,
                                  ServiceHostBase host) 
   {
      DurableServiceAttribute durable = new DurableServiceAttribute();
      durable.SaveStateInOperationTransaction = true;
      description.Behaviors.Add(durable);

      PersistenceProviderFactory factory;
      if(AutoCompleteInstance)
      {
         factory = new TransactionalInstanceProviderFactory();
      }
      else
      {
         factory = new TransactionalMemoryProviderFactory();
      }

      PersistenceProviderBehavior persistenceBehavior = 
                                new PersistenceProviderBehavior(factory);
      description.Behaviors.Add(persistenceBehavior);

      if(TransactionRequiredAllOperations)
      {
         foreach(ServiceEndpoint endpoint in description.Endpoints)
         {
            foreach(OperationDescription operation in endpoint.Contract.Operations)
            {
               OperationBehaviorAttribute operationBehavior =  
                  operation.Behaviors.Find<OperationBehaviorAttribute>();
               operationBehavior.TransactionScopeRequired = true;
            }
         }
      }
   }
   ...
} 

Ao usar o atributo TransactionalBehavior com os valores padrão, não é necessário que o cliente gerencie ou interaja de qualquer maneira com a ID da instância. É necessário somente que o cliente use o proxy sobre uma das ligações de contexto e deixe que a ligação gerencie a ID da instância, conforme mostrado na Figura 5. Observe que o serviço está interagindo com um inteiro normal como sua variável de membro. O interessante é que, por causa do comportamento durável, a instância ainda é, é claro, desativada como um serviço por chamada nos limites de método, mas o modelo de programação é o de um objeto comum do .NET.

Figura 5 Usando o atributo TransactionalBehavior

[ServiceContract]
interface IMyCounter
{
   [OperationContract]
   [TransactionFlow(TransactionFlowOption.Allowed)]
   void Increment();
}

[Serializable]
[TransactionalBehavior]
class MyService : IMyCounter
{
   int m_Counter = 0;

   public void Increment()
   {
      m_Counter++;
      Trace.WriteLine(m_Counter);
   }
}
//Client side:
MyCounterClient proxy = new MyCounterClient();

using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment();
   scope.Complete();
}    

//This transaction will abort since the scope is not completed 
using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment();
} 

using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment();
   scope.Complete();
}

proxy.Close();

//Traces:
1
2
2

Adicionar contexto à ligação IPC

TransactionalBehavior exige uma ligação que ofereça suporte ao protocolo de contexto. Embora o WCF ofereça suporte a contexto para os serviços da Web (WS) e ligações TCP básicas, não há nessa lista a ligação de comunicação entre processos (IPC; também chamada de pipes). Seria precioso ter esse suporte para ligações IPC, já que isso permitiria o uso de TransactionalBehavior sobre IPC, gerando os benefícios do IPC chamadas íntimas. Para esse fim, defini a classe NetNamedPipeContextBinding:

public class NetNamedPipeContextBinding : NetNamedPipeBinding
{
   /* Same constructors as NetNamedPipeBinding */

   public ProtectionLevel ContextProtectionLevel
   {get;set;}
}

NetNamedPipeContextBinding é usada exatamente como sua classe base. É possível usar essa ligação programaticamente como qualquer outra ligação interna. No entanto, ao usar uma ligação personalizada no arquivo .config do aplicativo, será necessário informar ao WCF onde a ligação personalizada está definida. Embora seja possível fazer isso por aplicativo, a opção mais fácil é fazer referência à classe auxiliar NetNamedPipeContextBindingCollectionElement em machine.config para afetar cada aplicativo na máquina, conforme mostrado aqui:

<!--In machine.config-->
<bindingExtensions>
   ...
   <add name = "netNamedPipeContextBinding" 
        type = "ServiceModelEx.                NetNamedPipeContextBindingCollectionElement,
                ServiceModelEx"
   />
</bindingExtensions>

É possível usar NetNamedPipeContextBinding também em seus aplicativos Workflow.

A Figura 6 lista um trecho da implementação de NetNamedPipeContextBinding e suas classes de suporte (a implementação completa pode ser encontrada no código para download deste mês). Os construtores de NetNamedPipeContextBinding delegam a construção real para os construtores base de NetNamedPipeBinding e a única inicialização que executam é definir o nível de proteção do contexto para ter como default ProtectionLevel.EncryptAndSign.

Figura 6 Implementação de NetNamedPipeContextBinding

public class NetNamedPipeContextBinding : NetNamedPipeBinding
{
   internal const string SectionName = "netNamedPipeContextBinding";

   public ProtectionLevel ContextProtectionLevel
   {get;set;}

   public NetNamedPipeContextBinding()
   {
      ContextProtectionLevel = ProtectionLevel.EncryptAndSign;
   }
   public NetNamedPipeContextBinding(NetNamedPipeSecurityMode securityMode) : 
                                                      base(securityMode)
   {
      ContextProtectionLevel = ProtectionLevel.EncryptAndSign;
   }
   public NetNamedPipeContextBinding(string configurationName)
   {
      ContextProtectionLevel = ProtectionLevel.EncryptAndSign;
      ApplyConfiguration(configurationName);
   }

   public override BindingElementCollection CreateBindingElements()
   {
      BindingElement element = new ContextBindingElement(ContextProtectionLevel,
                            ContextExchangeMechanism.ContextSoapHeader);

      BindingElementCollection elements = base.CreateBindingElements();
      elements.Insert(0,element);

      return elements;
   }

   ... //code excerpted for space
}

A essência de qualquer classe de ligação é o método CreateBindingElements. NetNamedPipeContextBinding acessa sua coleção de ligação base de elementos de ligação e adiciona a ela o ContextBindingElement. A inserção desse elemento na coleção agrega suporte ao protocolo de contexto.

O restante da implementação é somente uma estruturação para ativar a configuração administrativa. O método ApplyConfiguration é chamado pelo construtor, que leva o nome de configuração da seção de ligação. ApplyConfiguration usa a classe ConfigurationManager para analisar o arquivo .config na seção netNamedPipeContextBinding e, nela, analisar uma instância de NetNamedPipeContextBindingElement. Esse elemento de ligação é, em seguida, usado para configurar a instância de ligação chamando seu método ApplyConfiguration.

Os construtores de NetNamedPipeContextBindingElement adicionam à sua classe base Properties a coleção de propriedades de configuração de uma única propriedade para o nível de proteção de contexto. Em OnApplyConfiguration (que é chamado após NetNamedPipeContextBinding.ApplyConfiguration chamar ApplyConfiguration), o método primeiro configura seu elemento base e, em seguida, define o nível de proteção de contexto de acordo com o nível configurado.

O tipo NetNamedPipeContextBindingCollectionElement é usado para ligar NetNamedPipeContextBinding a NetNamedPipeContextBindingElement. Dessa maneira, ao adicionar NetNamedPipeContextBindingCollectionElement como uma extensão de ligação, o gerenciador de configurações sabe qual tipo deve ser instanciado e fornece os parâmetros de ligação.

InProcFactory e transações

O atributo TransactionalBehavior permite tratar quase todas as classes em seu aplicativo como transacionais sem comprometer o modelo de programação do familiar .NET. A desvantagem é que o WCF nunca foi designado para ser usado em um nível muito granular — será necessário criar, abrir e fechar vários hosts, e o arquivo .config do seu aplicativo se tornará impossível de gerenciar com pontuações de serviços e seções de cliente. Para tratar desse problema, em meu livro Programming WCF (Programando no WCF), 2ª Edição, defini uma classe chamada InProcFactory, que permite instanciar uma classe de serviço sobre o WCF:

public static class InProcFactory
{
   public static I CreateInstance<S,I>() where I : class
                                         where S : I;
   public static void CloseProxy<I>(I instance) where I : class;
   //More members
}

Ao usar InProcFactory, você utiliza o WCF no nível da classe, sem recorrer ao gerenciamento explícito do host ou ter arquivos .config de cliente ou serviço. Para tornar acessível o modelo de programação de TransactionalBehavior em todos os níveis de classe, a classe InProcFactory usa NetNamedPipeContextBinding com o fluxo de transações ativado. Usando as definições da Figura 5, InProcFactory ativa o modelo de programação da Figura 7.

Figure 7 Combinando TransactionalBehavior com InProcFactory

IMyCounter proxy = InProcFactory.CreateInstance<MyService,IMyCounter>();

using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment();
   scope.Complete();
}    

//This transaction will abort since the scope is not completed 
using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment();
} 
using(TransactionScope scope = new TransactionScope())
{
   proxy.Increment();
   scope.Complete();
}

InProcFactory.CloseProxy(proxy);

//Traces:
Counter = 1
Counter = 2
Counter = 2

O modelo de programação da Figura 7 é idêntico ao das classes C# simples, sem qualquer sobrecarga de propriedade e, ainda assim o código desfruta totalmente das transações. Vejo isso como um passo fundamental em direção ao futuro, onde a memória em si seja transacional e será possível que qualquer objeto seja transacional.

A Figura 8 mostra a implementação de GetValue com parte do código removido por questão de brevidade O construtor estático de InProcFactory's é chamado uma vez por domínio de aplicativo, alocando em cada uma nova base exclusiva usando um GUID. Isso permite que InProcFactory seja usado várias vezes na mesma máquina, entre domínios de aplicativos e processos.

Figura 8 A classe InProcFactory

public static class InProcFactory
{
   struct HostRecord
   {
      public HostRecord(ServiceHost host,string address)
      {
         Host = host;
         Address = new EndpointAddress(address);
      }
      public readonly ServiceHost Host;
      public readonly EndpointAddress Address;
   }
   static readonly Uri BaseAddress = new Uri("net.pipe://localhost/" + 
                                             Guid.NewGuid().ToString());
   static readonly Binding Binding;
   static Dictionary<Type,HostRecord> m_Hosts = new Dictionary<Type,HostRecord>();

   static InProcFactory()
   {
      NetNamedPipeBinding binding = new NetNamedPipeContextBinding();
      binding.TransactionFlow = true;
      Binding = binding;
      AppDomain.CurrentDomain.ProcessExit += delegate
                                             {
                         foreach(HostRecord hostRecord in m_Hosts.Values)
                                                {
                                                 hostRecord.Host.Close();
                                                }
                                             };
   }


public static I CreateInstance<S,I>() where I : class
                                         where S : I
   {
      HostRecord hostRecord = GetHostRecord<S,I>();
      return ChannelFactory<I>.CreateChannel(Binding,hostRecord.Address);
   }
   static HostRecord GetHostRecord<S,I>() where I : class
                                          where S : I
   {
      HostRecord hostRecord;
      if(m_Hosts.ContainsKey(typeof(S)))
      {
         hostRecord = m_Hosts[typeof(S)];
      }
      else
      {
         ServiceHost host = new ServiceHost(typeof(S),BaseAddress);
         string address = BaseAddress.ToString() + Guid.NewGuid().ToString();
         hostRecord = new HostRecord(host,address);
         m_Hosts.Add(typeof(S),hostRecord);
         host.AddServiceEndpoint(typeof(I),Binding,address);
         host.Open();
      }
      return hostRecord;
   }
   public static void CloseProxy<I>(I instance) where I : class
   {
      ICommunicationObject proxy = instance as ICommunicationObject;
      Debug.Assert(proxy != null);
      proxy.Close();
   }
}

InProcFactory gerencia internamente um dicionário que mapeia tipos de serviço para uma instância de host específica. Quando CreateInstance é chamado para criar uma instância de um tipo específico, ele examina o dicionário, usando um método auxiliar chamado GetHostRecord. Se o dicionário ainda não contiver o tipo de serviço, esse método auxiliar cria uma instância de host para ele e adiciona um ponto de extremidade ao host, usando um novo GUID como o nome de pipe exclusivo. CreateInstance, em seguida, captura o endereço do ponto de extremidade do registro de host e usa ChannelFactory<T> para criar o proxy.

Em seu construtor estático, que é chamado no primeiro uso da classe, InProcFactory assina no evento de saída de processo para fechar todos os hosts quando o processo é encerrado. Por fim, para ajudar os clientes a fecharem o proxy, InProcFactory fornece o método CloseProxy, que consulta o proxy por ICommunicationObject e o fecha. Para aprender como é possível aproveitar a memória transacional, consulte a barra lateral Percepções "O que é a memória transacional?".

O que é memória transacional?

Você já deve ter ouvido falar na memória transacional, a nova tecnologia para gerenciar dados compartilhados que, segundo muitos afirmam, resolverá todos os problemas encontrados na criação de código simultâneo. Talvez você já tenha ouvido também que a memória transacional promete mais do que cumpre, e que não passa de um simples mecanismo de pesquisa. A verdade está entre esses dois extremos.

A memória transacional permite evitar o gerenciamento de bloqueios individuais. Em vez disso, você pode estruturar seu programa em blocos sequenciais bem definidos — unidades de trabalho, ou transações, como são chamados no universo dos bancos de dados. É possível deixar que o sistema de tempo de execução, o compilador ou o hardware subjacente, ou uma combinação deles, forneça as garantias desejadas de isolamento e consistência.

Normalmente, o sistema de memória transacional subjacente fornece um controle de simultaneidade otimista bastante refinado. Em vez de sempre bloquear um recurso, o sistema de memória transacional presume que não há contenção. Ele também detecta quando essas suposições são incorretas e reverte todas as alterações provisórias feitas na transação. Dependendo da implementação, o sistema de memória transacional poderá tentar executar novamente o bloco de código até conseguir concluí-lo sem contenção. Novamente, o sistema é capaz de detectar e gerenciar a contenção, sem que seja necessário especificar ou codificar por conta própria mecanismos criativos de retirada e repetição. Quando você tem controle de simultaneidade otimista refinado e gerenciamento de contenção, e não é necessário especificar nem gerenciar bloqueios específicos, pode pensar em solucionar seu problema de forma serial usando, ao mesmo tempo, componentes que tirem proveito da simultaneidade.

A memória transacional promete fornecer a composição, um recurso que não é realizado com facilidade pelos mecanismos de bloqueio já existentes. Para combinar várias operações ou vários objetos, em geral, é necessário aumentar a granularidade do bloqueio. Normalmente, isso é feito encapsulando essas operações ou objetos em um só bloqueio. A memória transacional gerencia automaticamente o bloqueio refinado em nome do seu código, ao mesmo tempo em que evita deadlocks. Isso possibilita fornecer a composição sem afetar a escalabilidade e sem introduzir deadlocks.

Ainda não há implementações comerciais da memória transacional em grande escala. Há abordagens experimentais de software usando bibliotecas, extensões de linguagem ou diretivas de compilador publicadas no meio acadêmico e na Web. Na verdade, já existe hardware capaz de fornecer uma memória transacional limitada em alguns ambientes de ponta com alto nível de simultaneidade, mas o software que utiliza esse hardware oculta seu uso explícito. A comunidade de pesquisa está entusiasmada com a memória transacional e com certeza você verá a transformação de parte dessas pesquisas em produtos mais acessíveis ao longo da próxima década.

A coluna Fundamentos descreve a criação de gerenciadores de recursos voláteis, que podem ser usados em conjunto com as tecnologias transacionais atuais para fornecer atomicidade — características de execução do tipo "tudo ou nada" — e as demais vantagens decorrentes da programação transacional, como capacidade de gerenciamento, qualidade e outras. A memória transacional oferece recursos semelhantes, mas para qualquer tipo de dados arbitrário, usando um software de tempo de execução relativamente leve ou primitivos de hardware. Ela se concentra no fornecimento de escalabilidade, isolamento e composição, além de atomicidade, sem a necessidade de criar seu próprio gerenciador de recursos. Quando a memória transacional estiver amplamente disponível, os programadores obterão não somente as vantagens dos gerenciadores de recursos voláteis, como também um modelo de programação mais simples. Além disso, perceberão os ganhos de escalabilidade trazidos por um gerenciador de memória transacional.

— Dana Groff é gerente sênior de programa na Equipe da plataforma de computação paralela da Microsoft

Envie perguntas e comentários para mmnet30@microsoft.com.

Juval Lowy é arquiteto de software da IDesign e presta serviços de treinamento em WCF e consultoria em arquitetura. Seu livro recente é o Programming WCF Services, 2ª Edição (O'Reilly, 2008). Ele também é diretor regional da Microsoft no Vale do Silício. Contate Juval pelo site www.idesign.net.