WCF – MessageQueue

Ao efetuar uma chamada para uma operação de um determinado serviço, desejamos que ela seja sempre executada. Mas nem sempre há como garantir isso, já que o serviço que atende as requisições, por algum motivo, está indisponível naquele momento. Isso fará com que as requisições sejam rejeitadas e o cliente somente conseguirá executá-la quando o serviço estiver novamente no ar. Para garantir a entrega da mensagem e o processamento assíncrono da operação (mesmo quando o serviço estiver offline), o WCF faz uso do Microsoft Message Queue. Este artigo irá explorar as funcionalidades e, principalmente, os benefícios fornecidos por essa integração.

O Microsoft Message Queue é uma tecnologia que está ligada diretamente ao sistema operacional que, entre suas diversas funcionalidades, temos a durabilidade, suporte à transações, garantia de entrega, etc. O Message Queue é um componente adicional, e que pode ser instalado a partir dos recursos do Windows. O Windows XP e 2003 trazem a versão 3.0 do Message Queue, enquanto o Windows Vista e 2008 disponibilizam a versão 4.0.

A disponibilidade é uma das principais características de uma aplicação que a faz utilizar o Message Queue. O fato do serviço não estar online nem sempre é um problema; é perfeitamente possível que algum cliente seja um dispositivo móvel, fazendo com que o serviço esteja inalcançável e, com isso, em um ambiente tradicional, qualquer chamada para alguma operação iria falhar. Com a durabilidade, o Message Queue garante que a mensagem seja persistida fisicamente e, quando a conexão for restabelecida, a mesma será enviada para o serviço.

Podemos interagir com o Message Queue de duas formas: a primeira é utilizando a console de gerenciamento que é criada quando você instala o Message Queue; já a segunda é através do .NET, que fornece um Assembly chamado System.Messaging.dll com vários tipos para criar, enviar e remover mensagem de uma fila. O namespace System.Messaging existe desde a versão 1.0 do .NET Framework, mas o WCF encapsula o uso dele e, felizmente, não precisaremos recorrer a qualquer classe deste namespace para fazer com que o Message Queue funcione em conjunto com o WCF.

Filas Públicas e Privadas

O Message Queue possibilita a criação de dois tipos de filas, a saber: públicas e privadas. As filas públicas obrigatoriamente devem estar registradas em um domínio, através do Active Directory, podendo ser acessadas por todas as máquinas que estão sob aquele domínio. Já as filas privadas tem um escopo bem mais restrito, ou seja, podem ser acessadas apenas dentro da máquina onde elas foram criadas.

Durante a criação de uma fila, via console de gerenciamento ou através da API System.Messaging, podemos definir se a mesma será ou não transacionada. Marcando a fila como transacionada, tanto a inserção de uma nova mensagem com a remoção de uma mensagem existente será protegida por uma transação. Quando falamos especificamente sobre transações no WCF com o Message Queue, há alguns detalhes que temos que nos atentar e que veremos mais tarde, ainda neste mesmo artigo.

Chamadas Enfileiradas e Processamento Assíncrono

Ao invocar uma operação onde temos o Message Queue envolvido, ele trará vários benefícios. Em um formato tradicional, utilizando outros protocolos mais convencionais, como o HTTP, TCP ou IPC, demanda que o serviço esteja disponível para que a mensagem chegue até ele e seja processada e, caso contrário, uma exceção será disparada no cliente.

Com a integração do Message Queue, o WCF persistirá a mensagem localmente em uma fila caso o serviço não esteja disponível. Ao persistir a mensagem, a garantia de entrega será assegurada pelo Message Queue de forma transparente para a aplicação cliente e, quando o serviço ficar novamente ativo, a mensagem será encaminhada para o serviço para efetuar o processamento da(s) operação(ões) (algo que já era suportado no COM+). É importante dizer que não há uma relação entre chamada à uma operação e uma mensagem na fila do Message Queue; poderá haver mensagens que acomodarão mais que uma operação (falaremos detalhadamente sobre isso mais tarde, ainda neste artigo) A imagem abaixo ilustra superficialmente como as mensagens são enviadas/recebidas quando o serviço é exposto via Message Queue:

Figura 1 – Serviço exposto via Message Queue.

Como a fila em que as operações serão persistidas estará sempre disponível, o WCF sempre armazenará localmente e, com isso, a aplicação poderá continuar trabalhando sem esperar que a mensagem seja entregue, garantindo assim o que chamamos de processamento assíncrono. Pelo fato das mensagens estarem persistidas, elas conseguirão sobreviver a possíveis reinicializações do cliente e, quando o mesmo retornar, novas tentativas serão realizadas até que a mensagem seja efetivamente entregue ao destino. Obviamente que se o cliente e o serviço estiverem online, a mensagem será entregue imediatamente.

