WCF – WS-Discovery

by Israel Aece 7. June 2009 16:25

Ao referenciar um serviço WCF em uma aplicação cliente, um proxy é gerado para abstrair toda a complexidade necessária para efetuar a comunicação entre o cliente e o serviço. Ao efetuar essa referência, além da classe que representa o proxy, o arquivo de configuração da aplicação cliente também é alterado, efetuando todas as configurações necessárias para que o WCF possa efetuar a requisição ao serviço remoto.

Entre essas configurações que são realizadas, uma delas é a criação do endpoint do lado do cliente, com o endereço, binding e o contrato necessário para efetuar as requisições. Ao fazer isso, o endereço ficará fixado no arquivo de configuração e não teremos problemas até que o endereço mude de local. Se, por algum motivo, o serviço não estiver mais disponível naquele endereço (endpoint) que foi inicialmente publicado, todos as aplicações deixarão de funcionar, até que alguém altere o endereço manualmente, apontando para o novo endereço.

Para solucionar este problema, a Microsoft estará incorporando ao WCF 4.0 a implementação do protocolo WS-Discovery. Como o próprio nome diz, WS-Discovery trata-se de um padrão criado pela OASIS, que define um mecanismo para o descobrimento de serviços de uma determinada rede, removendo a necessidade dos consumidores conhecerem o endereço do serviço até que a aplicação execute, e além disso, os serviços podem mudar constantemente de endereço, que os eventuais consumidores não deixarão de funcionar. A finalidade deste artigo é abordar como podemos proceder para criar, publicar e consumir serviços utilizando o WS-Discovery. Vale lembrar que este artigo será baseado na versão Beta do Visual Studio .NET 2010 e do .NET Framework 4.0, ou seja, poderá haver alguma mudança até a versão final.

Antes de analisar as classes que são necessárias para fazer com que o descobrimento de serviços funcione, precisamos primeiramente conhecer um pouco mais sobre o procotolo WS-Discovery em si, ou seja, analisar como as notificações são enviadas aos clientes e como esses clientes podem interrogar o serviço. Segundo as especificações deste protocolo, as mensagens utilizadas por ele para descobrimento do serviço são formatadas em padrão SOAP, enviadas através do protocolo UDP. Assim como vários outros padrões WS-*, o WS-Discovery utiliza várias mensagens para notificar e/ou descobrir um serviço na rede. Para entender o fluxo, vamos analisar a imagem abaixo, extraída da especificação do WS-Discovery:

Antes de compreender a imagem acima, é importante estar familiarizado com dois termos: multicasting e unicasting. Ambos são considerados routing schemes, e definem para quem enviar a mensagem. No formato multicast, a mensagem (seja ela qual for) será enviada de forma simultânea para um grupo de destinatários dentro da rede. Já no formato unicast, as informações são enviadas para um único destinatário da rede. Ainda existem outros formatos, como o broadcast e anycast, mas não serão utilizados aqui.

(1) Ao entrar na rede, o serviço envia uma mensagem conhecida como “hello”, no formato multicast, e os clientes que fazem parte do mesmo grupo, podem detectar que o serviço está online, evitando que o cliente fique repetidamente consultando para ver se o serviço está ou não online, reduzindo a quantidade de informações trafegadas na rede. Em analogia à programação assíncrona, seria mais ou menos como receber um “callback” ao invés de utilizar o pooling. (2) Os clientes também podem enviar uma mensagem conhecida como “probe” no formato multicast para a rede, em busca de um serviço de um determinado tipo. (3) Serviços que atendam aquele critério, retornam uma mensagem conhecida como “probe match”, no formato unicast, para o respectivo cliente. O cliente pode querer procurar um serviço através de seu nome. (4) Neste caso, o cliente deve enviar uma mensagem conhecida como “resolve message” através do formato multicast, e caso um serviço seja encontrado, (5) ele responderá através de uma mensagem conhecida como “resolve match”. (6) Finalmente, ao deixar a rede, o serviço envia uma mensagem conhecida como “bye” no formato multicast, para notificar que ele está saindo.

Depois de uma pequena introdução ao protocolo WS-Discovery, vamos analisar os tipos que temos a disposição a partir do WCF 4.0, que permitem ao serviço e ao cliente efetuar o descobrimento. Esses tipos estão debaixo do namespace System.ServiceModel.Discovery, que por sua vez está definido dentro do Assembly System.ServiceModel.Discovery.dll.

Em princípio, o contrato e a classe que representa o serviço não sofrerão qualquer alteração. As mudanças começam na configuração do serviço, onde você deverá criar um endpoint para possibilitar o descobrimento do serviço. Esse endpoint permitirá ao serviço monitorar as requisições (as mesmas mostradas na imagem acima) que este protocolo envia ou recebe para garantir o seu funcionamento. Para configurá-lo, você pode optar pela programação declarativa ou imperativa. No modo imperativo, basta você passar uma instância da classe UdpDiscoveryEndpoint para o método AddServiceEndpoint da classe ServiceHost, enquanto pelo modo declarativo, você pode recorrer ao atributo kind do elemento endpoint, especificando o valor “udpDiscoveryEndpoint”, que determina um standard endpointpreconfigurado.

