Os Elementos do Sistema de Identidade

No artigo anterior, falamos um pouco sobre os problemas conhecidos quando lidamos com a autenticação e autorização em uma aplicação, e no final dele, falei superficialmente sobre os produtos que a Microsoft tem desenvolvido para tornar a utilização do modelo baseado em claims mais simples. A partir de agora, vamos analisar os elementos que compõem o sistema de identidade, analisando o fluxo e alguns conceitos que circundam este modelo, e que é independente de tecnologia.

Tudo o que veremos a seguir é conhecido como Identity MetaSystem, que consiste em uma infraestrutura que abstrai todas as operações necessárias para promover tudo o que é preciso para suportar identidades em cima da internet, fornecendo uma arquitetura interoperável, que permite as mais variadas plataformas implementar e dar suporte à este modelo. Existem três grandes elementos que fazem parte dele, e que são responsáveis pela propagação das identidades, a saber:

  • Identity Providers: É o responsável pela validação e emissão de tokens, que são fornecidos para alguém, contendo um conjunto de claims.
  • Relying Parties: A aplicação que recebe e usa esses tokens que são emitidos por algum Identity Provider.
  • Subjects: É alguma coisa ou alguém que possui uma identidade digital (e suas claims), e que na maioria das vezes, representará um usuário.

Cada produto desenvolvido pela Microsoft tem como alvo um dos elementos acima, sendo o ADFS 2.0 uma espécie de identity provider; já o Cardspace permite o gerenciamento das identidades de um subject e, finalmente, o WIF ajuda na construção de aplicações que recebem esses tokens. Como dito anteriormente, todos esses elementos seguem padrões de mercado, e que são rigidamente gerenciados por órgãos independentes. Os protocolos que são utilizados para troca de informações foram desenhados para cruzar limites, que antes deles, eram invioláveis (plataforma e firewalls).

Outro elemento importante que faz parte deste modelo é o Security Token Service (STS). Como o próprio nome diz, trata-se de um serviço que está dentro de um identity provider, e que é responsável por emitir e empacotar as claims que são geradas para alguém, seguindo alguns padrões de mercado que analisaremos mais tarde, ainda neste artigo.

Há algumas técnicas que podem ser utilizadas para criar ambientes baseados em claims. Por exemplo, aplicações web e serviços SOAP (WCF) podem ser considerados relying parties, pois podem fazer uso de claims fornecidas por algum identity provider em nome de alguém. Já o navegador (browser) e aplicações Smart Client, são consideradas os subjects, já que são os responsáveis por gerenciar o fluxo do processo de autenticação, que direta ou indiretamente, irão direcionar o usuário para efetuar a sua autenticação no identity provider em que a aplicação confia.

Quando utilizamos este novo modelo, temos dois ambientes, onde cada um deles trabalha de forma ligeiramente diferente. Esses ambientes são conhecidos como ambiente passivo e ativo. A partir daqui, vamos analisar cada um desses ambientes, tentando abordar como esse modelo trata cada um deles, sem abordar as tecnologias que estão envolvidas. Para ilustrar melhor como cada um deles funciona, vamos analisar a imagem abaixo:

No ambiente passivo, o subject é o browser, enquanto a relying party é uma aplicação web. Quando um usuário requisita uma página que está protegida (1), a aplicação faz essa verificação e nota que o usuário não está autenticado. Com isso, a aplicação redireciona o usuário para o identity provider em que ela confia (2). Este, por sua vez, faz a autenticação do usuário. Aqui não importa o modelo de autenticação que é utilizado (Windows, UserNames, certificados, etc.). Depois de devidamente autenticado, o identity provider retorna o token correspondente aquele usuário (3), que a partir de agora, enviará esse token para todas as requisições subsequentes (4). Como a verificação da existência do token acontece em todas as requisições, uma vez que o usuário estiver autenticado, ele não será mais redirecionado para o identity provider.

Já no ambiente ativo, o fluxo muda um pouco. Neste cenário, o subject é uma aplicação Smart Client, enquanto a relying party é um serviço SOAP (WCF). O primeiro passo passa a ser a autenticação no respectivo identity provider (1), que uma vez autenticado, um token é emitido para aquele usuário (2). Com isso, todas as requisições irão embutir em seus respectivos headers, o token deste usuário (3). Como a relying party também conhece e confia naquele mesmo identity provider, então o acesso às operações do serviço será garantido.

É importante dizer que no ambiente ativo, o processo acaba sendo mais rápido quando comparado com o ambiente passivo, pois o cliente não precisa visitar o serviço para saber qual é o identity provider que o mesmo utiliza. Isso já aconteceu durante a referência do serviço na aplicação Smart Client, que traz, além da descrição do serviço em si, informações inerentes ao identity provider em que o serviço confia.

Identity Federation

O que vimos acima consiste nos cenários onde todos os participantes estão dentro de um mesmo domínio. Mas um dos principais cenários que este modelo atende, é justamente quando temos os participantes do sistema separados, em domínios diferentes. Da mesma forma que vimos anteriormente, a aplicação ou serviço também poderá aceitar claims que são emitidas por outros identity providers, que estão além do seu domínio, mas que indiretamente há uma relação de confiança estabelecida entre eles. A possibilidade de integração entre os dois domínios também é conhecida como Identity Federation.

Utilizar esta técnica, facilitará muito a possibilidade de SSO (Single Sign-On), que é a possibilidade de se autenticar uma única vez, e reutilizar aquela mesma credencial por todas as aplicações e serviços (relying parties) que aquele usuário utiliza, mesmo que essas aplicações estejam hospedadas nos servidores dos parceiros da nossa empresa. Outro grande ponto positivo desta opção, é que se o usuário não fizer mais parte da minha empresa, tudo o que eu preciso fazer, é remover/desabilitar a sua respectiva conta no meu domínio e, consequentemente, ele não terá mais acesso aos parceiros, já que os parceiros somente confiam em usuários que possuem os tokens emitidos por mim.

Da mesma forma que vimos acima, “cenários federados” também suportam os ambientes passivo e ativo, com mudanças simples no fluxo das informações. E para clarear como o fluxo ocorrerá, vamos utilizar imagens para ilustrar, começando com o ambiente passivo:

Note que o usuário, que está no domínio 1, tenta acessar uma aplicação web que está hospedada no domínio 2 (1). A aplicação detecta que o usuário ainda não está autenticado, e o redireciona para o identity provider do domínio onde a aplicação está hospedada, que é o orgão que ela confia. Como parte deste redirecionamento, o identity provider do domínio 2 conhece o identity provider do usuário, que está no domínio 1. Isso fará com que o usuário seja novamente redirecionado para o identity provider que o conhece, que é aquele que está debaixo do domínio 1 (2). Ao validar o usuário, o token que o representa dentro da empresa é emitido (3). Depois disso, o token gerado no domínio 1, é encaminhado para o identity provider do domínio 2 (4), que o validará e fará eventuais transformações, criando ou mapeando as claims para claims que a aplicação espera. Isso fará com que um novo token seja emitido (5), e que será utilizado pelo usuário para enviar para as requisições subsequentes (6).