MSMQ e o WCF

Quando formos desenhar um contrato para ser exposto através do Message Queue, um cuidado que devemos ter é com relação ao tipo da operação. No tópico anterior falamos sobre as necessidades da utilização do Message Queue e, analisando essas características, vemos que as operações que serão expostas através do Message Queue não devem retornar nenhum resultado, e também possíveis exceções nunca chegarão até o cliente, já o WCF desabilita os contratos de faults em operações enfileiradas. A finalidade do contrato é apenas definir a semântica da aplicação e, durante a execução, a chamada poderá ou não ser persistida e mais tarde processada.

Com isso, o WCF nos obriga a definir todas as operações de um contrato que serão expostas através do Message Queue como sendo one-way (mais detalhes neste artigo). Caso você exponha uma das operações sem antes definí-la como one-way, uma exceção do tipo InvalidOperationException será disparada antes da abertura do host. Com exceção deste detalhe, não há nada diferente a ser realizado em relação à implementação ou chamadas às operações enfileiradas. Note que o código abaixo exibe a criação deste contrato:

using System;
using System.ServiceModel;

[ServiceContract]
[DeliveryRequirements(QueuedDeliveryRequirements = QueuedDeliveryRequirementsMode.Required)]
public interface IContrato
{
    [OperationContract(IsOneWay = true)]
    void EnviarDados(string msg);
}

O WCF também permite a você especificar no contrato que o mesmo deverá ser exposto sob um binding que suporte chamadas enfileiradas. Para isso, basta recorrermos ao atributo DeliveryRequirementsAttribute, definindo a propriedade QueuedDeliveryRequirements com uma das três opções definidas no enumerador QueuedDeliveryRequirementsMode:

  • Allowed: O binding pode ou não suportar chamadas enfileiradas.

  • Required: O binding deve suportar chamadas enfileiradas.

  • NotAllowed: O binding não deve suportar chamadas enfileiradas.

Esse atributo ainda fornece duas outras propriedades: RequireOrderedDelivery e TargetContract. A primeira propriedade determina se o binding deverá ou não garantir a entrega ordenada das mensagens. Já a segunda propriedade espera um objeto do tipo Type, que determina em qual contrato essa técnica será aplicada. Essa propriedade somente faz sentido quando o atributo é aplicado na classe que representa o serviço, ao invés do contrato.

Hosting e Binding

Como já sabemos, o binding contém os aspectos de comunicação, especificando o meio de transporte, codificação, etc. Para expor um serviço via Message Queue, devemos recorrer a um binding exclusivo para isso, o NetMsmqBinding. Apesar de não expor todas as propriedades suportadas pelo Message Queue, este binding traz as principais funcionalidades necessárias para a utilização do mesmo através do WCF.

Para especificar o endereço onde o serviço será exposto, devemos utilizar a seguinte convenção (note que não há o caracter $): net.msmq://NomeDaMaquina/Private/NomeDaFila. Como o WCF não pode publicar o documento WSDL através do Message Queue, é necessária a criação de um endpoint exclusivo para a publicação do mesmo, através de algum outro protocolo, como o HTTP ou TCP. Caso isso não seja feito, os clientes não conseguirão referenciar o serviço e criar o proxy. O trecho de código abaixo ilustra como devemos proceder para configurar o host:

using System;
using System.ServiceModel;
using System.ServiceModel.Description;

using (ServiceHost host =
    new ServiceHost(typeof(Servico), new Uri[] { 
        new Uri("net.msmq://localhost/private/FilaDeTestes"), 
        new Uri("http://localhost:8383/") }))
{
    host.Description.Behaviors.Add(new ServiceMetadataBehavior());

    host.AddServiceEndpoint(
        typeof(IContrato), 
        new NetMsmqBinding(NetMsmqSecurityMode.None), 
        string.Empty);

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

    host.Open();
    Console.ReadLine();
}
C# VB.NET  

A configuração do host não tem muitas diferenças em relação a um serviço exposto via qualquer outro tipo de binding. Estamos utilizando o NetMsmqBinding com sua configuração padrão, ou seja, nenhuma das propriedades expostas por ele foi customizada. Como falamos acima, este binding traz várias propriedades que podem ser configuradas (de forma imperativa ou declarativa) para customizar o envio/processamento das mensagens. A tabela abaixo lista essas propriedades e suas respectivas descrições:

Propriedade Descrição
CustomDeadLetterQueue Recebe uma instância da classe Uri (com o formato “net.msmq”) representando uma dead-letter queue customizada a ser utilizada pela aplicação. Essa propriedade trabalhará em conjunto com a propriedade DeadLetterQueue.
DeadLetterQueue Esta propriedade irá especificar o tipo da dead-letter queue. O tipo poderá ser definido com uma das três opções fornecidas pelo enumerador DeadLetterQueue:

  • Nome: A dead-letter queue não é requerida. Se a entrega da mensagem falhar, ela não será gravada em lugar algum. Essa opção é utilizada quando a propriedade ExactlyOnce é definida como False.

  • System: Determina que a dead-letter queue do sistema será utilizada para armazenar as mensagens que falharem. Há dead-letter queue transacional e não transacional. Quando a propriedade ExactlyOnce estiver definida como True, essa opção será utilizada.

  • Custom: Nesta opção você pode especificar uma fila própria para ser a sua dead-letter queue. Esta opção trabalhará em conjunto com a propriedade CustomDeadLetterQueue.

Durable Propriedade do tipo booleana que quando definida como True (padrão), o binding irá garantir a durabilidade da mensagem persistindo-a no disco. Quando definida como False, a mensagem será armazenada de forma “volátil”, ou seja, ela não conseguirá sobreviver a possíveis reinicializações do serviço do Message Queue.
ExactlyOnce Outra propriedade do tipo booleana e, quando definida como True (padrão), garantirá que uma vez que a mensagem for entregue ao serviço, ela não será duplicada. Caso a mensagem não seja entregue por algum motivo, a mesma será movida para a dead-letter queue. Devido a finalidade desta propriedade, obrigatoriamente a fila deverá ser transacional.
MaxRetryCycles A tentativa de processamento/entrega da mensagem (da fila para o serviço) consiste em ciclos, e cada ciclo contém um número de tentativas. Esta propriedade define um número inteiro que especifica a quantidade de ciclos de tentativas de entrega/processamento a serem realizadas. Como valor padrão, esta propriedade possui 2 ciclos.
QueueTransferProtocol Esta propriedade determinará qual será o meio de comunicação utilizado pelos gerenciadores do Message Queue de ambas as partes. Essa propriedade poderá ser configurada com uma das opções definidas pelo enumerador QueueTransferProtocol:

  • Native: Utiliza o protocolo nativo do Message Queue (padrão).
  • Srmp: Utiliza o protocolo Soap Reliable Messaging Protocol.
  • SrmpSecure: Utiliza o protocolo Soap Reliable Messaging Protocol Secure.

Os protocolos SRMP e SRMPS são utilizados quando desejamos expor a fila através do protocolo HTTP, mas isso está fora do escopo deste artigo. Caso a propriedade UseActiveDirectory estiver definida como True e algum protocolo diferente do Native for utilizado, uma exceção será disparada.

ReceiveErrorHandling Esta propriedade determinará o comportamento que o serviço WCF deverá ter quando todas as tentativas de entrega/processamento se esgotarem. Essa propriedade aceita uma das quatro opções fornecidas pelo enumerador ReceiveErrorHandling:

  • Fault: Quando um erro for encontrado ao processar a mensagem e a opção Fault estiver definida, o host será comprometido e a mensagem deverá ser removida da fila através do administrador ou outra aplicação antes de continuar a execução e, além disso, como esta opção compromete a vida do host, ele deverá ser reinicializado. Nenhuma notificação (Acknowledgement) do problema será enviada ao cliente.

  • Drop: Como o próprio nome indica, essa mensagem será efetivamente excluída da fila, continuando o processamento das outras mensagens. O Drop notifica (Acknowledgement) o cliente mas, do seu ponto de vista, a mensagem foi processada com sucesso. A mensagem será colocada na dead-letter queue do cliente caso a mensagem não tenha expirado (TimeToLive); caso contrário, ela não aparecerá em lugar algum.

  • Reject: Envia uma notificação (Acknowledgement) para o cliente informando que a mensagem não pode ser recebida pela aplicação (serviço). A mensagem é colocada na dead-letter queue do cliente. O cliente receberá uma notificação (Acknowledgement) negativa informando que a mensagem não pode ser processada. A mensagem será colocada na dead-letter queue do cliente.

  • Move: Move a mensagem para a Poison Message Queue, permitindo que a mensagem seja analisada posteriormente. Falaremos mais sobre esse tipo especial de fila ainda neste artigo. Nenhuma notificação (Acknowledgement) será enviada ao cliente, pois a idéia é enviá-la quando a mensagem poison for processada.

