WCF – Roteamento de Mensagens

Ao desenvolver um serviço WCF, disponibilizamos um endpoint de acesso ao mesmo, permitindo que clientes o consumam diretamente. Independentemente da tarefa que ele venha a desempenhar, fica sob responsabilidade do mesmo, através de algum ponto de extensibilidade ou até mesmo em sua implementação, efetuar alguma customização ou reutilização em termos de infraestrutura, como segurança, caching, etc.

Esse tipo de customização visa centralizar alguns processos, facilitando a reutilização e gerenciamento dessas tarefas. Outro ponto importante no desenvolvimento de serviço, é a questão do balanceamento de carga. Ao publicar um serviço e muitos clientes passarem a consumí-lo, provavelmente o servidor onde ele ficar hospedado não comportará esse aumento. Neste caso, cria-se um segundo servidor para distribuir a execução do serviço, e através de algum software ou hardware, faz a configuração necessária para direcionar as requisições de acordo com a sua capacidade/disponibilidade.

Esses são alguns dos típicos cenários para o uso de roteamento de mensagens. A ideia do roteador é receber uma mensagem e encaminhá-la, podendo ou não fazer alguma verificação. Até a versão atual do WCF (3.5), é necessário uma grande quantidade de código para a criação deste roteador, enquanto na versão 4.0, que está por vir, já trará esse serviço nativamente, e é o que veremos no decorrer deste artigo.

Todos os tipos necessários para criarmos este roteador estão abaixo de um novo namespace, chamado System.ServiceModel.Routing (assembly System.ServiceModel.Routing.dll). Assim como a implementação manual que existia antes da versão 4.0, o roteador será disponibilizado como um serviço WCF qualquer, mas implementando alguns contratos (Interfaces) específicos, que determinarão como as mensagens serão encaminhadas para o respectivo serviço. A classe responsável por representar o serviço de roteamento é chamada de RoutingService, que por sua vez, implementa as seguintes Interfaces: ISimplexDatagramRouter, ISimplexSessionRouter, IRequestReplyRouter e IDuplexSessionRouter.

Cada uma dessas Interfaces descrevem as funcionalidades suportadas pelo serviço de roteamento. A primeira delas, ISimplexDatagramRouter, traz suporte ao processamento assíncrono de uma mensagem, suportando tipos “one-way”; já a Interface ISimplexSessionRouter, possibilita o processamento de mensagens que requerem sessões; a Interface IRequestReplyRouter possibilita ao serviço de roteamento, processar mensagens do tipo requisição-resposta, podendo ou não suportar sessões e, finalmente, a Interface IDuplexSessionRouter, que permite ao roteador processar mensagens “duplex” (aquelas que suportam callbacks).

Todas essas Interfaces estão implementadas na classe RoutingService, podendo ela tratar qualquer requisição, para os mais variados tipos de mensagens. A idéia de ter isso tudo isolado em Interfaces é que, eventualmente, você possa vir a criar um serviço de roteamento que suporte apenas um dos tipos.

A implementação e a forma como criamos e hospedamos um serviço não muda em nada. Continuamos criando os contratos, criação da classe que representa o serviço e o hosting do mesmo, com os respectivos endpoints. Em princípio, as aplicações que consomem o serviço também não mudam em nada. A única – grande – diferença é que entre essas duas partes haverá um intermediário, que como vimos acima, será o responsável por encaminhar as mensagens do cliente para o serviço e as mensagens do serviço para o cliente.

Uma vez que o serviço estiver construído, é necessário criarmos um serviço que servirá como roteador. Como vimos acima, a classe que representa isso é a RouterService, e podemos hospedá-la em qualquer hosting suportado pelo WCF. Já as Interfaces, que também foram comentadas acima, serão utilizadas para construir os endpoints do roteador, nos obrigando a escolher a Interface correta, em sincronia com o tipo de mensagem exposto pelo serviço efetivo.

Como o roteador será um serviço qualquer, também temos que configurar o(s) endpoint(s) e behavior(s). Como sabemos, uma das características do endpoint é o endereço, e neste caso, ele será utilizado pelos clientes para enviar a mensagem para qualquer um dos serviços que estão atrás do roteador, que por sua vez, se baseará em filtros para encaminhar a mensagem ao serviço correto.

Para o exemplo teremos dois serviços: um responsável pelo gerenciamento dos usuários e outro pelo gerenciamento de clientes, e ambos estarão acessíveis através do roteador. Um dos serviços (“ServicoDeUsuarios”) foi criado e disponibilizado utilizando o binding BasicHttpBinding, e as mensagens são do tipo requisição-resposta. Já o segundo serviço (“ServicoDeClientes”) possuirá apenas um método do tipo “one-way”, sendo disponibilizado através do binding WSHttpBinding. Dessa forma, o roteador será criado com dois endpoints distintos, onde o primeiro deles é configurado com o binding BasicHttpBinding e com o contrato IRequestReplyRouter, enquanto o segundo, utilizará o binding WSHttpBinding e o contrato definido como ISimplexDatagramRouter. Abaixo temos a configuração parcial do roteador:

<?xml version=”1.0″ encoding=”utf-8″ ?>
<configuration>
  <system.serviceModel>
    <services>
      <service name=”System.ServiceModel.Routing.RoutingService”
               behaviorConfiguration=”routerConfig”>
        <host>
          <baseAddresses>
            <add baseAddress=”http://localhost:9997/Router”/&gt;
          </baseAddresses>
        </host>
        <endpoint address=”rr”
                  binding=”basicHttpBinding”
                  contract=”System.ServiceModel.Routing.IRequestReplyRouter” />
        <endpoint address=”ow”
                  binding=”wsHttpBinding”
                  contract=”System.ServiceModel.Routing.ISimplexDatagramRouter” />
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name=”routerConfig”>
          <serviceMetadata httpGetEnabled=”true”/>
          <serviceDebug includeExceptionDetailInFaults=”true”/>
          <routing filterOnHeadersOnly=”false” 
                       routingTableName=”RouterMapping” />
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <!– Outras Configurações –>
  </system.serviceModel>
</configuration>