Da mesma forma, o ambiente ativo segue o mesmo fluxo do ambiente passivo, apenas evitando o handshake necessário no ambiente passivo, que é necessário para descobrir qual é o identity provider responsável pela autenticação. Assim como comentado acima, todas as informações necessárias já foram fornecidas durante a referência do serviço, e as requisições (5) já enviarão o token embutido em seus headers.

Observação: Acima vimos os ambientes passivo e ativo sem mencionar os detalhes de implementação de cada um deles. Isso será alvo de futuros artigos, que irão detalhar cada um dos ambientes, mostrando como proceder para configurar cada um deles.

Os Padrões

Uma das principais características que este modelo autenticação e autorização deve ter, é a interoperabilidade entre as partes envolvidas. Como a ideia permitir uma espécie de layer na internet para gerenciar toda a segurança, inclusive entre domínios, uma das principais exigências é a garantia de que toda e qualquer plataforma pudesse tirar proveito disso.

Atualmente já há várias especificações que regem grande parte das comunicações distribuídas entre plataformas, e que são conhecidas como padrões WS-*. Os padrões que compõem as especificações WS-* já estão há bastante tempo no mercado, e são gerenciados por órgãos independentes. Há padrões para grande parte das necessidades que temos hoje em dia nos sistemas distribuídos, tais como: transações, mensagens confiáveis e segurança. Como esses padrões já estão quase todos finalizados, então porque não utilizá-los? É justamente isso que ocorreu, ou seja, grande parte de toda a comunicação que é efetuada entre as partes deste modelo de autenticação, são realizadas seguindo esses padrões, mas felizmente são transparentes para o usuário final, e abstraídas através dos produtos criados pela Microsoft. Abaixo temos esses padrões elencados, com uma breve descrição de cada um deles:

  • WSDL: Descreve quais funcionalidades (operações) são expostas pelo serviço.
  • WS-Policy: O WSDL não descreve nada sobre os requerimentos de segurança necessários para invocar o serviço. O WS-Policy aborda este caso, fornecendo uma forma genérica de descrever os requerimentos de segurança relacionados ao respectivo serviço.
  • WS-Security: Basicamente, este padrão especifica como aplicar a criptografia nas mensagens SOAP, fazendo com que as informações trafeguem de forma protegida, garantindo a confidencialidade e integridade. A forma de segurança aplicada aqui está condicionada ao formato de autenticação que você utilizará entre o subject e o identity provider. Como a interoperabilidade é um dos aspectos mais importantes, o WS-Security efetua a criptografia das mensagens através de token profiles, que descreve como mapear as tecnologias de autenticação que temos atualmente (KerberosUserNames, certificados, etc.), para um modelo genérico.
  • WS-SecurityPolicy: Fornece um padrão para representar as capacidades e requerimentos dos serviços através de políticas.
  • WS-Trust: Este padrão fornece extensões para o padrão WS-Security, definindo operações específicas de emissão, renovação e validação de tokens.
  • WS-Federation: Organiza em uma linguagem de mais alto nível os padrões WS-Trust e WS-Security, definindo mecanismos para permitir que a autenticação e autorização sejam feitas entre domínios.
  • WS-MetadataExchange: Permite uma forma de extrair, não somente os dados que descrevem as operações do serviço, mas também todas as funcionalidades (de infraestrutura) expostas por aquele serviço.

Além dos padrões acima, ainda temos o SAML (Security Assertion Markup Language). Enquanto o padrão WS-Security define como inserir as informações dentro do envelope SOAP, o SAML ajuda a definir o que essas informações são. O SAML é uma especificação, também baseada em XML, que permite alguém emitir afirmações a respeito de outro alguém ou para alguma coisa, independentemente da identidade que está sendo utilizada. Entre essas afirmações que fazemos sobre algo, elas também podem descrever os atributos deste alguém, que são também conhecidos como claims.

Como disse acima, o padrão WS-Trust fornece um contrato com quatro operações: Issue, Validate, Renew e Cancel. Cada uma dessas operações autoexplicativas, manipulam tokens que são gerados por um STS para seus subjects. Em todos os ambientes que vimos acima (passivo e ativo), em algum momento há um diálogo entre o subject e o STS do identity provider. Nestes casos, o diálogo é realizado para a emissão (Issue) de um token, que depois de emitido será utilizado para enviar para a aplicação (relying party) que está requerendo. Cada uma das operações expostas troca mensagens conhecidas como RST (Request Security Token) e RSTR (Request Security Token Response), especificadas pelo WS-Trust, que entre várias informações, incluem o tipo de token a ser emitido (que nada mais é que a versão do SAML que está sendo utilizada) e as claims que estão sendo solicitadas pela relying party.

Conclusão: No decorrer neste artigo, analisamos os ambientes possíveis que temos quando trabalhamos com este modelo de autenticação, incluindo o cenário federado. Além disso, analisamos como o fluxo das mensagens acontece nestes ambientes, detalhando em alto nível, os passos necessários para atingir o objetivo. Em futuros artigos, vamos explorar detalhadamente como implementar esse modelo em cada um cenários, analisando como configurar e manter um sistema que faz uso destes elementos.

Uma nova forma de Autenticação/Autorização

Grande parte das aplicações que desenvolvemos nos dias de hoje, exigem um alto nível de segurança, o que nos obriga a desenhar um sistema consistente e evolutivo, que permite de forma simples, manipulá-lo de acordo com as necessidades que surgem durante a vida do mesmo. Isso faz com que todos os desenvolvedores, além de se preocuparem com as regras de negócio, ainda precisam saber como lidar com dois aspectos importantes de toda aplicação: autenticação e autorização.

A autenticação consiste em saber quem o usuário é identificá-lo dentro do sistema; já a autorização, determina quais privilégios esse usuário tem dentro do sistema, e para isso, obrigatoriamente deverá ocorrer depois da autenticação, já que primeiro preciso saber quem ele é para depois conseguir encontrar os direitos que ele possui dentro do sistema.

Isso não seria uma tarefa difícil se não houvesse vários mecanismos diferentes, e que permitem configurar e gerenciar tanto a autenticação quanto a autorização. Cada um destes mecanismos tem como alvo um cenário distinto, e é complicado achar uma solução que atenda à todos os cenários em que a aplicação está exposta, tornando extremamente complicado gerenciar esses aspectos em um ambiente mais amplo, e que muitas vezes acarreta em duplicação de código e possíveis vulnerabilidades.

Além disso, é importante dizer que para cada forma de autenticação e autorização que utilizamos, temos que entender o mínimo sobre como ela funciona. Como disse antes, há varias opções que temos atualmente, como por exemplo: Kerberos, Forms Authentication, Url Authorization, certificados, etc. Cada uma delas tem suas peculiaridades, e exige de nós um entendimento mínimo para saber qual desses modelos se encaixa melhor com a nossa necessidade.

Em uma análise rápida, o Kerberos é a melhor opção se queremos autenticar um usuário que consome uma aplicação ASP.NET dentro da empresa, mas não traz nenhuma informação extra além de seu token. Esse modelo é fácil de implementar até que alguém, fora da empresa, também precise acessar essa mesma aplicação. Como esse modelo exige que o usuário esteja devidamente cadastrado no Active Directory da respectiva empresa, pessoas que estão além dela, não conseguirão acessá-la, e como sabemos, manter contas para usuários “voláteis”, não é uma tarefa viável. Para entender a complexidade em que vivemos atualmente, veja a imagem abaixo:

Note que dentro do quadrado verde temos uma empresa qualquer, que por sua vez, possui vários funcionários. Cada funcionário, toda manhã, efetua o login no Windows, que vai até o controlador de domínio (Active Directory), valida o usuário, e caso seja válido, permite o acesso ao sistema operacional. Felizmente, na empresa que temos como exemplo acima, os desenvolvedores se preocuparam em reutilizar as credenciais do Windows para as aplicações que rodam dentro dela, onde uma delas é uma aplicação Windows (chamada de App) e que consome um serviço WCF, e a própria intranet, hospedada no IIS, e que também utiliza autenticação integrada ao Windows.

Agora repare que este mesmo usuário também acessa outras aplicações, que estão fora do domínio da empresa em que ele trabalha. O acesso a loja para comprar presentes, possui seu próprio sistema de autenticação e autorização, obrigando este usuário a criar uma conta ali. Já quando ele tenta acessar o site do banco, um certificado é exigido para que ele proceda com o login. E, como se não bastasse, a empresa em que ele trabalha mantém relações comerciais com outros parceiros, e cada um desses parceiros possui um sistema próprio de pedidos, e como já era de se esperar, cada um possui seu próprio esquema de autenticação e autorização.

Mas note que no caso do parceiro 1, a aplicação somente permite acesso para aqueles usuários que estão cadastrados dentro do AD da respectiva empresa, sendo assim, como acessá-la? Na maioria das vezes, o parceiro 1 cria uma aplicação simples, baseada em internet, e permite o acesso aos parceiros. Mas o que vai acontecer é que o parceiro 1 precisará, novamente, de uma nova forma para controlar os usuários que irão acessar tal aplicação, e muito provavalmente, irá recorrer à uma base de dados customizada para isso (se a aplicação for ASP.NET, podemos recorrer ao Membership). Mas essa solução dá início a outro problema, que ocorrerá no momento em que este usuário não fizer mais parte da minha empresa, pois terei que lembrar à quais parceiros ele tinha acesso, para assim removê-lo, já que ele não poderá continuar acessando, e isso ainda obrigará ao parceiro, criar uma interface (tela) para a administração de usuários.

Deixar para cada um implementar, de forma arbitrária, o modelo de autenticação e autorização, dá margem para que os desenvolvedores façam isso de qualquer modo, não se preocupando com pontos importantes, como por exemplo, o armazenamento das senhas de forma segura (hash), tráfego das credenciais através de um protocolo que não permita ninguém interceptar, evitar ataques de brutal-force, DoS, etc. Além destes problemas de infaestrutura, ainda temos as políticas de senhas, já que muitas aplicações não asseguram que a senha escolhida pelo usuário seja uma senha segura; a reutilização de senhas é um problema grave, pois para evitar o esquecimento, muitas pessoas utilizam a mesma senha para todas as aplicações que elas utilizam, e uma vez que isso cai em mãos erradas, o estrago pode ser irreparável. A reutilização é fácil até que você encontre aplicações com políticas de senha diferentes, pois o que pode ser uma senha válida para uma aplicação, não é para outra, obrigando a você escolher outra senha que se enquadre com as políticas desta aplicação, voltando assim, ao inferno das senhas diferentes.

Outro grande desejo que existe e que é difícil de implementar, é a capacidade de termos SSO (Single Sign-On) entre as aplicações, ou seja, a capacidade que damos ao usuário de se autenticar uma única vez, e assim poder acessar todas aquelas aplicações que confiam naquele autenticador. Algo mais ou menos como o que acontece com o Windows Live Id, onde você se autentica uma única vez, e tem acesso à todos os sites da Microsoft que são protegidos por ele.

Qual seria então o modelo ideal para resolver grande parte dos problemas que vimos acima? O modelo ideal seria a possibilidade de centralizar a autenticação em um único local, que determinará como ela deverá ser realizada e protegida. A centralização evitará com que toda a aplicação se preocupe com a lógica de autenticação, já que não competirá mais a ela esse papel. Os desenvolvedores podem se concentrar apenas no desenvolvimento da aplicação em si, com as regras de negócios, interfaces (telas), serviços, etc. Com a autenticação “externalizada”, não precisamos mais misturar códigos de infraestrutura com códigos de regras de negócios, tornando a aplicação muito mais limpa, e, além disso, outro grande benefício com a refatoração é a facilidade que teremos em dividir o que compete ao desenvolvedor e o que compete ao administrador da rede.

O modelo que visa esse cenário é conhecido como Claims-Based Identity Model, ou seja, modelo de identidade baseado em claims, que traduzindo significa “afirmações”. Nesse modelo, as aplicações que desenvolveremos não serão mais responsáveis por autenticar e muito menos por armazenar suas senhas. Para fazer uma analogia ao mundo real de como esse modelo trabalha, podemos recorrer ao exemplo da compra de algum produto através do cartão de crédito. Quando você vai pagar por esse produto, você fala que vai pagar com o cartão de crédito, mas a loja não confia em você, quando você diz: “Olha, eu tenho R$ 2.000,00 de crédito, pode me vender!”. Na verdade, o que a loja faz é “validar” aquele cartão que você apresentou, junto aquele que o emitiu, que é a operadora. É ela quem dirá se aquele cartão é ou não válido e dizer até o quanto você poderá gastar. A loja utilizará essas informações para proceder ou não com a venda.

Neste cenário descrito acima, note que a loja nada sabe sobre o cliente, mas confia em alguém que o avaliza. A partir daí, a loja geralmente faz o cadastro do cliente, colhendo o seu e-mail, endereço, etc., para mais tarde conseguir enviar um catálogo dos novos produtos. Da próxima vez que você vai comprar novamente a validação é realizada na operadora do cartão, mas seus dados cadastrais já estão catalogados, podendo ser apenas atualizados.

Outro ponto importante deste cenário, é que a autorização ainda deve ser realizada pela aplicação, ou seja, ela ainda é responsável por determinar o que usuário pode ou não acessar. Mas atualmente, os direitos de acessos que concedemos à determinadas funcionalidades dentro da aplicação, geralmente são determinadas por grupos, ou seja, se o usuário fizer parte do grupo Administradores, então ele pode ter acesso às contas corrente dos clientes. Mas no cenário descrito acima, ele vai além disso, já que a autorização é concedida baseando-se no limite do cartão de crédito.

Isso é mais um dos benefícios das aplicações baseadas em claims. As afirmações emitidas por alguém contra outro alguém, pode ser qualquer tipo de informação, e você refina o acesso através delas. Muitas vezes a autorização vai muito além dos grupos (simples strings) que o usuário pertence, pois há situações em que podemos negar ou conceder acesso de acordo com a data de nascimento, ou seja, se ele tiver mais do que 21 anos, então poderá acessar o conteúdo.