Além da criação de um endpoint para descobrimento, ainda precisamos adicionar um behavior em nível de serviço, que é representado pela classe ServiceDiscoveryBehavior (ou pelo elemento serviceDiscovery em modo declarativo), que indica ao serviço que ele possa ser descoberto. Esse behavior ainda fornece uma coleção chamada de AnnouncementEndpoints, que permite aos consumidores do serviço a receberem notificações de quando o serviço estiver online ou offline. Basicamente, dentro desta seção temos também os standard endpoints, que encapsulam toda a comunicação para fazer o protocolo WS-Discovery funcionar, sem interferir na comunicação com o serviço em si. O trecho de código abaixo exemplifica a configuração do protocolo WS-Discovery em um serviço, utilizando o modelo declarativo:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="Service.Servico" behaviorConfiguration="bhv">
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:8383/Srv/"/>
          </baseAddresses>
        </host>
        <endpoint address="" binding="basicHttpBinding" contract="Service.IContrato" />
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
        <endpoint name="udpDiscovery" kind="udpDiscoveryEndpoint" />
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="bhv">
          <serviceMetadata />
          <serviceDiscovery>
            <announcementEndpoints>
              <endpoint name="udpAnnouncement" kind="udpAnnouncementEndpoint" />
            </announcementEndpoints>
          </serviceDiscovery>

        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

Consumindo o Serviço

Como falado acima, geralmente efetuamos a referência do serviço no cliente, onde a IDE ou o svcutil.exe faz todo o trabalho para a geração do proxy e do arquivo de configuração correspondente. Você pode continuar utilizando estas técnicas para construir os artefatos necessários para efetuar a comunicação, mas ao invés de definir o endereço de forma estática no arquivo de configuração, vamos fazer uso do protocolo WS-Discovery. O WS-Discovery é útil quando o serviço pode mudar frequentemente de endereço, e fixá-lo fará com que uma exceção seja disparada quando o mesmo não estiver mais respondendo naquele endereço, obrigando o administrador ou desenvolvedor alterar para o novo endereço de forma manual.

Como já falamos acima, utilizando o WS-Discovery não há mais a necessidade de conhecer o endereço, pois compete a este protocolo procurar pelos serviços que estão rodando naquela rede. Antes de invocar efetivamente a operação que o serviço disponibiliza, há alguns passos extras que devem ser realizados para que o cliente consiga determinar o endereço onde o serviço está rodando. Felizmente o WCF fornece uma classe chamada DiscoveryClient, que dado um endpoint de descobrimento e um critério de pesquisa, dispara as mensagens “probe” ou “resolve” para tentar encontrar o serviço em questão. O endpoint utilizado pelo cliente segue as mesmas características do endpoint utilizado pelo serviço, ou seja, fazendo uso do padrão UDP.

O critério a ser pesquisado deve ser criado a partir da classe FindCriteria, que em seu construtor recebe um parâmetro que especifica o contrato do serviço a ser pesquisado na rede. É importante dizer que neste momento é necessário que o cliente conheça, de alguma forma, o contrato que os possíveis serviços que estão rodando na rede implementem. Esse contrato pode ter sido fornecido out-of-band (por exemplo via e-mail), através do compartilhamento de assemblies que especificam os contratos e tipos que são utilizados por estes serviços, ou até mesmo via IDE ou através do utilitário svcutil.exe. A instância desta classe é passada como parâmetro para o método Find da classe DiscoveryClient, que por sua vez, retorna uma instância da classe FindResponse, contendo o resultado da pesquisa. O código abaixo ilustra essa primeira etapa do descobrimento:

using (DiscoveryClient dc = new DiscoveryClient(new UdpDiscoveryEndpoint()))
{
    FindResponse fr = dc.Find(new FindCriteria(typeof(IContrato)));
}

É importante dizer que o método Find é sobrecarregado, ou seja, há uma segunda versão dele que não recebe parâmetros, retornando todos os serviços ativos na rede, independentemente de quais contratos ele venha a implementar. Além disso, a classe DiscoveryClient ainda fornece um método chamado FindAsync, que como o próprio nome diz, possibilita a busca por serviços de forma assíncrona, pois caso este processo demore, você estará livre para continuar trabalhando em outras áreas do sistema, efetuando outras tarefas. Utilizando a forma assíncrona, você pode se vincular aos eventos FindProgressChanged e FindCompleted, para ser notificado a respeito do progresso da busca e quando ela for finalizada, respectivamente.

