Polling Duplex

Como já falei há algum tempo, um dos tipos de mensagens suportados pelo WCF é o padrão Duplex. A finalidade desta técnica é permitir uma comunicação bidirecional, ou seja, o cliente invocar método de um serviço, bem como um serviço invocar um método do cliente. Esse tipo de comunicação permite, na maioria das vezes, um determinado serviço notificar o cliente de que algum evento ocorreu, dando a ele, uma chance de conseguir interagir com um cliente específico ou até mesmo vários clientes, através de um sistema publicador-assinante.

Aplicações Silverlight também podem precisar deste tipo de recurso, onde um serviço irá se comunicar com o cliente (que está rodando no navegador) para notificar sobre algum resultado, ou até mesmo para reportar a execução de alguma tarefa mais complexa. Felizmente, a partir da versão 2.0 do Silverlight, ele prove esta funcionalidade, e a partir a versão mais recente, Silverlight 3.0 (ainda em Beta), tornou esse trabalho mais simples. Para quem já está habituado a trabalhar com WCF, utilizar a API do mesmo em uma aplicação Silverlight (independentemente do tipo de comunicação) traz algumas limitações, quais falaremos neste artigo, principalmente dando mais foco no formato Duplex.

Ao utilizar um binding que suporte o tipo de comunicação Duplex, ele criará internamente um endpoint para que o serviço consiga invocar o callback do lado do cliente. Como a aplicação cliente está rodando dentro do navegador, ela não poderá criar um “listener” para receber os eventuais callbacks que o serviço possa disparar, impedindo a utilização dos bindings tradicionais para este cenário. Para resolver essa deficiência, o Silverlight utiliza o recurso conhecido como “poll”. Com esta técnica, o cliente ficará interrogando o serviço para determinar se há alguma mensagem que deve ser recebida por ele.

Antes de falarmos sobre as classes que dão suporte a esta técnica, vamos analisar os assemblies necessários. O Silverlight 2.0 e 3.0 traz dois assemblies com o nome de System.ServiceModel.PollingDuplex.dll, que estão em diretórios diferentes. Um deles contido no diretório %ProgramFiles%Microsoft SDKsSilverlightVERSAOLibrariesServer e outro no diretório %ProgramFiles%Microsoft SDKsSilverlightVERSAOLibrariesClient. Como podemos perceber, um dos assemblies será utilizado pelo lado do serviço, onde trará os tipos necessários para a criação de serviços Duplex, enquanto o segundo deverá ser utilizado por aplicações cliente, para o consumo de serviços Duplex.

O binding responsável por fornecer toda a infraestrutura necessária para a comunicação Duplex no Silverlight é chamado de PollingDuplexHttpBinding. Este binding herda diretamente da classe BasicHttpBinding, e customiza o mesmo para suportar o “poll” através do protocolo HTTP. Este binding utiliza os protocolos abertos Net Duplex e WS-Make Connection (WS-MC). O protocolo Net Duplex é utilizado para estabelecer uma sessão entre o cliente e o serviço, para que seja possível correlacionar as mensagens que serão trocadas. Já o protocolo WS-MC, faz o trabalho necessário para a criação de um canal dentro da sessão previamente criada, e que será utilizado pelo serviço para criar a mensagem que deve ser entregue ao cliente ou para o cliente interrogar o serviço.

Em termos de implementação do serviço nada muda, ou seja, a criação de uma Interface que representará o callback continuará sendo necessária. Além disso, a forma de invocarmos o callback continua sendo através do método GetCallbackChannel<T> da classe OperationContext. Para entender mais detalhes de como implementar um serviço que suporte callbacks, consulte este artigo. Se você já sabe construir serviços que utilizam callbacks, você notará que não há nenhuma diferença na criação destes mesmos serviços para Silverlight.

Depois de criado o contrato do serviço, de callback e da classe que representará o serviço, precisamos configurar o host para expor o serviço através do novo binding que mencionamos acima. Infelizmente, até a versão atual (Silverlight 3.0 Beta), ainda não existe suporte para a configuração deste tipo de serviços através de arquivos de configuração (como o Web.config). Na maioria das vezes o serviço estará hospedado no IIS (mas poderá utilizar outros hosts) e a deficiência na configuração declarativa, nos obrigará a customizar o ServiceHost e o ServiceHostFactoryBase (responsável por criar as instâncias da classe ServiceHost). Aqui não há diferenças em relação a forma que já conhecemos para a configuração de um endpoint, ou seja, continuamos necessitando o endereço, binding e contrato. A única atenção que temos que ter é especificar um binding que suporte este tipo de comunicação.

Utilizaremos o IIS como hosting (via arquivo *.svc), e como mencionado acima, será necessário criarmos uma especialização da classe ServiceHost e também da ServiceHostFactoryBase. Isso é mostrado através do código abaixo, e note que dentro do método InitializeRuntime adicionamos os endpoints necessários (através do método AddServiceEndpoint), incluindo um com um binding customizado, e entre os elementos que irão compor ele, temos a instância da classe PollingDuplexBindingElement, que habilita a comunicação Duplex entre o serviço e o cliente.

public class PollingDuplexServiceHostFactory : ServiceHostFactoryBase
{
    public override ServiceHostBase CreateServiceHost(
        string constructorString, Uri[] baseAddresses)
    {
        return new PollingDuplexServiceHost(baseAddresses);
    }

    private class PollingDuplexServiceHost : ServiceHost
    {
        public PollingDuplexServiceHost(params Uri[] addresses)
            : base(typeof(Servico), addresses)
        {
            this.Description.Behaviors.Add(
                new ServiceMetadataBehavior() { HttpGetEnabled = true });
        }

        protected override void InitializeRuntime()
        {
            this.AddServiceEndpoint(
                typeof(IContrato),
                new CustomBinding(
                    new PollingDuplexBindingElement(),
                    new TextMessageEncodingBindingElement(),
                    new HttpTransportBindingElement()),
                “”);

            this.AddServiceEndpoint(
                typeof(IMetadataExchange),
                MetadataExchangeBindings.CreateMexHttpBinding(),
                “mex”);

            base.InitializeRuntime();
        }
    }
}

É importante dizer ao runtime do WCF que criamos uma factory própria, e para vinculá-la ao serviço, utilizamos a diretiva @ServiceHost no arquivo *.svc. Essa diretiva possui um atributo chamado Factory, onde podemos especificar o tipo de uma classe que implemente a classe base ServiceHostFactoryBase.

<%@ ServiceHost
    Language=”C#”
    Debug=”true”
    Factory=”Web.UI.Host.Network.PollingDuplexServiceHostFactory” %>