Para fazer com que tudo isso funcione, a Microsoft tem trabalhado em três produtos que, juntos ou separadamente, fornecerão grande parte do que precisamos para atingir o nosso objetivo, que é a centralização e o gerenciamento do processo de autenticação. Cada um desses produtos, implementam padrões de mercado, e por isso podem ser utilizados separadamente, ou melhor, podemos ter uma dessas ferramentas Microsoft falando com outras desenvolvidas pela IBM utilizando Java, pois todas falam o mesmo “idioma”. A Microsoft criou a solução completa, composta de três produtos que estão listados abaixo, com suas respectivas descrições:

  • WIF – Windows Identity Foundation: Trata-se de um framework para a construção de aplicações baseadas em claims, e que abstrai toda a complexidade do processo de autenticação que é realizado seguindo uma série de padrões de mercado (WS-*). Além disso, a sua API traz uma série de tipos que habilitam a extensibilidade, podendo mais tarde, criar novas opções de autenticação.
  • ADFS 2.0 – Active Directory Federation Services 2.0: Uma ferramenta que ajuda os administradores de rede, a gerenciar o ambiente de autenticação que será fornecido por aquelas aplicações que queiram fazer uso do modelo baseado em claims. Essa ferramenta implementa os mesmos protocolos do WIF, permitindo assim que eles interajam.
  • Windows Cardspace 2.0: Uma ferramenta que é instalada nos clientes, como uma espécie de carteira virtual, que cataloga os cartões que podem ser enviados para as aplicações baseadas em claims. Trata-se de uma interface amigável que permite ao usuário final uma visualização simples do que ele tem e do que será efetivamente enviado à aplicação, podendo ele decidir se deve ou não proceder.

Conclusão: Com as informações acima, pudemos entender um pouco mais dos cenários de complexidade que possuímos e dos problemas que temos atualmente. Cada cenário exige um modelo diferente, mas que não atende a 100% dos casos. A finalidade deste artigo introdutório foi apresentar os desafios que temos, e quais soluções estão sendo oferecidas pela Microsoft, para poder tornar essa tarefa extremamente importante, em algo bem mais simples e reutilizável do que temos hoje em dia. A partir de uma série de artigos, vamos explorar cada uma das ferramentas e elementos que compõem este novo modelo de autenticação.

Tecnologias que circundam o WCF

Em 2006 a Microsoft lançou a versão 3.0 do .NET Framework, que nada mais era do que “grandes blocos” que foram adicionados ao 2.0. Entre esses grandes blocos, temos o WCF. Como todo mundo sabe, ele é o novo pilar para comunicação dentro da plataforma .NET. A estrutura deste framework, facilitou a entrada de novos produtos, também criados pela Microsoft, para atender cenários específicos.

Isso acaba facilitando bastante, já que grande parte da complexidade do WCF acaba sendo abstraída do desenvolvedor. Depois do .NET Framework 3.0, veio a versão 3.5, que incorporou novas funcionalidades, e mais tarde, no PDC 2009, a Microsoft publicou novos serviços, construídos em cima do WCF. Atualmente temos os seguintes tipos de serviços disponíveis:

  • Serviços SOAP: É o WCF em si. Possibilita a construção de serviços baseando-se em padrões de mercado, que tentam manter a interoperabilidade entre várias plataformas ou com outras tecnologias, como COM+, MSMQ, .NET Remoting, etc. Esses padrões regem transações, segurança, entre outras funcionalidades. A idéia aqui é permitir a construção de serviços orientado à operações que você precisa expor ao mundo, através dos mais diversos protocolos.
  • Serviços WebHttp: A partir da versão 3.5, a Microsoft trouxe a capacidade de construir serviços REST dentro do WCF. Usando métodos como POST, GET, PUT, etc., em conjunto URLs (onde você pode formatar do jeito que desejar), temos a flexibilidade de expor operações para serem consumidas diretamente, sem envolver essas requisições em envolopes SOAP, facilitando assim o consumo por aplicações AJAX, por exemplo.
  • Serviços para Dados: Semelhante a anterior, mas a idéia é expor via REST as informações contidas em um banco de dados. Inicialmente levava o nome de ADO.NET Data Services, mas depois do PDC foi renomeado para WCF Data Services.
  • Serviços de Workflow: Basicamente, a ideia é permitir que um workflow (construído pelo Windows Workflow Foundation (WF)) possa ser consumido e coordenado por serviços WCF. Situações onde você tem operações que possuem uma longa duração, a necessidade de manter o estado entre chamadas, esse tipo de serviço poderá ajudar.
  • Serviços RIA: WCF RIA Services estará disponível juntamente com o Silverlight 4.0, e simplificará a forma como você escreverá uma aplicação N-tier, onde o cliente será o próprio Silverlight.

Granularidade de Serviços

Independentemente de qual tecnologia estamos utilizando, uma das grandes perguntas que nos fazemos ao criar serviços, é o que realmente devemos disponibilizar em cada um deles, que ao meu ver, vai muito além os parâmetros e do resultado que cada operação recebe e/ou retorna. Existe uma série de aspectos que devemos nos atentar ao projetar ou construir um conjunto de serviços, que muitas vezes atenderão as aplicações que rodam dentro dessa mesma companhia.

O primeiro aspecto que temos que verificar é a questão da granularidade dos serviços, que é utilizada para mensurar a profundidade de abstração que foi aplicado. A granularidade pode ser dividida em duas partes, sendo: granularidade fina (fine-grained) e granularidade grossa (coarse-grained), onde granularidade fina determina que precisamos de muitos “grãos”, enquanto na granularidade grossa, teremos poucos “grãos”, bem maiores.

O que eu quero mostrar com o parágrafo acima, é que com a granularidade fina, teremos serviços com poucas operações, mas dividiremos essas operações por vários serviços. Já com a granularidade grossa, isso se inverte, ou seja, teremos poucos serviços, mas cada um deles conterá uma porção bem maior de operações. Cada uma das técnicas tem suas vantagens e desvantagens, e ao meu ver, quando temos uma granularidade fina, temos pequenos “blocos” de funcionalidades bem específicas e muitas vezes independentes, e que ficam bem mais fáceis de serem atualizadas, distribuídas e gerenciadas, mas isso pode se tornar complexo demais para aqueles que consomem os serviços, já que terão que compor e sincronizar suas operações, para que atinja um determinado objetivo. Por outro lado, a granularidade grossa pode tornar os serviços mais auto-suficientes, mas o problema disso é que cada serviço poderá, acidentalmente, fazer muito mais trabalho do que ele realmente deveria, e que muitas vezes precisará recorrer à outros serviços, para que, também, atinjam o seu objetivo, aumentando assim o acoplamento.

Acoplamento é um outro problema que pode ocorrer, que nada mais é do que a – forte – dependência que um serviço tem de outro. Isso infringe um dos princípios do SOA, que diz que os serviços precisam ser autônomos, ou seja, não depender de outros serviços. Mas em algumas situações isso pode ser benéfico, principalmente em um ambiente de composição. Imagine que você queira criar um serviço para resolver um problema maior, com uma complexidade muito grande. Ao invés de todos os consumidores acessarem esses serviços e ficar sob responsabilidade de cada um organizar isso, você poderá criar um serviço para compor essas tarefas que, como dissemos acima, precisarão recorrer à outros serviços.

Esses conceitos que vimos acima não são novidades. Eles já são (ou deveriam ser) aplicados na programação orientada à objetos, exatamente para termos os mesmos benefícios. Essas características não são exclusividades da computação, pois podemos adotar esses mesmos princípios no nosso dia-à-dia.