Analisando essa primeira parte do arquivo de configuração do serviço de roteamento, podemos notar que o serviço que está sendo exposto é o RoutingService. Na sua definição vemos o baseAddress e dois endpoints. O endereço especificado no baseAddress, é o endereço que os clientes utilizarão para efetuar a comunicação com o roteador. Logo em seguida temos dois endpoints, onde o primeiro define o binding BasicHttpBinding e o contrato IRequestReplyRouter, ou seja, aceitará requisição através deste binding, suportando o tipo de mensagem requisição-resposta. Já o segundo, utiliza o binding WSHttpBinding com o contrato ISimplexDatagramRouter, ou seja, suporte à operações do tipo “one-way”.

Podemos reparar também que o serviço está referenciando um behavior chamado “routerConfig”. Dentro desta seção de configuração, além das opções comuns, como a disponibilidade de metadados, exceções, temos um novo behavior, chamado de RoutingBehavior (representando pelo elemento <routing />). Esse elemento possui apenas dois atributos: filterOnHeadersOnly e routingTableName. O primeiro atributo recebe um valor boleano indicando se poderemos ou não utilizar o corpo da mensagem para aplicar um determinado filtro (veremos mais sobre isso abaixo). O segundo atributo, define o nome de uma seção (que deve estar no mesmo arquivo de configuração), onde definiremos todos os filtros necessários para avaliar e, consequentemente, efetuar o encaminhamento da mensagem para o respectivo serviço.

Antes de falarmos efetivamente sobre os filtros, há uma seção muito importante e que é necessário efetuarmos a configuração da forma correta. Esta seção, delimitada pelo elemento <client />, muitas vezes é utilizada do lado de aplicações consumidoras, para especificar o endpoint que será utilizado por ela para efetuar a comunicação com o serviço. Neste contexto, esse elemento tem uma finalidade diferente, ou seja, de especificar o nome, endereço, binding e contrato dos serviços para os quais, eventualmente, o roteador encaminhará as mensagens. Abaixo podemos visualizar como fica a configuração dele:

<client>
  <endpoint name=”Servico1″
            address=”http://localhost:9998/usuarios&#8221;
            binding=”basicHttpBinding”
            contract=”*” />
  <endpoint name=”Servico2″
            address=”http://localhost:9999/clientes&#8221;
            binding=”wsHttpBinding”
            contract=”*” />
</client>

Na configurações destes endpoints, elencamos o endereço de cada serviço para qual o roteador enviará a mensagem, e a única e principal diferença em relação a uma configuração tradicional, é a presença o caracter “*” como contrato. Isso quer dizer que o serviço poderá receber a mensagem de qualquer contrato, obviamente, desde que passe pelos critérios que serão estabelecidos nos filtros.

A terceira e última parte do arquivo de configuração do roteador, consiste na definição dos filtros e como eles serão avaliados. No trecho de código abaixo, o elemento <routing /> agrupa dois sub-elementos que compõem o sistema de filtragem. O primeiro deles é a seção <filters />. Como o próprio nome diz, é uma coleção de filtros, onde cada filtro é represetado pelo elemento <filter />, que por sua vez, possui três atributos: name, filterType e filterData. O atributo name é autoexplicativo; já o atributo filterType especifica como será analisado o filtro. No exemplo abaixo, estou verificando a propriedade Action no header da mensagem. Finalmente, o atributo filterData é o valor a ser comparado com o qual foi extraído da mensagem.

O segundo sub-elemento é chamado de <routingTables />. Este elemento também possui uma coleção de entradas, onde ele relaciona um filtro à um endpoint cadastrado previamente (através do elemento <client />), por exemplo, se o “FiltroServico1” for avaliado como verdadeiro, a mensagem será encaminhada para o endpoint “Servico1”, e o mesmo acontecerá para o endpoint “Servico2” se o filtro “FiltroServico2” for atendido.

<routing>
  <filters>
    <filter name=”FiltroServico1″
            filterType=”Action”
            filterData=”http://tempuri.org/IUsuarios/Adicionar&#8221; />
    <filter name=”FiltroServico2″
            filterType=”Action”
            filterData=”http://tempuri.org/IClientes/Notificar&#8221; />
  </filters>
  <routingTables>
    <table name=”RouterMapping”>
      <entries>
        <add filterName=”FiltroServico1″ endpointName=”Servico1″ />
        <add filterName=”FiltroServico2″ endpointName=”Servico2″ />
      </entries>
    </table>
  </routingTables>
</routing>

Observação: Se existir dois endpoints do tipo “one-way” ou “duplex”, e que apontam para um mesmo filtro, que por sua vez, foi atendido, a mensagem será encaminhada para ambos endpoints.

Action é um dos filtros possíveis. Você pode utilizar o filtro do tipo XPath, para que através de uma query XPath, você consiga efetuar validações/consultas mais complexas, podendo inclusive analisar o corpo da mensagem. Com essa opção, você terá uma grande flexibilidade, já que conseguirá extrair mais informações e ter maior precisão na hora de avaliar/aplicar o filtro. Outro tipo de filtro é o MatchAll, que como o próprio nome já diz, acatará todas as mensagens. Basicamente, um filtro nada mais é do que uma classe que herda de MessageFilter, e sendo assim, você pode criar os teus próprios filtros, controlando como eles serão aplicados.

É importante dizer que os filtros são avaliados de acordo com a prioridade. Para determiná-la, podemos utilizar o atributo priority na coleção de filtros do elemento <routingTables />. Para o exemplo deste artigo, isso não faz muito sentido, já que temos dois filtros e cada um deles lidará com um tipo de mensagem específico, mas em um cenário onde você conseguir detectar a frequência de acesso, você pode determinar a prioridade para tirar melhor proveito em termos de performance, evitando que ele gaste tempo na avaliação dos outros filtros. Ao encontrar um filtro que atenda a requisição, a mensagem é encaminhada para o endpoint correspondente, caso contrário, uma exceção será disparada.

Outras Funcionalidades