Com isso finalizamos tudo o que é necessário por parte do serviço. Com o endereço do mesmo em mãos, devemos referenciá-lo no cliente através da IDE do Visual Studio .NET ou através do utilitário de linha de comando chamado slsvcutil.exe. Ao fazer isso, uma classe que corresponderá ao proxy será criada mas o arquivo de configuração não terá nenhuma informação. Isso se deve ao fato de que a versão atual (Silverlight 3.0 Beta) ainda não traz suporte a este tipo de funcionalidade. Um outro detalhe importante que você notará ao explorar a classe que representa o proxy, é que não haverá a versão síncrona do método, pois o Silverlight não possui isso nativamente. Assim, todos os métodos expostos pelo contrato, o cliente somente terá a versão assíncrona dos mesmos, que é composta por um par de métodos, BeginOperacao e EndOperacao. Apesar destes dois métodos existirem, eles são privados e não estão acessíveis além do proxy, que disponibiliza o modelo baseado em eventos para o consumo. Para maiores detalhes de como o processo assíncrono funciona, consulte este artigo.

Da mesma forma que utilizamos um binding customizado do lado do serviço, temos que fazer o mesmo do lado do cliente. Temos apenas que tomar os devidos cuidados para utilizar o assembly System.ServiceModel.PollingDuplex.dll que está no diretório %ProgramFiles%Microsoft SDKsSilverlightVERSAOLibrariesClient. Veja que estamos montando o binding customizado com os mesmos elementos (e na mesma ordem) que utilizamos na configuração do serviço, apenas nos atentando que a classe PollingDuplexBindingElement está no assembly que faz parte do cliente. O código abaixo mostra como devemos proceder para utilizar o proxy que foi gerado:

private Servico.ContratoClient _proxy;

public MainPage()
{
    InitializeComponent();

    EndpointAddress ea = new EndpointAddress(“http://127.0.0.1.:51116/Servico.svc”);
    CustomBinding cb =
        new CustomBinding(
            new PollingDuplexBindingElement(),
            new TextMessageEncodingBindingElement(),
            new HttpTransportBindingElement());

    _proxy = new ContratoClient(cb, ea);
    _proxy.OnCallbackReceived += (o, e) => MessageBox.Show(e.msg);
}

private void Button_Click(object sender, RoutedEventArgs e)
{
    _proxy.PingAsync(“Teste”);
}

Antes de invocar o método Ping, é necessário nos vincularmos ao evento OnCallbackReceived, que será disparado quando a operação finalizar. Note que as operações são sufixadas com a palavra “Async”, e haverá sempre um evento que corresponde ao término da execução da respectiva operação.

Limites e Timeouts

As classes que vimos acima possuem algumas propriedades que podemos utilizar para ter um maior controle em alguns timeouts, e também na quantidade máxima de sessões e de mensagens que podem ser criadas. Para iniciar vamos falar sobre as propriedades fornecidas pela classe PollingDuplexBindingElement do serviço. As propriedades que gerenciam as quantidades máximas são: MaxPendingSessions e MaxPendingMessagesPerSession. A primeira determina a quantidade máxima de sessões suportadas, enquanto a segunda determina a quantidade máxima de mensagens para cada sessão criada. Com relação aos timeouts, temos as propriedades InactivityTimeout e ServerPollTimeout. A primeira propriedade determina um intervalo de tempo que o serviço pode passar sem nenhuma atividade antes do canal se tornar inválido. Já a segunda propriedade, especifica o tempo em que o serviço irá monitorar o cliente anter de retornar.

Do lado do cliente apenas temos duas propriedades, também fornecidas pela classe PollingDuplexBindingElement: InactivityTimeout e ClientPollTimeout. A primeira propriedade tem a mesma finalidade da propriedade InactivityTimeout do lado do serviço, enquanto a propriedade ClientPollTimeout determina o intervalo de tempo que o cliente ficará interrogando o serviço, para verificar a existência de novas mensagens. Todas essas propriedades devem ser configuradas antes da conexão ser estabelecida entre o cliente e o serviço.

Conclusão: Felizmente o Silverlight já traz suporte à comunicação Duplex. Como vimos no decorrer deste artigo, o único binding que possibilita isso em aplicações Silverlight é o PollingDuplexHttpBinding. Como estamos atualmente na versão Beta, ainda há algumas deficiências, como é o caso da ausência do suporte a configuração declarativa deste binding (via arquivo Web.config), que torna o processo de desenvolvimento um pouco mais trabalhoso e propício a erros. De qualquer forma, conseguimos atingir o nosso objetivo, que é proporcionar uma boa interatividade, já que podemos inicar uma tarefa e o serviço nos notificar quando a mesma finalizar.

SLComWCF.zip (92.46 kb)

Extraindo informações a partir do WSDL

O WCF fornece um conjunto de classes que nos permite extrair informações a respeito de um determinado serviço. Como já sabemos, o serviço pode ou não expor as informações (binding, contrato, endereço) através de um documento, que é conhecido como WSDL. Ao extrair essas informações, podemos ter acesso aos contratos, o endereço onde o serviço está publicado e também ao respectivo binding, que define como será o canal de comunicação. Com essas informações, podemos tornar o nosso código mais dinâmico, e menos dependente de uma configuração que, muitas vezes, está em hard-code.

A primeira classe que nos ajudará nisso é a MetadataExchangeClient. Com o endereço até o documento WSDL, ela é capaz de fazer o download do mesmo através do protocolo WS-MetadataExchange. Como podemos notar no exemplo abaixo, ainda em seu construtor especificamos se o modo de requisição será através de uma requisição WS-Transfer GET ou uma requisição HTTP GET. Para capturar o resultado, utilizamos o método GetMetadata, que retorna uma instância da classe MetadataSet, contendo uma coleção de seções (representadas pela classe MetadataSection), que serão utilizadas a seguir, pela classe WsdlImporter.

A classe WsdlImporter converte as informações expostas pelo WSDL, em classes que representam os endpoints, e disponibiliza vários métodos, onde podemos extrair esses endpoints individualmente, ou retornar todos os endpoints encontrados no serviço. O método ImportAllEndpoints retorna uma coleção, onde cada elemento é representado pela classe ServiceEndpoint, que por sua vez, descreve através de várias propriedades, todas as informações que estão relacionadas à um determinado endpoint, ou seja, o famoso ABC.

MetadataExchangeClient proxy =
    new MetadataExchangeClient(
        new Uri("net.tcp://localhost:9292/mex"),
        MetadataExchangeClientMode.MetadataExchange);

ServiceEndpointCollection endpoints = new WsdlImporter(proxy.GetMetadata()).ImportAllEndpoints();

foreach (ServiceEndpoint se in endpoints)
{
    Console.WriteLine("Address: {0}", se.Address);
    Console.WriteLine("Binding: {0}", se.Binding);
    Console.WriteLine("Contract: {0}", se.Contract.Name);

    foreach (OperationDescription od in se.Contract.Operations)
        Console.WriteLine("tOperation: {0} – IsOneWay: {1}", od.Name, od.IsOneWay);
}

Todas as classes utilizadas aqui estão debaixo do namespace System.ServiceModel.Description. Além disso, é importante dizer que isso não substitui o protocolo WS-Discovery, pois conhecemos qual é o endereço até o documento WSDL.

TimeoutException em serviços WCF

Uma das exceções mais comuns quando se utiliza WCF é a TimeoutException, com a seguinte mensagem:

“The request channel timed out while waiting for a reply after 00:01:00. Increase the timeout value passed to the call to Request or increase the SendTimeout value on the Binding. The time allotted to this operation may have been a portion of a longer timeout.”

Muitas vezes, quando se consome um serviço WCF, muitas pessoas esquecem de fechar o proxy, que é o responsável pela comunicação entre o cliente e o serviço. Por padrão, o modo de gerenciamento de instâncias do serviço é o PerSession. Isso quer dizer que haverá uma instância do serviço para atender cada instância do proxy que é criada pelas aplicações clientes.

O problema que mencionei acima começará acontecer quando a quantidade máxima de conexões for atingida. Por mais que você aumente o throttle, a limitação é imposta pelo sistema operacional. Windows Vista Home e Premium tem um limite de 3 conexões; já o Vista Ultimate e Professional (e acredito que o XP Professional também), estão limitados à 10 conexões; e, por fim, as versões de servidor, possuem um número ilimitado. Somente em ambientes de servidores que o throttle poderá, de forma mais clara, ter uma maior interferência/utilidade.

Ao manter o proxy aberto, fará com que esse “contador” nunca decremente, e facilmente chegará ao limite estabelecido pelo sistema operacional ou pelo throttle, e além desse limite atingido, há sempre recursos que estão sendo presos de forma desnecessária. Sendo assim, sempre feche explicitamente o proxy através do método Close, ou se preferir, basta envolvê-lo em um bloco using (com os devidos cuidados), que implicitamente o método Dispose será chamado, que internamente invocará o método Close.

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)