Outro grande ponto a ser considerado na construção de serviços, é a criação de serviços com interfaces CRUD (Create, Read, Update e Delete). A proposta destes tipos de serviços é permitir, na maioria das vezes, a manipulação de registros dentro de uma determinada base de dados. Nestes casos, o serviço será apenas uma espécie de wrapper para os dados, não fazendo nada além do que as operações básicas que todo banco de dados possui (INSERT, SELECT, UPDATE e DELETE).

Quando você trabalha com uma aplicação data-centric, onde a toda a regra se concentra em manipular a base de dados, talvez esses tipos de serviços sejam úteis. Considere aqui o uso do ADO.NET Data Services, que evitará conhecer e criar toda a estrutura necessária para expor via WCF.

Mas o ideal é não ter isso em mente ao construir serviços. Interfaces (contratos) CRUD induzem à granularidade fina, onde cada serviço será responsável por manipular uma determinada entidade/tabela. Como vimos acima, a granularidade fina em si não é o problema. A questão aqui é que muitas vezes, um cadastro de um cliente não consiste apenas em um INSERT na base de dados, ao contrário, vai muito além disso.

Imagine um novo cliente que deseja abrir uma conta bancária em um determinado banco, com um cartão de crédito vinculado. O processo de cadastro do cliente consistirá em:

  1. Validar os dados, como idade, renda, endereço, etc.;
  2. Consultar outras instituições financeiras para se certificar de que ele é um bom pagador;
  3. Definir o limite que ele terá no cheque especial;
  4. Inserir o cliente na base de dados;
  5. Criar a conta corrente para este cliente;
  6. Efetuar o lançamento da taxa de cadastro/abertura na conta corrente recém criada;
  7. Comunicar com o serviço de cartões de crédito, para que ele gere um novo cartão para este cliente;
  8. Notificar outros departamentos do banco de que uma nova conta foi aberta, para oferecimento de novos produtos.

Como podemos perceber, o cadastro de um novo cliente não consiste apenas em adicioná-lo na base de dados. Há muito mais do que isso. Se modelarmos nossos serviços orientado à dados, eu teria um serviço que manipula os clientes, outro serviço que manipula a conta corrente, outro de notificação e por aí vai. Quanto mais entidades/tabelas você tiver envolvidas em uma mesma tarefa, mais complicado ficará para gerenciar tudo isso, principalmente do ponto de vista daquele que consumirá esses serviços.

O consumo de serviço é um processo caro para se fazer à todo momento, e neste cenário, para efetuar o cadastro de um cliente, eu precisarei chamar, no mínimo, quatro serviços e tudo o que eu precisarei passar para eles, é exatamente os dados do cliente que eu desejo avaliar/cadastrar. Outro ponto importante é com relação ao fluxo de informações. Neste caso, fica sempre sob responsabilidade do cliente que consome esses serviços, configurar a ordem de chamadas, e em um ambiente onde múltiplas aplicações podem incluir clientes, eventualmente uma delas poderá alterar essa ordem, fazendo com que o processo fique em um estado inválido, comprometendo assim a veracidade e consistência das informações. Nada impedirá que uma pessoa maliciosa invoque apenas o serviço de cadastro de cliente diretamente, sem passar pelas políticas de validação necessárias.

Quando temos várias “sub-tarefas” que se juntam para algo maior, em muitos casos queremos garantir a atomicidade, que garantirá que todos os passos sejam efetuados com sucesso, ou tudo falhará. O que garante a atomicidade são as transações, e transações distribuídas são caras, e todas as chamadas para esses serviços devem estar envolvidas dentro dessa transação, que será, também, coordenada pelo cliente, que será o responsável por avaliar se tudo deu certo. Se sim, ele efetivará (Commit), do contrário, irá desfazer (Rollback).

Um segundo cenário que também ilustra isso: você possui clientes e cada um deles possui um flag que determina a situação dele dentro da sua empresa: Ativo, Bloqueado, EmProcessoJuridico, etc. Da mesma forma que vimos antes, alterar situação dele vai muito além de um simples comando de UPDATE na base de dados. Quando eu mover um determinado cliente para a situação de EmProcessoJuridico, eu terei que inserir um item no histórico deste cliente, desativar algumas opções que o mesmo tem site, e alterar a sua situação na tabela do banco de dados. Se movê-lo para Bloqueado, terei que inserir um item no seu histórico, efetuar um lockdown em todas as contas de acesso desse cliente no site, notificar o gerente responsável e, finalmente, efetuar o UPDATE da coluna onde armazeno a situação atual na tabela de clientes.

Como podemos perceber, interfaces CRUD definem os serviços como sendo data-centric, que modelando dessa forma, nós perderemos o contexto de negócio que será executado pelo cliente, tornando bem mais complicado de se entender o processo como um todo. Ao modelar os serviços, o ideal seria pensar em task-centric, ou seja, o serviço fornecerá operações que englobam grande parte do processo, não sendo o consumidor o responsável por isso. Nos dois cenários que vimos acima, teríamos um serviço de cliente, e que me forneceria uma operação para criar um novo cliente dentro do banco (IncluirNovoCliente), outra operação para bloquear o cliente (Bloquear), outro para criar um processo contra este cliente (AbrirProcessoJuridico), que o moverá para a situação EmProcessoJuridico, e assim por diante.

Note que as operações que serão expostas pelo serviço expressarão claramente o negócio, fazendo internamente tudo o que for necessário para atingir o respectivo objetivo. O interessante é que neste caso, não temos o overhead de chamar N serviços, problemas de fluxo e, principalmente, evitando transações distrubuídas.

Claro que poderá haver situações em que, mesmo que você utilize o modelo task-centric, os teus serviços estarão, coincidentemente, alinhados à interfaces CRUD, mas o importante é que isso apenas seja uma coincidência e não uma regra. Isso muitas vezes acontece quando as regras não estão tão aparentes. Por exemplo, se você mantém um cadastro de cidades e permite a alteração delas, provavelmente você poderá ter: 1 – Valinhos e 2 – Campinas. Os clientes cadastrados na sua base de dados e que são de Valinhos guardam o número 1, e os de Campinas, o número 2. Se agora, você diz que Valinhos será o 2 e Campinas o 1, você corromperá todos os clientes que fazem uso dessas cidades. Poderia haver aqui uma regra que, ao efetuar a alteração da cidade (swap), você atualizasse todos os clientes relacionados.

É importante dizer que serviços task-centric, em algum momento, precisarão efetuar operações de CRUD dentro da base de dados. Com isso, podemos criar duas categorias de serviços: Task Service e Entity Service. Task Services são os tipos de serviços que vimos acima, que serão responsáveis por orquestrar toda a regra de validação, fluxo, manipulação e persistência das informações. É neste ponto que entra em cena os Entity Services, que são responsáveis por gerenciar a vida/estado de uma entidade específica, incluindo seus respectivos relacionamentos, tendo esses serviços, uma interface semelhante à interface CRUD.