ReceiveRetryCount Cada ciclo (especificado através da propriedade MaxRetryCycles) possui uma quantidade de tentativas. Através desta propriedade, conseguimos determinar a quantidade de tentativas de cada ciclo. Como valor padrão, esta propriedade possui 5 tentativas.
RetryCycleDelay Esta propriedade define um intervalo de tempo (TimeSpan) entre os ciclos de tentativas. Como valor padrão, esta propriedade está definida como 10 minutos.
TimeToLive Essa propriedade define um TimeSpan indicando o período em que a mensagem irá expirar. Mensagens que estão dentro da fila e que não são acessadas por uma aplicação dentro do intervalo especificado serão expiradas, movendo-as para a dead-letter queue. Quando omitido, o valor padrão desta propriedade é 1 dia.
UseActiveDirectory O endereço do Message Queue consiste em dois formatos: path names ou direct format names. A primeira opção faz com que o Message Queue utilize o Active Directory para resolver o nome da fila (exemplo: NomeDoComputadorprivate$NomeDaFila), enquanto a segunda opção, tentará resolver o nome da fila utilizando o DNS ou o IP (exemplo: FormatName:Direct=TCP:192.168.1.3NomeDaFila).

Por padrão, o WCF converte a URI do serviço no formato direct format name e, através da propriedade UseActiveDirectory você poderá definir um valor booleano (que por padrão é False), fazendo com que ele utilize o formato path name. Algumas funcionalidades disponibilizadas pelo binding NetMsmqBinding, como é o caso da criptografia de mensagens utilizando a segurança a nível de transporte, somente funcionarão se utilizar o Active Directory.

UseMsmqTracing Valor booleano que indica se o processamento das mensagens será ou não logado. O valor padrão é False.
UseSourceJournal Journaling é um recurso do Message Queue que permite salvar uma cópia das mensagens entregues com sucesso ao destino. Por padrão essa funcionalidade está desabilitada, mas você pode definir a propriedade UseSourceJournal como True para colocar em funcionamento este recurso.

Dentre as propriedades acima, algumas se referem a dois tipos especiais de filas: dead-letter queue e poison message queue. Esses tipos especiais, criados pelo sistema, são extremamente importantes para garantir o funcionamento de algumas configurações expostas pelo Message Queue. Em outras palavras, as dead-letter queues lidam com problemas relativos à comunicação e as poison message queues se limitam a tratar problemas que ocorrem dentro da execução da operação.

Há vários problemas que podem acontecer durante a tentativa de entrega da mensagem; entre esses problemas temos falhas na infraestrutura, a fila foi excluída, falha na autenticação, etc. Esses tipos de problemas fazem com que a mensagem seja enviada para uma fila especial, chamada de dead-letter queue, ficando a mensagem ali até o momento em que uma outra aplicação ou o administrador do sistema tome alguma decisão (atente-se a expiração que a mensagem poderá ter). O Windows já disponibiliza dois tipos de dead-letter queue: Dead-letter messages para mensagens não transacionadas e Transactional dead-letter messages para mensagens transacionadas.

Como as filas que mencionamos acima são fornecidas pelo próprio sistema operacional, elas são compartilhadas entre todas as aplicações que rodam naquela máquina. Apesar da fila aceitar as mensagens independente de onde elas vieram, ficará difícil a manutenção nelas. Muitas aplicações já fornecem o suporte para processamento das mensagens nesta fila, mas como distinguir qual mensagem pertence àquela aplicação? Visando sanar este problema é que recorremos à criação de uma fila customizada (através das propriedades DeadLetterQueue e CustomDeadLetterQueue) para catalogar as mensagens problemáticas, fornecendo um isolamento entre as aplicações.

Quando optamos por criar uma fila customizada para servir como dead-letter queue, esta é como uma fila normal, não havendo nada de especial, mas atentando-se à definí-la ou não como transacional, dependendo da sua necessidade. A configuração do binding muda ligeiramente, definindo agora a fila customizada como dead-letter queue, assim como é mostrado no código:

using System;
using System.ServiceModel;
using System.ServiceModel.Description;

