Code Snippets para WCF

Tenho escrito diversos hosts de serviços WCF para artigos que tenho escrito bem como para alguns testes que estamos fazendo na empresa onde trabalho. Como a idéia é apenas criar um projeto, na maioria das vezes, do tipo Console para servir de host, tenho que a todo momento instanciar a classe ServiceHost, especificar o tipo do serviço, o endereço, contrato, bindings, endpoints, description, etc.

Isso me levou a criar alguns code snippets (neste momento somente para C#) para automatizar essas tarefas mais comuns, evitando escrever o mesmo código toda vez que preciso criar um host. Neste momento tenho apenas nove snippets que estão me ajudando muito, poupando grande parte do trabalho que antes eu tinha para a criação um serviço/host dummy. Com isso, eu decidi compartilhar esses recursos. Abaixo estão listados cada um deles e suas respectivas utilidades e, entre paranteses, está o atalho que utilizamos dentro do Visual Studio:

  • asyncContract.snippet (asyncContract): estrutura para gerar um contrato assíncrono (BeginXXX/EndXXX).
  • basicServiceHost.snippet (bsh): estrutura do ServiceHost altamente customizável (endereço, bindings, etc.).
  • basicServiceImplementation.snippet (basicServiceImplementation): implementação padrão para uma classe que representa o serviço, podendo definir o contrato, gerenciamento de instancia e concorrencia.
  • callback.snippet (wcfCallback): estrutura para invocar um método de callback.
  • httpDuplexServiceHost.snippet (wsduplexsh): ServiceHost customizado para WSDuplexBinding.
  • httpServiceHost.snippet (httpsh): ServiceHost customizado para BasicHttpBinding.
  • tcpServiceHost.snippet (tcpsh): ServiceHost customizado para NetTcpBinding.
  • webServiceHost.snippet (websh): ServiceHost customizado para WebHttpBinding (Ajax).
  • wsServiceHost.snippet (wssh): ServiceHost customizado para WSHttpBinding.

Para utilizá-los, basta extrair e colocá-los no seguinte endereço: UsuarioCorrenteDocumentsVisual Studio 2008Code SnippetsVisual C#My CodeSnippets. Gostaria muito de utilizar os elementos Reference e Imports dentro dos arquivos de snippets para automatizar a referencia ao assembly System.ServiceMode.dll e adicinar a diretiva using para o namespace System.ServiceModel mas, infelizmente, isso é uma funcionalidade que está apenas disponível para snippets escritos para Visual Basic.

Habilitando REST em projetos WCF

Quando criamos projetos através das templates WCF Service Application ou WCF Service, por padrão, o serviço será disponibilizado através do binding wsHttpBinding e utilizando o objeto ServiceHost como sendo o gerenciador da instancia do serviço.

Como vimos neste artigo, para disponibilizarmos o serviço para que ele possa ser consumido via REST, precisamos utilizar a classe WebServiceHost ou invés do ServiceHost e também utilizar o binding webHttpBinding. Para realizar essas mudanças nessas templates, primeiramente precisamos definir a Factory resposável pela criação do host. Para configurar isso, recorremos ao atributo Factory da diretiva @ServiceHost, que encontra-se no markup do serviço, ou seja, no arquivo *.svc. Quando omitida (que é o padrão), ela assume o tipo System.ServiceModel.Activation.ServiceHostFactory que, por sua vez, retornará instancias da classe ServiceHost. Como estamos trabalhando com REST e já vimos que devemos utilizar o WebServiceHost, então devemos alterar o valor do atributo Factory para System.ServiceModel.Activation.WebServiceHostFactory. Abaixo a configuração na íntegra:

<%@ ServiceHost
    Language=”C#”
    Debug=”true”
    Service=”WcfService1.Service1″
    CodeBehind=”Service1.svc.cs”
    Factory=”System.ServiceModel.Activation.WebServiceHostFactory” %>

Além disso, atente-se para trocar o valor do atributo binding do elemento endpoint para webHttpBinding, que está definido no arquivo Web.config. As classes necessárias para habilitar o REST estão contidas no Assembly System.ServiceModel.Web.dll.

WCF, IIS e Threads

Sabemos que podemos utilizar o IIS como hosting para um serviço WCF. O Visual Studio 2008 já traz uma template chamada WCF Service. A finalidade desta template é possibilitar a criação de vários serviços, onde cada um será representado por um arquivo com a extensão *.svc. Depois de desenvolvido, podemos publicar este serviço em um servidor com IIS e com o .NET Framework devidamente instalado.

O WCF fornece dois tipos não documentados dentro do namespace System.ServiceModel.Activation, a saber: HttpHandler e HttpModule. A finalidade destes tipos é acoplá-los ao pipeline do ASP.NET e, quando uma requisição chegar para um serviço WCF (*.svc), o ASP.NET irá desviar a execução para o HttpHandler.

Um grande problema que ocorre neste procedimento é com relação ao uso de threads. Como a requisição é inicialmente passada pelo ASP.NET e, quando ele detecta que se trata de um serviço WCF, uma thread de I/O é criada para executar o serviço mas, a worker thread que é utilizada por tratar as requisições do ASP.NET, continuará bloqueada até que a requisição à operação esteja completa. Isso impedirá que a worker thread atenda à outras requisições enquanto espera que o processo seja finalizado.

A configuração padrão do WCF (mesmo depois do SP1 instalado) continua utilizando esta mesma técnica, ou seja, o processamento síncrono da operação. Mas, a partir do SP1 do .NET Framework 3.5, foram introduzidos mais dois tipos que tem o papel de executar, de forma assíncrona, as operações que chegam pelo ASP.NET ao WCF. Esses novos tipos são: ServiceHttpModule e ServiceHttpHandlerFactory.

Com estes dois novos tipos acoplados no pipeline do ASP.NET, permitirá que a execução seja feita de forma assíncrona, liberando a worker thread utilizada pelo ASP.NET para atender a outras requisições. Para habilitar o processamento assíncrono, podemos utilizar uma ferramenta chamada WcfAsyncWebUtil.exe, desenvolvida pelo Wenlong Dong.

É importante dizer que há uma relação entre o processamento assíncrono do runtime do ASP.NET/WCF com o contrato assíncrono (AsyncPattern = true). Se combinarmos o handler/módulo assíncrono com o contrato assíncrono, nenhuma thread será consumida durante a espera pelo processamento assíncrono da operação. Mesmo quando o contrato não é assíncrono, ao utilizar o handler/módulo assíncrono, teríamos uma diminuição de threads rodando concorrentemente e assim, tendo um ganho considerável.

WCF – Throttling e Pooling

Através do gerenciamento de instância de um serviço podemos definir qual a forma de criação de uma instância para servir uma determinada requisição. Essa configuração que fazemos à nivel de serviço, através de um behavior, não impõe nenhuma restrição na quantidade de instância e/ou execuções concorrentes que são realizadas e, dependendo do volume de requisições que o serviço tenha ou até mesmo a quantidade de recursos que ele utiliza, podemos degradar consideravelmente a performance.

O Throttling possibilita restringirmos a quantidade de sessões, instâncias e chamadas concorrentes que são realizadas para um serviço. Além do Throttling, ainda há outra funcionalidade que pode ser utilizada em um serviço, que é o Pooling de objetos, muito comum dentro do Enterprise Services (COM+). Este artigo explicará como proceder para efetuar a configuração do Throttling e suas implicações; também falaremos supercialmente sobre a estrutura do Pooling e como implementá-lo.

Throttling

Seria muito interessante conseguir atender a todas as requisições que chegam para um serviço mas infelizmente, devido à limitação de alguns recursos, isso nem sempre será possível. Cada serviço (assim como qualquer outra aplicação) está limitado à disponibilidade do processador, memória, conexão com base de dados, etc. Um grande número de chamadas faz com que um grande número de instâncias sejam criadas ou um grande número de acesso concorrente (dependerá do modo de gerenciamento de instância escolhido). Cada instância ou cada thread exige recursos do sistema para poder realizar a operação.

Para sanar problemas como estes temos duas alternativas: a primeira deleas é ter um hardware mais potente e que consiga atender as todas as requisições mais, se a quantidade de requisições aumentar, precisará de mais hardware; já a segunda alternativa é restringir o acesso à tais recursos, limitando o número de chamadas concorrentes e/ou sessões ativas. Caso o limite for atingido, as requisições serão enfileiradas, aguardando a sua vez ou, se essa espera pelo processamento demorar muito tempo, falhará por timeout.

O WCF te possibilita controlar esses limites através do Throttling. Basicamente a idéia do Throttling é limitar o número de sessões, instâncias e execuções concorrentes de um serviço. Essa configuração é realizada a partir de um behavior de serviço, ou seja, essa configuração refletirá na(s) instância(s) do serviço, independente de endpoints. Temos três propriedades que podemos definir para especificar esse comportamento:

  • MaxConcurrentInstances: Esta propriedade especifica o número máximo de instâncias concorrentes permitidas para o serviço. Quando o modo de gerenciamento de instância está definido como Single, esta informação é irrelevante, pois existirá apenas uma única instância servindo todas as requisições; já no modo PerCall o número de instâncias será o mesmo número de chamadas concorrentes.

  • MaxConcurrentCalls: Esta propriedade especifica o número máximo de mensagens concorrentes em que o serviço poderá processar. O valor padrão é definido como 16.

  • MaxConcurrentSessions: Esta propriedade especifica o número máximo de sessões concorrentes que o serviço permitirá. É importante lembrar que os clientes são responsáveis por inicializar ou terminar as sessões, realizando várias chamadas entre esse período. O problema das sessões é que quando elas são longas, ou melhor, duram muito tempo, outros clientes podem ser bloqueados. Os bindings que estão sob HTTP e estão com a sessão desabilitada não interferem no valor, pois não mantém uma conexão ativa com o serviço e, conseqüentemente, não haverá instância servindo-o. O valor padrão é definido como 10.

A configuração do Throttling afeta diretamente o serviço, independente de que endpoint a requisição está vindo. Por isso, a configuração poderá ser realizada via arquivo de configuração ou de forma imperativa. No modelo declarativo utilizamos o elemento serviceThrottling, que faz parte de um behavior que está diretamente ligado à um serviço; já no modelo imperativo, utilizamos a classe ServiceThrottlingBehavior (namespace System.ServiceModel.Description) que, depois de configurada, a adicionamos à coleção de behaviors, exposta pela classe ServiceHost. O trecho de código abaixo exemplifica as duas formas de configurar o Throttling:

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

using(ServiceHost host = new ServiceHost(typeof(ICliente), 
    new Uri[] { new Uri("net.tcp://localhost:8377") }))
{
    //Configuração dos Endpoints

    ServiceThrottlingBehavior t = new ServiceThrottlingBehavior();
    t.MaxConcurrentCalls = 40;
    t.MaxConcurrentInstances = 20;
    t.MaxConcurrentSessions = 20;
    host.Description.Behaviors.Add(t);

    host.Open();

    Console.ReadLine();
}

A configuração do Throttling deve ser realizada antes da abertura do serviço. A configuração equivalente ao que vimos acima, mas utilizando o arquivo de configuração é mostrado abaixo:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="Host.ServicoDeClientes" behaviorConfiguration="Config">
        <!-- Endpoints -->
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="Config">
          <serviceThrottling
            maxConcurrentCalls="40"
            maxConcurrentInstances="20"
            maxConcurrentSessions="20" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

Pooling

Já sabemos que o runtime do WCF cria instâncias da classe que representa o serviço para atender uma determinada requisição. A criação das instâncias dependerá do modo de gerenciamento escolhido, que foi constantemente abordado neste artigo. Para relembrar, o modo PerSession cria uma instância para cada cliente; já o modo PerCall cria uma instância por chamada e, finalmente, o modo Single cria uma única instância.

Criar e destruir instâncias a todo momento pode ser extremamente custoso, pois às vezes existem tarefas árduas que são realizadas no momento da sua inicialização. Para ter um ganho considerável em performance, podemos recorrer à uma técnica chamada de Pooling. Esta técnica consiste em, ao invés de destruir o objeto por completo e removê-lo da memória colocá-lo em um repositório para reutilizá-lo. Essa técnica também terá influencia do modo de gerenciamento de instâncias.

Quando uma requisição chega para um serviço que está exposto via modo PerCall, o WCF verifica se há objetos “pré-criados” no pool para atender a requisição. Caso exista, ele utilizará essa instância para servir a requisição. Quando a requisição encerrar, o objeto será devolvido para o pool, podendo ser reaproveitado para uma futura requisição. Quando utilizarmos o modo PerSession, a semântica é a mesma, mas o objeto somente será retornado ao pool quando o cliente encerrar a sessão. Já o modo Single não se aplica ao pooling. Por questões de segurança e também de consistência, ao retornar um objeto para o pool, os dados que são utilizados pelo mesmo (campos internos) devem ser reinicializados.

Por padrão, o WCF não suporta nativamente a técnica de Pooling. Com toda a estensibilidade (behavior) que o WCF possui, fica fácil acoplar uma extensão ao mesmo para suportar o Pooling. Assim como o cliente possui o proxy, o serviço possui o dispatcher*. Para cada endpoint temos um endpoint dispatcher relacionado, sendo ele o responsável por converter as mensagens que chegam para o serviço (mais especificamente para o endpoint) em chamadas para as operações que o serviço fornece e, depois disso, retornar uma mensagem contendo a resposta.

* O cliente também pode criar um dispatcher, mas isso acontece quando estamos utilizando um contrato Duplex e está fora do escopo deste artigo.

O endpoint expõe uma propriedade chamada DispatchRuntime, do tipo DispatchRuntime, que representa o dispatcher. Esta classe, por sua vez, fornece uma propriedade chamada InstanceProvider, que é onde podemos acoplar a instância do objeto que fará a extração e a devolução das classes (instâncias) do serviço ao pool. Essa propriedade recebe um objeto que implementa a Interface IInstanceProvider (namespace System.ServiceModel.Dispatcher) e a utilizaremos quando formos definir nosso próprio mecanismo de criação de instâncias.

A Interface IInstanceProvider disponibiliza três membros, os quais devem obrigatoriamente ser implementados na classe que gerenciará o pool. Abaixo temos uma tabela com a explicação para esses três membros, mas é importante dizer que a freqüência para a chamada dos métodos abaixo está condicionada ao modo de gerenciamento de instâncias utilizado pelo serviço (propriedade InstanceContextMode do atributo ServiceBehaviorAttribute):

Método Descrição
GetInstance Quando a mensagem chega para o dispatcher ele invocará este método para criar uma nova instância da classe e, em seguida, processá-la.
GetInstance A mesma finalidade do método GetInstance, exceto que este é invocado quando não existe uma classe Message relacionada à requisição atual.
ReleaseInstance Quando o tempo de vida de uma instância chega ao fim, o dispatcher chama este método, passando a instância do objeto corrente para decidirmos o que iremos fazer com a mesma. Como estamos criando um pool, devemos armazená-la para uso posterior.

A implementação do pool consiste em duas etapas: a primeira delas é a criação da classe que gerenciará o pool e uma classe para acoplarmos ao runtime do WCF. Como já sabemos, a classe que irá gerenciar o pool deve implementar a Interface IInstanceProvider; para facilitar o trabalho, iremos criá-la de forma genérica. Internamente ela manterá um membro estático chamado _pool do tipo Stack (coleção do tipo LIFO (Last-In First-Out)), que armazenará as instâncias para serem reutilizadas. Como já é de se esperar, o método GetInstance irá verificar se existe ou não algum item disponível dentro desta coleção e, caso exista, ele será utilizado; caso contrário, uma nova instância será criada (você não precisa adicioná-la à coleção antes de utilizá-la, pois quem fará isso será o método ReleaseInstance).

A implementação do pool para efeito de exemplo não é muito complexa. Utilizamos os métodos Pop e Push da Stack para remover ou adicionar um item, utilizando cada um deles no momento propício (GetInstance e ReleaseInstance). Além disso, a classe está tipificada com TService, obrigando que a mesma seja uma classe e também possua um construtor padrão. Além dos métodos fornecidos pela Interface IInstanceProvider, foi criado um método chamado CreateNewInstance, que retornará uma nova instância da classe que representa o serviço, caso seja necessário. O código abaixo exibe a classe na íntegra:

using System;
using System.Diagnostics;
using System.Collections;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;

public class PoolInstanceProvider<TService> : 
    IInstanceProvider where TService : class, ICleanup, new()
{
    private static object _lock = new object();
    private static Stack _pool;

    static PoolInstanceProvider()
    {
        _pool = new Stack();
    }

    public object GetInstance(InstanceContext instanceContext, Message message)
    {
        object obj = null;

        lock (_lock)
        {
            if (_pool.Count > 0)
            {
                obj = _pool.Pop();
                Debug.WriteLine("From Pool");
            }
            else
            {
                obj = CreateNewInstance();
                Debug.WriteLine("New Object");
            }
        }

        return obj;
    }

    public object GetInstance(InstanceContext instanceContext)
    {
        throw new NotImplementedException();
    }

    public void ReleaseInstance(InstanceContext instanceContext, object instance)
    {
        lock (_lock)
        {
            ((ICleanup)instance).Cleanup();

            _pool.Push(instance);
        }
    }

    private static object CreateNewInstance()
    {
        return new TService();
    }
}

Como podemos notar, a classe PoolInstanceProvider faz o uso de um tipo genérico chamado TService, onde TService deve ser tipificado com uma classe que representará o serviço. Há algumas condições que o tipo que é especificado no parâmetro TService deverá satisfazer: class (deve ser uma classe), ICleanup (deve implementar esta Interface) e new (deve ter um construtor sem parâmetros). A Interface ICleanup fornece um método chamado Cleanup, que tem a finalidade de restaurar os dados (membros internos) utilizados por uma instância antes de retorná-la para o pool.

É importante lembrar que a classe acima é apenas um exemplo que, para um mundo real, talvez precisaria ser melhorada. Com ela criada, nos resta definir o ponto de entrada para acoplarmos esta classe dentro do WCF. Assim como o Throttling, o Pooling também deverá refletir para o serviço como um todo, independente de qual endpoint a requisição venha e, devido à isso, criaremos um behavior de serviço, o que nos leva a implementar a Interface IServiceBehavior em uma classe, mais especificamente o método ApplyDispatchBehavior. Esse método fornece a possibilidade de inserir objetos de estensibilidade na execução do WCF, permitindo modificar ou inspecionar o host que está sendo construído para a execução do objeto que representará o serviço.

A instância do host é fornecida como parâmetro para este método e, através dela, temos a coleção de dispatchers relacionados ao mesmo (lembrando que existe um dispatcher para cada endpoint). Inicialmente iremos percorrer a coleção de dispatchers fornecida pelo host e, a partir daí, a coleção de endpoints. Cada endpoint fornece uma propriedade chamada DispatchRuntime que, como vimos acima, é onde o WCF transforma as mensagens em objetos. A partir dessa propriedade podemos vincular a instância do pool que criamos anteriormente, através da propriedade InstanceProvider. É importante dizer que, como o mesmo pool deverá atender à todas as requisições, você deverá definir a mesma instância para todos os endpoints expostos pelo host. O código abaixo exibe como efetuar essa configuração:

using System;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
using System.ServiceModel.Description;
using System.Collections.ObjectModel;

internal class PoolServiceBehavior<TService> : 
    IServiceBehavior where TService : class, ICleanup, new()
{
    public void AddBindingParameters(ServiceDescription serviceDescription, 
        ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, 
        BindingParameterCollection bindingParameters) { }

    public void ApplyDispatchBehavior(ServiceDescription serviceDescription, 
        ServiceHostBase serviceHostBase)
    {
        PoolInstanceProvider<TService> pool = new PoolInstanceProvider<TService>();

        foreach (ChannelDispatcherBase cdb in serviceHostBase.ChannelDispatchers)
        {
            foreach (EndpointDispatcher ed in ((ChannelDispatcher)cdb).Endpoints)
            {
                ed.DispatchRuntime.InstanceProvider = pool;
            }
        }
    }

    public void Validate(ServiceDescription serviceDescription, 
        ServiceHostBase serviceHostBase) { }
}

Finalmente, com toda essa infraestrutura pronta, devemos adicionar a instância da classe PoolServiceBehavior à coleção de behaviors do host do serviço. Por se tratar de uma classe genérica, ao criar a sua instância você precisará especificar o mesmo tipo da classe que está sendo exposta pelo serviço. O código abaixo ilustra como devemos proceder para instalar a mesma no host:

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

using (ServiceHost host = new ServiceHost(typeof(ServicoDeClientes), 
    new Uri[] { new Uri("net.tcp://localhost:8377") }))
{
    host.Description.Behaviors.Add(new PoolServiceBehavior<ServicoDeClientes>());

    //endpoints

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

Observação: Como já sabemos, há outras formas de adicionar um behavior, como por exemplo via arquivo de configuração, ou até mesmo via atributo, mas isso está além do foco deste artigo.

Conclusão: Este artigo explicou como devemos proceder e quais as formas que temos para dizer ao runtime do WCF quando e como criar a instância do objeto que representará o serviço, bem como o impacto que estas técnicas causarão. As configurações que vimos neste artigo (Throttling e Pooling) melhoram consideravelmente a performance de um serviço mas, se utilizado de forma indevida, pode prejudicar ao invés de melhorar.

ThrottlingPooling.zip (180.90 kb)

Chamadas assíncronas à Serviço via modelo de eventos

O WCF fornece, não por padrão, a possibilidade de invocar as operações de um serviço de forma assíncrona. Quando efetuamos a referencia para um serviço via IDE do Visual Studio 2008 ou através do utilitário svcutil.exe (com a opção /async) automaticamente, além da versão síncrona do método, dois métodos chamados BeginXXX/EndXXX (onde XXX é o nome do método síncrono) são adicionados, permitindo que voce invoque assincronamente uma determinada operação.

Além do tradicional modelo de chamadas assíncronas (APM), temos a possibilidade da chamada assíncrona baseada em eventos. A idéia aqui é, antes de invocar a operação, voce poderá assinar à um evento que será disparado somente quando o processo assíncrono finalizar. Isso evitará de ter um trabalho manual para analisar se o processo finalizou ou não (poll, waiting, etc.). Internamente durante a geração do proxy, o código que é auto-gerado já inclui a implementação necessária para o modelo baseado em eventos.

Basicamente é criado mais uma versão do método, agora com o sufixo XXXAsync que, internamente, faz a chamada para os métodos BeginXXX/EndXXX que, como já sabemos, dispararam a operação de forma assíncrona. Além disso, um delegate do tipo EventHandler<T> também será criado para representar o callback que, quando disparado, invocará o evento do lado de quem está consumindo o serviço. Abaixo um exemplo de como efetuar a chamada assíncrona baseada em eventos:

using (ClienteClient p = new ClienteClient())
{
    p.CalcularComissaoCompleted += 
        new EventHandler<CalcularComissaoCompletedEventArgs>(p_CalcularComissaoCompleted);

    p.CalcularComissaoAsync(“2”);
    Console.ReadLine();
}

static void p_CalcularComissaoCompleted(object sender,
    CalcularComissaoCompletedEventArgs e)
{
    Console.WriteLine(“Fim.”);
}

Se notarmos a implementação interna do proxy, veremos que o método XXXAsync faz o uso do método InvokeAsync, da classe ClientBase<T>. Este método está disponível somente a partir do .NET Framework 3.5. Sendo assim, alguns detalhes durante a geração do proxy precisam ser analisados:

  • Via “Add Service Reference”: se voce estiver fazendo a referencia em um projeto que esteja utilizando o .NET Framework 3.5 e voce opta pela geração dos métodos que dão suporte ao processamento assíncrono, ele também criará os tipos necessários para suportar o modelo de eventos.
  • Via svcutil.exe: neste caso voce precisará especificar através do parametro /async e, além disso, especificar a versão do .NET Framework através do parametro /targetClientVersion, apontando para Version30 ou, se quiser utilizar o modelo baseado em eventos, utilizar a opção Version35.

WCF – Gerenciamento de Instâncias

O gerenciamento de instância é uma técnica que é utilizada pelo WCF ou qualquer outra tecnologia de computação distribuída que determina como e por quem as requisições dos clientes serão atendidas. A escolha do modo de gerenciamento de instâncias interfere diretamente na escalabilidade, performance e transações de um serviço/componente, além de termos algumas mudanças à nível de implementação de contrato, que precisamos nos atentar para garantir que o mesmo funcione sob o modelo de gerenciamento escolhido.

A finalidade do artigo consiste, basicamente, em mostrar cada uma das três técnicas disponíveis pelo WCF mas, também, abordando os seus respectivos benefícios e algumas técnicas que circundam esse processo e que, de alguma forma, estão ligadas e influenciam na escolha e/ou implementação. O artigo também abordará os cuidados que devemos ter na escolha e implementação de cada uma das técnicas fornecidas.

Em primeiro lugar, é importante dizer que a escolha do modo de gerenciamento de instância não compromete diretamente o desenho do contrato do serviço. A escolha do modo aplica-se à classe de implementação do serviço que, por sua vez, faz o uso (implementação) da referida Interface. A escolha do modo de gerenciamento deverá ser realizada a partir de um behavior* de serviço pré-definido dentro do .NET Framework como um atributo, chamado de ServiceBehaviorAttribute.

* Em uma resposta curta, um behavior customiza a execução do serviço. Para ser mais específico, um behavior consiste em um pedaço de código que implementa uma determinada Interface (dependendo de onde o mesmo será aplicado) que permite ao runtime do WCF adicioná-lo no processo de execução e, conseqüentemente, executar o código customizado por você. Eles podem ser “plugados” via atributos ou arquivo de configuração. A criação de behaviors customizados está fora do escopo deste artigo.

Esse atributo possui uma propriedade chamada InstanceContextMode que recebe uma das opções fornecidas pelo enumerador InstanceContextMode. Entre as opções fornecidas por ele temos: PerSession, PerCall e Single. São essas três opções que abordaremos extensivamente a partir de agora.

PerSession

O modo PerSession que, quando omitido é o modo utilizado pelo WCF, cria uma instância da classe que corresponde ao serviço para cada cliente. A ativação é realizada quando o primeiro método é invocado por ele e a instância ficará ativa, respondendo à todas as requisições subseqüentes até que o cliente feche a conexão, que tipicamente acontece quando o método Close do proxy (ClientBase) é invocado (lembrando que o método Dispose também invoca o método Close). Justamente pelo objeto ficar ativo entre a abertura e o fechamento, ele é tipicamente referido como Session do lado do cliente e como Private Session do lado do serviço.

Cada instância ativa do lado do serviço correspondente à uma instância ativa do proxy que está do lado do cliente, ou seja, se um único cliente criar cinco instâncias do mesmo proxy, mesmo que ele aponte para o mesmo serviço, será criada uma classe para cada um deles do lado do serviço, para atender as respectivas chamadas, não podendo uma Session ser compartilhada com outra, mesmo sendo oriunda do mesmo cliente.

Mesmo que o cliente crie várias threads, utilizando a mesma instância do proxy para fazer várias chamadas, o serviço conseguirá atender somente uma única thread e, sendo assim, as outras threads terão que aguardar a liberação para serem processadas. É possível mudarmos esse comportamento utilizando as técnicas de concorrências fornecidas pelo WCF, mas que estão fora do escopo deste artigo.

O trecho de código abaixo ilustra como podemos aplicar o atributo ServiceBehaviorAttribute na classe de implementação do serviço, definindo ele como sendo PerSession mas, se omitirmos o atributo, o comportamento será o mesmo pois, como dito acima, é a configuração padrão do WCF:

using System;
using System.ServiceModel;

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
public class ServidoDeClientes : IClientes
{
    private Dictionary<int, string> _clientes;

    //Construtor

    public int Adicionar(string nome)
    {
        int key = this._clientes.Count + 1;
        this._clientes.Add(key, nome);

        return key;
    }
}

Como podemos notar, a classe está definida como PerSession. Neste caso, como a instância ficará ativa enquanto durar o proxy, podemos armazenar os dados entre as requisições na própria classe, ou seja, em seus membros internos, como é o caso do objeto _clientes. Quando o proxy for encerrado, ele notificará o serviço que a sessão foi finalizada e, caso o mesmo implemente a Interface IDisposable, o método Dispose será invocado*.

* Como o método Dispose será invocado assíncronamente, quer dizer, em uma worker thread, o contexto da operação não será propagado para dentro do método Dispose e, sendo assim, o mesmo não pode depender destas informações contextuais.

Mas para que o suporte a sessão funcione corretamente, precisamos analisar sob qual binding a classe será exposta. Para que seja possível correlacionar as mensagens vindas de um determinado cliente para uma determinada instância, o WCF precisa ser capaz de identificar o cliente e, quando utilizamos bindings que fornecem isso nativamente através do protocolo (como é o caso do TCP ou IPC), não há problema, pois eles mantém uma conexão contínua, conhecida também como sessão à nível de transporte e, com isso, pode-se facilmente identificar quem é o cliente. Pela natureza do protocolo HTTP isso já é um pouco mais complexo, pois a cada chamada uma nova conexão é realizada e, com isso, o BasicHttpBiding não suporta Sessions; já o WSHttpBinding é capaz de emular a sessão à nível de transporte, incluindo um SessionID nos headers da mensagem que identifica unicamente o cliente, muito semelhante ao que acontece com o ASP.NET.

A SessionID é representada por uma GUID e tanto o cliente quanto o serviço são capazes de recuperá-la para que ela possa ser utilizada durante a execução, para alguma determinada tarefa, bem como para efeitos de depuração/logging. O WCF (assim como funcionava também no COM+) fornece uma classe contendo informações contextuais (falaremos mais sobre contexto abaixo), que estão relacionadas à chamada corrente que, além de trazer informações a respeito de transações e segurança, informa também a SessionID. Essa informação é acessada de forma diferente no cliente em relação ao servidor. O trecho de código abaixo ilustra como proceder para recuperar tal informação em ambos os lados:

//Serviço
Debug.WriteLine(OperationContext.Current.SessionId);

//Cliente
Debug.WriteLine(proxy.InnerChannel.SessionId);

Ainda há mais um tipo que precisamos nos atentar. Trata-se do enumerador SessionMode que pode ser atribuído à propriedade SessionMode do atributo ServiceContractAttribute que é aplicado na Interface de contrato do serviço. Essa informação será disponibilizada no contrato do serviço, para que o runtime do WCF saiba que ele deve utilizar uma Session. Esse enumerador fornece três opções, que estão descritas abaixo:

  • Allowed: Especifica que o contrato suporte sessão desde que o binding também suporte. Por padrão, esta opção é definida nos contratos quando a mesma não estiver explícita.
  • NotAllowed: Determina que o contrato não aceite bindings que iniciem sessões.
  • Required: Especifica que o contrato necessite de um binding que suporte sessões e, caso isso não aconteça, uma exceção do tipo InvalidOperationException será lançada durante a carga do serviço.

Como pudemos notar, a finalidade deste enumerador é garantir que o contrato será exposto a partir de um binding que suporte (ou não) as Sessions. A configuração padrão determina a propriedade SessionMode para Allowed e, com isso, se estiver utilizando algum binding que não suporte sessões, como é o caso do BasicHttpBinding, nenhuma exceção será lançada, mas ele não se comportará como o esperado, ou seja, não conseguirá manter o estado entre as chamadas.

Já quando escolhemos a opção NotAllowed e disponibilizamos o contrato a partir de um binding que, nativamente, traz suporte a sessões, como é o caso do TCP, então uma exceção será lançada durante a carga do serviço. Finalmente, a opção Required obriga que o contrato seja exposto sob um binding que suporte sessões e, caso não suporte, assim como a opção anterior, uma exceção será lançada durante a carga do serviço. O trecho de código abaixo ilustra como definir essa propriedade no contrato do serviço:

using System;
using System.ServiceModel;

[ServiceContract(SessionMode = SessionMode.Required)]
public interface ICliente
{
    [OperationContract]
    int Adicionar(string nome);
}

Ordem de Execução

Quando utilizamos contratos que suportam sessão, às vezes precisamos determinar a ordem de execução, garatindo assim que um método seja capaz de inicializar a sessão; já outro método poderá somente ser executado caso a sessão já esteja devidamente criada, e ainda há a possibilidade de um método encerrar a sessão.

Para termos um controle sobre isso, utilizamos duas propriedades chamadas IsInitiating e IsTerminating do atributo OperationContractAttribute. Essas propriedades irão demarcar o limite da sessão dentro do serviço e uma consistência é realizada durante a carga do mesmo para garantir que ele esteja com a opção SessionMode definida como Required. Por padrão, a propriedade IsInitiating é definida como True e IsTerminating como False, permitindo que qualquer método inicie a sessão. O interessante é que esta técnica evitará que você faça consistências dentro do método para saber se a sessão já está ou não ativa, pois o próprio WCF garante isso, uma vez que você está tentando acessar um método que não permite inicializar a sessão e a mesma ainda não esteja criada.

Para exemplificar, vamos imaginar que temos um contrato que define algumas operações típicas para um comércio eletrônico. As operações consistem em identificar o cliente a partir de um login, possibilidade de adicionar produtos, recuperar o valor total dos produtos e, finalmente, um método para finalizar a compra. O código abaixo ilustra como devemos fazer para configurar esses métodos dentro da Interface para garantir a ordem das chamadas:

using System;
using System.ServiceModel;

[ServiceContract(SessionMode = SessionMode.Required)]
public interface IComercioEletronico
{
    [OperationContract(IsInitiating = true)]
    bool EfetuarLogin(string nome);

    [OperationContract(IsInitiating = false)]
    void AdicionarProduto(int codigoDoProduto, int quantidade);

    [OperationContract(IsInitiating = false)]
    decimal RecuperarValorTotal();

    [OperationContract(IsInitiating = false, IsTerminating = true)]
    void FinalizarCompra();
}

Apesar da propriedade IsInitiating ser definida, por padrão, como True, não seria necessário especificá-la no método EfetuarLogin; já os dois métodos intermediários, AdicionarProduto e RecuperarValorTotal não permitem inicializar ou finalizar a sessão (lembre-se de que a propriedade IsTerminating por padrão é sempre False) e, finalmente, o método FinalizarCompra encerra a sessão, pois define IsInitiating como False e IsTerminating como True.

Com este contrato devidamente implementado podemos, do lado do cliente, criar a referência para o serviço, instanciar o proxy e consumir os métodos. Caso você tente invocar um método que não crie a sessão antes de invocar o método que tem essa finalidade, você receberá uma exceção do tipo InvalidOperationException, informando que o método que está tentando invocar somente poderá ser disparado depois da sessão criada. O trecho de código abaixo mostra como invocar os métodos em sua seqüência correta (repare que isso não muda em nada o código cliente):

using (ComercioEletronicoClient proxy = new ComercioEletronicoClient())
{
    proxy.EfetuarLogin("Israel Aece");

    proxy.AdicionarProduto(12, 4);
    proxy.AdicionarProduto(12, 3);
    proxy.AdicionarProduto(3, 3);

    Console.WriteLine(proxy.RecuperarValorTotal());

    proxy.FinalizarCompra();
}

Se tentar chamar qualquer método antes do método EfetuarLogin, uma exceção será lançada, dizendo que isso não é possível, obrigando você a invocar os métodos na ordem correta. Além disso, é importante dizer que como o método FinalizarCompra está com a propriedade IsTerminating definida como True, ao invocá-lo, o WCF removerá a instância corrente, invalidando o proxy. Caso você necessite fazer novas chamadas, você terá que criar um novo proxy.

Apesar desta técnica estar sendo abordada dentro da seção que discute o modo PerSession, ela pode também ser adotada em um serviço que é exposto através do modo Single, que será abordado mais abaixo.

Desativação da Instância

O modelo PerSession permite que várias mensagens estejam correlacionadas à uma mesma instância, mantendo o objeto ativo do lado do serviço, atendendo às requisições de um determinado proxy. Na verdade as instâncias estão contidas dentro de um contexto e, quando uma sessão é criada, o host cria um novo contexto para acomodar a instância dentro dele. A imagem abaixo ilustra graficamente como esse processo acontece:

Figura 1 – A relação entre o contexto e a instância do serviço.

Como podemos notar, a instância sempre estará contida dentro de um contexto que, por sua vez, tem o mesmo tempo de vida do objeto. Entretanto, em um ambiente onde você quer ter um maior controle em relação ao gerenciamento dos contextos e também das instâncias, talvez por questões de performance ou mesmo de consistência, o WCF desacopla o gerenciamento da instância em relação ao contexto onde ela reside e, com isso, você poderá definir quando descartar a instância (antes ou depois da chamada do método), mas tendo ainda o acesso ao contexto corrente, algo que não era possível antes. Esse controle é útil em momentos em que o método utiliza recursos valiosos, onde não se pode esperar até que a sessão finalize para que eles sejam liberados.

Esse controle pode ser realizado de forma imperativa ou declarativa. Isso quer dizer que podemos, no interior de um método, invocar um método do WCF que libera a instância ou, da forma declarativa, onde podemos decorar os métodos que fazem parte do serviço a descartar a instância antes ou depois da execução do mesmo. Para a primeira forma, recorremos ao método ReleaseServiceInstance da classe InstaceContext, como mostra o exemplo de código abaixo:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
public class ServicoDeComercioEletronico : IComercioEletronico
{
    //Outros métodos

    public void FinalizarCompra()
    {
        //finalizar a compra
        OperationContext.Current.InstanceContext.ReleaseServiceInstance();
    }
}

Já no modelo declarativo, recorremos ao atributo OperationBehaviorAttribute. Este atributo fornece uma propriedade chamada ReleaseInstance que aceita os dois itens contidos dentro do enumerador ReleaseInstanceMode, que dizem ao WCF que a instância deve ser reciclada em um ponto específico do processo. As opções deste enumerador são abordadas abaixo:

  • None: É a opção padrão que recicla o objeto de acordo com o valor definido na propriedade InstanceContextMode (PerSession, PerCall e Single).
  • BeforeCall: Recicla o objeto antes da chamada da operação.
  • AfterCall: Recicla o objeto depois da chamada da operação. Semelhante ao método ReleaseServiceInstance.
  • BeforeAndAfterCall: Recicla o objeto antes e depois da chamada da operação. Esta opção é muito semelhante aos serviços configurados como PerCall.

Abaixo o mesmo exemplo que vimos acima, só que agora no modo declarativo. É importante notar que em ambos os casos essas configurações são realizadas na classe que implementa o contrato:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
public class ServicoDeComercioEletronico : IComercioEletronico
{
    //Outros métodos

    [OperationBehavior(ReleaseInstanceMode = ReleaseInstanceMode.BeforeCall)]
    public void FinalizarCompra()
    {
        //finalizar a compra
    }
}

Determinar qual das três opções de sessão escolher (Allowed, NotAllowed ou Required), dependerá muito do que o teu contrato se propõe a realizar. Serviços configurados como PerSession tem um alto custo e não escalam bem em relação aos serviços configurados como PerCall. Isso acontece porque enquanto o proxy permanecer ativo do lado do cliente, a classe correspondente que serve as suas requisições também ficará ativa do lado do serviço. Caso as chamadas forem feitas com bastante freqüência, manter o proxy pode ser uma boa solução; já quando as chamadas forem esporádicas ou se precisar de escalabilidade ou consistência, então o melhor é recorrer ao modo PerCall, que será abordado logo abaixo.

PerCall

Enquanto a característica de serviços configurados como PerSession é manter o objeto ativo do lado do serviço enquanto existir o proxy do lado do cliente, o PerCall é o oposto. Muitas vezes os clientes prendem a referência para o objeto por um longo período de tempo e, na verdade, fazem o uso dele em poucos momentos. Isso fará com que os objetos consumam memória desnecessária do lado do serviço, sem ao menos ser utilizada.

Serviços configurados como PerCall criarão uma nova instância do serviço a cada chamada para qualquer operação. Quando o método retornar, o objeto será removido da memória, existindo apenas durante a execução do método solicitado pelo cliente. Para classes que implementam a Interface IDisposable, momentos antes da classe ser removida, o método Dispose será disparado mas, ao contrário do modelo PerSession, o método Dispose será executado na mesma thread da operação e, conseqüentemente, você ainda terá acesso ao contexto.

Neste modo de gerenciamento, você precisa se atentar à alguns detalhes que mudam consideravelmente em relação ao modo PerSession. O primeiro deles é com relação a concorrência; como cada chamada criará uma nova instância dedicada, então você não precisará lidar com ambiente multi-threading, pois ele nunca irá acontecer. Outro detalhe importante é com relação ao gerenciamento de estado. Como a instância que servirá a requisição será destruída quando o método retornar, não será possível que você guarde alguma informação entre as requisições e, caso queria, você precisará criar e gerenciar um repositório próprio, além do que precisará também identificar unicamente o cliente a cada chamada e, na maioria das vezes, isso influencia em uma mudança no contrato do serviço.

Seguindo o mesmo exemplo que utilizamos no modo PerSession, mudaremos ligeiramente a classe ServidoDeClientes para, neste momento, utilizar o modelo PerCall. Tudo o que precisamos é definir a propriedade InstanceContextMode do atributo ServiceBehaviorAttribute para PerCall e tudo já funcionará com este novo comportamento:

using System;
using System.ServiceModel;

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public class ServidoDeClientes : IClientes
{
    private Dictionary<int, string> _clientes;

    //Construtor

    public int Adicionar(string nome)
    {
        int key = this._clientes.Count + 1;
        this._clientes.Add(key, nome);

        return key;
    }
}

Como já é de se esperar, a cada chamada para o método Adicionar, ele sempre criará uma nova instância da classe e, conseqüentemente, todo o conteúdo será recriado, não mantendo as informações entre as solicitações. Como dito acima, caso você precise manter alguma informação entre as requisições, você precisará inicializar essas informações a partir de algum repositório, como banco de dados, sistema de arquivos ou até mesmo variáveis estáticas.

Single

Já no modelo Single (Singleton) o comportamento é muito diferente em relação às duas opções que vimos acima. A idéia deste modelo é ter uma única instância servindo à todas as requisições que chegarem para este serviço, independente de qual endpoint venha a requisição. Este objeto viverá enquanto o host que a hospeda viver.

Como uma das principais vantagens, o modo Single não cria para todo cliente ou para toda chamada uma nova instância do objeto mas, em contrapartida, há algumas desvantagens que são um pouco difícies de lidar. O primeiro caso é com relação à concorrência: como você tem múltiplos clientes acessando a mesma instância, objetos contidos dentro dela também poderão ser acessados ao mesmo tempo e, se não se preocupar com a forma de acessá-los, poderá ter problemas no resultado final do processo ou até mesmo deadlocks. Outro problema conhecido mas que não se pode fazer muito a respeito, justamente pela necessidade de ter um único objeto fornecendo todas as requisições, é que ela (instância) ficará ativa mesmo quando não há nenhuma tarefa para ela executar e, dependendo da quantidade de recursos que a mesma armazena, poderemos ter problemas futuros.

Para utilizar o modo Single, assim como os outros, devemos recorrer ao atributo ServiceBehaviorAttribute, definindo a propriedade InstanceContextMode como Single. Mas é importante ressaltar que, para membros internos, será necessário que você entenda os possíveis problemas que podem acontecer pelo fato de múltiplos clientes fazerem requisições simultâneas, e isso exige uma mudança na forma de programar e/ou acessar os recursos internos da classe. A concorrência não é tratada aqui, pois precisa de um artigo a parte para abordar isso. O código abaixo ilustra o mesmo exemplo (ServidoDeClientes), mas utilizando o modo Single:

using System;
using System.ServiceModel;

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class ServidoDeClientes : IClientes
{
    private Dictionary<int, string> _clientes;

    //Construtor

    public int Adicionar(string nome)
    {
        int key = this._clientes.Count + 1;
        this._clientes.Add(key, nome);

        return key;
    }
}

A classe ServiceHost, que é responsável pelo gerenciamento da instância da classe de serviço, possui alguns overloads do construtor que, em uma das versões, aceita uma instância do tipo (Type) do serviço; neste caso o ServiceHost é responsável por instanciar a classe, invocando o construtor padrão da mesma. Já em outra versão do construtor do ServiceHost, ele recebe um parâmetro do tipo object, permitindo que você instancie a classe de implementação do serviço. Isso é útil quando a implementação do serviço necessita de algumas informações adicionais (via construtor ou propriedades) para poder trabalhar. Abaixo há um exemplo que mostra essa técnica:

ServicoDeClientes srv = new ServicoDeClientes("connection_string");
srv.NumeroDeTentativas = 5;

using(ServiceHost host = new ServiceHost(srv, 
    new Uri[] { new Uri("net.tcp://localhost:9292") }))
{
    //...
}

Conclusão: Este artigo mostrou as três formas que temos para gerenciar a instância de uma classe que serve as requisições. É importante analisar as vantagens e desvantagens de cada uma delas para decidir qual delas é a ideal para o serviço que está sendo desenvolvido. Além disso, a escolha do modo de gerenciamento interfere diretamente no modo de escrita de código, justamente para possibilitar que o serviço se comporte bem em um ambiente multi-threading, já que os modos PerSession e Single permitem que isso aconteça.

GerenciamentoDeInstancias.zip (636.53 kb)

WCF – Transferência e Codificação de Dados

Um dos maiores benefícios que serviços da Web tem em relação à tecnologias de comunicação distribuídas é o uso de XML como base da codificação, permitindo assim, a interoperabilidade entre as mais diversas plataformas. Entretanto, a escolha do padrão de codificação e a forma de transferência destas informações a serem adotados pelo serviço influenciará diretamente na performance e interoperabilidade do mesmo e também daqueles que o consomem.

O WCF – Windows Communication Foundation – fornece algumas técnicas e alternativas que podemos adotar durante a configuração ou criação de um determinado serviço. A finalidade deste artigo é mostrar como implementar tal configuração/criação e analisar os impactos (inclusive a nível de contrato), benefícios e restrições de cada uma destas técnicas.

Quando desenhamos um contrato para um serviço, definimos alguns métodos para satisfazer as regras de negócio que ele se propõe a fazer. Por padrão, para cada método criado, o WCF cria duas versões do método (mais popularmente conhecido como mensagens), uma para requisição (Request) e outro para resposta (Response). Cada mensagem é representada por um envelope SOAP que, por sua vez, contém o cabeçalho (header) e corpo (body). Enquanto o cabeçalho traz informações a respeito da segurança, correlação, contexto, etc., o corpo da mensagem é responsável por conter os dados que estão sendo enviados para um serviço ou quando a resposta de um serviço está sendo retornada para o cliente.

As informações contidas no cabeçalho podem ser utilizadas pelo destinatário final bem como por algum intermediário, caso exista. Já o conteúdo do corpo da mensagem, por conversão, deve ser utilizado pelo último recebedor, ou melhor, o destinatário final, independente de quantos firewalls ou roteadores existam.

Quando temos um contrato que expõe métodos que recebem ou devolvem tipos de dados simples, como strings, números ou até mesmo objetos complexos, mas que disponibiliza propriedades também simples, não vemos muitos problemas em termos de performance. O problema começa a aparecer quando o serviço que estamos desenhando irá aceitar um conteúdo mais “pesado”, como é o caso de arquivos, imagens, multimídia, etc. Neste caso, se não nos atentarmos a alguns detalhes, podemos sofrer grandes penalidades na performance do serviço.

O padrão Text (texto estruturado) consiste em codificar os dados em um formato legível e, sendo assim, é possível ler o seu conteúdo em qualquer editor de texto, e qualquer sistema será capaz de processar tal texto. Com o padrão Text, além de cada item estar envolvido por um elemento ou atributo, dados “não-texto” serão convertidos para texto antes de serem enviados, contribuindo para aumentar o tamanho das mensagens que trafegam pela rede. Para um serviço mais simples, o overhead é mínimo. Mas e quando precisamos transferir grandes quantidades de dados, como por exemplo: imagens, multimídia, etc. (representados por array de bytes)?

O comportamento atual do WCF faz com que ele aplique a codificação Base64 a um conteúdo binário se desejarmos continuar utilizando o formato Text, resultando assim em um texto mais compactado mas, mesmo assim, ainda será um conteúdo muito maior em relação ao seu tamanho original. Além disso, o parser do XML não trabalha muito bem com conteúdos extensos, o que poderá acarretar em problemas durante o runtime.

Em situações onde o conteúdo binário é uma exigência e não há necessidade de interoperabilidade (onde cliente e servidor fazem uso do WCF), podemos recorrer à utilização da codificação binária, que é utilizada em todos os bindings para uso em ambiente homogêneo. Falaremos mais sobre os tipos de codificação e os bindings na seção Encodings, um pouco mais abaixo.

Por último, temos ainda uma terceira alternativa que é o MTOM (Message Transmission Optimization Mechanism), que é o mesmo padrão utilizado quando enviamos a um e-mail um conteúdo binário. Este padrão aberto, regido pelo W3C, ao invés de codificar o conteúdo binário como Text, ele é enviado de forma intacta como sendo um “anexo” da mensagem, seguindo a especificação do padrão MIME (Multipurpose Internet Mail Extension). Assim como antes, qualquer texto da mensagem original será codificado como um XML Infoset, mas o conteúdo binário será representado como uma referência, apontando para o anexo MIME. Quando a mensagem chega ao seu destino, o runtime do WCF é capaz de reconstituir a mensagem original a partir do envelope SOAP e seus respectivos “anexos”.

Quando fazemos o uso do MTOM, o WCF é capaz de determinar o tamanho da mensagem e, caso ela seja pequena demais, ao invés de fazer o uso do MTOM, ele opta por codificar a mensagem utilizando a codificação Base64. Já quando o conteúdo é grande e o WCF vê que terá benefícios com o uso do WCF, ele criará uma nova seção MIME e colocará o conteúdo binário lá e, no envelope SOAP, teremos uma referência para tal conteúdo. A imagem abaixo ilustra o uso do MTOM:

Figura 1 – O MTOM define externamente o conteúdo binário.

Encodings

Um encoding dentro do WCF define as regras de como será realizada a codificação dos dados, ou melhor, da mensagem. As mensagens (Request e Response) são representadas dentro da infraestrutura do WCF através de uma classe chamada Message. Temos um outro elemento importante dentro do runtime do WCF, chamado encoder, que nada mais é que uma implementação de um determinado encoding e que fará tarefas distintas dependendo do momento, a saber: do lado do remetente, o 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 seqüência de bytes da rede e a transforma em uma instância da classe Message novamente, para que a mesma possa ser processada.

Todos os padrões de codificação (encoding) que falamos acima estão fortemente relacionados ao binding e, sendo assim, isso se trata de uma configuração que pode ser realizada sem interferir na criação ou implementação do contrato. Cada um dos bindings pré-definidos dentro do .NET Framework já possui um encoding específico. A tabela abaixo ilustra as quatro opções (classes) que temos e que falamos acima:

Encoding Descrição
TextMessageEncodingBindingElement Este encoding é o padrão para todos os bindings baseados em HTTP e também é o apropriado para todos os customizados que você venha a criar e que exigem interoperabilidade. Lembrando que este encoding lê e escreve o conteúdo baseando-se no formato SOAP 1.1/1.2, em formato Text, sem nenhum tratamento específico para conteúdo binário.
MtomMessageEncodingBindingElement Este encoding fornece uma customização importante para o conteúdo binário e não é utilizado por padrão em nenhum binding.
BinaryMessageEncodingBindingElement Este encoding é o padrão para todos os bindings Net* e é a escolha ideal para quando a comunicação estiver envolvendo o WCF em ambas as partes (cliente e servidor). Este encoding utiliza o formato .NET Binary XML, que é uma representação binária da Microsoft para XML Infosets.
WebMessageEncodingBindingElement * Este encoding habilita a codificação em formato plain-text XML e JSON (JavaScript Object Notation). Este tipo de encoding é comumente utilizado quando necessitamos expor o serviço para consumo via POX, REST, RSS ou AJAX.

Você deve avaliar cuidadosamente qual destes encodings escolher. É importante lembrar que se você precisar um combinado destes detalhes, como por exemplo, suportar interoperabilidade e boa performance, você pode expor diferentes endpoints, onde cada um deles é específico para cada uma dessas características.

Todas essas classes estão contidas dentro do namespace System.ServiceModel.Channels, herdando de uma classe base chamada MessageEncodingBindingElement. A hierarquia dos encodings está sendo exibida através da imagem abaixo:

Figura 2 – Hierarquia dos encodings.

A tabela abaixo ilustra o encoding correspondente (padrão) para cada um dos bindings existentes dentro do .NET Framework:

Binding Encoding
BasicHttpBinding TextMessageEncodingBindingElement
WebHttpBinding * WebMessageEncodingBindingElement *
WSDualHttpBinding TextMessageEncodingBindingElement
WSFederationHttpBinding TextMessageEncodingBindingElement
WSHttpBinding TextMessageEncodingBindingElement
NetTcpBinding BinaryMessageEncodingBindingElement
NetPeerTcpBinding BinaryMessageEncodingBindingElement
NetNamedPipeBinding BinaryMessageEncodingBindingElement
NetMsmqBinding BinaryMessageEncodingBindingElement

* Tipos contidos dentro do Assembly System.ServiceModel.Web.dll que está disponível a partir do .NET Framework 3.5.

Agora que já sabemos quais são os encodings definidos como padrão para cada um dos bindings existentes, precisamos entender como devemos proceder para customizar e/ou alterar os encodings dos bindings existentes. Todo binding herda direta ou indiretamente de uma classe base denominada Binding. Essa classe fornece um método, também abstrato, chamado CreateBindingElements, que fornece como retorno uma coleção do tipo BindingElementCollection. A finalidade deste método, quando sobrescrito nas classes derivadas (BasicHttpBinding, NetPeerTcpBinding, etc.), é retornar uma coleção contendo todos os elementos (segurança, transação, codificação, etc.) que compõem o binding, devidamente configurados e na ordem correta.

Para alternarmos entre padrão de codificação Text ou MTOM, podemos recorrer à propriedade MessageEncoding, que aceita como parâmetro um dos itens especificados pelo enumerador WSMessageEncoding. O trecho de código abaixo ilustra como efetuar a configuração de um determinado binding para suportar o formato MTOM.

using System;
using System.ServiceModel;
using System.ServiceModel.Channels;

BasicHttpBinding b = new BasicHttpBinding();
b.MessageEncoding = WSMessageEncoding.Mtom;

Observação Importante: A propriedade MessageEncoding está disponível apenas para bindings que suportam interoperabilidade, como é o caso dos bindings HTTP. Como dissemos acima, os outros bindings são úteis e tem uma melhor performance em ambiente totalmente homogêneo.

A customização na codificação que fizemos através do código acima também é suportada à nível de configuração. Para cada um dos encodings há um elemento correspondente que podemos utilizar durante a configuração do binding no arquivo de configuração da aplicação. Os elementos são: <textMessageEncoding />, <mtomMessageEncoding />, <binaryMessageEncoding /> e <webMessageEncodingElement />. É importante lembrar que o mesmo tipo de codificação terá que ser utilizado em ambos os lados, caso contrário, uma exceção do tipo ProtocolException será atirada.

Para finalizar, você pode recorrer à técnica de logging (já abordada neste artigo) para que você consiga visualizar/interceptar a geração ou leitura do conteúdo da mensagem e analisar a sua codificação. A imagem abaixo ilustra o mesmo conteúdo sendo trafegado em um formato normal (Text) e, em seguida, fazendo o uso do MTOM.

Figura 3 – Diferenciando o padrão de codificação utilizado.

Formas de Transferência

Uma vez que determinamos qual será o binding e o encoding a serem utilizados pelo serviço, precisamos agora entender as possíveis formas de transferência de dados e como incorporá-las ao serviço. É importante dizer que as funcionalidades aqui abordadas não são válidas para todos os bindings e que, no decorrer desta seção, veremos como e onde podemos aplicar essas técnicas.

O WCF suporta quatro modos de transferência de mensagens (opções do enumerador TransferMode):

  • Buffered: Com esta opção, a mensagem é carregada totalmente na memória antes de ser enviada ao destinatário. Esta opção é a configuração padrão para todos os bindings existentes dentro do .NET Framework.

  • Streamed: Já com a opção Streamed, somente o header da mensagem será carregado em memória (não devendo ultrapassar a quantidade máxima especificada na propriedade MaxBufferSize), enquanto o body será definido como um stream, permitindo que pequenas partes de um conteúdo sejam lidas por vez. Essa configuração afetará tanto a requisição quanto a resposta.

  • StreamedRequest: Apenas aplicará o stream em uma requisição, enquanto a resposta será colocada em buffer. É uma boa opção apenas quando aceitamos Stream como parâmetro, em operações one-way.

  • StreamedResponse: Apenas aplicará o stream em uma resposta, enquanto a requisição será colocada em buffer. Esta opção é útil apenas quando o retorno do método devolve um Stream.

Para efetuar tal configuração, devemos recorrer ao uso da propriedade TransferMode que, por sua vez, aceita uma das quatro opções contidas no enumerador TransferMode. Essa propriedade está apenas exposta em bindings que suportam a técnica de streaming, e são eles: BasicHttpBinding, NetTcpBinding e NetNamedPipeBinding. O motivo de apenas estes bindings suportarem o streaming é que há algumas regras funcionais que devem ser explicitamente seguidas, como por exemplo, assinaturas digitais são definidas e computadas em cima do conteúdo da mensagem e, com a opção de stream habilitada, o conteúdo não estará totalmente à disposição para executar a tarefa de verificação da mesma. Como a opção Buffered é a padrão e não necessita nenhuma mudança, não há o que falar sobre ela a não ser como o WCF a trata internamente, como já vimos acima.

Além de restrições funcionais impostas pelo modo Streamed, ainda há algumas regras que precisamos seguir para conseguir expor um serviço como stream, e essas regras envolvem a mudança em como desenhamos o contrato de serviço. A idéia é que os métodos que irão compor o contrato devem receber ou retornar uma instância da classe Stream, contida dentro do namespace System.IO; isso irá depender se o serviço irá retornar algum conteúdo binário para o cliente ou se o cliente enviará algum conteúdo binário para o serviço.

Nestes casos (uso da classe Stream), o parâmetro ou o retorno de um método (operação) corresponderá ao corpo da mensagem. Para exemplificar o uso desta técnica em nossos serviços, podemos criar um contrato (Interface) que possui dois métodos: Upload e Download, onde recebe e retorna uma instância da classe Stream, respectivamente:

using System;
using System.IO;
using System.ServiceModel;

[ServiceContract]
public interface IImagens
{
    [OperationContract]
    Stream Download(string imagemId);

    [OperationContract]
    string Upload(Stream imagem);
}

Como podemos notar, a forma como desenhamos o contrato muda drasticamente. A sua implementação terá que saber como lidar com o Stream; no caso do método Download a idéia é, dado uma string que representa o nome do arquivo, devemos capturá-lo e retornar o mesmo através do método; já no método Upload, recebem o Stream que corresponde à imagem e salvamos a mesma no disco e retornamos o nome que é randomicamente gerado.

Observação: Por questões de espaço, a implementação do contrato não será exibida aqui, mas está contida dentro do projeto de exemplo, relacionado ao artigo.

Como mostrado e comentado acima, você terá o Stream como sendo o body da mensagem. Mas e se, em algum momento, quisermos passar alguma informação extra? Como o body é exclusivamente reservado para o conteúdo do Stream, podemos recorrer aos headers da mensagem para acomodar tais informações. Mais uma vez, isso mudará a forma como criamos o contrato, necessitando especificar, declarativamente, o contrato da mensagem, informando os headers adicionais e o corpo da mensagem, que continuará sendo o Stream. O trecho de código ilustra como devemos proceder para a criação deste contrato:

using System;
using System.IO;
using System.ServiceModel;

[MessageContract]
public class StreamData
{
    [MessageHeader]
    public string NomeDoArquivo;

    [MessageBodyMember]
    public Stream Conteudo;
}

No exemplo acima, o atributo MessageContractAttribute define que uma classe irá corresponder à mensagem SOAP que será trafegada; já o atributo MessageHeaderAttribute especifica um membro que fará parte do header da mensagem e, finalmente, o atributo MessageBodyMemberAttribute determinará o membro que será o corpo da mensagem. Com isso, a Interface que representa o contrato do serviço também sofrerá mudanças: ao invés dela lidar diretamente com Streams, ela receberá e retornará instâncias da classe StreamData. O código abaixo ilustra uma Interface bem semelhante a que utilizamos acima (IImagens), mas agora com essa nova técnica:

using System;
using System.IO;
using System.ServiceModel;

[ServiceContract]
public interface IArquivos
{
    [OperationContract]
    StreamData Download();

    [OperationContract]
    void Upload(StreamData dados);
}

Observação: Mais uma vez, por questões de espaço, a implementação do contrato não será exibida aqui, mas está contida dentro do projeto de exemplo, relacionado ao artigo.

Independente da técnica que você utilize durante a criação do contrato, um detalhe muito importante e que precisa ser comentado é em relação ao fechamento do Stream que trafega de um lado para outro. Quem será responsável por fechá-lo? Geralmente o Stream será enviado tanto do cliente para o servidor, quanto do servidor para o cliente. Quem envia o Stream não deve fechá-lo; o runtime do WCF fará isso. Já do lado de quem receberá o Stream, deverá fechá-lo depois que utilizá-lo.

Um último detalhe: quando estamos operando com dados que excedem a configuração padrão para envio e recebimento de informações, precisamos nos atentar para especificar um valor que consiga acomodar as informações que serão trocadas. Os bindings relacionados com esta técnica possuem duas propriedades que afetam diretamente o Streamed, sendo elas: MaxBufferSize e MaxReceivedMessageSize. Como essas configurações não estão contempladas dentro do contrato (WSDL) do serviço, então fica a cargo do desenvolvedor especificar um valor, diferente do padrão, caso queira aceitar um conteúdo maior do que o especificado.

O formato Streamed causa dependência de plataforma e, conseqüentemente, não é interoperável como o MTOM. Além disso, tem restrições à nível de contrato, está restrito a ser utilizado por apenas três bindings e a segurança poderá ser somente garantida pelo transporte. Já como ponto positivo, temos um consumo de memória menor e também uma melhor performance em virtude da natureza do envio. Pensando nestas características, é provável que você já consiga saber se deve ou não optar pelo uso dele.

Serialização

É importante dizer que o processo de serialização é independente da transferência (encoding). A serialização é um processo que define como os objetos serão mapeados para XML Infosets, enquanto o encoding determinará como estes XML Infosets serão escritos (em bytes) e enviados para algum dos encodings que vimos acima e que, por sua vez, ocorre em nível de transporte.

Um outro ponto importante é em relação ao contrato. Independentemente do encoding escolhido, ele não acarretará em nenhuma mudança no contrato do serviço, ao contrário do processo de serialização que, por sua vez, poderá influenciar na criação do mesmo. Como a serialização é um processo importante e um pouco extenso, ela não será abordada neste artigo.

Conclusão: Como pudemos notar no decorrer do artigo, o WCF desacopla o máximo possível a implementação da execução, permitindo que o contrato não tenha conhecimento à respeito de como o serviço está sendo exposto e consumido. Mas, em alguns casos, podemos mudar o comportamento padrão, não se resume a apenas mudar uma configuração mas sim a mudança da Interface que determina o contrato do serviço. É importante analisar cuidadosamente cada um dos encodings e os modos de transferência para saber qual deles se encaixa melhor em sua solução.

TransferenciaCodificacaoDeDados.zip (1.29 mb)

WcfTestClient

O utilitário WcfTestClient.exe que está integrado ao Visual Studio .NET não precisa ser necessariamente incializado a partir de um projeto do tipo WCF Service Library que, por sua vez, tem a finalidade de gerar uma DLL contendo o contrato e implementação do serviço e, o utilitário é utilizado para testá-los, sem a necessidade de criar um host, pois ele já faz isso.

É importante dizer que podemos iniciá-lo a partir do prompt de comando do Visual Studio, passando como parametro uma Url já existente para que possamos testá-lo, sem a necessidade de criar um projeto dummy para isso. Abaixo está o exemplo:

C:>WcfTestClient http://IADevServer/ServicoDePublicacaoDeArquivos/

WCF – Chamadas Assíncronas

Muitas vezes desenvolvemos um método para desempenhar alguma tarefa e, depois de devidamente codificado, invocamos o mesmo a partir de algum ponto da aplicação. Dependendo do que este método faz, ele pode levar certo tempo para executar e, se o tempo for consideravelmente alto, podemos começar a ter problemas na aplicação, pois como a chamada é sempre realizada de síncrona, enquanto o método não retornar, a execução do sistema que faz o uso do mesmo irá congelar, aguardando o retorno do método para dar seqüência na execução.

Entretanto, para fornecer uma melhor experiência ao usuário e tornar o desempenho da aplicação muito mais performático, podemos optar por executar, uma determinada tarefa de forma assíncrona. Isso irá possibilitar o disparo de um processo custoso em uma thread de trabalho a parte da aplicação, permitindo assim que a aplicação continue funcionando enquanto o processo custoso é executado em paralelo e, quando o mesmo finalizar, podemos recuperar o resultado e exibir o mesmo ao usuário.

A finalidade deste artigo é mostrar como implementar o processamento assíncrono tanto do lado do cliente (proxy) bem como do lado do servidor (contrato) em serviços WCF. Antes de prosseguir, é necessário que estejamos familiarizados com a arquitetura de processamento assíncrono dentro da plataforma .NET. Basicamente, temos alguns métodos dentro do próprio .NET Framework que existem 3 versões para o mesmo: o primeiro deles é para uso síncrono; já os outros dois (BeginXXX e EndXXX) são para a chamada assíncrona deste mesmo processo. Convencionou-se que métodos síncronos são nomeados de acordo com a sua funcionalidade, exemplo: Read, Write; já, as versões assíncronas destes mesmos métodos são: BeginRead, EndRead, BeginWrite e EndWrite.

A versão 2.0 do ADO.NET também já traz suporte ao processamento assíncrono para tarefas comuns de acesso a dados, como a execução de queries complexas para modificação de registros no banco de dados e também quando necessitamos recuperar dados com alguma query mais complexa (mais detalhes podem ser vistos neste artigo). O suporte ao processo assíncrono não para por aí, acesso aos arquivos no disco, acesso a serviços e até mesmo os delegates trazem nativamente esse suporte.

Processamento Assíncrono no Cliente

Neste cenário, nada é necessário ser realizado durante a criação e implementação do contrato. Isso funcionará de forma muito semelhante ao que já acontecia com os artigos ASP.NET Web Services, ou seja, para cada método criado no contrato, é também criado uma versão assíncrona (BeginXXX e EndXXX) do método quando fazemos a referência ao serviço no cliente. Aqui há ligeira mudança: os métodos que dão suporte assíncrono não são implicitamente criados. A imagem abaixo ilustra o local que devemos marcar para definir a criação dos métodos que dão suporte ao processo assíncrono:

Figura 1 – Ao efetuar uma Add Service Reference, clique na opção Advanced para marcar a opção que está marcada em vermelho.

Por padrão, esta opção não está marcada. Uma vez que você opta por marcá-la, será criada uma versão assíncrona para cada método que há dentro do contrato. A partir daí o desenvolvedor que consume o proxy do serviço precisará se atentar para efetuar a chamada para o método de forma assíncrona, o que exigirá a forma em que a chamada será executada. Quando se trata um processo assíncrono, temos alguns cuidados a respeito do disparo do método: o método que dá início ao processo (BeginXXX) geralmente espera os parâmetros necessários para a execução da tarefa (caso exista), uma instância de um delegate do tipo AsyncCallback (explorado mais abaixo) e, finalmente, um object que representa uma informação “contextual”.

Observação: A opção “Generate asynchronous operations” é o mesmo que especificar o parâmetro /async do utilitário svcutil.exe.

Para exemplificar, temos um contrato chamado ICliente que possui um método chamado CalcularComissao que, por sua vez, retornará um valor que representa o valor calculado das comissões. Essa tarefa trata-se de um processo que, para fins de exemplo, será encarado como uma tarefa custosa e que levará certo tempo para executar. O código abaixo ilustra o contrato a ser utilizado (note que não há nada de especial):

using System;
using System.ServiceModel;

[ServiceContract]
public interface ICliente
{
    [OperationContract]
    decimal CalcularComissao(string codigo);
}

Uma vez adicionado à referência, podemos notar que haverá três métodos a nossa disposição do lado do cliente: CalcularComissao para processo síncrono, BeginCalcularComissao e EndCalcularComissao para processamento assíncrono. Durante a geração do proxy o método BeginXXX é decorado com o atributo OperationContractAttribute que, por sua vez, tem uma propriedade chamada AsyncPattern e, neste caso, está definida como True. Com essa configuração, o runtime não invocará o método BeginCalcularComissao do contrato, mesmo porque ele não existe; na verdade o runtime irá utilizar uma thread do ThreadPool para disparar sincronamente o método definido pela propriedade Action e, que neste caso é o método CalcularComissao.

Vou supor que a chamada para o método de forma síncrona já é conhecida e está sedimentada. O foco aqui será entender o funcionamento do processo assíncrono. O ponto de partida é o método BeginCalcularComissao: ele dará início ao processo e, além de receber o callback e o object como parâmetro (como já foi dito acima), esse método também espera os parâmetros que o próprio método exige para desempenhar a tarefa a qual ele se destina a fazer. Seguindo a arquitetura de processamento assíncrono do .NET Framework, o método BeginCalcularComissoes retorna um objeto que implementa a Interface IAsyncResult; esse objeto armazena uma referência para o processo que está sendo executado em paralelo.

Como já pudemos notar, não é o método BeginXXX que retorna o resultado. Isso é uma tarefa que pertence exclusivamente ao método EndXXX. A questão é quando invocá-lo. Uma vez que você invoca o mesmo, e o processo ainda não tenha sido finalizado, o programa trava a execução, esperando pelo retorno do processo. Dependendo do quanto tempo ainda falta para finalizar, podemos cair no mesmo problema do processamento síncrono. O trecho de código abaixo ilustra como consumir o método BeginCalcularComissoes:

using (ClienteClient proxy = new ClienteClient())
{
    IAsyncResult ar = proxy.BeginCalcularComissao("123", null, null);

    //fazer algum trabalho

    decimal resultado = proxy.EndCalcularComissao(ar);
    Console.WriteLine(resultado);
}

No exemplo acima podemos ter algum benefício, pois o método BeginCalcularComissao dispara o processamento assíncrono e já devolve o controle da execução para o programa e, conseqüentemente, permite que façamos algum trabalho em paralelo enquanto o serviço calcula as comissões. O problema é que se no momento da chamada do método EndCalcularComissao (que retornará o resultado) o processo ainda não finalizou, a execução irá bloquear a execução até que o processo assíncrono retorne. Vale lembrar que essa técnica às vezes é necessária: imagine um momento em que o programa não pode continuar a sua execução, pois depende o resultado do processo assíncrono para continuar.

Há ainda uma outra possibilidade de testar o retorno do processo assíncrono, que é chamado de pooling. Essa técnica consiste em antes de invocar o método EndCalcularComissao e possivelmente bloquear a execução, podemos testar se o processo finalizou ou não. Se repararmos no exemplo de código acima, o método BeginCalcularComissao retorna um objeto que implementa a Interface IAsyncResult. Essa Interface fornece um método chamado IsCompleted que retorna um valor booleano indicando se o processo foi ou não finalizado. Isso garantirá que chamaremos o método EndCalcularComissao somente que o processo realmente finalizou. O código abaixo ilustra o uso desta propriedade:

using (ClienteClient proxy = new ClienteClient())
{
    IAsyncResult ar = proxy.BeginCalcularComissao("123", null, null);

    //fazer algum trabalho

    if (ar.IsCompleted)
    {
        decimal resultado = proxy.EndCalcularComissao(ar);
        Console.WriteLine(resultado);
    }
    else
    {
        Console.WriteLine("O processo ainda não finalizou.");
    }
}

Finalmente, a última técnica que temos para disparar um método de forma assíncrona é com a utilização de callbacks. Com esta alternativa, ao invés de ficarmos testando se o processo finalizou ou não, ao invocar o método BeginCalcularComissao, passamos uma instância da classe AsyncCallback com a referência para um método do lado do cliente que deve ser disparado com o processo assíncrono for finalizado.

Uma vez especificado o método que será utilizado como callback, não é mais necessário que você armazene o objeto que armazena a referência para o processo assíncrono (IAsyncResult), pois isso será automaticamente fornecido para o callback; além disso, ainda é importante dizer que o método de callback é executado sob a thread de trabalho, antes dela ser devolvida para o ThreadPool. Como já foi comentado acima, informamos o método a ser disparado a partir de uma instância do delegate AsyncCallback, como é mostrado no código abaixo:

private static ClienteClient _proxy;

private static void TestandoProcessoAssincronoComCallback()
{
    _proxy = new ClienteClient();
    _proxy.BeginCalcularComissao("123", new AsyncCallback(Callback), null);
    Console.ReadLine();
}

private static void Callback(IAsyncResult ar)
{
    decimal resultado = _proxy.EndCalcularComissao(ar);
    Console.WriteLine(resultado);
}

Observação: Podemos recorrer à utilização de métodos anônimos ou até mesmo das expressões lambda para evitar a criação de um método a parte para ser disparado quando o callback acontecer.

Além do tradicional modelo de chamadas assíncronas (APM), temos a possibilidade da chamada assíncrona baseada em eventos. A idéia aqui é, antes de invocar a operação, você poderá assinar à um evento que será disparado somente quando o processo assíncrono finalizar. Isso evitará de ter um trabalho manual para analisar se o processo finalizou ou não (poll, waiting, etc.). Internamente durante a geração do proxy, o código que é auto-gerado já inclui a implementação necessária para o modelo baseado em eventos.

Basicamente é criado mais uma versão do método, agora com o sufixo XXXAsync que, internamente, faz a chamada para os métodos BeginXXX/EndXXX que, como já sabemos, dispararam a operação de forma assíncrona. Além disso, um delegate do tipo EventHandler<T> também será criado para representar o callback que, quando disparado, invocará o evento do lado de quem está consumindo o serviço. Abaixo um exemplo de como efetuar a chamada assíncrona baseada em eventos:

using (ClienteClient proxy = new ClienteClient())
{
    proxy.CalcularComissaoCompleted += 
        new EventHandler<CalcularComissaoCompletedEventArgs>(proxy_CalcularComissaoCompleted);

    proxy.CalcularComissaoAsync("2");
    Console.ReadLine();
}

private static void proxy_CalcularComissaoCompleted(object sender, 
    CalcularComissaoCompletedEventArgs e)
{
    Console.WriteLine("Fim.");
}

Se notarmos a implementação interna do proxy, veremos que o método XXXAsync faz o uso do método InvokeAsync, da classe ClientBase<T>. Este método está disponível somente a partir do .NET Framework 3.5. Sendo assim, alguns detalhes durante a geração do proxy precisam ser analisados:

  • Via “Add Service Reference”: se voce estiver fazendo a referencia em um projeto que esteja utilizando o .NET Framework 3.5 e voce opta pela geração dos métodos que dão suporte ao processamento assíncrono, ele também criará os tipos necessários para suportar o modelo de eventos.

  • Via svcutil.exe: neste caso voce precisará especificar através do parametro /async e, além disso, especificar a versão do .NET Framework através do parâmetro /targetClientVersion, apontando para Version30 ou, se quiser utilizar o modelo baseado em eventos, utilizar a opção Version35.

Processamento Assíncrono no Servidor

Tudo que vimos acima consiste em permitir que o cliente que consome o serviço execute o método de forma assíncrona, evitando que a aplicação não seja bloqueada enquanto o método está sendo executado. A partir de agora iremos analisar como implementar o processamento assíncrono no servidor. Isso consistirá em uma mudança considerável no contrato do serviço e que veremos mais abaixo.

A finalidade do processo assíncrono do lado do servidor é permitir aumentar a escalabilidade e a performance sem afetar os clientes que consomem o serviço. É importante dizer que os processos assíncronos do lado do cliente e do lado do servidor trabalham de forma independente, visando benefícios totalmente diferentes. A idéia do processamento assíncrono do lado do servidor visa principalmente a execução de tarefas que tem um custo alto para execução e, alguns casos comuns, são consultas a alguma informação no banco de dados, leitura/escrita de arquivos no disco, etc.

Isso permitirá a liberação da thread que está executando o serviço seja liberada enquanto o processo custoso acontece em paralelo, permitindo à você executar alguma tarefa em paralelo enquanto o processo ocorre ou até mesmo não ter threads sendo bloqueadas enquanto aguarda este processamento. Mais uma vez, você pode combinar este recurso as técnicas de processamento assíncrono que já existem dentro do .NET Framework, como é o caso do ADO.NET 2.0, classes do namespace IO, etc. Para o exemplo, iremos simular um processo custoso na base de dados.

Como já foi dita acima, o contrato do serviço mudará. Precisaremos desenhá-lo para se enquadrar no padrão de processamento assíncrono do .NET Framework que, como já sabemos, para cada método, teremos na verdade um par onde, o primeiro corresponde ao início da execução (BeginXXX) e, o segundo, corresponde à finalização do processamento (EndXXX). O código abaixo ilustra como devemos proceder para definirmos tais métodos:

using System;
using System.ServiceModel;

[ServiceContract]
public interface ICliente
{
    [OperationContract(AsyncPattern = true)]
    IAsyncResult BeginRecuperar(AsyncCallback callback, object state);

    Cliente[] EndRecuperar(IAsyncResult ar);
}

O código acima ilustra exatamente os passos que devemos seguir para possibilitar a chamada assíncrona pelo WCF. Os métodos que desejamos que sejam invocados assincronamente pelo WCF devem, ao invés de ter apenas um único método para realizar a tarefa, devemos obrigatoriamente dividi-los em duas partes (métodos): BeginXXX e EndXXX. É importante dizer que apenas o método BeginXXX será decorado com o atributo OperationContractAttribute, definindo a propriedade AsyncPattern para True. Ainda seguindo o padrão do processamento assíncrono do .NET Framework, o método Begin deve receber como parâmetro um objeto do tipo AsyncCallback (que mais tarde apontará para o método EndXXX) e um Object, retornando um objeto que implementa a Interface IAsyncResult. Já o método EndXXX deverá efetivamente retornar a informação que o método destina-se a fazer e, como parâmetro, deve receber um objeto que implementa a Interface IAsyncResult.

Quando o contrato acima for exposto pelo WCF e algum cliente consumi-lo, apenas terá um único método chamado Recuperar. Como foi dito anteriormente, a idéia aqui é evitar que o cliente saiba como isso está implementado dentro do serviço. Neste caso, não faz sentido você ter “uma versão síncrona do método” e, caso tenha, por padrão, o WCF sempre irá utilizá-la.

Uma vez que o contrato esteja definido, devemos implementá-lo na classe e, conseqüentemente, expor a mesma para ser consumida. A implementação da Interface ICliente assim como qualquer outra que exige o processamento assíncrono, demanda alguns cuidados no momento da implementação em relação ao formato tradicional. Como estamos utilizando o ADO.NET 2.0 e o mesmo já traz nativamente o suporte assíncrono a execução de queries de forma assíncrona, podemos integrá-lo a execução:

using System;
using System.Threading;
using System.Data.SqlClient;
using System.Collections.Generic;

public class ServicoDeClientes : ICliente
{
    private const string SQL_CONN_STRING = "...;Asynchronous Processing=True";
    private SqlConnection _connection;
    private SqlCommand _command;

    public IAsyncResult BeginRecuperar(AsyncCallback callback, object state)
    {
        this._connection = new SqlConnection(SQL_CONN_STRING);
        this._command = new SqlCommand("SELECT Nome FROM Cliente", this._connection);

        this._connection.Open();
        return this._command.BeginExecuteReader(callback, state);
    }

    public Cliente[] EndRecuperar(IAsyncResult ar)
    {
        List<Cliente> resultado = new List<Cliente>();

        using (this._connection)
            using(this._command)
                using (SqlDataReader dr = this._command.EndExecuteReader(ar))
                    while (dr.Read())
                        resultado.Add(new Cliente() { Nome = dr.GetString(0) });

        return resultado.ToArray();
    }
}

No método que inicia o processo abrimos a conexão com a base de dados, informamos a query e, finalmente, invocamos o método BeginExecuteReader passando o callback e o object que é passado como parâmetro para o método BeginRecuperar. Finalmente, o método EndRecuperar é invocado e através dele coletamos o resultado da execução do processo pesado, preparamos o mesmo e retornamos. Este será o resultado que será encaminhado ao cliente.

O código do lado do cliente não muda em nada. Independentemente da implementação que você utilize do lado do servidor, do lado do cliente nada mudará, ou seja, mesmo que você crie dois métodos (Begin e End) para compor o processamento assíncrono, o WCF sempre disponibilizará um único método para o cliente. Ele poderá invocá-lo de forma síncrona ou assincronamente.

Conclusão: No decorrer deste artigo pudemos entender como integrar o processamento assíncrono em serviços WCF. É possível realizar essa técnica em ambos os lados (cliente e servidor), mas é importante lembrar que os mesmos trabalham de forma independente que, apesar de serem semelhantes, tem finalidade completamente diferente e, sendo assim, necessitamos entender o contexto e aplicar a implementação ideal.

ChamadasAssincronas.zip (211.70 kb)

Criação de projeto “dummy” para cliente WCF

Quando estamos fazendo testes com serviços WCF, a idéia é sempre criar um serviço e consumí-lo em alguma aplicação cliente para testar se tudo corre como esperado. Mas quando o exemplo é simples, onde queremos testar apenas algumas funcionalidades, podemos utilizar uma técnica para criar o host e também, no mesmo projeto, poderíamos consumir o serviço sem a necessidade de criar um projeto dummy para isso.

As primeiras linhas consistem na criação e configuração do host e, depois que ele estiver aberto, então podemos recorrer ao uso da classe genérica ChannelFactory<TChannel> que podemos informar o contrato que estamos utilizando/testando, o ponto de acesso (endpoint) e o binding. O método CreateChannel utiliza o endpoint e o binding especificado no construtor para criar e estabelecer o canal de comunicação.

string address = “http://localhost:9388“;

using (ServiceHost host = new ServiceHost(typeof(Servico), new Uri[] { new Uri(address) }))
{
    host.AddServiceEndpoint(typeof(IContrato), new BasicHttpBinding(), string.Empty);
    host.Open();

    using (ChannelFactory<IContrato> srv =
        new ChannelFactory<IContrato>(new BasicHttpBinding(), new EndpointAddress(address)))
    {
        Console.WriteLine(srv.CreateChannel().Metodo(“Israel Aece”));
    }
}