Os Entity Services levam o nome de uma entidade, como por exemplo: Cliente, ContaCorrente, PoliticasDeValidacao, etc., não fazendo nada além do que o nome diz, ou seja, disponibilizará apenas as informações referentes à respectiva entidade, não conhecendo nada sobre negócios. Já os nomes dos Task Services são voltados para o negócio em si: AdministracaoDeClientes, GestorDeCredito, CobrancaDeTitulos, etc. E ainda, os Task Services poderão utilizar um ou vários Entity Services para executar uma determinada tarefa, atentanto-se sempre aos conceitos que vimos acima, como é o caso do baixo/alto acoplamento e a granularidade.

Tudo o que foi falado aqui poderá, em alguns cenários, não ser a melhor opção, mas utilizar esse tipo de visão para a construção de serviços, ajudará a ter e criar uma representação muito mais consolidada de sua estrutura, e que será relativamente fácil de gerenciar, mas que refletirá, virtualmente, o seu negócio.

Plataforma de Identidade

A Microsoft anunciou hoje os nomes oficiais do projeto “Geneva”:

Geneva Server -> Volta a ser Active Directory Federation Services – ADFS
Windows Cardspace Geneva -> Volta a ser Windows Cardspace
Geneva Framework -> Windows Identity Foundation – WIF

Já há a lista aqui de artigos que estou preparando para introduzir, desenvolver e fazer uso destas tecnologias em aplicações .NET.

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)

WCF – WS-Discovery

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

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

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

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

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

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

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

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

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

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

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

Consumindo o Serviço

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Notificação através de Announcements

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

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

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

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

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

    Console.ReadLine();
}

Extensions

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

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

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

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

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

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

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

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

WSDiscovery.zip (14.63 kb)

Migrando de ASMX para WCF

Junto com a primeira versão do Visual Studio .NET e do .NET Framework, temos a possibilidade de criarmos serviços Web, baseados em XML e utilizando a tecnologia ASP.NET Web Services (ASMX). Isso ainda continua disponível nas templates de projeto da versão mais atual do Visual Studio .NET, mas, para a criação de novos projetos, ou melhor, de novos serviços, o ideal é recorrer ao WCF – Windows Communication Foundation.

De qualquer forma, os ASP.NET Web Services já existem há algum tempo e há muitas aplicações que ainda o utilizam, e este artigo ajudará a entender melhor as diferenças entre ASMX e o WCF, desde a sua estrutura de projeto até detalhes relacionados à execução do mesmo. Cada uma das seções a seguir irá analisar e discutir essas mudanças, falando também sobre alguns detalhes importantes que, se não se atentar, poderá ter um comportamento “estranho” durante a execução.

Templates e Estrutura de Projeto

Quando você opta por criar um projeto ASMX, então você deve recorrer à template ASP.NET Web Service Application. Ao criar esse tipo de projeto, você poderá adicionar dentro dele arquivos com extensão *.asmx. Esse tipo de arquivo representará um serviço. Dentro desta classe, teremos os métodos que queremos publicar. Vale lembrar que o modificador de acesso do método (public, private, etc.) não tem validade. O que determinará a visibilidade do método é se ele estiver ou não decorado com o atributo WebMethodAttribute.

Além disso, a classe que representa o serviço pode, opcionalmente, herdar da classe WebService. Essa classe fornece acesso direto aos objetos ASP.NET, como Application e Session. Assim como nas aplicações ASP.NET tradicionais (UI), o arquivo ASMX apenas possui uma diretiva chamada @WebService, que define alguns atributos utilizados pelo compilador do ASP.NET. As classes necessárias para trabalhar com o ASMX estão contidas no Assembly System.Web.Services.dll e debaixo do namespace System.Web.Service.

Já com o WCF, trabalhamos de forma bem parecida. Para trabalhar com ele, é necessário que você, ao criar um novo projeto, escolha a template WCF Service Application. Neste projeto, adicionaremos arquivos com extensão *.svc, que representará o serviço. Esse tipo de arquivo também possui uma diretiva própria, chamada de @ServiceHost, que também leva informações ao compilador do ASP.NET. As classes necessárias para trabalhar com o WCF estão contidas no Assembly System.ServiceModel.dll e debaixo do namespace System.ServiceModel.

Apesar de existirem templates distintas para cada uma das tecnologias, isso não quer dizer que você está obrigado a criar um projeto específico para os teus serviços. Caso você já possua uma aplicação ASP.NET criada, é perfeitamente possível adicionar arquivos com extensão *.asmx ou *.svc neste projeto. Uma aplicação ASP.NET (UI) consegue coexistir com qualquer uma dessas tecnologias.

Contratos

O ASMX irá se basear nos métodos decorados com o atributo WebMethodAttribute para gerar o documento WSDL. Você deverá controlar a visibilidade dos teus métodos adicionando um removendo este atributo. Qualquer tipo complexo referenciado nos métodos, será automaticamente inserido na descrição do serviço sem nenhuma configuração extra.

Já o WCF trabalha de forma bem diferente. Ele utiliza interfaces para determinar os contratos que o serviço possui. Essas interfaces são aquelas tradicionais, que já utilizamos no nosso dia-à-dia, mas decorada com um atributo chamado ServiceContractAttribute. Dentro das interfaces teremos os métodos, e controlamos se eles serão ou não expostos através do atributo OperationContractAttribute.

Em relação a tipos complexos vinculados ao contrato do serviço, a exposição deles será determinado pela versão do WCF que está utilizando. Se estiver utilizando a versão 3.0, então você precisará decorar essas classes com o atributo DataContractAttribute, e para cada propriedade que desejar expor, decorá-la com o atributo DataMemberAttribute (opt-in). Com o Service Pack 1 para o .NET Framework 3.5, esses parâmetros são opcionais (opt-out), tornando-os POCOs. Mas há um detalhe: quando você decorar a classe com o atributo DataContractAttribute, então você deverá explicitamente determinar quais propriedades deseja disponibilizar, utilizando o atributo DataMemberAttribute.

Há um outro detalhe importante durante a execução das operações. Quando você utiliza ASMX, ele concatena o namespace com o nome da mensagem para determinar o Action. O WCF concatena o namespace, o nome do serviço e o nome da operação. Para manter compatibilidade com possível clientes ASMX, você deve manter a mesma fórmula, e para isso, pode recoorer a propriedade Action do atributo OperationContextAttribute.

Serialização/Deserialização

A serialização e deserializaão estão condicionadas ao serializador que cada tecnologia utiliza. O ASMX utiliza o XmlSerializer (System.Xml.Serialization) para transformar os objetos em XML e vice-versa. O XmlSerializer serializa todos os membros públicos (propriedades e campos), sem a necessidade de definir algum atributo. Você ainda pode controlar como essa serialização será realizada, utilizando vários atributos que existem debaixo deste mesmo namespace. Para maiores detalhes sobre o funcionamento do XmlSerializer e dos atributos, consulte este artigo.

O WCF, por outro lado, utiliza o serializador DataContractSerializer por padrão. Este serializador trabalha de forma semelhante ao XmlSerializer, com poucos diferenças. Entre essas diferenças temos o entendimento por parte do DataContractSerializer do atributo SerializableAttribute, para manter a compatibilidade com objetos que foram criados para serem utilizados pelo .NET Remoting. Além disso, uma outra diferença é a capacidade que este serializador tem de persitir também membros definidos como private e protected. Este serializador ainda gera um XML mais simplificado, melhorando a interoperabilidade entre as plataformas. Se desejar utilizar o XmlSerializer, então basta decorar o seu contrato com o atributo XmlSerializerFormatAttribute. Somente utilize o XmlSerializer para cenários onde você precisa ter controle total sob como o XML é gerado.