WCF – MessageContracts

Quando desenvolvemos serviços WCF geralmente iniciamos com a criação do contrato, suas respectivas operações e possíveis parâmetros que elas aceitam e/ou retornam. Os tipos que são enviados e recebidos por um serviço podem ser desde um tipo simples, como um DateTime, Integer ou String até mesmo tipos mais complexos, como classes que representam alguma estrutura específica da nossa aplicação.

Esses dados (simples ou complexos) que definimos no contrato serão sempre serializados e enviados como parte do body da mensagem SOAP, ficando sob responsabilidade do WCF, montar a mensagem (headers e body) de acordo com as informações que serão enviadas ao destino (body) e como o binding será configurado (headers). Mas poderemos ter situações em que esse comportamento padrão não nos atenderá, e é neste momento que entra em cena os contratos de mensagem, ou Message Contracts.

Através dos contratos de mensagem, podemos ter o controle total da estrutura da mensagem SOAP, customizando como e onde os dados que fazem parte do contrato serão acomodados dentro da mesma, já que podem fazer parte do header ou do body. Em algumas situações você precisará dessa customização, como por exemplo, quando você necessitar de uma melhor interoperabilidade com um determinado cliente, que possivelmente poderá exigir a mensagem em um formato diferente do qual o WCF cria. Além disso, você poderá ter um objeto não fará somente parte do body, mas também há propriedades dentro dele que deverão ser inseridos como elementos na coleção de headers.

Quando formos trabalhar com contratos de mensagem, além dos contratos que somos obrigados a criar em qualquer tipo de serviço (interface que representa o contrato do serviço e as classes que serão utilizadas como contratos de dados), temos também que criar as classes que representarão o contrato (formato) da mensagem que será enviada/recebida pela operação. Nesse contrato, basicamente temos propriedades que expõem tipos (complexos ou não) e que serão definidas como parte dos headers ou do body da mensagem.

Em primeiro lugar, precisamos definir que a classe será utilizada como contrato de mensagem, e para isso, decoramos ela com o atributo chamado MessageContractAttribute. É importante dizer que as classes que servem como contrato de mensagem devem, obrigatoriamente, ter um construtor sem parâmetros. Em seguida, para determinar se um campo desta classe vai fazer parte dos headers ou body, você deverá utilizar os atributos MessageHeaderAttribute ou MessageBodyMemberAttribute, respectivamente.

Para exemplificar o uso de contratos de mensagem, vamos imaginar a seguinte situação: uma aplicação poderá enviar para o serviço a relação de contas de um determinado cliente que ela quer recuperar os respectivos saldos. O serviço, por sua vez, extrairá de algum repositório o saldo atual de cada uma das contas, e retornará para o solicitante. Neste caso, teremos duas classes: CriterioDeBusca e ResultadoDaBusca.

A primeira delas representará a mensagem que o cliente irá enviar para o serviço. Essa classe disponibiliza três campos: Cliente, Codigo e Contas, utilizadas para a aplicação informar o cliente e as contas que ele deseja extrair o saldo. Já a classe ResultadoDaBusca irá representar o resultado da pesquisa, com os campos Cliente, Codigo e Saldos. A última propriedade desta classe retorna um array, onde cada elemento é representa por um objeto do tipo Saldo, que é composto pela identificação da conta e seu respectivo saldo. Abaixo temos a estrutura de cada uma delas:

[MessageContract]
public class CriterioDeBusca
{
    [MessageHeader] public string Cliente;
    [MessageHeader] public int Codigo;
    [MessageBodyMember] public string[] Contas;
}

[MessageContract(IsWrapped = true, WrapperName = “contas”)]
public class ResultadoDaBusca
{
    [MessageHeader] public string Cliente;
    [MessageHeader] public int Codigo;
    [MessageBodyMember] public Saldo[] Saldos;
}

public class Saldo
{
    public string Conta { get; set; }
    public decimal Valor { get; set; }
}

Como podemos notar, as classes CriterioDeBusca e ResultadoDaBusca estão decoradas com o atributo MessageContractAttribute, que diz ao runtime do WCF que elas devem ser consideradas como a mensagem SOAP em si, e não como simples contratos de dados. Os campos destas classes também possuem os atributos para determinar o que irá compor o header e o body.

Já comentamos a finalidade de cada atributo que foi decorado nos membros acima. Apesar de eles estarem com a configuração padrão (com exceção da classe ResultadoDaBusca), ainda há uma série de propriedades que cada um deles disponibiliza, para que possamos definir algumas outras configurações que serão utilizadas pelo WCF durante o processamento/serialização da mensagem. Antes de prosseguirmos, é necessário entender cada uma dessas propriedades que podemos, opcionalmente, utilizar. As tabelas abaixo sumarizam cada uma delas.

MessageContractAttribute
Propriedade Descrição
HasProtectionLevel Recebe um valor boleano indicando se a mensagem deverá ter um nível de proteção.
IsWrapped Recebe um valor boleano indicando se o corpo da mensagem terá um elemento que servirá como wrapper. Caso True, o WCF utilizará as informações definidas nas propriedades WrapperName e WrapperNamespace para criá-lo.
ProtectionLevel Propriedade que recebe um dos valores definidos pelo enumerador ProtectionLevel, indicando se a mensagem deverá ser encriptada, assinada, ambos ou não precisará de nenhuma espécie de proteção.
WrapperName Define o elemento que servirá como wrapper para o body da mensagem. Quando omitido ou quando a propriedade IsWrapped estiver definida como False, o corpo será colocado imediatamente após o elemento <soap:Body>.
WrapperNamespace Define o namespace para o elemento que servirá como wrapper da mensagem.