A classe FindResponse, que representa o resultado da pesquisa, possui apenas uma única propriedade chamada Endpoints. Essa propriedade retorna uma coleção, onde cada elemento é do tipo EndpointDiscoveryMetadata. Cada um destes elementos representam os serviços que estão ativos na rede e que implementam o contrato que você especificou no critério de busca, disponibilizando informações através de suas propriedades, de cada serviço descoberto, como por exemplo, o endereço (Address) e os contratos que o serviço implementa (ContractTypeNames).

A partir deste momento passamos a utilizar as classes já conhecidas, e que existem desde a primeira versão do WCF. Como falado acima, temos duas opções: a primeira delas consiste em criar o proxy através da IDE ou do utilitário svcutil.exe. O proxy criado possui um overload do contrutor que recebe uma instância da classe EndpointAddress, que representa o endereço de acesso ao serviço. Como antes o endereço estava fixado no arquivo de configuração, não era necessário informar explicitamente, já que com a configuração padrão, ele envia a requisição para aquele endereço preconfigurado. Já a segunda alternativa, geralmente utilizada quando estamos compartilhando o contrato através de assemblies, podemos recorrer a classe ChannelFactory<TChannel>, que dado uma instância da classe EndpointAddress e o binding em seu construtor, também estabelece a comunicação com o serviço em questão.

A propriedade Address, exposta pela classe EndpointDiscoveryMetadata, retorna uma instância da classe EndpointAddress contendo o endereço do serviço descoberto. Como já podemos assimilar, essa instância é passada para o proxy ou para a classe ChannelFactory<TChannel>. Para exemplificar, estarei utilizando a segunda opção, onde utilizarei a classe ChannelFactory<TChannel> para estabelecer a conexão e executar a operação no serviço descoberto pela classe DiscoveryClient:

using (DiscoveryClient dc = new DiscoveryClient(new UdpDiscoveryEndpoint()))
{
    FindResponse fr = dc.Find(new FindCriteria(typeof(IContrato)));

    if (fr.Endpoints.Count > 0)
    {
        EndpointAddress address = fr.Endpoints[0].Address;

        using (ChannelFactory<IContrato> factory =
            new ChannelFactory<IContrato>(new BasicHttpBinding(), address))
        {
            IContrato client = factory.CreateChannel();
            Console.WriteLine(client.Ping(DateTime.Now.ToString()));
        }
    }
    else
    {
        Console.WriteLine("Nenhum serviço encontrado.");
    }
}

Se estivermos utilizando o proxy que foi gerado automaticamente através do WSDL, então somente devemos substituir o ChannelFactory<TChannel> pela instância do proxy, informando o endereço do serviço que foi descoberto. É importante notar também que o arquivo App.Config não possui nenhuma configuração do serviço.

A principal vantagem do descobrimento é conseguir identificar um serviço que muda frequentemente de endereço. Como vimos acima, ao encontrar o serviço, criamos a instância da classe ChannelFactory<TChannel> com o respectivo endereço e informamos o binding BasicHttpBinding. Como estamos em um projeto de testes, onde tudo é facilmente controlado, conseguimos definir o mesmo tipo de binding, já que sabemos qual deles foi utilizado para expor o serviço. Mas em um mundo real, o serviço pode, por algum motivo, também alterar o binding que está sendo exposto pelo serviço, e por mais que você saiba o endereço até o mesmo, se o binding não corresponder ao mesmo utilizado pelo serviço, a comunicação não será possível.

O binding é uma informação que é exposta através do documento WSDL. Caso o serviço disponibilize este documento através de um endereço (isso não é obrigatório para o WS-Discovery funcionar), você pode utilizar o método estático Resolve da classe MetadataResolver (existente desde a primeira versão do WCF), para efetuar o download dos metadados de um determinado contrato, onde também teremos uma coleção com os endpoints disponíveis, e cada elemento desta coleção é representado pela classe ServiceEndpoint. Essa classe expõe uma propriedade chamada Binding, que como o próprio nome diz, retorna a instância de um binding que corresponde ao binding que está sendo utilizado pelo endpoint remoto.

É importante dizer que o método Resolve necessita do endereço até o endpoint que expõe o documento WSDL, e sendo assim, o critério de busca passa a ser pelo endereço do WSDL e não mais pelo endereço do serviço diretamente. O código cliente está totalmente dinâmico, ou seja, tanto o endereço como o binding são descobertos durante a execução. Aplicando essas mudanças, o código de exemplo que vimos acima passa a ficar da seguinte maneira:

using (DiscoveryClient dc = new DiscoveryClient(new UdpDiscoveryEndpoint()))
{
    FindResponse fr = dc.Find(FindCriteria.CreateMexEndpointCriteria(typeof(IContrato)));
    if (fr.Endpoints.Count > 0)
    {
        EndpointAddress mex = fr.Endpoints[0].Address;

        ServiceEndpointCollection sec =
            MetadataResolver.Resolve(typeof(IContrato), mex);

        if (sec.Count > 0)
        {
            ServiceEndpoint se = sec[0];

            using (ChannelFactory<IContrato> factory =
                new ChannelFactory<IContrato>(se.Binding, se.Address))
            {
                IContrato client = factory.CreateChannel();
                Console.WriteLine(client.Ping(DateTime.Now.ToString()));
            }
        }
    }
    else
    {
        Console.WriteLine("Serviço não encontrado.");
    }
}