Ainda temos um terceiro serializador que é o NetDataContractSerializer. A diferença em relação ao DataContractSerializer é que ele armazena no XML gerado, informações a respeito do tipo (CLR), como versão, assembly e full name. Este serializador é rico nas informações referente ao tipo, ele compartilha tipos, ao contrário do DataContractSerializer, que compartilha contratos. Este ainda possui baixa interoperabilidade, e pode ser utilizado em cenários onde se tem .NET Remoting.

Protocolo/Hosting

O ASMX pode somente ser hospedado no IIS, utilizando o protocolo HTTP/HTTPS. Já o WCF tem uma arquitetura muito mais flexível e é independente do protocolo, ou seja, ele pode rodar em HTTP, HTTPS, TCP, MSMQ, etc. Isso quer dizer que ao construir um serviço através do WCF, você não deve mencionar e/ou confiar em nenhum momento que o mesmo será exposto através de um determinado protocolo, já que você não conseguirá garantir isso.

O WCF também pode utilizar o IIS como host. Mas além dele, podemos fazer uso de outras aplicações para expor um serviço, como um Windows Service, ou ainda, uma simples aplicação Windows/Console. Para mais detalhes sobre cada um dos tipos de host, consulte este artigo.

Extensibilidade

O ASMX permite você interceptar as requisições através de SOAP Extensions. Com elas, podemos acoplar um código customizado no processamento da mensagem, efetuando logs, tratando a autenticação/autorização, etc. A estrutura para a criação de um extensão é simples: basta herdar da classe SoapExtension e implementar o método ProcessMessage. Como parâmetro, este método traz uma propriedade chamada Stage, que podemos identificar o estágio do processamento da mensagem. Ela nos fornece quatro opções auto-explicativas: BeforeSerialize, AfterSerialize, BeforeDeserialize e AfterDeserialize. Para utilizá-la, basta criar um atributo que herda de SoapExtensionAttribute, e sobrescrever o método ExtensionType.

O WCF traz inúmeros pontos de extensibilidade tanto do lado do serviço quanto do cliente. Através destes pontos, podemos interceptar e inspecionar os parâmetros que estão sendo enviados, a escolha da operação a ser disparada, a criação da instância da classe que representa o serviço, a serialização e deserialização da mensagem (é o que a classe SoapExtension faz), entre outros. O WCF fornece vários tipos (interfaces e classes) para você customizar o que for necessário. Para entender detalhadamente sobre todos as possibilidades que temos, consulte este artigo.

Segurança

O ASMX pode confiar somente na segurança baseada no transporte, ou seja, ele somente será seguro se você expor o serviço através de HTTPS. Você somente conseguirá abrir mão do HTTPS se utilizar a segurança baseada na mensagem, que está disponível no ASMX através do WSE – Web Services Enhancements. Muitas vezes se utiliza um SoapHeader com usuário e senha. Isso somente terá alguma segurança se utilizar HTTPS ou segurança em nível de mensagem. Do contrário, qualquer um que intercepte a requisição, conseguirá extrair o conteúdo da mensagem e seus respectivos headers.

Como já era de se esperar, o WCF fornece ambos níveis de segurança nativamente. São configurações que você realiza (de forma imperativa ou declarativa), e que o serviço utilizará para efetuar a autenticação e autorização do cliente. Uma das grandes dificuldades que o pessoal encontra ao utilizar o WCF, é que se configurar o WCF para autenticar o cliente através de usuário e senha, ainda assim será necessário utilizar um certificado para garantir a segurança.

Configuração

O ASMX possibilita que algumas configurações sejam feitas de forma declarativa, ou seja, aquela que é realizada através do arquivo Web.config. Entre essas configurações, temos a possibilidade de definir as SoapExtesions, página de ajuda/WSDL customizada, os protocolos que podem ser utilizados para acessar o serviço (HttpSoap, HttpPost e HttpGet) e mais algumas outras.

No WCF, a seção de configurações são extremamente ricas. Grande parte de tudo que utilizamos no WCF pode ser configurado a partir do arquivo de configuração. Segurança, transações, know types, behaviors, bindings, endpoints, contratos, etc. O arquivo de configuração é tão rico que as vezes chega a ser complexo. A Microsoft melhorou isso no WCF 4.0, como já foi discutido neste artigo. Aqui não há muito o que se comparar, já que grande parte do que temos no WCF não existe nativamente no ASMX. Devido a isso, muitos desenvolvedores experientes na construção de serviço utilizando o ASMX, sofrem bastante quando passam a usar o WCF.

Compatibilidade com o ASP.NET

Dentro de métodos criados no ASMX, você pode tranquilamente acessar os objetos nativos do ASP.NET, como caching, Session, Application, Cookies, etc. Você pode utilizar esses repositórios para manter as informações de acordo com o contexto. Todas essas informações são disponibilizadas através da propriedade estática Current da classe HttpContext.

A configuração padrão do WCF não permite você acessar essas informações. Na verdade, isso não é uma boa prática. Se fizer uso dessas classes dentro do seu serviço, ele ficará dependente do protocolo HTTP. Se quiser expor o mesmo serviço através de TCP ou MSMQ, essas informações não terão nenhuma utilidade. De qualquer forma, se quiser manter a compatibilidade e continuar utilizando os mesmos recursos, então você deverá explicitamente habilitá-los. Para fazer isso, você deve decorar a classe que representa o serviço com o atributo AspNetCompatibilityRequirementsAttribute, definindo a propriedade RequirementsMode como Required.

Dependendo do tempo de vida e do escopo que deseja manter alguma informação, você pode recorrer a técnicas nativas do WCF, como o compartilhamento de estado, utilizando a interface IExtension<T>, como é abordado no final deste artigo.

Interoperabilidade

Há várias especificações que foram definidas por grandes players do mercado, que regem a estrutura do envelope SOAP para suportar alguma funcionalidade. Essas especificações são conhecidas como WS-* e estão divididas em várias categorias, sendo Messaging, Security, Reliable Messaging, Transaction, Metadata, entre outros. Cada uma das empresas utiliza essas especificações e as implementam em sua plataforma. Como todos seguem (teoricamente) as mesmas especificações, haverá interoperabilidade entre serviços construídos em plataformas diferentes.

O ASMX não possui nativamente suporte a elas. A Microsoft criou um Add-on para o Visual Studio .NET, chamado WSE – Web Services Enhancements, que atualmente está na versão 3.0. Ao instalá-lo, além de várias ferramentas que ele adiciona na IDE para auxiliar na configuração destes protocolos, adiciona o runtime necessário para fazer tudo isso funcionar. É importante dizer que o WCF consegue também interoperar com o WSE, já que ambos implementam os mesmos padrões.

Como esses padrões já foram implementados nativamente no WCF, não exige nenhum complemento. Todas as especificações são configuradas através do binding, podendo inclusive efetuar essa configuração através do arquivo Web.config. Antes de habilitar ou desabilitar essas funcionalidades, é importante que se tenha o devido conhecimento, para evitar qualquer problema ao até mesmo criar alguma vulnerabilidade. 

