Desenvolvimento - WCF/WPF

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.

por Israel Aéce



Baixe o código-fonte.

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"/>
</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"
binding="basicHttpBinding"
contract="*" />
<endpoint name="Servico2"
address="http://localhost:9999/clientes"
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" />
<filter name="FiltroServico2"
filterType="Action"
filterData="http://tempuri.org/IClientes/Notificar" />
</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.

Israel Aéce

Israel Aéce - Especialista em tecnologias de desenvolvimento Microsoft, atua como desenvolvedor de aplicações para o mercado financeiro utilizando a plataforma .NET. Como instrutor Microsoft, leciona sobre o desenvolvimento de aplicações .NET. É palestrante em diversos eventos Microsoft no Brasil e autor de diversos artigos que podem ser lidos a partir de seu site http://www.israelaece.com/. Possui as seguintes credenciais: MVP (Connected System Developer), MCP, MCAD, MCTS (Web, Windows, Distributed, ASP.NET 3.5, ADO.NET 3.5, Windows Forms 3.5 e WCF), MCPD (Web, Windows, Enterprise, ASP.NET 3.5 e Windows 3.5) e MCT.