Para finalizar, note que utilizamos uma instância da classe FindCriteria que foi retornada através do método estático CreateMexEndpointCriteria, pois o foco agora é procurar pelo documento WSDL, para conseguir extrair também o respectivo binding que está sendo exposto pelos endpoints.

Notificação através de Announcements

Announcements
é uma funcionalidade que é disponibilizada juntamente com o WS-Discovery que possibilita o cliente ser notificado quando o serviço estiver online ou offline. Isso permitirá você interceptar essas notificações e tomar alguma decisão para determinar se deve ou não invocar o serviço. Podemos enxergar os announcements a partir das mensagens “hello” e “bye” da imagem acima, enviando-as a partir do formato multicast, ou seja, somente os clientes interessados irão receber essas mensagens.

Para que isso funcione, primeiramente você precisa configurar o serviço para que ele notifique os possíveis clientes quando entrar no ar ou quando ele for deixar a rede (quando fechar o host). Para efetuar essa primeira configuração, você precisa adicionar um endpoint de announcement (também UDP) no elemento announcementEndpoints, do behavior serviceDiscovery. O código do arquivo de configuração que vimos acima, já está com esse endpoint devidamente configurado.

Já do lado do cliente, há algumas classes que precisamos utilizar para que o mesmo seja notificado. É importante dizer que não será o proxy que irá monitorar essas mensagens, mas sim um serviço chamado AnnouncementService. Essa classe irá “ouvir” as mensagens de “hello” e “bye”, enviadas pelo serviço, e notificará a aplicação cliente através dos eventos OnlineAnnouncementReceived e OfflineAnnouncementReceived. Como disse anteriormente, essa classe será um serviço hospedado pelo próprio cliente (self-hosted), ou seja, devemos configurar os eventos e passar a instância dela para um ServiceHost. A única configuração necessária no host é a adição de um standard endpoint, que é representado pela instância da classe AnnouncementEndpoint. O código a seguir mostra o quanto é simples fazer a aplicação cliente receber essas mensagens:

AnnouncementService announSrv = new AnnouncementService();
announSrv.OfflineAnnouncementReceived += (sender, e) => Console.WriteLine("Serviço Offline.");
announSrv.OnlineAnnouncementReceived += (sender, e) => Console.WriteLine("Serviço Online.");

using (ServiceHost announHost = new ServiceHost(announSrv))
{
    announHost.AddServiceEndpoint(new UdpAnnouncementEndpoint());
    announHost.Open();

    Console.ReadLine();
}

Extensions

A classe EndpointDiscoveryMetadata ainda fornece uma propriedade chamada Extensions, que nos permite passar informações extras do serviço para o cliente, que estarão acessíveis durante o processo de descobrimento. Para que você consiga adicionar essas informações, você deverá recorrer à um behavior de endpoint, chamado EndpointDiscoveryBehavior. Essa classe fornece uma propriedade chamada Extensions, que expõe uma coleção de elementos do tipo XElement, representando as informações customizadas que desejamos enviar ao cliente.

Primeiramente é necessário criar a instância da classe EndpointDiscoveryBehavior e configurar as extensões através da propriedade Extensions. Depois disso, basta adicioná-la na coleção de behaviors de um endpoint específico, acessível a partir do host (ServiceHost). A escolha de qual endpoint adicionar, dependerá de qual critério utilizará para busca, e no caso do exemplo acima, estamos procurando pelo endpoint que expõe o WSDL. O código abaixo ilustra a criação da classe EndpointDiscoveryBehavior e como adicioná-la ao endpoint:

using (ServiceHost host = new ServiceHost(typeof(Servico), new Uri[] { }))
{
    EndpointDiscoveryBehavior edb = new EndpointDiscoveryBehavior();
    edb.Extensions.Add(new XElement("InfoExtra", "ViaDiscovery"));
    host.Description.Endpoints[1].Behaviors.Add(edb);

    host.Open();
    Console.ReadLine();
}

Já do lado do cliente, tudo o que precisamos fazer (caso o endpoint seja encontrado), é também recorrer a propriedade Extensions, assim como é mostrado abaixo:

using (DiscoveryClient dc = new DiscoveryClient(new UdpDiscoveryEndpoint()))
{
    FindResponse fr = dc.Find(FindCriteria.CreateMexEndpointCriteria(typeof(IContrato)));

    if (fr.Endpoints.Count > 0)
        Console.WriteLine(fr.Endpoints[0].Extensions[0].Value);
    else
        Console.WriteLine("Serviço não encontrado.");
}