Ainda há alguns outras funcionalidades que estão diretamente ligadas ao sistema de roteamento de mensagens. A primeira delas é a capacidade de aplicar filtros no corpo da mensagem. Como falado acima, tudo o que você precisa fazer é definir o atributo filterOnHeadersOnly para False e vasculhar o corpo da mensagem em busca dos parâmetros necessários para avaliar/aplicar o filtro, e para isso, você pode utilizar a seção <namespaceTable />. É através dela que conseguimos estabelecer a relação de namespaces com seus respectivos prefixos, para que assim, consiga encontrar e navegar pelos elementos que estão dentro da mensagem.

Como vimos acima, relacionamos um filtro à um determinado endpoint, e caso esse filtro seja atendido, a mensagem será encaminhada para o endpoint correspondente. Mas e se houver alguma falha de comunicação, como por exemplo, timeout? Para isso, a Microsoft também disponibilizou um elemento chamado <alternateEndpoints />. Através dele, podemos relacionar uma lista de endpoints, e caso a mensagem falhe, automaticamente o WCF tentará reencaminhar para o outro endpoint desta lista, até que algum deles processe com sucesso. Além deste elemento, tudo o que precisamos fazer é relacionar a lista de endpoints ao filtro, através do atributo alternateEndpoints, como vemos abaixo:

<routing>
  <!– Outras Configurações –>
  <routingTables>
    <table name=”RouterMapping”>
      <entries>
        <add filterName=”FiltroServico1″
             endpointName=”Servico1″
             alternateEndpoints=”alternateEndpoints” />
      </entries>
    </table>
  </routingTables>
  <alternateEndpoints>
    <list name=”alternateEndpoints”>
      <endpoints>
        <add endpointName=”Servico1Servidor2″/>
        <add endpointName=”Servico1Servidor3″/>
      </endpoints>
     </list>
  </alternateEndpoints>
</routing>

Para finalizar, outra funcionalidade que temos é a capacidade que o serviço de roteamento tem para trocar o binding que está sendo utilizado na comunicação entre o cliente e o roteador e entre o roteador e o cliente. Isso quer dizer que você pode definir que a comunicação com que o cliente terá com o roteador seja através do binding WSHttpBinding, enquanto a mensagem será encaminhada para o serviço através do binding NetTcpBinding.

Conclusão: Baseando-se nos problemas que vimos no início deste artigo, um roteador pode ajudar imensamente a resolvê-los e, felizmente, com este recurso disponível a partir da versão 4.0 do WCF, trará novas capacidades e funcionalidades para incorporarmos em nossas aplicações, tornando-as muitos mais poderosas, e resumindo alguns processos que antes eram complexos de serem realizados, em configurações extremamente simples.

RoteamentoDeMensagens.zip (38.73 kb)

Mensagens One-Way não são assíncronas

Uma das possibilidades que temos ao construir um serviço WCF, é a capacidade de enviar mensagens conhecidas como one-way. Quando você tem operações que não retornam valores e, principalmente, o cliente não está interessado no sucesso ou falha da mesma, podemos recorrer à esta técnica. Ela permite que o proxy envie a mensagem ao seu destino, sem esperar pela sua execução. Há um bloqueio mínimo do lado do cliente, que é apenas necessário para que o proxy mande a mensagem e o serviço a enfileire. A partir daí, o cliente está livre para dar continuidade na execução do programa.

Dependendo do problema que ocorra, o retorno pode ser imediato, como acontece quando o respectivo endpoint não é encontrado, e dificilmente você notará esse bloqueio. Já em outros casos, quando a quantidade de informação enviada ao serviço é muito grande, isso também pode demorar a retornar, o que pode gerar uma má experiência (bloqueio) ao usuário. Outro problema que é muito comum, é quando você define o modelo de sincronização como Single e utiliza um binding que dá suporte à sessões, obrigando ao serviço (dispatcher) assegurar a entrega das mensagens de forma ordenada, impossibilitando que outra mensagem seja lida até que o processamento da mensagem atual seja completada.

Uma forma de contornar esse tipo de problema é trabalhar com mensagens assíncronas, como é mostrado aqui. Isso evitará a necessidade do cliente ficar aguardando até que a mensagem one-way seja efetivamente entregue ao serviço, mas como um ponto negativo, que é a saturação do ThreadPool, dependendo do volume de requisições que são realizadas. Outra alternativa é definir o gerenciamento de instância do serviço como PerCall e com modo de sincronização Multiple, onde cada requisição será atendida por uma instância diferente e podendo mais que uma thread enviar as mensagens.

IIS, WCF e Partial Trust

Há algum tempo eu falei sobre a possibilidade de invocar serviços WCF em aplicações parcialmente confiáveis. Mas ainda há um segundo cenário, que é a exposição de serviços através de aplicações ASP.NET Web Site/WCF Service. Para criar um serviço WCF, você não precisa necessariamente criar um tipo de projeto exclusivo como o WCF Service. Um projeto do tipo ASP.NET Web Site pode, tranquilamente, servir como host de um serviço WCF, e para isso, basta adicionar um arquivo *.svc e configurá-lo corretamente no Web.config.

Independentemente de qual das alternativas utilize, você poderá se deparar com uma restrição de segurança, e dependendo das funcionalidades (mais precisamente do binding e seus elementos) que utiliza, o serviço não rodará. Isso muitas vezes acontece quando você faz o deployment para um servidor, em que a configuração padrão do .NET Framework foi alterada (visando uma maior segurança). Uma das regras mais importantes que se deve ter ao configurar um servidor Web (IIS), é não permitir que as aplicações que rodem ali executem em “Full Trust”. Para isso, se altera o arquivo Web.config (que está em nível de servidor), definindo o atributo level do elemento trust para “Medium” ou qualquer nível abaixo disso.

Neste ambiente, você poderá utilizar os bindings BasicHttpBinding, WSHttpBinding ou o WebHttpBinding, desde que eles estejam com a segurança desabilitada ou com a proteção em nível de transporte. O binding WSDualHttpBinding também não pode ser utilizado neste cenário, já que algumas tarefas que ele desempenha exige um nível de segurança mais elevado. Finalmente, para tentar resolver este problema, podemos fazer uso das técnicas mostradas pelo Juval Lowy neste artigo, disponibilizando alguns helpers para facilitar a criação de hosts em ambientes parcialmente confiáveis.