MessageHeaderAttribute
Propriedade Descrição
HasProtectionLevel Recebe um valor boleano indicando se a mensagem deverá ter um nível de proteção.
Name Define o nome do elemento que será serializado. Quando omitido utilizará o nome do próprio campo.
Namespace Fornece um namespace para o header em questão e seus possíveis filhos, a menos que eles sobrescrevam isso.
ProtectionLevel Propriedade que recebe um dos valores definidos pelo enumerador ProtectionLevel, indicando se a mensagem deverá ser encriptada, assinada, ambos ou não precisará de nenhuma espécie de proteção.
Actor Define uma URI que determina a quem se destina aquele atributo.
MustUnderstand Valor boleano que indica se o ator a quem se destina aquela informação deverá ou não entendê-la. Caso esteja definido como True, e o ator não entende aquele header, uma fault deverá ser lançada.
Relay Também define um valor boleano, que indica se o header deverá ser encaminhado para o próximo nó, caso ele não seja interpretado pelo ator atual.

MessageBodyMemberAttribute
Propriedade Descrição
HasProtectionLevel Recebe um valor boleano indicando se a mensagem deverá ter um nível de proteção.
Name Define o nome do elemento que será serializado. Quando omitido utilizará o nome do próprio campo.
Namespace Fornece um namespace para o header em questões e seus possíveis filhos, a menos que eles sobrescrevam isso.
Order  Como o próprio nome diz, é uma propriedade que recebe um valor inteiro indicando a ordem de serialização de cada elemento dentro do corpo da mensagem. Quando omitido, os elementos são serializados de forma alfabética, seguido dos campos que tem essa propriedade definida explicitamente.
ProtectionLevel  Propriedade que recebe um dos valores definidos pelo enumerador ProtectionLevel, indicando se a mensagem deverá ser encriptada, assinada, ambos ou não precisará de nenhuma espécie de proteção.


Ainda temos uma especialização da classe MessageHeaderAttribute, que é a MessageHeaderArrayAttribute. Este atributo pode ser somente aplicado a membros do tipo array e que serão acomodados no header da mensagem. Decorando uma propriedade ou campo com este atributo, os elementos do array serão serializados de forma independente, ao invés de estarem envolvidos por um wrapper. Por exemplo, se tivermos um array de saldos, por padrão, ele será serializado da seguinte forma:

<header value=”1″ />
<saldos>
  <conta numero=”123″ valor=”100.00″ />
  <conta numero=”456″ valor=”200.00″ />
</saldos>
<header value=”2″ />

Já quando aplicamos o atributo MessageHeaderArrayAttribute, ele ficará da seguinte forma:

<header value=”1″ />
<conta numero=”123″ valor=”100.00″ />
<conta numero=”456″ valor=”200.00″ />
<header value=”2″ />

Depois disso, o próximo passo é a criação do contrato. Agora, ao invés de trabalharmos diretamente com os tipos mais simples e/ou complexos nos parâmetros e resultado das operações, elas devem começar a trabalhar diretamente com as classes previamente criadas. As classes que criamos para o exemplo são autoexplicativas, onde uma representa a entrada de dados e a outra o resultado, e como ambas estão decoradas como o atributo MessageContractAttribute, elas definirão a estrutura da mensagem SOAP. Abaixo está o contrato já configurado com elas:

[ServiceContract]
public interface IContrato
{
    [OperationContract]
    ResultadoDaBusca RecuperarSaldo(CriterioDeBusca criterio);
}

Observação: Você ainda tem uma segunda alternativa para a definação de contratos de mensagem, que também é conhecida como “inline”. Neste modo, você pode decorar os parâmetros do teu contrato com os atributos MessageHeaderAttribute ou MessageBodyAttribute.

RPC vs. Messaging

Uma das idéias das aplicações orientadas a serviços é que elas devem trocar mensagens, ou seja, ao invocar uma operação, teoricamente, você deveria criar uma mensagem explicitamente e enviá-la para o seu destino, enquanto do outro lado, você deve capturá-la, analisar o seu conteúdo, e executar a tarefa que ela solicita. O contrato que vimos acima segue essa linha, já que devemos lidar com objetos que representam a mensagem. Esse tipo de formato é conhecido como Messaging.

Existe também uma outra possibilidade, conhecida como RPC (Remote Procedure Call). Neste estilo, nós trabalhamos com os serviços como se fossem componentes/classes tradicionais, ou seja, não precisamos lidar com as mensagens diretamente, pois o método irá receber e/ou retornar os tipos corretos e a tecnologia, que neste caso é o WCF, se encarregará de montar e serializar a mensagem que será enviada de um lado a outro.

O WCF não está limitado a apenas um desses estilos de mensagens, mas na maioria das vezes utilizamos o estilo RPC. Mas o tema deste artigo é justamente mostrar como podemos desenvolver serviços que utilizem o padrão Messaging, e como já vimos acima, vamos trabalhar diretamente com as classes que representarão as mensagens.

Implementação

Ambos os lados da comunicação precisarão trabalhar de forma diferente, já que não vamos mais passar ou receber os parâmetros diretamente; agora vamos lidar com uma classe que representará a resposta e uma classe que representará a requisição. Ambas já foram criadas acima, e o contrato foi desenvolvido utilizando essas duas classes.

Apesar de mudar a forma que desenvolvemos, não há muita complexidade, já que basta instanciarmos uma das classes (dependendo do contexto), abastecer as respectivas propriedades e enviá-la para o WCF, que seguirá os atributos definidos para configurar o formato da mensagem que está sendo enviada. Se analisarmos a classe que representará o serviço, veremos o contrato IContrato implementado nela, e com isso podemos analisar e compreender as classes que representam as mensagens:

public class Servico : IContrato
{
    public ResultadoDaBusca RecuperarSaldo(CriterioDeBusca criterio)
    {
        List<Saldo> saldos = new List<Saldo>(criterio.Contas.Length);
        ResultadoDaBusca resultado =
            new ResultadoDaBusca() { Cliente = criterio.Cliente, Codigo = criterio.Codigo };

        foreach (var item in criterio.Contas)
            saldos.Add(new Saldo() { Conta = item, Valor = 1.0M });

        resultado.Saldos = saldos.ToArray();
        return resultado;
    }
}

Em termos de hosting e binding, nada muda. Já do lado do cliente, ao fazer a referência devemos nos atentar a um pequeno detalhe, que é justamente a criação dos contratos de mensagem. Por padrão, quando fazer a referência através da IDE do Visual Studio ou através do utilitário svcutil.exe, ele não irá criar o proxy adequadamente, ou seja, ele sempre irá trabalhar no estilo RPC, o que nos obrigará a passar os parâmetros individualmente para cada operação.

