Desenvolvimento - C#

Implementando um framework de gerenciamento transacional

O que eu pretendo, neste artigo, é propor uma arquitetura flexível para gerenciamento de transações no ambiente Microsoft. É claro que não será possível listar todo o código, linha a linha, mas vou dar algumas idéias básicas que acredito serem simples de se seguir.

por Mateus Velloso



.csharpcode { font-size: 11; color: black; font-family: Courier New , Courier, Monospace; background-color:#dddddd; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

Quem já definiu pelo menos uma vez a arquitetura de um sistema .Net, provavelmente se fez algumas perguntas com relação a gerenciamento de transações:

  • Devo usar COM+ tradicional, registrando componentes?
  • Devo usar COM+ SWC e evitar registrar componentes?
  • Devo gerenciar manualmente as transações?
  • Como posso instrumentar minhas transações e saber quais componentes e métodos estão executando?
  • Como eu posso simplificar a programação dos componentes transacionais?
  • Devo me preocupar com transações distribuídas?
Acredito que a resposta para essas perguntas seria: Você deve poder escolher entre usar ou não COM+, COM+ SWC, DTC ou transações simples a qualquer momento, sem precisar alterar uma linha sequer de código. Também não pode se esquecer que em breve teremos o Indigo, então deve ser razoavelmente fácil a migração do que você tem hoje para essa tecnologia. A partir dessa premissa, você pode ter também um monitor de componentes e métodos no nível de detalhes que desejar para acompanhar o desempenho de suas transações e regras de negócio.

Parece um tanto impossível, mas isso é perfeitamente viável.

O que eu pretendo, neste artigo, é propor uma arquitetura flexível para gerenciamento de transações no ambiente Microsoft. É claro que não será possível listar todo o código, linha a linha, mas vou dar algumas idéias básicas que acredito serem simples de se seguir.

Requisitos da arquitetura proposta

Bom, antes de começar a explicar a minha abordagem, acho que vale repassar quais são nossos requisitos para este pequeno Framework de gerenciamento de transações:

Requisito 1: Possibilidade de trocar entre COM+ tradicional, COM+ SWC, DTC, transações simples (sem DTC e XA) automatizadas a qualquer momento via arquivos de configuração, sem alterar código.

Esse me parece o mais lógico e o que menos precisa de explicações. Seja lá o que eu estiver usando, eu não quero perder dias, meses ou anos reprogramando cada componente simplesmente porque mudei de idéia entre uma tecnologia de gerenciamento de transações ou outra.

Requisito 2: Possibilidade de fazer com que um mesmo assembly use um provider de gerenciamento de transações específico, como COM+ SWC em um caso, e outro provider em outro caso, dependendo apenas da aplicação que o executa.

Esse é um pouco mais complicado, mas para mim faz todo sentido. Vamos supor que eu tenha uma classe que trate dados de clientes num banco de dados SQL Server. Se a aplicação que estiver usando esta classe trabalha apenas com esse mesmo SQL Server, não faz sentido eu utilizar o DTC. Estarei perdendo desempenho e sobrecarregando recursos que são totalmente desnecessários nesse caso.

Mas, por outro lado, essa classe pode ser executada por uma outra aplicação, que está acessando uma base Oracle. Provavelmente terei que envolver ambos os bancos de dados numa transação distribuída. Agora eu vou precisar do COM+, ou pelo menos do DTC para suportar essa estrutura, correto?

A conclusão é que eu preciso suportar mais de uma configuração para o mesmo assembly.

Requisito 3: Quero instrumentar meus métodos transacionais.

O que significa instrumentar? Significa que, se um belo dia algo der errado e o consumo de processador do servidor for parar nas alturas, ou se uma transação der deadlock, eu quero saber quem está travado, qual método, qual componente em qual namespace e qual processo.

Muitas vezes não basta saber apenas o assembly e a classe. Eu posso ter 20 processos diferentes usando o mesmo assembly e a mesma classe, como vou descobrir qual deles está causando deadlock no banco? Precisarei de mais detalhes...

Requisito 4: Quero facilitar um futuro upgrade do meu sistema para o Índigo ou outras tecnologias, reduzindo ao mínimo alterações de código.

Se você já ouviu falar do Indigo, provavelmente ficou tão entusiasmado quanto eu. O único problema é que não podemos cruzar os braços e parar de desenvolver aplicações até o Indigo ser lançado. Portanto, temos que ir nos virando com o que temos, garantindo que a migração para o Indigo seja simples e sem traumas.

Requisito 5: Configuração do comportamento transacional em XML.

Teremos, em um arquivo XML, a opção de dizer quais componentes requerem transação, quais requerem uma nova transação, etc. no mesmo estilo do COM+. A diferença é que, com isso configurado em XML, podemos trocar de provider transacional sem perder essas configurações.

Requisito 6: Minha arquitetura prevê uma seqüência específica de chamadas de componentes. Não quero permitir que o programador viole esta seqüência.

Esse é um “plus” que resolvi incluir aqui, pois senti esta necessidade e precisei implementar recursos para suportar isto.

A minha arquitetura prevê está representada na figura 1.



Figura 1: Seqüência permitida de chamadas entre componentes

Esta arquitetura é algo um tanto simples e já discutido em outros artigos. Basicamente os componentes de Business Process (BP) recebem as chamadas da aplicação e chamam quantos componentes de Business Logic (BL) forem necessários. Também é permitido que um componente BP converse com outro componente BP.

O problema é que muitas vezes o programador, por falta de informação ou por falta de tempo, começa a fazer coisas estranhas, como chamar um componente BP de um componente no Data Acess Layer (DAL). Não sei quanto a você, mas eu não quero permitir esse tipo de coisa na minha arquitetura.

Mas como garantir isso? Bom, já que vamos cuidar de coisas muito mais complicadas, como gerenciamento de transações e providers, por que não fazer com que a biblioteca de código não permita que o programador crie uma classe DAL que chame um BP, ou um BP que chame direto um DAL sem passar por um BL e assim por diante?

.csharpcode { font-size: 11; color: black; font-family: Courier New , Courier, Monospace; background-color:#dddddd; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

Implementação

Por hora, acho que já temos requisitos suficientes. Então vamos a alguns aspectos práticos da implementação disso.

A primeira conclusão lógica ao analisar estes requisitos é que precisamos encontrar uma forma de interceptar cada chamada a métodos dessas classes (BP, BL e DAL) para poder dizer quando uma transação deve ocorrer e de que forma. Algo no estilo da figura 2.


Figura 2: Exemplo dos “Interceptadores”, aspectos que atuam de forma transparente entre as chamadas de métodos dos componentes transacionais

A idéia é que a aplicação não saiba o que está acontecendo, o programador simplesmente chama um componente, mas alguém entra no meio do caminho e verifica que componente está sendo chamado, quem está chamando e o que deve ser feito.

Como implementar isso? Basta usar atributos e alguns recursos da AOP (Programação Orientada a Aspectos).

Não vou entrar em muitos detalhes sobre a programação orientada a aspectos para não prolongar demais este artigo, mas a idéia é simples: Vamos criar atributos que, uma vez que sejam declarados em uma classe, passam a interceptar todas as chamadas a quaisquer métodos desta classe e das classes herdadas a partir dessa.

Para isso, vou recorrer a uma das várias abordagens disponíveis para AOP no .Net. Entre os frameworks disponíveis para .Net com suporte a aspectos, o mais conhecido é o Spring, que vejo do Java. Mas uma outra possibilidade pouco divulgada já está codificada dentro das classes do .Net Framework, e é usada pela infra-estrutura com. Sugiro a leitura do seguinte artigo:

http://msdn.microsoft.com/msdnmag/issues/02/03/aop/

Então vou usar essa infra-estrutura já existente no .Net no meu exemplo. Vou criar uma classe que será meu atributo transacional. Está classe vai cuidar de dois eventos básicos: Executar algo antes do método do meu componente transacional iniciar, e executar algo após o método do meu componente transacional executar. Seguindo o artigo que eu citei acima, esse atributo é herdado do ContextAttribute do .Net Framework.

Implementarei o dois eventos, um que executa antes do método chamado e outro que executa logo após a execução do método. Para mais detalhes, veja no artigo acima a implementação do “PreProcess” e do “PostProcess”. Com esses dois eventos, terei tudo o que preciso para iniciar um gerenciamento de transações básico.

Veja uma idéia simples de como essa classe vai ficar na Listagem 1.

using System;
using AOP.CBO;
using System.Runtime.Remoting.Messaging;
 
namespace Mateus
{
 [AttributeUsage(AttributeTargets.Class)]   
 public class TransactionAttribute:AspectAttribute 
 {
  /// <summary>
  /// Isso executa antes de cada método das minhas classes transactionais
  /// </summary>
  /// <param name="mCall">Esses são os dados a respeito da chamada (método, etc)</param>
  /// <returns>Este retorno eu defino como desejar para o PostInvoke usar.</returns>
  public override object PreInvoke(IMethodCallMessage mCall)
  {
   //TODO: Verificar quem está chamando e quem está sendo chamado para decidir se
   // a chamada é permitida (BP chama BLL que chama DAL). Caso contrário, disparar 
   // uma Exception
 
   //TODO: Verificar nos arquivos de configuração qual o provider de transação
   // está sendo usado, e chamá-lo solicitando que tome controle do processo
   return null;
  }
 
  /// <summary>
  /// Isso executa ao final de cada método das minhas classes transactionais
  /// </summary>
  /// <param name="mCall">Dados a respeito da chamada</param>
  /// <param name="mRet">Dados a respeito do retorno ao chamador.</param>
  /// <param name="preResult">Esse parametro é o retorno do PreInvoke</param>
  public override  void PostInvoke(IMethodCallMessage mCall,
                                   IMethodReturnMessage mRet,
                                   object preResult)
  {
    //TODO: Chamar novamente o provider de transação escolhido, dizendo a ele que o 
    //método em questão foi encerrado.
  }      
 }
}

Listagem 1: Esqueleto do atributo transacional.

Bom, agora precisamos criar uma classe transacional básica, de onde todas as outras serão herdadas. Para que isso funcione, terei que herdar essa classe do AspectTarget, uma classe proprietária que eu criei, herdada do ContextBoundObject do .Net.

using System;
using AOP.CBO;
 
namespace Mateus
{
         [ContextAspect]
         [Transaction]
         public abstract class BaseTransactionalClass: AspectTarget 
         {
 
         }
}

Listagem 2: Esqueleto da classe transactional.

Pronto! Simples, não? Experimente agora criar uma classe e herde de BaseTransactionalClass. Crie um método de exemplo, como na listagem 3 e faça um debug.

using System;
 
namespace Teste
{
         
         public class Class2:Mateus.BaseTransactionalClass 
         {
                 public void MetodoExemplo1()
                 {
 
                 }
         }
}

Listagem 3: Exemplo de uma classe interceptada pelos aspectos.

Você vai perceber que seu atributo vai executar antes e depois do construtor, e novamente antes e depois do método que você está chamando. Perceba o poder que isso da ao seu código. Você agora tem total controle sobre o stack de métodos nas classes herdadas da BaseTransactionalClass. Dentro do PreInvoke, agora posso perguntar uma série de coisas sobre o que está ocorrendo. Veja a listagem 4.

public override object PreInvoke(IMethodCallMessage mCall)
{                
 if (mCall.MethodBase.IsConstructor) return null;
 if (mCall.MethodBase.IsStatic) return null;
 
 string AssemblyName = mCall.MethodBase.ReflectedType.Assembly.GetName(false).Name;
 string AssemblyVersion = mCall.MethodBase.ReflectedType.Assembly.GetName(false).Version.ToString();
 string RunningNamespace = mCall.MethodBase.ReflectedType.Namespace;
 string RunningClass = mCall.MethodBase.ReflectedType.Name;
 string RunningMethod = mCall.MethodBase.Name;
 return null;
}

Listagem 4: O atributo de transação investiga a chamada para saber o que está ocorrendo.

Perceba que eu já estou tomando uma decisão um tanto particular: Não vou controlar construtores nem métodos estáticos. Por que? Porque nos padrões que defini neste caso, classes transacionais não guardam estado (dispensando construtores) e utilizam uma abordagem voltada para SOA. Nada de propriedades ou coisas do gênero. É claro que poderia ter implementado isso de outras formas, mas aí eu deixo para a sua imaginação.

.csharpcode { font-size: 11; color: black; font-family: Courier New , Courier, Monospace; background-color:#dddddd; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

Com base nas informações sobre assembly, versão e etc, já podemos implementar o primeiro TODO, que é checar se a ordem das chamadas está correta (BP-BLL-DAL). Não vou implementar este código aqui para não prolongar demais o artigo. Com isso, resolvemos o requisito 6.

Sugiro que você crie 3 subclasses a partir da classe transacional: Uma para BP, uma para BLL e outra para DAL. Elas vão herdar o comportamento proposto. Você também pode decidir que não serão todas estas classes que farão parte do controle transacional. Isso não é necessariamente um problema, muitas abordagens não incluem essas 3 camadas no gerenciamento.

Agora, vou implementar um “provider” de transações para COM+ SWC. Veja a implementação básica na Listagem 5.

using System;
using System.Runtime.Remoting.Messaging;
using System.EnterpriseServices;
 
namespace Mateus
{
         
  public class SWCTransactionProvider
  {
   //TODO: Essa classe deve implementar uma interface padrão para todos os providers
 
   [ThreadStatic] internal static System.Collections.Hashtable DeadContexts;
   [ThreadStatic] internal static ComponentStack componentStack;
                 
   public ComponentStack CurrentStack
   {
    get
    {
     return componentStack;
    }
   }
 
   public SWCTransactionProvider()
   {
    if (DeadContexts==null) DeadContexts=new System.Collections.Hashtable();               
   }
 
   public Context GetCurrentContext()
   {
    return componentStack.context;
   }
 
   public object OnPreInvoke(IMethodCallMessage mCall,
                             TransactionOption transactionOption,
                             TransactionIsolationLevel transactionIsolationLevel,
                             int transactionTimeOut,
                             string runningNamespace,
                             string runningClass, 
                             string runningMethod)
   {
    //Empilha o método para controlar a sequência de chamadas
    ComponentStack componentStructure=new ComponentStack();
   
    if (componentStack==null)
     componentStructure.CallerComponent=null;
    else
     componentStructure.CallerComponent=componentStack;
    componentStack=componentStructure;
    componentStack.RunningNamespace=runningNamespace;
    //TODO: Preencher os outros parâmetros do componentStack...
                          
    //Configura o SWC COM+                          
    ServiceConfig config = new ServiceConfig();
    config.TrackingEnabled = true;
    //Teremos um "pacote" virtual com o nome do namespace
    config.TrackingAppName = runningNamespace;
    //Teremos um "componente" virtual com o nome da classe + o método
    config.TrackingComponentName = runningClass + "." + runningMethod;
    //Configuiramos a opção de transação, timeout, etc que veio do arquivo config
    config.Transaction = transactionOption;     
    config.IsolationLevel = transactionIsolationLevel;     
    config.TransactionTimeout=transactionTimeOut;
    config.TransactionDescription=runningNamespace + "." + runningClass;
 
    //Verificando: Já estou numa transação SWC?
    System.Guid ContextGuid=System.Guid.Empty;
    try
    {
     if (ContextUtil.IsInTransaction) ContextGuid=ContextUtil.TransactionId;
    }
    catch
    {
    }
    //Se já estou numa transação, essa transação já está sendo abortada? 
    //Se estiver, não posso continuar.
    if (ContextGuid!=System.Guid.Empty)
    {
      lock(DeadContexts)
      {
       if (DeadContexts.ContainsKey(ContextGuid))
       {
        throw new ApplicationException("Uma vez que uma transação será abortada, “ + 
                                       “nenhum novo componente pode participar da mesma.");
       }
      }
    }
    //Tudo ok, então podemos entrar no COM+
    ServiceDomain.Enter(config);
    //Se ninguém der SetComplete depois, o método vai abortar por default.
    if (ContextUtil.IsInTransaction)
    {
      ContextUtil.MyTransactionVote=System.EnterpriseServices.TransactionVote.Abort;
    }
    return null;
   }
 
  public void OnPostInvoke(IMethodCallMessage mCall, IMethodReturnMessage mRet, object preResult)
  {
   //Remove o cara da pilha
   componentStack.context.Dispose();
   componentStack=componentStack.CallerComponent;
 
   if (ContextUtil.IsInTransaction)
   {
    if (ContextUtil.MyTransactionVote==System.EnterpriseServices.TransactionVote.Abort)
    {
     lock(DeadContexts)
     {
      //Devo abortar a transação, então não vou permitir que ninguém mais entre nela
      if (DeadContexts.ContainsKey(ContextUtil.TransactionId))
       DeadContexts[ContextUtil.TransactionId]=System.DateTime.Now;
      else
       DeadContexts.Add(ContextUtil.TransactionId,System.DateTime.Now);
     }
    }
   }
   ServiceDomain.Leave();                   
  }
 
  public void SetComplete()
  {
   ContextUtil.SetComplete();
  }
  public void SetAbort()
  {
   ContextUtil.SetAbort();
  }
 }
}

Listagem 5: Implementação básica do Provider SWC COM+

Bom, vamos às explicações, pois o código da Listagem 5 deve ter levantado algumas dúvidas. A idéia é que o atributo de transação, ao verificar num arquivo de configuração qual o provider adequado, chame por sua vez esse provider. Nesse caso, o provider escolhido trabalha com SWC (Service without components, uma abordagem para usar o COM+ sem registrar componentes). Portanto, temos dois métodos básicos, o PreInvoke e o PostInvoke.

O provider, por sua vez, guarda um mapa das chamadas de componentes (o ComponentStack) que simplesmente é uma pilha que vai aumentando a cada método transacional chamado. Uma idéia básica do ComponentStack está na Listagem 6.

using System;
 
namespace Mateus
{
         public class ComponentStack
         {
                 public string AssemblyName;
                 public string AssemblyVersion;
                 public string RunningNamespace;
                 public string RunningClass;
                 public string RunningMethod;
                 public object Transaction;
                 public ComponentStack CallerComponent;
                 public bool TransactionVote;
                 public Context context;
                 public System.Type ComponentType;
         }
 
}

Listagem 6: Implementação básica do ComponentStack

Repare que a propriedade “CallerComponent” é do tipo ComponentStack. Ou seja, dentro de um ComponentStack eu coloco outro, e dentro deste, coloco outro, e assim por diante. Este é o mapa das minhas chamadas dentro de um contexto transacional (Não necessariamente a mesma transação). Eu preciso dele para saber que método fez o que. Por exemplo, este método deu SetComplete ou SetAbort? No caso do SWC, basta eu verificar com o ContextUtil essa informação, mas se eu construir um provider que não usa nada do COM+, precisarei registrar nesse Stack qual foi o voto. Por isso, o SetComplete e o SetAbort podem ter implementações diferentes em cada provider.

.csharpcode { font-size: 11; color: black; font-family: Courier New , Courier, Monospace; background-color:#dddddd; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

Outra classe que eu usei foi o “Context”. Ele vai guardar as conexões a banco que cada método vai usar. Isso significa que métodos diferentes podem ou não usar as mesmas conexões. Quando eu implementar o “TODO: Preencher os outros parâmetros do ComponentStack”, terei de decidir quando criar um “Context” novo e quando usar um já existente. No SWC, posso sempre criar um context novo e novas conexões, já que o ContextUtil vai colocá-las para trabalhar juntas numa transação quando necessário. Mas quando eu criar providers que não usarem o COM+, terei que fazer isso manualmente, verificando se o componente requer uma transação, uma nova transação e etc, e controlando as transações uma a uma.

Já os DeadContexts são necessários apenas com o SWC. O caso é o seguinte: Se você executa um SetAbort num método do SWC, e depois tenta entrar no ServiceDomain novamente, dentro da mesma transação, o SWC irá impedir que todo e qualquer componente entre em novas transações naquele processo, o que poderia ser desastroso se não tivermos um controle disso. Não sei se isso é “by design” ou se é um enorme bug. Então, o que fazemos? Guardamos a informação de que essa transação vai “morrer”, e não deixamos que o ServiceDomain entre novamente em uma transação que será abortada. Outros providers não precisam desse controle adicional.

Quanto ao [ThreadStatic], ele tem um motivo simples: Não vou permitir que os programadores criem Threads dentro de componentes transacionais, e portanto, todo o meu mapa de contextos é isolado por Threads. Por que? Porque o controle disso em multiThread seria enorme. Essa restrição não é novidade para quem trabalha com COM+. Sempre houveram recomendações da Microsoft para que se tome MUITO cuidado com criação de threads dentro de componentes COM+, talvez pelo mesmo motivo. Para mais informações, sugiro ler as documentações do COM+ com relação a threads.

Quando eu coloco esses providers juntos para trabalhar, temos algo mais ou menos como o diagrama da Figura 3.


Figura 3: Aplicações diferentes, com arquivos de configuração diferentes podem chamar as mesmas classes e fazer com que elas usem providers transacionais diferentes, de acordo com a necessidade. Nenhuma alteração de código é necessária.

Com um pouco de imaginação, já é possível ver onde podemos chegar com o modelo proposto na Figura 3. Praticamente todos os requisitos listados podem ser resolvidos com este modelo. Cada provider tem as suas particularidades e seus controles específicos, mas a idéia básica é sempre a mesma: Interceptar métodos, decidir o que fazer com eles e controlar como as conexões a banco e transações serão fornecidas aos componentes.

Provavelmente algumas dúvidas devem ter ficado para aqueles que leram este artigo, vamos a elas:


COM+ Tradicional: Como incluí-lo neste modelo?

O grande problema em construir um provider para COM+ tradicional é que eu preciso herdar minhas classes do System.EnterpriseServices.ServicedComponent. Fazendo isto, estarei jogando fora tudo o que foi feito até aqui. Como eu posso manter a idéia dos arquivos de configuração e ao mesmo tempo compatibilizar a possibilidade de colocar componentes dentro do COM+?

A solução está no versionamento de assemblies. Em primeiro lugar, vou assinar meu assembly que fornece o provider de gerenciamento de transações, e vou defini-lo como versão 1.0.0.0. Depois, vou criar uma nova versão, com o mesmo SNK, mas de número 2.0.0.0. Nesta versão, a classe BaseTransactionalClass seria como na Listagem 7.

[System.EnterpriseServices.Transaction(TransactionOption.Required)]
public abstract class BaseTransactionalClass:System.EnterpriseServices.ServicedComponent 

Listagem 7: Classe BasetransactionalClass agora é um ServicedComponent

Sugiro, inclusive, que o código da versão 1 e da versão 2 seja idêntico, usando uma cláusula de precompilador (“#if REGISTERCOMPLUS”) para decidir se a declaração da classe deve ser de um jeito ou de outro. Assim, ao compilar, basta decidir qual das duas usar.

Pois bem, uma vez feito isso, posso colocar ambos os assemblies na GAC, e definir, para cada aplicação, quando usar um assembly e quando usar o outro, via arquivos de configuração. Veja a Figura 4.


Figura 4: Assemblies com duas versões, cada qual com suporte aos gerenciadores de transação específicos.

É claro que, para que essa mudança seja fácil, é necessário orientar aos desenvolvedores que sempre declarem os GUIDS de suas classes e demais informações no assemblies que serão exigidas pelo COM+ tradicional, já que para os demais providers, isso não é necessário.

Também, evidentemente, os atributos transacionais não terão efeito no assembly 2.0.0.0, o que nos impede de fazer o controle sobre a seqüência das chamadas e as instrumentações, como fazíamos no 1.0.0.0.

Ainda assim, podem existir casos onde seja necessário utilizar o COM+ tradicional, e é melhor que deixemos essas duas opções disponíveis.

.csharpcode { font-size: 11; color: black; font-family: Courier New , Courier, Monospace; background-color:#dddddd; /*white-space: pre;*/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }

E a instrumentação?

A instrumentação já foi implementada, em uma das possíveis formas, no provider de SWC. Quando informo ao ServiceDomain o nome do meu package e dos meus componentes, estou fazendo com que, no Windows 2003, eu consiga ver cada método executando, separado por namespaces e processos. Veja um printscreen de uma tela de um sistema meu para que você tenha uma idéia na Figura 5.


Figura 5: Os namespaces ficam organizados por processo, e dentro deles temos a listagem de componentes e métodos, conseguindo detalhes sobre tempo de execução de cada método.

Esse é um dos motivos pelos quais eu gosto tanto de usar o provider para SWC. O nível de detalhes que ele irá lhe fornecer vai além do que o COM+ tradicional irá dar.

Mas nada impede que você crie outras formas de instrumentação nos demais providers. Você sabe quando cada método começa a executar e quando termina, sabe quantas transações estão iniciando e quantas estão encerrando, quantos SetCompletes e quantos SetAborts. O que mais falta? Falta registrar isso em contadores. Apenas tome cuidado para não matar a performance de sua aplicação. Talvez você queira montar, no arquivo de configuração, um “gatilho” que pode ser ativado ou não para controlar a instrumentação.

E o arquivo de configuração?

Esse arquivo pode ser um simples XML. Você pode usar algum bloco, como o ConfigurationManagement Application Block para isso. Veja o meu na Listagem 8.

<configuration>
  <applications> 
      <DataAccessConfiguration>
        <TransactionProviderAssembly>Mateus.Data…,Version=...</TransactionProviderAssembly>
        <TransactionProviderClass>....SWCTransactionProvider</TransactionProviderClass>
        <CommandTimeOut>60</CommandTimeOut>
      </DataAccessConfiguration>
      <DatabaseConnections>
        <ConnectionDefinition>
          <ConnectionName>ConexaoSQL</ConnectionName>
          <ConnectionString>Data Source=...</ConnectionString>
          <ConnectionType>Mateus.Data.SQLData, Mateus.Data,Version=…</ConnectionType>
        </ConnectionDefinition>
        <ConnectionDefinition>
          <ConnectionName>ConexaoOracle</ConnectionName>
          <ConnectionString>Provider...</ConnectionString>
          <ConnectionType>Mateus.Data.OleDbData…</ConnectionType>
        </ConnectionDefinition>
      </DatabaseConnections>
      <TransactionComponents>
        <TransactionComponentConfiguration>
          <ComponentName>Mateus.Exemplo.BP.TesteBP</ComponentName>
          <Transaction>Required</Transaction>
          <IsolationLevel>ReadCommitted</IsolationLevel>
          <TransactionTimeOut>60</TransactionTimeOut>
        </TransactionComponentConfiguration>
        <TransactionComponentConfiguration>
          <ComponentName>Mateus.Exemplo.BLL.TesteBLL</ComponentName>
          <Transaction>RequiresNew</Transaction>
          <IsolationLevel>ReadCommitted</IsolationLevel>
          <TransactionTimeOut>60</TransactionTimeOut>
        </TransactionComponentConfiguration>
      </TransactionComponents>
  </applications>
</configuration>

Listagem 8: XML de configuração das transações.

É claro que a configuração de comportamento transacional será ignorada na versão 2 do assembly, já que neste caso valerá a configuração visual feita no COM+. Nos demais providers, basta seguir a mesma lógica e o arquivo de configuração será sempre o mesmo.

Como trafegar uma transação pela rede entre servidores de aplicação?

A resposta mais curta e simples é: Aguarde pelo Indigo, onde teremos objetos de transação serializáveis. Caso contrário, você terá que partir para o COM+ tradicional ou Remoting, que eu não recomendo por não oferecer os requisitos mínimos de segurança necessários.

Hoje eu utilizo quase que somente Web Services entre minha interface com o usuário e minhas regras de negócio, portando meu modelo é mais ou menos como o descrito na Figura 9:


Figura 6: XML de configuração das transações.

Isso quer dizer que, uma vez que meu front end chega ao Web Service, daí em diante toda a comunicação é feita dentro do processo. Isso me da performance e elimina o problema de trafegar transações entre servidores de aplicação. É possível, inclusive, colocar um NLB de servidores de Web Service se a preocupação for escalabilidade.

Mas, como eu disse, isso pode não atender ao seu cenário, e nesse caso, você talvez terá que, em alguns casos, usar COM+ tradicional e comunicação DCOM para alcançar outros servidores de aplicação.

Conclusão

Tudo o que eu citei acima faz parte do cenário real que venho usando em algumas aplicações. Não tenho enfrentado problemas de performance, mesmo utilizando Web Services. Acredito que o Indigo irá eliminar muitas das necessidades que tivemos de implementar via código no cenário antigo, mas, como eu disse, não pude esperar por ele.

Também realizei testes de performance entre os vários providers, e talvez você esteja interessado em conhecer os resultados

. Em um cenário simples, com uma aplicação fazendo algumas milhares de transações em um banco de dados SQL Server, rodando em um pc comum com Windows 2003, o tempo necessário para completar as transações foi:

COM+ tradicional: (sem atributos de aspectos e com os componentes já registrados e carregados na memória): 20 segundos

COM+ 1.5 SWC: (com atributos de aspectos e contando o tempo de inicialização): 15 segundos

DTC puro: (API’S do DTC sem COM+, mas com atributos de AOP) 14 segundos

Transação automática: (sem DTC e sem suporte a two phase commit, com atributos de AOP) 11 segundos

Sem transações: (com atributos de AOP, apontando para um provider que não faz nada) 11 segundos

Podemos perceber que essa abordagem não atrapalha a performance da aplicação. Muito pelo contrário, temos a liberdade de escolher entre o provider mais performático em cada caso. Não preciso, por exemplo, usar DTC ou COM+ para uma transação local.

Essa é uma das coisas que o Indigo em conjunto com o .Net Framework 2 vai resolver, já que não fará o enlist uma transação no DTC a menos que perceba que ela será distribuída.

Dependendo de como você implementar esses providers, eles suportarão praticamente qualquer Framework de acesso a dados, do NHibernate ao Microsoft Data Access Application Block, usando DataReaders, DataSets ou qualquer outra coisa. O importante é ter flexibilidade, sem perder a simplicidade.

Mateus Velloso

Mateus Velloso - MVP de Visual Developer - Solutions Architect
Bacharel em administração de empresas, ex-sócio e consultor em tecnologia da Flag Intelliwan, atualmente atua como Principal Software Architect na Telecom New Zealand, possui também as certificações: MCP, MCSA, MCAD, MCSD, MCSE, MCDBA, MCT, MCTS SQL 2005, OCP, SCJP e SAP Business One Consultant. Atua em projetos de tecnologia, construção de frameworks de desenvolvimento e também na área de automação industrial.