Serviços Declarativos

Uma nova funcionalidade que estará presente no WCF 4.0, é a capacidade de criar serviços de forma declarativa. Quando trabalhamos com WCF, tudo o que devemos fazer é criar uma Interface que representará o contrato, implementamos ela em uma classe que representará o serviço e, finalmente, criamos o código necessário para servir como hosting para o serviço.

A criação do hosting conseguimos fazer totalmente de forma declarativa, enquanto as outras duas, somente podemos utilizar o modelo imperativo (via C# ou VB.NET) para criá-los. A proposta da Microsoft é permitir que a criação de um serviço WCF seja feita totalmente (definição e implementação) de forma declarativa, ou seja, utilizando o XAML. Ainda há o que chamamos de “projeção de contratos”, que permitirá separar a definição da representação (mensagens). Com isso, vamos ter apenas uma única definição, mas podendo ser representada de diferentes formas (SOAP, REST, etc.). Para mais detalhes sobre serviços declarativos, você pode consultar a documentação oficial desta funcionalidade.

Utilizando Generics em Serviços

Uma das grandes desilusões ao utilizar o WCF, é não conseguir utilizar tipos de dados genéricos nos serviços. Generics são tipos de dados exclusivos do .NET Framework, e utilizá-los viola os princípios da orientação a serviços, já que podem existir linguagens que não suportam essa funcionalidade. É importante dizer que isso não é uma limitação do WCF, mas sim do WSDL, que é o documento utilizado para expor os metadados para os consumidores.

O Visual Studio .NET/compilador não vão proibí-lo de utilizá-los nos contratos. O problema será na exposição das informações (via WSDL) para o cliente e, consequentemente, o proxy gerado possuirá tipos “estranhos”. Por exemplo, se uma operação do contrato retornar ou receber um tipo genérico como Teste<int>, ao gerar o proxy, ele irá criar um tipo (classe) chamado TesteOfInt, substituindo os parâmetros genéricos do mesmo, pelo tipo especificado na operação. Isso pode piorar um pouco se o parâmetro genérico informado for uma classe, como por exemplo Teste<OutraClasse>, resultando no seguinte tipo do lado do cliente: TesteOfOutraClasseKdjwuy4p, ou seja, acrescentando uma informação randômica para evitar eventuais conflitos com outros tipos no mesmo contrato.

Felizmente você pode sobrescrever esse comportamento, definindo a propriedade Name da atributo DataContractAttribute. Nela você pode especificar o formato do nome que deseja expor no WSDL. Se notarmos no primeiro exemplo abaixo, o parâmetro {0} será substituído pelo tipo genérico especificado. Caso a classe Teste suportasse mais do que um parâmetro genérico, então você pode também especificá-los para compor o nome, assim como mostrado no segundo exemplo.

[DataContract(Name = “TesteCom{0}”)]
public class Teste<T> { }

[DataContract(Name = “TesteComTipo{0}EComChave{1}”)]
public class Teste<T, D> { }

Um outro ponto importante é com relação as coleções. Elas também são características da plataforma, e não devem ser expostas além do serviço. Ao utilizar coleções que implementam direta ou indiretamente as Interfaces IEnumerable<T>, ICollection<T> ou IList<T>, o WCF automaticamente irá convertê-las em um array do tipo especificado no parâmetro genérico (<T>), facilitando a interoperabilidade com outras plataformas.

Ainda falando em coleções, ao criar uma coleção customizada, você poderá fazer uso do atributo CollectionDataContractAttribute, permitindo customizar o nome do tipo da coleção (através da propriedade Name) a ser colocado no WSDL e, consequentemente, criado no cliente (trabalhando da mesma forma que a propriedade Name do atributo DataContractAttribute). Ao utilizar este atributo, ainda temos a garantia de que o WCF verificará a existência do método Add durante a carga do serviço, e não existindo, uma exceção do tipo InvalidDataContractException será lançada. Este método se faz necessário para remontar a coleção durante a deserialização da mensagem.

Como dito acima, as coleções são características da plataforma, e não devem propagar além dos limites do serviço. Mas em algumas situações, isso pode ser interessante. Quando você está compartilhando tipos entre o cliente e o serviço (exige .NET/WCF presente dos dois lados), você pode optar por criar o proxy respeitando e preservando essas coleções. Para que isso seja possível, basta você utilizar a opções /reference/collectionType do utilitário svcutil.exe. A primeira opção determina o Assembly (reference) onde está a coleção, enquanto a segunda opção determina qual é a coleção. É importante dizer que este recurso também está disponível quando a criação do proxy é feita pela IDE do Visual Studio .NET.

Cuidados ao utilizar o WSDualHttpBinding

Há algum tempo eu escrevi um artigo que fala sobre a possibilidade que temos para efetuar callbacks do serviço para o cliente que, em outras palavras, significa o serviço se comunicando com o cliente, permitindo ao mesmo notificar sobre o término de alguma tarefa, simular eventos, etc. Esse tipo de comunicação é também referida/conhecida com “duplex”.

Quando queremos esta funcionalidade e o serviço está exposto via TCP, através do binding NetTcpBinding, o WCF utiliza o mesmo canal para efetuar a comunicação entre o serviço e o cliente. Já quando o serviço for exposto através do protocolo HTTP, utilizamos o binding WSDualHttpBinding, responsável por possibilitar callbacks através do HTTP. Como o HTTP é um protocolo unidirecional, esse binding é responsável por também criar um endpoint do lado do cliente (diferente do usado para envio das mensagens), que receberá os callbacks que o serviço enviará.

Enquanto você trabalha no desenvolvimento do serviço/cliente, que na maioria das vezes é na máquina local, tudo funciona perfeitamente. Alguns problemas começam a aparecer quando você faz a distribuição dos aplicativos que consomem o serviço via HTTP. O primeiro deles é quando você instala o serviço em uma máquina com Windows XP e que tenha o IIS instalado (que não tem o HTTP.sys). A ausência do HTTP.sys não permite ao Windows compartilhar uma mesma porta entre múltiplas aplicações. Por padrão, o WSDualHttpBinding irá criar e configurar o endpoint com a porta 80, ou seja, a mesma utilizada pelo IIS, resultando em uma exceção do tipo AddressAlreadyInUseException. Você pode facilmente resolver isso removendo ou parando o serviço do IIS ou especificando uma outra porta. Para utilizar uma porta diferente, basta utilizar o atributo clientBaseAddress (elemento binding) na configuração do binding do lado do cliente.

Já o segundo problema está relacionado a solução do primeiro. Se você altera a porta padrão, muito provavelmente ela estará barrada no firewall do Windows. Isso quer dizer que se o firewall estiver ativo (que é o recomendado), o serviço não conseguirá enviar o callback para o cliente. Para resolver isso, você pode adicionar uma exceção no firewall do Windows. Você pode fazer isso diretamente através do gerenciador do firewall no Painel de Controle, ou até mesmo via programação.

Novas funcionalidades para construção de serviços REST

Há algum tempo eu falei aqui sobre o WCF REST Starter Kit. A finalidade deste kit foi disponibilizar algumas novas funcionalidades para o desenvolvimento de serviços em HTTP, permitindo que a comunidade avaliasse, e baseando-se no feedback, a Microsoft está incorporando algumas dessas funcionalidades nativamente no WCF 4.0. Com isso, teremos suporte ao caching do ASP.NET, interceptors, novas formas para reportar erros no browser, etc., sem a necessidade de referenciar assemblies extras.

Como configurar serviços com WCF Test Client

Quando utilizamos a template de projeto WCF Service Library, todas as configurações do serviço estão no arquivo App.config. Ao rodar a aplicação, o utilitário conhecido como WCF Test Client é executado, fazendo tudo o que é necessário para efetuar o hosting do serviço recém criado. Como não temos acesso a criação e o gerenciamento do host (representado pela classe ServiceHost), não poderemos configurá-lo de forma imperativa.

Como grande parte das configurações do WCF pode ser realizada através do arquivo de configuração, podemos utilitizá-lo para adicionar behaviors existentes no WCF, mudar alguma característica do binding, etc. O problema aparece quando precisamos adicionar um behavior customizado. Neste caso, além da classe que o representa, ainda é necessário a criação de uma segunda classe, que definirá o respectivo behavior no arquivo de configuração.

Para isso, é necessário criar uma classe e herdar de BehaviorExtensionElement, implementando os membros BehaviorType e CreateBehavior, assim como mostro no final deste artigo. Depois desta classe criada, precisamos ainda registrar esta extensão no arquivo de configuração, antes de efetivamente fazer o uso dela. Para isso, utilizamos a seção behaviorExtensions, como mostrado abaixo:

<extensions>
  <behaviorExtensions>
    <add name=”errorService”
         type=”Host.ErrorServiceElement, Host, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null”/>
  </behaviorExtensions>
</extensions>

E, finalmente, utilizá-la:

<behaviors>
  <serviceBehaviors>
    <behavior name=”BehaviorConfig”>
      <errorService />
      <serviceMetadata httpGetEnabled=”True”/>
      <serviceDebug includeExceptionDetailInFaults=”False” />
    </behavior>
  </serviceBehaviors>
</behaviors>

WCF – Durable Services

Vimos nos artigos anteriores como criar, hospedar e consumir serviços com o WCF. Todas as tarefas que esses serviços disponibilizavam tinham uma duração curta, ou seja, o processo completo se resumia apenas na chamada de uma única operação. Mas é muito comum, em aplicações distribuídas, termos processos que podem durar muito mais tempo para completar toda a tarefa. Neste caso, pode ser complicado manter ativa a instância do serviço ou do cliente por todo esse tempo.

A Microsoft introduziu na versão 3.5 do WCF uma funcionalidade chamada de Durable Services. Como o próprio nome diz, ele possibilita a criação de serviços que podem ser persistidos, sobrevivendo a eventuais reciclagens do host que o hospeda e também de reinicializações do cliente. Com este artigo vamos analisar como devemos proceder para incorporar esta funcionalidade em nossos serviços.

Independentemente do modelo de gerenciamento de instância que utilize, a classe que representa o serviço tem um tempo de vida determinado, e se por algum motivo a aplicação que hospeda o mesmo for reinicializada, todo o estado do objeto será perdido. Isso ocorre porque todo esse estado, que cada objeto que representa o serviço mantém, é armazenado em memória, ou seja, é volátil e não conseguirá sobreviver durante possíveis reinicializações, e pode comprometer a regra de negócio, caso você dependa desta informação.

Com os Durable Services, podemos persistir as informações em algum repositório ao invés de utilizar a memória. Isso irá garantir que as informações sejam mantidas, mesmo que o processo demore dias para ser concluído. Mesmo que a sessão com o cliente seja destruída, você conseguirá restaurar o estado mais tarde. A Microsoft já disponibilizou um provider para armazenar as informações no banco de dados SQL Server, mas nada impede de você customizar, optando pela criação de um provider que armazene as informações em arquivos XML.

Para guiar os exemplos, teremos o seguinte cenário: um serviço que irá fornecer as operações necessárias de um comércio eletrônico, como por exemplo a criação de um carrinho, a inserção de novos itens, recuperação dos itens selecionados e a finalização da compra. A idéia é que os itens selecionados pelo usuário, sejam mantidos além das sessões e também do desligamento da aplicação cliente e, eventualmente, do serviço.

Os tipos que usaremos estão contidos no Assembly System.WorkflowServices.dll. Antes de efetivamente começarmos a ver os tipos disponibilizados por esse Assembly, precisamos preparar a base de dados para que ela consiga acomodar as informações. Felizmente já temos todo o script pronto, apenas será necessário executá-lo. Para isso, basta ir até o seguinte endereço: %windir%Microsoft.NetFrameworkv3.5SQLEN. Lá temos quatro arquivos, sendo dois para a criação e dois para a exclusão. No nosso caso, devemos executar os seguintes arquivos: SqlPersistenceProviderSchema.sql e SqlPersistenceProviderLogic.sql, nesta mesma ordem. O primeiro é responsável por criar a tabela InstanceData, enquanto o segundo cria as Stored Procedures com toda a lógica de inserção, exclusão e carregamento das instâncias.

Podemos serializar o estado do serviço de forma binária (o padrão) ou através de Xml, e para suportar isso, temos duas colunas na tabela InstanceData, chamadas de “instance” e “instanceXml”. A primeira é utilizada quando o estado é serializado em formato binário, enquanto a segunda apenas será utilizada quando o conteúdo for persistido em Xml. Além dessas duas colunas, ainda temos a coluna “id”, que terá o seu valor propagado do serviço para o cliente e sendo devolvido do cliente para o serviço. Esse ID representa a instância (estado) do serviço que foi armazenada. Falaremos detalhadamente sobre ela mais tarde, ainda neste artigo.

Implementação

O contrato do serviço não sofrerá qualquer alteração. Você deve continuar decorando a interface com o atributo ServiceContractAttribute e as operações que serão expostas, com o atributo ServiceOperationAttribute. As mudanças começam a aparecer na classe que representará o serviço. O primeiro passo para a criação de um serviço durável, é decorar a classe que representa o serviço com o atributo DurableServiceAttribute. Durante o carregamento do serviço, este atributo irá garantir que o modo de gerenciamento de concorrência não esteja definido como Multiple e que o modo de gerenciamento de instância deve ser PerSession. É importante dizer que o estado deve ser de uso exclusivo de uma sessão. Se desejar que o estado seja compartilhado com todos os clientes (sessões), então você deve optar pelo modo Multiple de gerenciamento de instância. E mais um detalhe importante, temos que aplicar o atributo SerializableAttribute, assim como todas as classes que desejamos serializar.

Cada operação que irá compor o serviço durável, deverá ser decorada com o atributo DurableOperationAttribute. Esse atributo indica ao runtime que ao completar cada operação, o estado do serviço deverá ser persistido fisicamente. Essa classe possui duas propriedades, que por padrão são sempre False: CanCreateInstance e CompletesInstance. A primeira delas indica se uma nova instância do serviço deve ser criada ao executar a respectiva operação. Já a segunda propriedade, indica se a instância será removida da memória e excluída do repositório quando a operação for executada. A classe abaixo exibe como configurar esses atributos, com a implementação omitida para poupar espaço:

[Serializable]
[DurableService]
public class ServicoDeComercioEletronico : IComercioEletronio
{
    private List<ItemDaCompra> _produtos;
    private string _usuario;

    [DurableOperation(CanCreateInstance = true)]
    public void CriarCarrinho(string usuario) { }

    [DurableOperation]
    public void AdicionarItem(ItemDaCompra item) { }

    [DurableOperation]
    public ItemDaCompra[] RecuperarItensDaCompra() { }

    [DurableOperation(CompletesInstance = true)]
    public void FinalizarCompra() { }
}

Não há nenhuma mudança drástica na implementação do serviço, apenas temos que nos atentar ao estado dos membros internos, que serão mantidos entre as chamadas (lembre-se de que essa persistência sobreviverá mesmo após o host ou o cliente ser encerrado).

Mudanças mais significativas são realizadas para expor o serviço. Isso se deve ao fato da necessidade de propagar o ID que representa o estado o serviço. O ID em questão é o mesmo que é gerado durante a inserção do registro na tabela acima mencionada. Quando o runtime encontra uma operação que possui o atributo DurableOperationAttribute e com a propriedade CanCreateInstance definida como True, ele irá criar um registro na tabela InstanceData, capturar o ID gerado e devolver para o cliente. Todas as operações subsequentes devem embutir esse ID.

Antes da operação ser efetivamente executada, o runtime irá extrair a instância da base de dados, abastecer os membros privados previamente serializados, e depois disso irá executar a operação; ao retornar, o runtime devolve os dados para a base de dados, com as informações atualizadas. Finalmente, quando o runtime encontra uma operação com o atributo DurableOperationAttribute e com a propriedade CompletesInstance definida como True, o respectivo registro que representa o estado do serviço é excluído da base de dados.

Como podemos perceber, todo o processo acontece utilizando o ID gerado durante a primeira requisição, e a necessidade de mantê-lo durante as requisições futuras se faz necessário, caso queira manter o estado. Visando essa manutenção do ID, a Microsoft criou três novos bindings: NetTcpContextBinding, BasicHttpContextBinding e WSHttpContextBinding. Cada um deles herda diretamente dos bindings tradicionais (NetTcpBinding, BasicHttpBinding e WSHttpBinding), apenas trazendo o suporte necessário para gerenciar o ID de persistência.

Cada um destes bindings sobrescrevem o método CreateBindingElements, criando uma instância da classe ContextBindingElement. Este elemento é responsável por gerenciar como o ID será propagado entre o serviço e o cliente (ou vice-versa). Em seu construtor, recebe uma das seguintes opções expostas pelo enumerador ContextExchangeMechanism:

    – ContextSoapHeader: O ID será enviado através de um header na mensagem SOAP. É o valor padrão.
    – HttpCookie: O ID será definido em um cookie.

Cada um dos bindings utiliza um mecanismo diferente. O binding NetTcpContextBinding utiliza a primeira opção, mesmo porque não é possível utilizar cookies através de TCP. Já o BasicHttpContextBinding utiliza cookies para manter o ID, e irá disparar uma exceção caso não haja suporte aos mesmos. Finalmente, o binding WSHttpContextBinding utiliza cookies quando suportado, e SOAP Headers quando não há tal suporte. Com isso, não precisamos nos preocupar como o ID será propagado entre as partes, apenas teremos a responsabilidade de armazenar o ID para conseguir carregar uma instância previamente criada. O trecho de código abaixo ilustra como proceder para configurar a classe ServiceHost, utilizando o binding NetTcpContextBinding:

using (ServiceHost host =
    new ServiceHost(typeof(ServicoDeComercioEletronico),
        new Uri[] { new Uri(“net.tcp://localhost:3832”) }))
{
    host.Description.Behaviors.Add(ConfigurarPersistencia());
    host.AddServiceEndpoint(typeof(IComercioEletronio), new NetTcpContextBinding(), “srv”);

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

Utilizar um dos bindings que vimos acima não é o bastante. Ainda precisamos configurar a persistência das informações, e como já era de se esperar, isso será feito através de um behavior de serviço, chamado PersistenceProviderBehavior. Note que há um método customizado chamado de “ConfigurarPersistencia”, que é o responsável por criar e retornar a instância do provider que fará a persistência das informações.

O construtor da classe PersistenceProviderBehavior recebe como parâmetro uma instância da classe PersistenceProviderFactory. Essa classe abstrata serve como base para todos os providers, inclusive aquele que a Microsoft já disponibilizou para efetuar a persistência no SQL Server. Caso você queira criar o seu próprio provider, então será necessário criar duas classes: o provider em si (herdando da classe PersistenceProvider) e a factory responsável por criar e gerir as instâncias do respectivo provider (herdando de PersistenceProviderFactory).

Como comentado acima, utilizaremos o provider para SQL Server, chamado SqlPersistenceProviderFactory, que está contido no namespace System.ServiceModel.Persistence. Para utilitizá-lo, é importante que você prepare a sua base de dados, rodando os scripts mencionados acima. Como estou utilizando a configuração imperativa, então vou criar a instância da classe SqlPersistenceProviderFactory, que em seu construtor receberá a string de conexão com a base de dados. Note que no código abaixo, além da string de conexão, ainda é passado um valor boleano, indicando como será efetuado a persistência, onde True indica que será serializada em Xml e False em formato binário (padrão).

static PersistenceProviderBehavior ConfigurarPersistencia()
{
    return new PersistenceProviderBehavior(
        new SqlPersistenceProviderFactory(
            ConfigurationManager.ConnectionStrings[“SqlConnString”].ConnectionString, true));
}

Consumindo o Serviço

Para referenciar e invocar operações que compõem serviços duráveis, não há diferenças em relação ao que já conhecemos. Apenas devemos ter duas preocupações: utilizar o binding correspondente ao binding utilizado pelo serviço e também como e onde armazenar o ID que representa a instância remota, que por sua vez, deverá ser devolvido do cliente para o serviço, afim de carregar o respectivo estado do mesmo.

O método “CriarCarrinho”, responsável por criar a instância, deve somente ser invocado uma única vez. Se você não se atentar a isso e chamá-lo sempre, uma nova instância será criada, perdendo todo o sentido da funcionalidade fornecida pelos serviços duráveis. Como também já foi falado acima, sempre quando o método retorna, o repositório é acionado para armazenar o estado atual do objeto, que eventualmente a operação alterou. Se você não invocar o método “FinalizarCompra” (responsável pela exclusão do registro do repositório), a instância sobreviverá a eventuais reinicializações do host ou da aplicação cliente, podendo iniciar uma tarefa e finalizá-la mais tarde, sem a preocupação de perder todo o trabalho realizado até aquele momento.

Toda a mensagem que é enviada do cliente para o serviço, deverá conter o ID que irá relacionar a mensagem a uma determinada instância. O desafio aqui é manter esse ID entre as chamadas, mas lembrando que elas podem ser feitas dias depois, e com isso, utilizar a memória não resolve o nosso problema. O que precisamos é persistir esse ID fisicamente, para que em eventuais reinicializações, sejamos capazes de restaurar o mesmo, e reenviar novas requisições para serem relacionadas aquela instância específica. Utilizaremos as classes já conhecidas do namespace System.IO para efetuar essa tarefa, juntamente com o serializador binário que o .NET disponibiliza (BinaryFormatter).

O segredo é como extrair o ID que foi enviado/gerado pelo serviço do lado do cliente. O proxy gerado durante a referência do serviço, herda diretamente da classe ClientBase<TChannel>. Essa classe possui uma propriedade chamada InnerChannel do tipo IClientChannel. A finalidade desta propriedade é expor a funcionalidade básica de comunicação e informações contextuais. Entre os membros fornecidas pela interface IClientChannel, temos o método GetProperty<T>. Este método genérico, recebe um objeto tipado que o método utilizará para efetuar a busca dentro da channel stack. Caso o objeto seja encontrado, ele é retornado; caso contrário, ele encaminha a busca para a próxima layer.

Utilizaremos este método para extrair uma classe que implementa a interface IContextManager. Como o próprio nome diz, ela representa o gerenciador do contexto do canal atual (contexto relacionado aos serviços duráveis), permitindo você ler ou definir um contexto, com informações específicas. Para isso, ela fornece dois simples métodos: GetContext e SetContext. O primeiro retorna uma cópia do contexto atual, representado por um dicionário de dados (onde a chave e o valor são do tipo string) com os itens que foram enviados pelo serviço. Já o segundo método, SetContext, recebe um dicionário de dados (do mesmo tipo anterior), com as informações que devem ser enviadas do cliente para o serviço. Para facilitar, criei uma classe chamada “GerenciadorDeEstado”, que tem como finalidade gerir o ID que é informado pelo serviço, e reenviado para ele. Por questões de espaço, alguns membros foram omitidos:

internal static class GerenciadorDeEstado
{
    private const string ARQUIVO_COM_CHAVE = “InstanceId.bin”;

    public static void Salvar(IClientChannel channel)
    {
        IContextManager context = channel.GetProperty<IContextManager>();
        if (context != null)
            using (FileStream fs = File.Create(ARQUIVO_COM_CHAVE))
                new BinaryFormatter().Serialize(fs, context.GetContext());
    }

    public static void Carregar(IClientChannel channel)
    {
        if (JaExisteArquivo)
        {
            IContextManager context = channel.GetProperty<IContextManager>();
            if (context != null)
                using (FileStream fs = File.Open(ARQUIVO_COM_CHAVE, FileMode.Open))
                    context.SetContext((IDictionary<string, string>)new BinaryFormatter().Deserialize(fs));
        }
    }

    //Outros Membros
}

Note que no método “Salvar” invocamos o método GetContext, enquanto no método “Carregar” utilizamos o método SetContext. No nosso exemplo, esse dicionário irá conter apenas uma única entrada, chamada de “instanceId”, que é justamente o GUID gerado pela inserção do registro no SQL Server.

O que irá determinar se existe ou não uma instância em aberto para esse cliente, é a existência do arquivo com o respectivo ID. Se notarmos o código abaixo, ele irá verificar a existência do arquivo. Caso não exista, então ele invoca o método “CriarCarrinho” e salvará o contexto atual (ID); caso contrário, ele apenas carregará o contexto (ID) existente, para que as chamadas para as operações sejam encaminhadas para a instância previamente criada. Na sequência, você inclui itens dentro do carrinho e, finalmente, será perguntado se deseja ou não finalizar a compra. Se disser não, então você poderá reabrir a aplicação cliente, que os produtos adicionados ainda estarão disponíveis. Se optar por finalizar a compra, então você deve invocar a operação “FinalizarCompra”, que como já sabemos, é responsável por remover o registro da base de dados.

using (ComercioEletronioClient proxy = new ComercioEletronioClient())
{
    if (!GerenciadorDeEstado.JaExisteArquivo)
    {
        proxy.CriarCarrinho(“Israel Aece”);
        GerenciadorDeEstado.Salvar(proxy.InnerChannel);
    }
    else
    {
        GerenciadorDeEstado.Carregar(proxy.InnerChannel);
    }

    //Incluir Itens

    Console.WriteLine(“nDeseja finalizar a compra? (S)im ou (N)ão”);
    if (Console.ReadLine() == “S”)
    {
        //Finaliza a Compra e remove o registro da base de dados.
        proxy.FinalizarCompra();
        GerenciadorDeEstado.Excluir();

        Console.WriteLine(“A compra foi finalizada com sucesso.”);
    }
}

Durante a execução, ou depois do término da aplicação cliente sem finalizar a compra, ao analisarmos a tabela InstaceData no SQL Server, notaremos um registro adicionado, onde a coluna “id” representa o ID que foi propagado para o cliente e está armazenado no arquivo “InstanceId.bin”, e a coluna “instanceXml” com os membros privados devidamente serializados em formato Xml. Abaixo temos informação formatada que está nesta coluna:

<ServicoDeComercioEletronico xmlns=”http://schemas.datacontract.org/2004/07/Host&#8221;
                             xmlns:i=”http://www.w3.org/2001/XMLSchema-instance&#8221;
                             xmlns:z=”http://schemas.microsoft.com/2003/10/Serialization/&#8221;
                             z:Id=”1″
                             z:Type=”Host.ServicoDeComercioEletronico”
                             z:Assembly=”Host, Version=1.0.0.0″>
  <_produtos z:Id=”2″>
    <_items z:Id=”3″
            z:Size=”4″>
      <ItemDaCompra z:Id=”4″>
        <NomeDoProduto z:Id=”5″>Mouse Microsoft</NomeDoProduto>
        <Quantidade>10</Quantidade>
        <Valor>100.00</Valor>
      </ItemDaCompra>
      <ItemDaCompra z:Id=”6″>
        <NomeDoProduto z:Id=”7″>Celular Motorola</NomeDoProduto>
        <Quantidade>10</Quantidade>
        <Valor>40.0</Valor>
      </ItemDaCompra>
      <ItemDaCompra i:nil=”true” />
      <ItemDaCompra i:nil=”true” />
    </_items>
    <_size>2</_size>
    <_version>2</_version>
  </_produtos>
  <_usuario z:Id=”8″>Israel Aece</_usuario>
</ServicoDeComercioEletronico>

Conclusão: Através deste artigo podemos compreender a finalidade e como implementar serviços duráveis. Notamos que não há nenhuma mudança muito radical em relação ao que conhecemos para a construção de serviços em WCF, mas há alguns detalhes importantes, que se não nos atentarmos, este recurso não funcionará como o esperado. Esse tipo de serviço permite enriquecer ainda mais a experiência com o usuário, não obrigando o mesmo a finalizar a tarefa naquele momento, podendo persistir e restaurar mais tarde.

WCFDurableServices.zip (73.37 kb)

Binary Encoding no Silverlight 3.0

Até a versão 2.0 do Silverlight, o binding BasicHttpBinding apenas codificava a mensagem em formato texto, enviando-a através de HTTP. Como já sabemos, esse binding com esta codificação pode ser facilmente interoperável com serviços baseados no protocolo SOAP 1.1, como é o caso dos ASP.NET Web Services (ASMX). Esse tipo de codificação também pode ser facilmente interceptado por qualquer ferramenta de monitoramento de requisições HTTP, como é o caso do Fiddler.

Uma inovação que está presente a partir da versão 3.0 do Silverlight, é o suporte a codificação binária do envelope SOAP que é transmitido entre o serviço e o cliente, e vice-versa. Apesar de, em alguns casos, diminuir o tamanho da mensagem trafegada, a grande finalidade deste tipo de codificação é ter uma velocidade maior entre a transferência das informações. Com esse benefício, esse tipo de codificação passa a ser o padrão quando criar um projeto baseando-se na template Silverlight-enabled WCF Service.

Para que isso funcione, temos um novo binding element chamado BinaryMessageEncodingBindingElement. Com a criação de um binding customizado, já podemos fazer a utilização do mesmo e, consequentemente, de seus benefícios. O código abaixo ilustra a sua utilização, configurando o binding através do arquivo de configuração:

<bindings>
   <customBinding>
      <binding name=”binaryHttpBinding”>
         <binaryMessageEncoding />
         <httpTransport />
      </binding>
   </customBinding>
</bindings>

Apenas fique atento para utilizar o mesmo codificador dos dois lados. Caso isso não aconteça, não será possível fazer a comunicação. Para finalizar, é importante dizer que ao utilizar esse codificador, não iremos mais conseguir monitorar as requisições e analisar o seu conteúdo. Enquanto estiver em ambiente de desenvolvimento, talvez seja melhor manter a codificação baseada em formato texto, para conseguir depurar facilmente eventuais problemas que venham a acontecer.