Quando utilizamos a IDE e queremos que ele mantenha o estilo Messaging, então devemos ir até as configurações da referência do serviço, e marcar a opção “Always generate message contracts”, ou se estiver utilizando o svcutil.exe, utilize a opção /mc. Isso irá nos permitir a trabalhar de uma forma semelhante ao que vemos no exemplo abaixo:

using (ContratoClient proxy = new ContratoClient())
{
    CriterioDeBusca cb =
        new CriterioDeBusca(“Israel Aece”, 5, new string[] { “12345-6”, “09876-2” });

    ResultadoDaBusca rb = proxy.RecuperarSaldo(cb);

    foreach (Saldo s in rb.Saldos)
        Console.WriteLine(“{0}: {1:C2}”, s.Conta, s.Valor);
}

Apesar de você mudar a forma que você desenvolve ou consome os serviços no estilo Messaging, não há muitas dificuldades. A grande mudança fica por parte do formato da mensagem que é criada pelo WCF, já que respeitará todos os atributos que decoramos nas classes que representarão as mensagens. Se analisarmos o tracing da mensagem que está sendo trafegada entre o serviço e o cliente, teremos um resultado como este (alguns trechos foram omitidos por questões de espaço):

<s:Envelope>
  <s:Header>
    <Action s:mustUnderstand=”1″>…/IContrato/RecuperarSaldoResponse</Action>
    <h:Cliente>Israel Aece</h:Cliente>
    <h:Codigo>5</h:Codigo>

    <ActivityId>dc2132ac-46c5-495d-a313-e9bb7152fe0a</ActivityId>
  </s:Header>
  <s:Body>
    <contas xmlns=”http://tempuri.org/”&gt;
      <Saldos>
        <d4p1:Saldo>
          <d4p1:Conta>12345-6</d4p1:Conta>
          <d4p1:Valor>1.0</d4p1:Valor>
        </d4p1:Saldo>
        <d4p1:Saldo>
          <d4p1:Conta>09876-2</d4p1:Conta>
          <d4p1:Valor>1.0</d4p1:Valor>
        </d4p1:Saldo>
      </Saldos>
    </contas>
  </s:Body>
</s:Envelope>

Se analisarmos detalhadamente a mensagem gerada, veremos que os campos Cliente e Codigo fazem parte do header da mensagem, enquanto o array Saldos é parte do corpo da mensagem, que está envolvido (wrapped) pelo elemento <contas />.

Conclusão: Vimos no decorrer deste artigo uma nova forma de trabalhar com o WCF, utilizando o contrato de mensagens. É importante dizer que na maioria das vezes, você utilizará o estilo “tradicional”, onde você deverá recorrer ao estilo RPC, que já é o padrão. Em casos mais específicos, como aqueles que comentamos acima, então esse estilo de comunicação poderá dar uma maior flexibilidade, já que permitirá ao cliente ou ao serviço, ler ou gerar uma mensagem em um formato diferente daquele que o WCF cria automaticamente.

MessageContracts.zip (66.48 kb)

Interfaces Explícitas e o WCF

O C# e o VB.NET possibilitam a implementação explícita de interfaces. Esse tipo de implementação quer dizer que ao implementar a interface em uma classe, a assinatura do membro (propriedade, evento, método, etc.) levará o “nome completo”. Para exemplificar, note o exemplo abaixo, que exibe as duas implementações (implítica e explicíta):

class Data : ILog
{
    public void WriteMessage(ILogger logger, string msg) { }
}

class IO : ILog
{
    void ILog.WriteMessage(ILogger logger, string msg) { }
}

Note que na segunda implementação, o método WriteMessage está prefixado com o nome da interface onde ele foi definido, que neste caso é ILog. A finalidade deste tipo “diferenciado” de implementação é reduzir possíveis conflitos de nomenclatura que possa haver e, principalmente, esconder a implementação da “visão” pública do tipo onde ela foi implementada. É importante dizer que este tipo de implementação não proibe o cliente de acessar os métodos; basta apenas fazer um cast da instância da classe para a interface, que o método WriteMessage estará acessível.

Esse tipo de implementação também é útil ao trabalharmos com o WCF. Quando estamos utilizando algum recurso de extensibilidade, é muito comum implementarmos as interfaces predefinidas por ele, como por exemplo IServiceBehavior, IOperationBehavior ou IExtesion<T>, para acoplar um código customizado durante a execução do serviço. Além disso, quando desejamos customizar a serialização de um tipo, também devemos implementar algumas interfaces (ISerializable ou IXmlSerializable) fornecidas pelo .NET Framework. Quando implementadas, essas interfaces são utilizadas exclusivamente pelo runtime do WCF/.NET, e em um primeiro momento, os métodos que elas expõem não serão acessados diretamente pelo desenvolvedor.

Um exemplo mais concreto disso é a classe ServiceMetadataBehavior, que implementa de forma explítica a interface IServiceBehavior. Se estivermos trabalhando com a configuração imperativa,  ao instanciar essa classe, apenas iremos visualizar as propriedades que ela disponibiliza para configuração dos metadados do serviço, e não os métodos que a interface expõe, tornando o tipo bem menos poluído e de fácil entendimento, não misturando o que deve ser acessado pelo desenvolvedor e o que deve ser acessado pelo runtime.

Por dentro da classe Message

Em qualquer tecnologia de aplicação distribuída, há vários elementos que trabalham em conjunto para fazer tudo funcionar. Na maioria das vezes que utilizamos alguma dessas tecnologias, não nos preocupamos como o processo acontece nos bastidores. Na verdade, a idéia é essa mesma, ou seja, no primeiro momento, não há necessidade de conhecer níveis mais baixos, justamente porque a tecnologia os abstrai.

Já quando as coisas começam a ficar mais complexas, talvez seja o momento de entender o funcionamento interno de forma mais aprofundada, a fim de analisar o porque daquele determinado comportamento, ou ainda, caso você queira customizar ou interceptar algum ponto da execução. A finalidade deste artigo é demonstrar a classe Message (System.ServiceModel.Channels), responsável por representar o envelope SOAP dentro da infraestrutura do WCF.

Em vários artigos anteriores, eu mencionei a classe Message, principalmente quando falado sobre a codificação e extensibilidade da mensagem. Do lado do remetente da mensagem, haverá um encoder, responsável por recuperar a instância da classe Message, transformá-la em um stream de bytes e enviá-la para o destino através da rede. Já do lado do destinatário, o encoder captura a sequência de bytes da rede e a transforma em uma instância da classe Message novamente, para que a mesma possa ser processada.