Conclusão: Vimos neste artigo como funciona a utilização do protocolo WS-Discovery, que estará disponível com a versão 4.0 do WCF. Isso traz grandes benefícios aos serviços que mudam constantemente de endereço, e principalmente, para os clientes que consomem, já que não precisam alterar nenhuma configuração caso isso aconteça. Esta nova funcionalidade é adicionada ao WCF seguindo as mesmas características anteriores, ou seja, com pequenas configurações habilitamos este recurso, sem a necessidade de entender como o protocolo funciona nos bastidores, nos permitindo continuar com o foco no desenvolvimento da regra de negócio.

WSDiscovery.zip (14.63 kb)

Tags:

CSD | WCF

WWS - Windows Web Services

by Israel Aece 11. January 2009 08:18

Sendo publicado como um componente a partir do Windows 7, o Windows Web Services - WWS, é uma API que permite ao código nativo, não gerenciado, criar e consuimir Web Services sem a necessidade do .NET Framework. O propósito de ser um componente é que a Microsoft pretende disponibilizá-lo para as outras versões do Windows, como XP, Vista, 2003 e 2008.

Com um modelo bem similar ao WCF, com bindings, channels, endpoints, etc., o WWS facilita a criação e hosting de serviços, bem como o consumo deles, fornecendo interoperabilidade com o WCF, ASMX e outras tecnologias não-Microsoft, que seguem os padrões WS-*.

Os tres grandes pilares do WWS são: Service Model, Channel LayerXML Layer. O primeiro deles gerencia a comunicação, ou seja, se estivermos consumindo o serviço, ele fornece tipos para facilitar o seu consumo (service proxy); já se estivermos expondo o serviço, então ele fornece tipos para efetuar o hosting (service host). A Channel Layer controla a comunicação, como os dados que são enviados e recebidos, abstração do protocolo, segurança, etc. E, finalmente, temos a XML Layer, fornece acesso completo ao conteúdo das mensagens.

Algumas funcionalidades ainda não foram implementadas, como é o caso do suporte a segurança em nível de mensagem e, pelo que parece, isso irá acontecer de acordo com a necessidade dos clientes. Assim como o WCF, o WWS também é orientado a contratos, permitindo trabalhar de forma bem semelhante no código não gerenciado. O WWS também fornece um utilitário chamado WsUtil.exe, que mapeia o WSDL/XSD para tipos C.

Para aqueles que possuem o Windows 7, verá que no diretório %windir%\System32\ existe uma DLL chamada Webservices.dll que gerencia grande parte desta comunicação. Além disso, o Windows SDK disponibilizado no PDC 2008, traz esta API (WWSAPI), contendo o Wsutil.exe, Webservices.h e webservice.lib.

É importante dizer que, segundo Nikola Dudar, o WWS não substitui o WCF. A finalidade do WWS é possibilitar aplicações nativas consumirem ou disponibilizarem um serviço, enquanto o WCF é a alternativa para o código gerenciado, e depende do .NET Framework.

Tags:

CSD | Interoperabilidade

WCF - Transações

by Israel Aece 1. December 2008 03:30

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

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

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

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

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

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

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

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

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
    }
}

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
    }
}

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.

Transacoes.zip (137.93 kb)

Tags: ,

CSD | WCF

WCF - Reliable Messages

by Israel Aece 12. November 2008 10:00

Ao consumir um serviço WCF podemos interagir com o mesmo através de diferentes mecanismos, tais como, request-reply ou one-way (os tipos de mensagens suportados pelo WCF já foram discutidos detalhadamente neste artigo). Sabemos que, independente do tipo que você utilize, a mensagem trafega entre o cliente e o serviço através da rede, utilizando o protocolo especificado pelo binding. Com isso, uma das principais preocupações que se tem é com relação a garantia de entrega da mensagem ao seu destinatário, pois problemas com a rede podem acontecer, fazendo com que a mensagem seja interceptada ou simplesmente perdida. A finalidade deste artigo é apresentar uma técnica disponibilizada pelo WCF, para evitar que problemas como estes comprometam a consistência e execução de um serviço.

Quando desejamos enviar uma mensagem para um determinado serviço, queremos que ela chegue até o mesmo e, caso isso não aconteça, talvez precisamos reenviá-la. Mas como saber se ela chegou ou não, para tomar a decisão se devemos ou não reenviá-la? Geralmente, alguns protocolos fornecem a garantia de entrega da mensagem, como é o caso do TCP e IPC. Mesmo quando o protocolo garante nativamente a entrega da mensagem, ele somente irá assegurar a entrega ponto-a-ponto, ou seja, se houver intermediários, corremos o risco da mensagem ser perdida.