Ainda falando sobre interoperabilidade, a Microsoft se preocupou em manter os investimentos feitos com o ASMX. O WCF consegue facilmente conversar com serviços construídos em ASMX em ambos os lados, ou seja, você pode construir um serviço ASMX e consumí-lo com a infraestrutura do WCF, bem como pode construir um serviço escrito em WCF e consumí-lo com a infraestrutura do ASMX. Na verdade, quando você for criar um novo serviço, opte sempre pelo WCF. Você somente teria essa interoperabilidade entre essas tecnologias, quando desejar substituir o ASMX pelo WCF em serviços que já estão rodando, e com vários clientes dependendo do mesmo.

Internals

As requisições para arquivos *.asmx são direcionadas pelo IIS para o ISAPI do ASP.NET (aspnet_isapi.dll). Em um determinado momento, o ISAPI direciona a requisição do pedido para o código gerenciado. As requisições para estes arquivos tem como alvo um dos handlers WebServiceHandlerFactory ou ScriptHandlerFactory. A primeira delas, se baseando no arquivo requisitado, construirá dinamicamente a classe que representa o serviço. Já a classe ScriptHandlerFactory construirá o handler baseando-se em uma requisição REST/AJAX.

O WCF também utiliza o pipeline do ASP.NET, e quando a requisição é entregue pelo IIS para o ASP.NET, ele verifica se trata-se de uma requisição para um arquivo *.svc. Caso positivo, o handler responsável pelo processamento do pedido é o HttpHandler (System.ServiceModel.Activation). Internamente o WCF faz o processamento síncrono da operação, não liberando a thread do ASP.NET, que ficará bloqueada até a finalização da operação. Para entender melhor esse problema e também como melhorá-lo, você pode recorrer a este artigo.

Deployment

Assim como qualquer aplicativo .NET, basta mover os serviços para o IIS remoto e tudo já funciona. Obviamente que você deverá se certificar que você tenha a mesma versão do .NET Framework (isso inclui o Service Packs) instalada no servidor. É importante dizer que ambas tecnologias necessitam de um diretório virtual devidamente criado no IIS, com as permissões também configuradas. Apenas atente-se ao WCF, que tem um pequeno bug quando você opta pela pré-compilação do projeto.

Cliente

Dentro do Visual Studio .NET você tem duas opções para referenciar um serviço: “Add Web Reference” e “Add Service Reference”. Podemos dizer que com a primeira opção, você deve utilizar quando for referenciar um serviço ASMX em uma aplicação; já com a segunda opção, você deve utilizar quando você for referenciar um serviço WCF em uma aplicação. Quando fizer isso, a IDE criará o proxy utilizando a API da tecnologia correspondente.

Isso não quer dizer que você precisa seguir sempre esta regra. Você pode referenciar um serviço ASMX na opção “Add Service Reference”. Ao fazer isso, toda a estrutura do lado do cliente, será criada utilizando a API do WCF, ou seja, o proxy será baseado na classe ClientBase<TChannel> ao invés da SoapHttpClientProtocol, e toda a configuração do endpoint do serviço será colocada no arquivo Web.config. O WCF criou um binding chamado BasicHttpBinding para a interoperabilidade com o ASMX, que você pode utilizar para interagir com o serviço criado através do ASMX, sem maiores problemas.

Conclusão: Como vimos neste artigo, há vários detalhes que precisamos nos atentar na construção ou migração de um serviço escrito em ASMX para WCF. Para efeito de compatibilidade, algumas funcionalidades continuam trabalhando da mesma forma, enquanto para outras há um jeito diferente de fazer, e que na maioria das vezes, acaba sendo mais flexível. O artigo também não aborda detalhadamente cada uma das funcionalidades do WCF. Se desejar construir um serviço utilizando WCF, ou se quiser entender mais detalhadamente cada uma das funcionalidades abordadas aqui, pode consultar este artigo, que no final dele, há uma relação com os principais artigos que falam sobre essas funcionalidades.

Serviços CRUD

Muitas pessoas utilizam ou já utilizaram o WCF (ou até mesmo os antigos ASP.NET Web Services (ASMX)), para servir como um wrapper de uma base de dados. Basicamente era criado um serviço para cada entidade desta base, onde cada um deles apenas define em sua interface as operações de CRUD, que nada mais são do que as operações básicas com uma determinada tabela relacional.

Se você ainda precisa criar algum tipo que serviço que exponha essas funcionalidades, então acredito que seria uma boa alternativa considerar o uso do ADO.NET Data Services. Este framework é construído em cima do próprio WCF, fornecendo a possibilidade de efetuar as operações CRUD em cima de contexto de dados do Entity Framework ou qualquer outra fonte de dados que implemente a interface IQueryable. Todas as funcionalidades são baseadas no padrão REST, que utiliza URIs predefinidas em conjunto com os verbos HTTP, para executar cada uma dessas operações. Já a serialização do resultado pode ser emitida em ATOM (Xml) ou até mesmo JSON, permitindo assim, que qualquer cliente HTTP (como um navegador) consuma o serviço.

Um detalhe importante é que a Microsoft incluiu no .NET Client Library, Silverlight e no AJAX tudo o que é necessário para efetuar a comunicação com serviços baseados no ADO.NET Data Services.

É importante dizer que o ADO.NET Data Services não é ideal para todos os cenários. Há muitas ocasiões onde a customização pode ser muito grande, e utilizando-o pode tornar o processo muito mais trabalhoso do que produtivo e, sendo assim, será mais viável utilizar o WCF, podendo inclusive, expor as funcionalidades do serviço utilizando o padrão REST, se assim desejar. Agora, se tudo o que precisa são as simples operações de CRUD, então criar serviços baseados no ADO.NET Data Services será uma opção boa e bastante produtiva.

Configurando o limite de conexões pendentes

Quando expomos um serviço através do binding NetTcpBinding, as requisições que chegam até ele, são processadas e, finalmente o retorno é enviado para o respectivo cliente. As mensagens que chegam são processadas no formato FIFO (first-in, first out), ou seja, elas aguardam o processamento em uma fila e, sob demanda, são encaminhadas para o processamento.

Se a velocidade do processamento da mensagem é mais lenta que a velocidade de envio das mensagens, essa fila pode aumentar. Por padrão o binding NetTcpBinding limita, através da propriedade ListenBacklog, o número de requisições pendentes que podem ser enfileiradas. O valor padrão é 10 e, dependendo da quantidade de requisições que chegam para o serviço, esse limite pode exceder e, a seguinte exceção será disparada:

System.ServiceModel.EndpointNotFoundException: Could not connect to net.tcp://localhost:8879/srv. The connection attempt lasted for a time span of 00:00:02.1404880. TCP error code 10061: No connection could be made because the target machine actively refused it 127.0.0.1:8879.  —> System.Net.Sockets.SocketException:No connection could be made because the target machine actively refused it 127.0.0.1:8879

A alternativa aqui é aumentar esse número, definindo um valor próximo a quantidade das conexões simultaneas que eventualmente podem chegar para o serviço. O exemplo abaixo ilustra a configuração desta propriedade no binding NetTcpBinding:

host.AddServiceEndpoint(typeof(IContrato), new NetTcpBinding() { ListenBacklog = 70 }, “srv”);