A instância da classe Message, que no primeiro momento é criada pelo runtime,  é a forma tipada de acesso ao envelope SOAP, que traz diversas informações importantes. Entre elas temos a operação requisitada pelo cliente, seus respectivos parâmetros e dados contextuais, refletindo o nível de segurança, transações, mensagens confiáveis, etc. O protocolo SOAP é baseado em XML, e sua estrutura consiste basicamente em duas seções: headers e body. Os headers são as informações contextuais da requisição (não confunda headers do SOAP com headers de um protocolo como o HTTP); já o body é a seção dentro do SOAP que armazena as informações (parâmetros) exigidos pela operação que será executada. Abaixo temos a representação XML da classe Message:

<s:Envelope
  xmlns:a="http://www.w3.org/2005/08/addressing&quot;
  xmlns:s="http://www.w3.org/2003/05/soap-envelope"&gt;
  <s:Header>
    <a:Action s:mustUnderstand="1">http://www.israelaece.com/srv/cadastrar</a:Action&gt;
  </s:Header>
  <s:Body>
    <Program.Usuario
      xmlns:i="http://www.w3.org/2001/XMLSchema-instance&quot;
      xmlns="http://schemas.datacontract.org/2004/07/ConsoleApplication1"&gt;
      <Nome>Israel Aece</Nome>
    </Program.Usuario>
  </s:Body>
</s:Envelope>

É importante dizer que nem sempre o conteúdo da classe Message vai ser representado por XML baseando-se no SOAP. Em alguns cenários, a serialização da mensagem deverá seguir um outro formato, como por exemplo, quando você expõe um serviço através de POX (Plain Old XML), eliminando a estrutura SOAP. Neste caso, a classe Message conseguirá se adaptar perfeitamente a esse formato.

Antes de analisarmos a estrutura da classe Message, devemos entender sobre alguns novos tipos que foram adicionados junto com o WCF para melhorar a serialização/deserialização da classe Message. A Microsoft adicionou três novos tipos debaixo do namespace System.Xml (Assembly System.Runtime.Serialization.dll): XmlDictionary, XmlDictionaryWriter e XmlDictionaryReader. Como o padrão XML utilizado pelo WCF possue várias características específicas, essas classes foram desenhadas exclusivamente para transformar a classe Message em um encoding específico (Text, Binary e MTOM).

Como o próprio nome diz, a classe XmlDictionary é um dicionário (chave/valor), utilizado pelo WCF para reduzir o tamanho das tags XML (não do conteúdo) que, consequentemente, irá gerar uma mensagem menor. Isso obrigará ambos os lados compartilharem o mesmo dicionário, para conseguir interpretar a mensagem.

A classe XmlDictionaryWriter trabalha diretamente com a serialização e codificação da classe Message. Ela herda da classe XmlWriter, mas ainda continua sendo uma classe abstrata. Essa classe define três métodos estáticos que recebem um stream como parâmetro (que servirá como output) e retornam as implementações concretas desta classe, sendo: CreateTextWriter, CreateBinaryWriter e CreateMtomWriter. Cada um desses métodos retornará uma classe que herda direta ou indiretamente da classe XmlDictionaryWriter, correspondendo as codificações suportadas pelo WCF, que por padrão são: Text, Binary ou MTOM, respectivamente. Ainda há um método estático chamado CreateDictionaryWriter, que serve como um facilitador quando temos um objeto do tipo XmlWriter e queremos “transformá-lo” em XmlDictionaryWriter.

De forma semelhante o XmlDictionaryWriter trabalha o XmlDictionaryReader. O XmlDictionaryReader herda diretamente da classe XmlReader, e serve como base para as classes que são responsáveis por ler o conteúdo serializado e codificado em XML. Esta classe também fornece métodos estáticos que retornam as implementações concretas de cada leitor, sendo: CreateTextReader, CreateBinaryReader, CreateMtomReader e CreateDictionaryReader. Esses métodos recebem como parâmetro um stream ou array de bytes representando o conteúdo codificado em um determinado tipo.

Além destas classes operarem com o formato de XML Documents, elas também podem manipular XML Infosets. O padrão Infosets permite expressar vários formatos de documentos XML (que possuem diferentes estruturas e regras de análise) em um formato único, definindo os elementos e atributos que o documento contém, de uma forma completamente diferente da sua representação. Isso torna tudo muito flexível, já que possibilita novos tipos de codificação. O WCF separa a serialização da codificação, o que faz com ele primeiramente serialize a classe Message em Infosets, e depois disso, codifica esses Infosets em um formato específico, utilizando os que já são fornecidos pela plataforma (Text, Binary ou MTOM), ou criando um customizado. Esse padrão também influencia no envelope SOAP gerado pelo WCF, mas vamos discutir isso mais adiante.

O exemplo a seguir mostra como podemos proceder para testar o funcionamento e analisar a geração de cada um dos tipos que vimos acima. Note que estou utilizando o método CreateMtomWriter, mas poderia ser qualquer um dos outros métodos, obviamente, alterando alguns parâmetros na chamada, de acordo com a exigência de cada um deles. Neste exemplo, ele irá serializar tudo o que estamos digitando em XML Infosets, e em seguida, codificar no padrão MTOM.

using (FileStream fs = new FileStream("Dados.mtom", FileMode.Create))
{
    using (XmlDictionaryWriter writer =
            XmlDictionaryWriter.CreateMtomWriter(fs, Encoding.UTF8, 1024, "application/xop+xml"))
    {
        writer.WriteStartElement("teste");
        writer.WriteString("algum valor");
        writer.WriteEndElement();
    }
}
 
De volta a classe Message, ela está fortemente vinculada a uma das versões do protocolo SOAP existente no mercado. Como já foi falado acima, assim como o envelope SOAP, a classe Message também possui as seções que determinam os headers e o body. As regras para ler e escrever os dados no body e nos headers são diferentes, por exemplo, os headers sempre serão buferizados na memória e podem ser acessados em qualquer ordem e quantas vezes desejar, enquanto o body pode somente ser lido uma única vez e pode ser streamed. Para maiores informações sobre a comparação entre buffered e streamed, consulte este artigo.

Na maioria das vezes, a criação da classe Message é feita pelo próprio WCF. Em alguns cenários, você pode utilizar alguns pontos de extensibilidade para interceptar a mensagem e, consequentemente, extrair e/ou gravar informações nesta classe. Também temos a possibilidade de confeccionar um contrato que aceite ou devolva instâncias das classe Message, e quando isso acontece, há algumas restrições, como por exemplo:

  • A operação não pode ter qualquer parâmetro de saída (out) ou de referência (ref).
  • Não pode haver mais do que um parâmetro. Se houver um parâmetro, ele deve ser do tipo Message ou ter o ser um contrato de mensagem.
  • O retorno de ser void, Message ou um contrato de mensagem.