Felizmente o WCF implementa um padrão proposto pela OASIS (Organization for the Advancement of Structured Information Standards), chamado WS-ReliableMessaging (WS-RM). Essa especificação define um protoloco interoperável para transmissão de mensagens entre um único remetente para um único destinatário, garantindo que a mensagem será entregue (sem duplicação), independentemente de quantos roteadores intermediam a ligação entre eles (end-to-end). Além da garantia de entrega, este protocolo ainda fornece uma outra importante funcionalidade: você pode, opcionalmente, garantir que a entrega das mensagens aconteça na mesma ordem em que elas partiram do cliente (falaremos mais sobre ela ainda neste artigo).

Funcionamento

O funcionamento das reliable messages é um pouco complexo, mas o protocolo WS-RM em conjunto com o WCF o abstrai, facilitando o processo para aqueles que desenvolvem e administram o serviço. De qualquer forma, veremos a seguir como o processo ocorre nos bastidores do WCF (tanto no cliente quanto no serviço) para suportar essa funcionalidade.

Quando invocamos um método a partir do proxy, uma mensagem é enviada até o serviço com as informações (parâmetros, credenciais, etc.) para que o mesmo seja processado. Se temos a reliable message habilitada, não enviaremos apenas uma mensagem para o serviço (a operação que queremos invocar), mas várias outras que consistem, basicamente, na verificação para saber se a mensagem chegou ou não até o seu destino. Depois que o cliente envia a mensagem da operação, ele fica questionando o serviço para saber se a mensagem chegou até lá. O cliente espera essa notificação por um tempo (configurável) e, se o tempo exceder e a notificação não chegar, o WCF entende que ela não foi entregue, podendo agora, reenviá-la. Caso a notificação venha dentro do tempo esperado, o cliente sabe que a mensagem foi transferida com sucesso.

É importante dizer que o protocolo WS-RM foi desenhado para controlar a garantia de entrega de uma ou uma seqüência de mensagens SOAP entre dois endpoints, independentemente de como eles estão conectados, ou melhor, de quantos intermediários existam entre eles e de quais protocolos estão envolvidos. Utilizaremos a imagem abaixo para ilustrar como o processo acontece, exibindo os responsáveis que fazem isso acontecer.

Figura 1 - Funcionamento das reliable messages.


Para que isso funcione, será necessário estabelecer uma conexão com o serviço e, neste primeiro momento, um identificador será criado (CreateSequence) para correlacionar as mensagens. Quando este identificador for criado, o serviço o retorna para o cliente (CreateSequenceResponse) que irá embutí-lo em mensagens subseqüentes. Podemos perceber que ao invocar uma operação, o remetente cria um cache temporário para efetuar o rastreamento das mensagens e, a partir deste momento, o proxy é responsável por gerenciar o envio da operação e aguardar pela notificação do recebimento. Da mesma forma, o servidor também cria um cache para receber as mensagens e entregá-las para o serviço. O cache do lado do serviço terá maior utilidade quando trabalharmos com mensagens ordenadas (mais abaixo).

Ao receber a mensagem, o serviço retorna para o cliente uma mensagem contendo o elemento SequenceAcknowledgement nos headers da resposta, indicando que o serviço a recebeu. Esse elemento traz várias informações e, entre elas, temos: Identifier, AcknowledgementRange e BufferRemaining. A primeira propriedade refere-se ao identificador; já a segunda traz dois números inteiros que representam a primeira e a última mensagem processada (permitindo ao cliente remover mensagens que já estão do outro lado); finalmente, a propriedade BufferRemaining indica a quantidade disponível dentro do buffer de mensagens. A LastMessage, como o próprio nome indica, indica ao serviço que uma última mensagem será enviada e, finalmente, a mensagem TerminateSequence que encerra o processo.

Observação Importante: Quando definimos o modo de gerenciamento de instância do serviço como PerSession e habilitamos a funcionalidade de garantia de entrega, essa combinação é referida como Reliable Sessions. O protocolo WS-RM não necessita que o serviço/binding suporte sessões para funcionar, bem como o suporte à sessão não necessita do protocolo WS-RM habilitado. Ainda através da figura que vimos acima, visualizamos duas operações sendo realizadas (o que pode caracterizar uma sessão), mas é importante dizer que todos os demais passos irão ocorrer, independente da sessão estar ou não habilitada.

Configuração

A configuração das reliable messages é realizada sob o binding onde o serviço será exposto. É importante dizer que nem todas as configurações suportadas estão diretamente disponíveis através do binding, ou seja, será necessária a criação de um binding customizado para editar as configurações padrão. Os bindings expõem uma propriedade chamada ReliableSession, do tipo OptionalReliableSession que, em seu construtor, recebe uma instância da classe ReliableSessionBindingElement, contida no namespace System.ServiceModel.Channels, onde podemos efetivamente customizar o comportamento deste tipo de mensagem. A tabela abaixo exibe as propriedades expostas pela classe ReliableSessionBindingElement e que estão disponíveis para uso:

Propriedade Descrição
AcknowledgementInterval Recebe um Timespan que representa um intervalo de tempo em que o serviço aguarda para enviar a notificação de recebimento (acknowledgment). A valor padrão é de 2 segundos. Antes do serviço enviar instantaneamente a notificação de recebimento, ele aguarda este intervalo com a finalidade de agrupar o máximo de mensagens, melhorando a escalabilidade e reduzindo o tráfego de informações.
FlowControlEnabled Trata-se de um mecanismo que assegura que o remetente não envia mais mensagens quando o buffer de mensagens do serviço chega ao seu limite. Essa quantidade é informada ao remetente através da mensagem SequenceAcknowledgement, através da propriedade BufferRemaining. Essa propriedade é do tipo booleana e, quando definida como True (valor padrão), irá parar de enviar mensagens enquanto o buffer do serviço estiver cheio.
InactivityTimeout Uma propriedade que recebe um Timespan representando a duração da sessão. Se nenhuma mensagem for transmitida (incluindo mensagens de infraestrutura, como acknowledgements) durante este período, a sessão será descartada. O valor padrão é 10 minutos.
MaxPendingChannels Quando temos reliable sessions habilitadas no serviço, diferentes clientes podem estabelecer a comunicação ao mesmo tempo. Ao estabelecer a conexão, há um handshake inicial (sequences, etc.) e, após isso, o channel é colocado em uma fila com status de pendente. Esta propriedade indica quantos channels podem ser colocados neste estado e, quando omitido, o padrão é 4. Se essa fila estiver cheia qualquer tentativa de nova conexão será rejeitada.
MaxRetryCount Número inteiro que especifica a quantidade máxima de tentativas de reenvio. Enquanto o remetente não recebe a notificação de que a mensagem foi recebida pelo destinatário, o WCF reenvia a mensagem até que este limite seja atendido. Se, mesmo depois das tentativas ele não receber a notificação de recebimento, uma exceção será disparada. O valor padrão desta propriedade é 8 tentativas.
MaxTransferWindowSize Outra propriedade do tipo inteiro que define a quantidade de mensagens que o buffer pode acomodar. Do lado do cliente, esse buffer aguarda as notificações do serviço; já do lado do serviço, esse buffer acumula as mensagens para garantir que elas sejam processadas na mesma ordem que elas foram enviadas. O valor padrão para esta propriedade é 8.
Ordered Propriedade booleana que indica se as mensagens serão ou não ordenadas. O padrão é True. Maiores detalhes sobre essa técnica, serão abordados mais tarde, ainda neste artigo.

Assim como quase tudo no WCF, a configuração das reliable messages pode ser realizada tanto de forma imperativa quanto declarativa. Como já foi dito acima, a configuração é uma característica do binding, e é através dele que iremos conseguir alterar qualquer uma das propriedades que vimos na tabela acima.

Os bindings NetTcpBinding, WSHttpBinding, WSFederationHttpBinding e WSDualHttpBinding suportam as reliable messages e permitem que você habilite ou desabilite, defina um tempo de timeout por inatividade e especifique se as mensagens serão ou não ordenadas. O código abaixo ilustra as duas formas de como como podemos proceder para configurar as reliable messages no binding:

WSHttpBinding ws = new WSHttpBinding();
ws.ReliableSession.Enabled = true;
ws.ReliableSession.Ordered = true;
ws.ReliableSession.InactivityTimeout = TimeSpan.FromMinutes(5);

 

<wsHttpBinding>
    <binding name="BindingConfig">
        <reliableSession
                enabled="true"
                ordered="true"
                inactivityTimeout="00:05:00" />
    </binding>
</wsHttpBinding>


Uma outra alternativa no modo imperativo é que os bindings possuem uma versão do construtor onde já podemos definir a propriedade Enabled da ReliableSession. As outras propriedades que vimos na tabela acima não estão expostas diretamente através do binding. Apesar de muitas vezes as configurações padrões serem suficientes, em algum momento talvez seja necessário alterá-las e, para que isso seja possível, somente poderemos efetuar essa modificação através de um binding customizado, como por exemplo:

using System;
using System.ServiceModel;
using System.ServiceModel.Channels;

ReliableSessionBindingElement rsbe = new ReliableSessionBindingElement();
rsbe.AcknowledgementInterval = TimeSpan.FromSeconds(5);
rsbe.FlowControlEnabled = true;
rsbe.InactivityTimeout = TimeSpan.FromMinutes(5);
rsbe.MaxPendingChannels = 10;
rsbe.MaxRetryCount = 10;
rsbe.MaxTransferWindowSize = 10;
rsbe.Ordered = true;

CustomBinding cb =
    new CustomBinding(
        new HttpTransportBindingElement(),
        new TextMessageEncodingBindingElement(),
        rsbe);

 