using (ServiceHost host =
    new ServiceHost(typeof(Servico), new Uri[] { 
        new Uri("net.msmq://localhost/private/FilaDeTestes"), 
        new Uri("http://localhost:8383/") }))
{
    host.Description.Behaviors.Add(new ServiceMetadataBehavior());

    NetMsmqBinding binding = new NetMsmqBinding(NetMsmqSecurityMode.None);
    binding.DeadLetterQueue = DeadLetterQueue.Custom;
    binding.CustomDeadLetterQueue = 
        new Uri("net.msmq://localhost/private/MensagensProblematicas");

    host.AddServiceEndpoint(
        typeof(IContrato),
        binding,
        string.Empty);

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

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

Observações: Lembre-se de que a fila criada para servir como dead-letter queue é uma fila normal e, para processar as mensagens que estão dentro dela, basta criar um novo serviço que extraia as mensagens e efetue o devido processamento. Se por algum motivo você queira acessar as filas de dead-letter do sistema, você poderá acessá-la partir dos seguintes endereços: net.msmq://localhost/system$;DeadLetter (Dead-letter messages) e net.msmq://localhost/system$;DeadXact (Transactional dead-letter messages).

Ainda falando sobre filas especiais, temos a poison queue. Há mensagens que podem falhar durante o processamento por vários motivos. Por exemplo, ao processar uma mensagem e salvar algumas informações em um banco de dados, algum problema pode acontecer, como é o caso de um deadlock, fazendo com que a transação seja abortada e a mensagem seja devolvida para a fila. Isso fará com que a mensagem seja novamente reprocessada e dependendo do problema que está acontecendo e não havendo estratégia para remover a mensagem da fila, poderemos ter um loop infinito.

Para evitar que problemas como este ocorram, o Message Queue possui algumas configurações que permitem determinar a quantidade de tentativas e, quando elas se esgotarem, a mensagem é enviada para uma fila do tipo poison. Para lidar com esta técnica, o Message Queue cria duas “sub-filas” abaixo da fila principal, chamadas de retry e poison. A primeira “sub-fila” armazenará as mensagens que estão em uma de suas tentativas de processamento; já a segunda “sub-fila”, poison, armazenará as mensagens que não foram processadas com sucesso, mesmo depois de todas as tentativas, evitando assim que o loop infinito não aconteça. O exemplo abaixo ilustra como configurar o binding para suportar essa técnica:

using System;
using System.ServiceModel;
using System.ServiceModel.Description;

using (ServiceHost host =
    new ServiceHost(typeof(Servico), new Uri[] { 
        new Uri("net.msmq://localhost/private/FilaDeTestes"), 
        new Uri("http://localhost:8383/") }))
{
    host.Description.Behaviors.Add(new ServiceMetadataBehavior());

    NetMsmqBinding binding = new NetMsmqBinding(NetMsmqSecurityMode.None);
    binding.DeadLetterQueue = DeadLetterQueue.Custom;
    binding.CustomDeadLetterQueue = 
        new Uri("net.msmq://localhost/private/MensagensProblematicas");

    binding.MaxRetryCycles = 2;
    binding.ReceiveRetryCount = 2;
    binding.RetryCycleDelay = TimeSpan.FromSeconds(10);
    binding.ReceiveErrorHandling = ReceiveErrorHandling.Move;

    host.AddServiceEndpoint(
        typeof(IContrato),
        binding,
        string.Empty);

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

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

Observação: Uma vez que as mensagens são movidas para uma fila do tipo poison, elas somente poderão ser acessadas acrescentando a palavra poison no final do nome da fila, separando por um “;”, assim como é mostrado a seguir: net.msmq://localhost/private/FilaDeTestes;poison.

Ainda há a possibilidade de efetuar o hosting de um serviço que utiliza o Message Queue utilizando o WPAS (Windows Process Activation Service), fazendo com que o serviço seja exposto através do IIS, tirando proveito de todos os benefícios fornecidos por ele. Essa opção está desabilitada e é necessário instalar este recurso explicitamente a partir do Windows. Isso fará com que um novo serviço, chamado Net.Msmq Listener Adapter, seja instalado e deverá estar funcionando para permitir o serviço.

Transações

O Message Queue também é considerado um resource manager transacional. Isso quer dizer que tanto a entrada quanto a extração de uma mensagem na fila poderá ser envolvida através de uma transação. Como já falado anteriormente, isso somente será possível se durante a criação da fila você especifique que a mesma seja uma fila transacional.

Quando estamos falando de uma fila transacional, temos alguns detalhes e técnicas que podemos fazer uso para tirar o melhor proveito das transações. Antes de mais nada, precisamos entender como as transações estão distribuídas durante o processo de criação, entrega e processamento da mensagem. Cada uma destas etapas exige uma transação e, para ter uma visão mais detalhada, vamos analisar a mesma imagem que vimos acima só que exibindo onde estão essas possíveis transações:

Figura 2 – Transações que envolvem Message Queue quando exposto via WCF.
  • 1 – Client Transaction: Caso a chamada para a operação esteja envolvida em uma transação, a inserção da mensagem no Message Queue também será protegida por esta mesma transação. Depois da mensagem persistida no Message Queue e, se por algum motivo, a transação for abortada, automaticamente a mensagem será descartada. Felizmente a tentativa de entrega não acontecerá até que a transação seja “comitada”.

  • 2 – Delivery Transaction: A transação neste caso protegerá a entrega da mensagem entre o cliente e o servidor (obviamente quando a fila for transacional). Se a entrega falhar por qualquer razão, a mensagem será seguramente devolvida para o cliente e, mais tarde, o Message Queue efetuará uma nova tentativa.

  • 3 – Playback Transaction: Uma vez que a mensagem foi entregue com sucesso para o servidor, entra em cena uma nova transação, chamada de playback transaction. A finalidade desta transação é proteger a mensagem durante o processamento da mesma. Se qualquer problema acontecer durante a execução da operação, a mensagem será devolvida para a fila, valendo a partir daqui o mecanismo de tentativas automáticas, que vimos anteriormente.

Uma questão que aparece quando isso é apresentado é como fazer parte da transação já criada pela própria plataforma ou como criar uma nova transação. Com exceção do processo 2 (Delivery Transaction), podemos criar códigos para fazer parte da transação que coloca a mensagem na fila quanto remover a mensagem dela. Para que isso seja possível não há muito segredo, bastando apenas recorrer à alguns tipos fornecidos pelo próprio WCF como pelo namespace System.Transactions e que já foram detalhadamente falados neste artigo.

Se quisermos criar um código que faça parte da mesma transação que coloca a mensagem na fila do lado do cliente (passo 1), basta instanciar a classe TransactionScope e envolver a chamada da operação dentro deste escopo transacional. Já no passo 3, para que o processamento da operação faça parte da mesma transação que é usada para extrair a mensagem, basta definirmos para True a propriedade TransactionScopeRequired do atributo OperationBehaviorAttribute que, por definição, se existir uma transação em aberto, a operação fará parte da mesma. Finalmente, se quisermos que a operação execute dentro de uma nova transação, basta criarmos um escopo transacionado através da classe TransactionScope e, em seu construtor, especificamos a opção RequiresNew, fornecida pelo enumerador TransactionScopeOption.

Gerenciamento de Instâncias

A escolha do modo de gerenciamento de instâncias do serviço implicará durante a execução do processamento das operações. Quando o serviço é exposto através do modo PerCall, cada chamada a qualquer operação será criada uma nova mensagem dentro da fila. Já no modo PerSession, se a sessão for requerida, as operações invocadas a partir de uma instância do proxy serão agrupadas em uma única mensagem. Finalmente, como o modo Single não pode definir uma sessão, cada chamada será sempre mapeada para uma mensagem dentro da fila.

Quando o host (ServiceHost) é criado para expor um serviço sob o Message Queue, o WCF cria de forma transparente um listener chamado MSMQ Channel Listener e tem um papel extremamente importante durante o processamento das mensagens. Como sabemos, cada modo de gerenciamento determina a criação da instância da classe que representa o serviço para atender às requisições. Este listener é responsável por extrair as mensagens da fila, criar a instância da classe e encaminhar as chamadas que estão na mensagem do Message Queue e encaminhá-las para sua respectiva instância.

Conclusão: Como pudemos ver no decorrer deste artigo, o WCF permite uma forte integração com o Message Queue para enriquecer ainda mais as características de um serviço, incrementado-o com a garantia de entrega da mensagem e ordenação, durabilidade, processamento assíncrono, chamadas enfileiradas e podendo tudo isso ser envolvido por transações para assegurar a consistência do processo. E, por fim, o uso do Message Queue permitirá aos clientes continuarem seu trabalho, mesmo quando o serviço não esteja acessível.

MessageQueue.zip (98.24 kb)

Compartilhando tipos entre o serviço e o cliente

Uma questão que sempre é levantada quando falamos de contratos de serviços WCF, refere-se ao compartilhamento dos tipos que são expostos pelo documento WSDL. Para retornar e/ou receber uma instancia de uma classe customizada, basta referenciarmos este tipo em um contrato que, automaticamente, ele já será embutido no documento WSDL. Com isso, ao referenciar o serviço no cliente, uma representação desta classe será criada para que seja possível enviar e/ou receber uma instancia deste objeto complexo.

Durante a referencia (independentemente de qual artefato é utilizado para realizá-la (através da IDE ou utilitário svcutil.exe)), ele olhará para o projeto onde o serviço está sendo referenciado, para determinar se o tipo que o serviço expõe/retorna já existe dentro do mesmo. Caso ele não exista, automaticamente uma classe será criada para representá-la que, obviamente, será baseada no documento WSDL. Essa técnica fornece um baixo acoplamento, assim como já era o caso dos ASP.NET Web Services (ASMX).

Como sabemos, a serialização apenas persiste o estado de um objeto. Isso quer dizer que possíveis métodos e/ou atributos especiais não são propagados através deste processo. Há casos em que se quer compartilhar não somente o estado de um objeto, mas também outras funcionalidades que o mesmo forneça, e que está além das capacidades do processo de serialização.

É neste cenário que o compartilhamento de tipos entra em cena. Isso deve mudar ligeiramente a estrutura das aplicações, ou seja, o tipo a ser compartilhado entre o serviço e o cliente deverá ser isolado em um Assembly (DLL), referenciado pelas duas aplicações. O contrato do serviço agora passará a receber e enviar instancias da classe que está neste Assembly compartilhado e com isso, ao invés de criar uma nova classe para representar o objeto, a IDE (ou o utilitário svcutil.exe) irá fazer o uso deste tipo que, além do estado das propriedades que foram fornecidas pelo serviço, temos todas as possíveis funcionalidades que este objeto fornece. Ao contrário do que vimos acima, esta técnica traz um forte acoplamento entre os envolvidos, bem como já acontecia com o .NET Remoting, obrigando as “duas pontas” serem WCF.

A vantagem do compartilhamento é que a cada nova mudança no contrato, ou melhor, nas propriedades do objeto, não há necessidade de atualizar a referencia do serviço para que ele sincronize o objeto local com o objeto exposto pelo documento WSDL. Se todos os projetos estão dentro da mesma solução, basta uma simples compilação e tudo já estará funcionando. Esse comportamento melhora consideravelmente a produtividade e conveniencia durante o desenvolvimento do serviço e do cliente, enquanto o ponto negativo gira em torno da interoperabilidade e do acoplamento que se tem entre os participantes.

WCF – Transações

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)

Capturando mensagens genéricas

A primeira etapa para a construção de um serviço WCF é a definição do contrato do serviço que, durante a escrita do código, é determinado pela criação de uma interface tradicional. Os métodos que são criados ali são, em tempo de execução, expostos através do WSDL para que os clientes possam consumí-los.

Se criarmos uma referencia para o serviço a partir da IDE do Visual Studio ou do utilitário svcutil.exe, automaticamente um proxy será criado e podemos invocar os métodos a partir dele, com a impressão de que se estivéssemos chamando um método localmente mas que, durante a execução, uma mensagem é criada e enviada ao respectivo método remoto.

Mas e quando não temos o essa flexibilidade de criar o proxy (exemplo)? Como fazemos para invocar o um método disponibilizado pelo serviço? Felizmente podemos recorrer ao Fiddler ou até mesmo as classes HttpWebRequest e HttpWebResponse para efetuar a requisição, montando o cabeçalho e o corpo (SOAP) para invocar um determinado método do serviço. Esse procedimento permite a quem estiver consumindo, colocar o nome errado do método a ser invocado e, consequentemente, o seu serviço irá rejeitar a requisição.

Felizmente o WCF nos permite lidar com mensagens genéricas, ou seja, podemos criar um método que capturará toda e qualquer mensagem que não for encontrada no contrato do serviço. Para isso, basta definirmos a propriedade Action e ReplyAction do atributo OperationContractAttribute para “*”. Isso fará com que todas as mensagens não encontradas na definição do contrato, sejam encaminhadas para este método, em que podemos efetuar logging, notificações, etc. Abaixo um exemplo ilustra como proceder:

[OperationContract(Action=”*”, ReplyAction=”*”)]
Message ProcessRequest(Message msg);

É importante notar que o método recebe e também retorna uma instancia da classe Message. Esta classe representa a mensagem dentro do WCF. Além disso, também podemos usar esta mesma técnica quando utilizamos o modelo REST, só que agora, devemos definir a propriedade UriTemplate do o atributo WebGetAttribute para “*” e, assim como o OperationContractAttribute, consiga capturar todas mensagens não mapeadas/existentes.

[OperationContract]
[WebGet(UriTemplate=”*”)]
Message ProcessRequest(Message msg);

Transição do Cliente para o Serviço (Debugger)

O Visual Studio .NET 2008 traz uma funcionalidade que permite aos desenvolvedores de serviços WCF, efetuar a depuração do mesmo através do cliente, ou seja, quando estamos executando o cliente em modo de depuração, ao chamar uma operação através da instancia do proxy (via F11 – Step Into), automaticamente seremos redirecionados para o método do serviço, executando-o passo-à-passo.

Ao pressionar F11, o debugger automaticamente irá se vincular ao processo que corresponde efetua o host do serviço e, ao finalizar a execução do método, o controle voltará ao cliente. O mais importante é que a janela Call Stack traz informações tanto do cliente como do serviço.

Caso voce não esteja fazendo uso destas funcionalidades, então provavelmente isso está desabilitado. Para habilitar este recurso, basta recorrer ao utilitário vsdiag_regwcf.exe. Este utilitário está disponível a partir do Prompt de comando do Visual Studio .NET 2008 (se estiver rodando no Windows Vista, então é necessário rodar como Administrador) e, via os seguintes parametros, voce poderá interagir com ele:

  • -i: Habilita o recurso.
  • -u: Desabilita o recurso.
  • -s: Exibe se o recurso está habilitado ou desabilitado.

UserName e Certificados

Os ASP.NET Web Services fornecem uma possibilidade de efetuar a autenticação do serviço através de SOAP Headers. Para isso, bastava criar uma classe que herde da classe SoapHeader e criar as propriedades UserName/Password e, via SoapHeaderAttribute, voce vinculava este header aos métodos que exigem a autenticação do usuário para poder funcionar.

Essa configuração funciona bem, mas é vulnerável. A questão é que o envelope SOAP irá trafegar entre o cliente e o serviço de forma desprotegida e, sendo assim, qualquer um que interceptar a requisição, conseguirá extrair essas informações confidenciais. A autenticação baseada em SOAP Headers apenas são seguras se o transporte garantir a segurança; em outras palavras, isso quer dizer HTTPS. Outra alternativa ainda utilizando os tradicionais ASP.NET Web Services, seria a utilização do WSE (Web Services Enhancements), que possibilita a segurança em nível de mensagens.

O WCF, que é seguro por padrão, tem um comportamento ligeiramente diferente em relação aos ASP.NET Web Services. Para utilizar a autenticação baseada em UserName/Password que o WCF fornece sob o protocolo HTTP, será necessário utilizar um certificado. Sem a utilização deste, não seria possível garantir a integridade e confidencialidade da mensagem, comprometendo as informações e, principalmente, permitindo que alguém intercepte a mensagem e capture os dados sigilosos. Assim como os ASP.NET Web Services, se o serviço WCF for exposto via HTTPS, isso não é necessário, mas utilizar segurança baseada em transporte tem seu ponto negativo: a segurança é apenas garantida ponto-a-ponto e, caso haja intermediários entre o cliente e o serviço, não temos a garantia de que a mensagem chegará segura até o destino.

WCF – Reliable Messages

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.

encodedValue

Quando fazemos uma referencia a um serviço WCF, e ele possuir um certificado definido para assegurar a troca de informações entre o cliente o e serviço, a IDE do Visual Studio ou o utilitário svcutil.exe, adicionanão um elemento chamado identity/certificate com o atributo encodedValue. O valor deste atributo é uma série de letras e números mas, o que isso representa?

Essa informação trata-se da chave pública do certificado, codificada no padrão Base64. Ela será utilizada pelo runtime do WCF para criptografar as informações que serão trocadas, mais precisamente, as credenciais. Quando essa informação não existir no cliente, a configuração do serviço deverá permitir a negociação ou informar a referencia para o certificado que, geralmente, esta armazenado em um dos repositórios do cliente.

Bloco “using” no cliente

É muito comum envolvermos objetos que utilizam recursos custosos dentro de um bloco using, fazendo com que o método Dispose do mesmo seja disparado independemente de exceções que sejam disparadas durante a execução. Vale lembrar que somente podemos envolver objetos que implementam a interface IDisposable.

Quando fazemos a referencia para algum serviço WCF em uma aplicação cliente, automaticamente a IDE do Visual Studio (ou através do utilitário svcutil.exe), se encarregará de criar uma classe que herda diretamente da classe abstrata, chamada ClientBase<TChannel>. Essa classe implementa tudo o que é necessário para possibilitar a comunicação entre cliente e o serviço (em outras palavras, trata-se do proxy) e, que por sua vez, faz uso de objetos caros (channels, etc.) e que precisam ser brevemente liberados. Como a classe ClientBase<TChannel> implementa a interface IDisposable, isso quer dizer que podemos envolver a instancia do proxy em um bloco using, para que o método Dispose seja automaticamente disparado.

Se envolvermos o proxy em um bloco using (lembre-se de que ele será transformado em try/finally), é necessário tomarmos um certo cuidado. Não porque o método Dispose não será chamado, mas sim onde que a execução deste método afetará a aplicação cliente. Vamos supor que algum erro ocorra durante a execução da operação e, como já era de se esperar, o bloco finally será disparado, chamando o método Dispose do proxy. Neste caso, o método Dispose não faz mais nada a não ser invocar o método Close. O problema aqui é que o método Close poderá exigir algumas atividades extras, necessitando fazer alguma outra comunicação com o serviço e, se neste momento algum erro ocorrer, a exceção que chegará ao cliente será:

System.ServiceModel.CommunicationObjectFaultedException: The communication object, System.ServiceModel.Channels.ServiceChannel, cannot be used for communication because it is in the Faulted state.

…, mascarando assim, o real problema. Finalmente, a opção para contornar esse possível problema, é a chamada do método Abort, que encerra imediatamente a comunicação entre o cliente o serviço, assim como é demonstrado neste link.

WSHttpBinding vs. WS2007HttpBinding

A Microsoft criou o WCF e implementou nele os protocolos de segurança, transações e sessões confiáveis baseando-se nas primeiras versões destes padrões, que são gerenciados pela OASIS.

Com o .NET Framework 3.5, a Microsoft atualizou o WCF, disponibilizando agora a implementação mais atualizada destes protocolos através de dois novos bindings: WS2007HttpBinding e WS2007FederationBinding.