Para iniciar, vamos analisar as – poucas – propriedades que essa classe fornece. Para começar, vamos analisar as propriedades Headers e Properties. A prmieira delas representa a coleção de headers existentes em uma mensagem e influenciarão no processamento dela, já que podem armazenar informações de correlação, transações, segurança, mensagens confiáveis, etc., tudo de acordo com as especificações WS-*, ou seja, esses headers são utilizados pela própria infraestrutura do WCF e ultrapassam possíveis intermediários, chegando até o seu destino final. Já a propriedade Properties expõe um dicionário de dados, e essas informações são utilizadas "localmente", não ultrapassando possíveis intermediários. O próprio WCF já utiliza isso em alguns casos como, por exemplo, nos protocolos existentes e suportados por ele. Caso o transporte seja realizado através do protocolo HTTP, os detalhes específicos da requisição/protocolo (HTTP Headers) são armazenados neste dicionário, "fora" da mensagem.

Há duas propriedades públicas boleanas, chamada de IsEmpty e IsFault. A primeira delas, indica se a mensagem está ou não vazia, enquanto a segunda, define se a mensagem é ou não uma mensagem que descreve uma falha (Fault).

Como o body da mensagem é um stream, ele pode ser lido ou escrito apenas uma única vez. Para assegurar esse comportamento, o WCF disponibiliza uma propriedade de somente-leitura chamada State. Essa propriedade retorna um dos valores definidos no enumerador MessageState: Created, Read, Written, Copied e Closed. O valor dessa propriedade vai alterando de acordo com o métodos de escrita ou leitura que você invoca a partir da instância.

Por último e não menos importante, temos a propriedade Version. O valor dessa propriedade é definida no momento da criação da mensagem e não pode ser mais alterada. Essa propriedade contém informações à respeito de qual versão do envelope SOAP e o protocolo de endereçamento (WS-Addressing) que será usado pela mensagem.

Atualmente temos duas versões de envelope SOAP: 1.1 e 1.2. Como falamos acima, a escolha do tipo de envelope traz consequências durante a criação da mensagem, ou seja, a versão 1.1 (que é a mais utilizada) é baseada na sintaxe XML, enquanto a versão 1.2 faz uso de XML Infosets.

Já o WS-Addressing é uma especificação que define um mecanismo para permitir o endereçamento e roteamento de mensagens, independentemente de qual protocolo esteja utilizando. Além de definir a estrutura de um endpoint, o WS-Addressing também adiciona vários headers no envelope SOAP, definindo para onde a mensagem está indo, como reagir a esta mensagem e como as mensagens de requisição/resposta estão vinculadas.

Para representar qual versão do SOAP e do WS-Addressing a mensagem vai utilizar, o WCF disponibiliza uma classe chamada MessageVersion. Em seu construtor, você deverá especificar a versão do SOAP e do WS-Addressing que ela irá representar. Para expor qual versão do SOAP e WS-Addressing que a instância da classe MessageVersion está armazenando, ela define duas propriedades de instância e de somente-leitura chamada Envelope (do tipo EnvelopeVersion) e Addressing (do tipo AddressingVersion).

Tanto a classe EnvelopeVersion quanto a AddressingVersion possui propriedades estáticas que retornam a instância dela mesma, pré-configurada com a versões suportadas do SOAP e WS-Addressing. Ainda possuem uma propriedade chamada None, para caso onde não se aplica o SOAP (como é o caso do AJAX/JSON) ou o WS-Addressing. Para ter uma melhor reusabilidade, a classe MessageVersion também possui várias propriedades estáticas (Default, None, Soap11, Soap11WSAddressing10, Soap11WSAddressingAugust2004, Soap12, Soap12WSAddressing10, Soap12WSAddressingAugust2004) que retornam instâncias dela mesma, pré-configurada com as combinações suportadas/mais utilizadas.

A classe OperationContext fornece uma propriedade chamada IncomingMessageVersion, que retorna uma instância da classe MessageVersion, para obter a versão da mensagem que está chegando até o serviço/cliente. Você pode utilizar essa propriedade para criar uma nova mensagem baseando-se na mesma versão da outra parte.

Criando a classe Message

Você não pode criar diretamente a instância da classe Message, pois ela não tem um construtor público. Ao invés disso, ela fornece um método estático chamado CreateMessage que, por sua vez, fornece vários overloads. Obrigatoriamente os overloads deste método recebem, entre os vários parâmetros, uma instância da classe MessageVersion, especificando qual a versão de SOAP e do WS-Addressing que aquela mensagem utilizará. Grande parte destes overloads também recebem uma string, representando a Action da mensagem, informação que o WCF confia e a utiliza para determinar para qual método ele deverá entregar a mensagem.

O overload mais simples recebe apenas a versão e a action, criando uma mensagem com o corpo vazio. Já outro overload recebe também um object, que cria a mensagem serializando aquele objeto como corpo da mensagem. Nestes casos, mensagem utilizará o DataContractSerializer com as configurações padrão para efetuar a serialização do objeto. Caso desejar mudar isso, você pode utilizar um overload específico, que recebe como parâmetro um objeto do tipo XmlObjectSerializer.

O código abaixo ilustra um exemplo simples de criação da classe Message. A finalidade do código é criar uma mensagem que armazena a instância da classe Usuario que possui apenas uma propriedade pública chamada Nome. Inicialmente criamos a instância da classe Message através do overload do método CreateMessage, que recebe a versão (SOAP e WS-Addressing), uma Action e o body. A instância da classe XmlDictionaryWriter recebe em seu construtor um objeto do tipo FileStream, para que o resultado seja armazenado em um arquivo físico. Na sequência, invocamos o método WriteMessage a partir da classe Message, passando a instância do XmlDictionaryWriter, que serializará a mensagem neste objeto.

using (Message msg = Message.CreateMessage(
    MessageVersion.Default, "Cadastrar", new Usuario() { Nome = "Israel" }))
{
    Console.WriteLine(msg.State); //Created

    using (FileStream fs = new FileStream("Message.xml", FileMode.Create))
        using (XmlDictionaryWriter xml = XmlDictionaryWriter.CreateTextWriter(fs))
            msg.WriteMessage(xml);

    Console.WriteLine(msg.State); //Written
}

O método WriteMessage é um dos métodos de escrita que a classe Message disponibiliza. Há outras versões que nos dará um maior controle em como as “partes” da mensagem são escritas. Esses métodos autoexplicativos são: WriteBody, WriteBodyContents, WriteStartBody, WriteStartEnvelope e WriteStartHeaders e todos eles recebem como parâmetro uma instância da classe XmlDictionaryWriter.

Lendo a classe Message

Um dos overloads do método CreateMessage recebe uma instância da classe XmlDictionaryReader. Esse overload é utilizado quando queremos fazer o processo inverso, ou seja, temos a mensagem serializada em algum local (como um arquivo no disco), e desejamos transformá-la novamente em uma classe Message. Além do XmlDictionaryReader, ainda precisamos informar ao método CreateMessage um número inteiro e uma instância da classe MessageVersion.