<customBinding>
  <binding name="BindingConfig">
    <reliableSession
      acknowledgementInterval="00:00:05"
      flowControlEnabled="true"
      inactivityTimeout="00:05:00"
      maxPendingChannels="10"
      maxRetryCount="10"
      maxTransferWindowSize="10"
      ordered="true"/>
  </binding>
</customBinding>


Habilitar a reliable message sob um determinado binding afetará o documento que descreve o serviço (WSDL), publicando nele o suporte a esse tipo de mensagem, para que os clientes consigam configurar corretamente o proxy. Algumas dessas configurações são propagadas para o cliente, como é o caso das propriedades InactivityTimeout e AcknowledgementInterval, enquanto as outras tratam-se de configurações exclusivas ao binding.

Ao invocar o método exposto pelo proxy, aparentemente apenas uma mensagem será enviada para o respectivo serviço mas, como notamos na imagem acima, várias outras mensagens são enviadas e recebidas entre o cliente e o serviço para garantir a entrega da mensagem que o cliente quer efetivamente enviar. Essas mensagens "extras" são criadas pelo próprio runtime do WCF quando habilitamos essa funcionalidade. Caso você queira visualizar para se certificar de que isso está realmente ocorrendo, basta você habilitar o tracing no cliente e conseguirá armazenar as mensagens que estão sendo trocadas. A imagem abaixo, ilustra isso (apesar de importante, o corpo da mensagem não será mostrado aqui por questões de espaço):

Figura 2 - Resultado capturado pelo tracing.


Mensagens Ordenadas

O que vimos até o momento é garantia de entrega, fornecida pelo protocolo WS-RM, e é importante ratificar que, quando habilitamos essa funcionalidade em um serviço que é exposto como PerSession, ela é referida como reliable session. Neste momento entra em cena uma configuração adicional que permite a entrega das mensagens na mesma ordem em que elas saíram. Nem sempre podemos assumir que a primeira mensagem chegará ao seu destino antes da segunda, pois elas podem optar por caminhos diferentes.

Quando esta opção está habilitada, as mensagens são enviadas para o serviço e são armazenadas no cache (ou fila) do mesmo. Se a mensagem chega na ordem correta, ela é imediatamente encaminhada para o serviço. Caso contrário, ela aguardará na fila esperando as demais mensagens para compor a seqüencia e, finalmente, ser encaminhada para o serviço. Imagine que o cliente envie as mensagens 1, 2, 3 e 4 e o serviço recebe as mensagens 1, 2 e 4, ou seja, está faltando a mensagem 3. Neste caso, a mensagem 1 e 2 serão encaminhadas para o serviço, enquanto a mensagem 4 irá aguardar a chegada da mensagem 3 para, depois disso, ser submetida para a execução.

Para habilitar este recurso (que já é o padrão) e configurá-lo, podemos utilizar o atributo DeliveryRequirementsAttribute sob a interface que representa o contrato ou sob a classe que o implementa. Onde definir dependerá do caso pois, se aplicá-lo no contrato, em qualquer classe que o implementar, ele seguirá essas configurações; se aplicar na classe que representa o serviço, então você terá uma flexibilidade para determinarem qual dos contratos você deseja aplicar essa técnica. Esse atributo fornece a propriedade RequireOrderedDelivery que é um valor booleano que indica se está ou não habilitado, e a propriedade TargetContract, que espera um objeto do tipo Type, que determina em qual contrato essa técnica será aplicada. A segunda propriedade somente faz sentido quando o atributo é aplicado na classe do serviço. O trecho de código abaixo exibe como proceder para efetuar essa configuração:

using System;
using System.ServiceModel;

[ServiceContract]
[DeliveryRequirements(RequireOrderedDelivery = true)]
public interface IContrato
{
    [OperationContract(IsOneWay = true)]
    void EnviarInformacao(string valor);
}


Conclusão: Este artigo demonstrou importantes funcionalidades que garantem a entrega e o processamento ordenado das mensagens e, como vimos, tudo isso é possível graças ao protocolo WS-ReliableMessaging. Com a implementação do protocolo WS-RM no WCF, a Microsoft conseguiu abstrair todo o trabalho complexo, permitindo aos desenvolvedores, com apenas algumas configurações simples, fazer com que ela entre em funcionamento, sem a necessidade de conhecer profundamente os detalhes que são necessários para que isso aconteça.

Tags:

CSD | WCF

Projeto Tango

by Israel Aece 19. March 2007 20:15

É bom saber que as coisas caminham para a interoperabilidade... e ainda dizem que a Microsoft não se preocupa com isso :)

Tags:

Interoperabilidade

Powered by BlogEngine.NET 1.5.0.0
Theme by Mads Kristensen

Sobre

Meu nome é Israel Aece e sou especialista em tecnologias de desenvolvimento Microsoft, atuando como desenvolvedor de aplicações para o mercado financeiro utilizando a plataforma .NET. [ Mais ]

Twitter

Host