O número inteiro permite controlar o tamanho máximo do header da mensagem, já que ele é buferizado. A versão da mensagem deve sempre refletir a mesma versão usada quando ela foi serializada. O código abaixo ilustra como podemos recuperar o conteúdo que foi salvo no arquivo “Message.xml” (através do exemplo anterior), e transformá-lo novamente em uma instância da classe Message. Note que utilizamos o método CreateTextReader para criar o XmlDictionaryReader, seguindo o mesmo exemplo acima:

using (FileStream fs = new FileStream("Message.xml", FileMode.Open))
{
    using (XmlDictionaryReader xml =
        XmlDictionaryReader.CreateTextReader(fs, XmlDictionaryReaderQuotas.Max))
    {
        using (Message msg = Message.CreateMessage(xml, 1024, MessageVersion.Default))
        {
            Usuario u = msg.GetBody<Usuario>();
        }
    }
}

Uma vez criado a instância da classe Message, precisamos saber como devemos proceder para extrair o seu conteúdo, ou melhor, extrair o que está contido no body da mensagem. Basicamente temos dois métodos: GetBody<T> e GetReaderAtBodyContents. O primeiro deles, recebe um parâmetro genérico, utilizado pelo mesmo para deserializar o body da mensagem neste tipo especificado, utilizando o DataContractSerializer. Há um overload deste método que aceita uma instância da classe XmlObjectSerializer, que te permitirá customizar o mecanismo de deserialização. Já o método GetReaderAtBodyContents retorna um novo XmlDictionaryReader, posicionado no elemento body do envelope SOAP.

Independentemente de qual dos métodos utilize para ler o body da mensagem, você poderá chamar apenas uma única vez. Chamando duas vezes um dos métodos de leitura, uma exceção do tipo InvalidOperationException será disparada. Em situações onde você precisa processar o body múltiplas vezes, então você precisará criar uma cópia buferizada da mensagem. Para isso, a classe Message fornece um método chamado CreateBufferedCopy que retorna uma instância da classe MessageBuffer. A instância dessa classe representa a mensagem em memória, disponibilizando um método chamado CreateMessage, que retorna uma cópia idêntica da mensagem original. O exemplo abaixo ilustra o seu uso:

using (Message msg = Message.CreateMessage(xml, 1024, MessageVersion.Default))
{
    MessageBuffer mb = msg.CreateBufferedCopy(int.MaxValue);
    Console.WriteLine(mb.CreateMessage().GetBody<Usuario>().Nome);
    Console.WriteLine(mb.CreateMessage().GetBody<Usuario>().Nome);
}

Criando Fault Messages

O método CreateMessage ainda possui dois overloads que permitem a criação de uma instância da classe Message que representa uma mensagem de falha. Um desses overloads aceita como parâmetro uma instância da classe FaultCode e o outro overload recebe uma instância da classe MessageFault. O código abaixo ilustra a criação de uma MessageFault, e em seguida utilizamos o método CreateMessage passando esta fault criada:

MessageFault mf =
    MessageFault.CreateFault(new FaultCode("Receiver"), new FaultReason("Dados inválidos"));

using (Message msg = Message.CreateMessage(MessageVersion.Default, mf, "Cadastrar"))
{
    //…
}

Ao chamar a propriedade IsFault em cima da instância da classe Message criada acima, o valor retornado será True. Ao visualizar o resultado gerado pelo código acima, teremos:

<s:Envelope
  xmlns:s="http://www.w3.org/2003/05/soap-envelope&quot;
  xmlns:a="http://www.w3.org/2005/08/addressing"&gt;
  <s:Header>
    <a:Action s:mustUnderstand="1">Cadastrar</a:Action>
  </s:Header>
  <s:Body>
    <s:Fault>
      <s:Code>
        <s:Value>s:Receiver</s:Value>
      </s:Code>
      <s:Reason>
        <s:Text xml:lang="en-US">Dados inválidos</s:Text>
      </s:Reason>
    </s:Fault>
  </s:Body>
</s:Envelope>

Headers e Properties

A propriedade Headers é uma coleção, enquanto a propriedade Properties é um dicionário e podem ser manipuladas através de métodos como Add, Insert, Remove, etc. Para maiores detalhes sobre a finalidade de cada uma dessas propriedades, consulte este artigo.

Conclusão: Neste artigo vimos como podemos proceder para a criação, leitura e manipulação de uma mensagem, que é utilizada por serviços WCF. É importante conhecer esses detalhes, principalmente quando você desejar interceptar algum ponto durante a execução do serviço. Além disso, é provável que em algum momento você precisará customizar a criação e/ou leitura da mensagem, e para isso, você deverá manipular instâncias da classe Message, ao invés de deixar o WCF criá-las.

ClientAccessPolicy.xml sem IIS

Um problema que sempre ocorre quando estamos tentando consumir um serviço WCF no Silverlight, é a ausência do arquivo ClientAccessPolicy.xml dentro do diretório onde está hospedado o serviço. Para que ele seja consumido por uma aplicação Silverlight, você deve criar esse arquivo na raiz do serviço, com a mesma estrutura definida neste artigo. Esse arquivo permite que o Silverlight invoque serviços a partir de um dominío diferente de onde ele reside.

Mas, por algum motivo, você está criando uma aplicação Console ou até mesmo um Windows Service, que servirá como o host para o serviço. Lá você configura a classe ServiceHost expondo o serviço através do BasicHttpBinding. Com isso você já consegue referenciá-lo no Silverlight, mas quando executar a aplicação, você receberá um erro de comunicação. Sabemos que é a ausência do arquivo acima mencionado, mas como estamos utilizando um host que não é o IIS, como devemos proceder para disponibilizar esse arquivo?

O Carlos Figueira criou uma solução interessante para isso, onde definiu uma interface que, quando implementada na classe que representa o serviço, gera a estrutura do arquivo ClientAccessPolicy.xml (ali está também suportando o padrão de arquivo necessário para que o Flash se comunique com o serviço WCF). Com o uso do atributo WebGetAttribute, ele configura a propriedade UriTemplate, redirecionando as requisições de cada um desses arquivos para o método correspondente.

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.

Utilizando o evento OperationCompleted

Recentemente eu falei aqui sobre grande parte dos membros expostos pela classe OperationContext. Além de tudo aquilo que foi visto lá, ela ainda fornece um evento chamado OperationCompleted. Ao se vincular a este evento, você será notificado quando a operação corrente foi finalizada, e com isso, poderá executar algo customizado, como alguma notificação, log, calcular o tempo de execução da operação, etc.

Além disso, podemos utilizar este evento para a liberação de recursos que estão sendo utilizados pela classe/serviço. Quando os parâmetros de entrada ou o objeto de retorno (resultado) não implementar a Interface IDisposable, então utilizamos este evento para descartar esses recursos, assim como é exibido no código abaixo:

internal class Servico : IContrato
{
    private FileStream _fs;

    public void ExecutarTarefa()
    {
        OperationContext.Current.OperationCompleted += (o, e) =>
        {
            if (this._fs != null)
                this._fs.Dispose();
        }; 

        //faz uso de _fs.
    }
}