WCF – Error Handling

Independentemente de que tipo de aplicação estamos criando, erros sempre podem acontecer. O mesmo serve para serviços, não estando isentos disso. Em se tratando de serviços, os erros podem ser os mais variados possíveis, havendo problemas a nível de transporte (protocolo), na entrega/recebimento da mensagem, no runtime ou até mesmo (e é o mais comum) na execução da operação (método).

O WCF fornece várias técnicas para analisar e tratar os possíveis erros que ocorrem durante a execução do serviço. O grande desafio aqui é como fazer com que este problema (erro) seja passado para o cliente que o consome independentemente da plataforma, dando à ele a capacidade de saber o que ocorreu e como contorná-lo, mantendo a aplicação cliente e proxy estáveis. Esse artigo abordará como devemos proceder para disparar erros, notificar o cliente e, como ele pode fazer para tratar os erros que ocorrem.

O .NET Framework representa um erro com uma classe que herda direta ou indiretamente da classe Exception. Códigos que são propícios à dispará-las (acesso à arquivos, banco de dados e serviços), devem estar envolvidos em blocos Try/Finally para evitar que essas exceções prejudiquem a aplicação, evitando que ela seja encerrada repentinamente. Isso tudo é válido quando estamos utilizando a forma tradicional de programar, invocando componentes, etc., mas quando estamos utilizando serviços, isso muda um pouco.

Exceções são características da própria linguagem e, por questões de interoperabilidade, não devem ser expostas dessa forma, já que nem todas as linguagens/plataformas utilizam exceções para representar erros. Felizmente, a especificação do SOAP também contempla a forma de representar erros que ocorrem durante a execução da operação. Essa especificação determina que todo e qualquer erro que ocorra deve ser representado por uma SOAP Fault, fazendo que essa informação seja colocada na seção body do envolope SOAP, trazendo informações à respeito do problema que aconteceu. As SOAP Faults são a forma interoperável do erro, permitindo que aplicações de diversas plataformas forneçam a sua própria forma de tratar o problema.

Na versão mais recente do protocolo SOAP (a versão 1.2), os elementos que representam a SOAP Fault são: Code (Requerido), Reason (Requerido), Role (Opcional), Detail (Opcional) e Node (Opcional). Apenas o binding BasicHttpBinding utiliza a versão 1.1 do protocolo por questões de interoperabilidade com os ASP.NET Web Services. Veremos mais adiante, ainda neste artigo, as formas que temos para especificar o valor para cada um destes elementos.

O WCF introduziu diversos tipos de exceções e, entre elas, a classe CommunicationException. Essa classe é base para todas as exceções que são disparadas e que estão relacionadas à execução do serviço, independentemente se o problema é relacionado ao runtime ou a operação. Entre as exceções que derivam desta classe, uma das mais importantes é a classe FaultException. Essa classe representa a SOAP Fault que vimos mais acima e, como já era de se esperar, fornece propriedades como Action, Code, etc.

Ainda há uma outra exceção, não menos importante, chamada FaultException<TDetail>. A versão genérica desta classe, que herda diretamente da classe FaultException, permite à você informar um tipo (TDetail) com mais detalhes à respeito do erro que ocorreu e, ao contrário do que muitos pensam, não há nenhuma constraint no tipo genérico que exige que TDetail seja uma classe derivada da classe Exception; você pode especificar qualquer tipo, desde que ele possa ser serializado (SerializableAttribute ou DataContractAtrribute). A imagem abaixo exibe a hierarquia destas exceções:

Figura 1 – A hierarquia das exceções.

Antes de nos aprofundarmos na utilização da classe FaultException, precisamos entender como será o comportamento da instância da classe/proxy quando uma exceção acontecer. O comportamento variará de acordo com o tipo de gerenciamento de instância adotado (PerSession, PerCall e Single).

No modo PerSession todas as exceções irão finalizar a sessão, descartando a instância da classe que representa o serviço e o proxy não poderá fazer chamadas subseqüentes, podendo apenas encerrá-lo. Já quando o serviço for exposto através do modo PerCall e uma exceção for lançada, a instância da classe que representa o serviço será descartada e o proxy não poderá fazer mais nenhuma chamada, apenas fechá-lo. Finalmente, no modo Single, quando uma exceção é lançada, a instância do serviço não é descartada e continuará ativa, mas o proxy não poderá fazer requisições subseqüentes.

Fault Contract

As classes FaultException e FaultException<TDetail> são utilizadas para representar uma SOAP Fault, e qualquer problema que ocorra dentro da operação, como por exemplo FileNotFoundException, MessageQueueException, DivideByZeroException, etc., será automaticamente “traduzido” para FaultException. Independente do tipo da exceção que aconteça do lado do serviço, ela chegará para o proxy sempre como FaultException e, neste caso, detalhes que expõem o funcionamento interno do serviço (como por exemplo a Stack Trace) não serão enviados para o cliente.

Dentro do .NET Framework, se consultarmos a documentação de um método qualquer, veremos quais os parâmetros que ele aceita, qual o tipo de retorno e as possíveis exceções que ele pode disparar. Seria muito interessante se o serviço também fosse capaz de informar a quem quisesse consumí-lo as exceções que ele pode disparar para uma determinada operação. Felizmente isso é possível graças aos Fault Contracts. A idéia é permitir que o cliente seja capaz de diferenciar entre os erros gerados pela execução do método em relação aos outros tipos de erros.

Para isso, o WCF fornece um atributo chamado FaultContractAttribute que pode somente ser aplicado aos métodos. Assim como o atributo DataContractAttribute, a finalidade deste atributo é especificar no WSDL as SOAP Faults que podem ser retornadas pela operação, propagando a informação correta para o cliente. O exemplo abaixo ilustra como devemos proceder para aplicar este atributo no contrato do serviço:

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

[ServiceContract]
public interface IArquivos
{
    [OperationContract]
    [FaultContract(typeof(FileNotFoundException))]
    string LerConteudo(string nomeDoArquivo);
}

É importante dizer que você não está limitado à aplicar este atributo apenas uma única vez; se a operação lançar três tipos diferentes de exceção, você pode aplicar o atributo FaultContractAttribute para cada uma delas. Quando este atributo for aplicado, o cliente será capaz de capturar a exceção mais detalhada, que é a FaultException<TDetail>. Esse classe expõe uma propriedade de somente leitura do tipo TDetail, que refletirá o tipo determinado no atributo FaultContractAttribute aplicado ao contrato. Caso alguma exceção aconteça na operação que não esteja definida em algum FaultContractAttribute, ela chegará até o cliente como FaultException.

Observação 1: Você não está restrito à especificar classes derivadas de Exception no atributo FaultContractAttribute. Nada impede de criar uma classe customizada com informações detalhadas à respeito do problema e definí-la como o tipo para o atributo, entretanto, utilizar esta técnica acaba possibilitando um código mais legível.

Observação 2: As operações do tipo one-way não retornam nenhum valor e, conseqüentemente, também não retornarão possíveis erros que possam acontecer quando ela estiver sendo executada. Decorar um método one-way com o atributo FaultContractAttribute resultará em uma exceção sendo disparada no momento da abertura do host.

Como vimos um pouco acima, independente do modo de gerenciamento de instância, quando qualquer exceção fosse disparada, ela chegaria até o cliente como FaultException e invalidaria o uso do proxy para chamadas subseqüentes, ao contrário de quando as exceções são “conhecidas” (aquelas que estão definidas com FaultContractAttribute), pois elas continuarão sendo disparadas do lado do cliente, mas não afetarão o funcionamento do proxy. O trecho de código abaixo ilustra implementar o contrato IArquivos que acabamos de criar:

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

public class ServicoDeArquivos : IArquivos
{
    public string LerConteudo(string nomeDoArquivo)
    {
        try
        {
            using (StreamReader sr = new StreamReader(nomeDoArquivo))
                return sr.ReadToEnd();
        }
        catch (FileNotFoundException ex)
        {
            throw new FaultException<FileNotFoundException>(ex);
        }
    }
}

Como podemos notar, interceptamos os erros que já estamos esperando (como é o caso do FileNotFoundException) no bloco catch e, dentro dele, capturamos a exceção disparada, instanciamos a classe FaultException<TDetail> especificando como tipo a exceção que foi atirada para que, no construtor dela, possamos passar a instância da exceção disparada. Neste momento, não fazemos nada mais que um wrapper, disparando uma exceção que o WCF consiga serializar da forma que todas as plataformas entendam como um erro.

O construtor da classe FaultException<TDetail> é sobrecarregado, possibilitando informar várias outras informações relacionadas ao problema que ocorreu. Como dito anteriormente, uma fault possui muito mais informações em relação à uma exceção tradicional. As principais informações para uma FaultException são Code e Reason, onde a primeira delas refere-se à um código (podendo ser customizado) de erro; já a segunda propriedade podemos especificar um ou vários motivos que ocasionaram o erro. Para especificar as propriedades Code e Reason, o código de implementação do serviço que criamos acima mudará ligeiramente:

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

public class ServicoDeArquivos : IArquivos
{
    public string LerConteudo(string nomeDoArquivo)
    {
        try
        {
            using (StreamReader sr = new StreamReader(nomeDoArquivo))
                return sr.ReadToEnd();
        }
        catch (FileNotFoundException ex)
        {
            throw new FaultException<FileNotFoundException>(
                ex, 
                new FaultReason("O arquivo informado não foi encontrado"), 
                new FaultCode("FileNotFound"));
        }
    }
}

Este código permite que você informe dados pertinentes ao erro, tendo a chance de customizar o código e mensagem de acordo com a sua regra de negócio e, além disso, pode-se mascarar o real problema ocorrido. A imagem abaixo trata-se da mensagem de erro que foi capturada pelo sistema de tracing do próprio WCF. Notem que as informações que foram customizadas estão sendo enviadas para o cliente.

Figura 2 – A mensagem de retorno contendo as informações customizadas sobre o erro.

Neste momento, ao referenciar o serviço no cliente, o proxy já contemplará a classe que representa os dados da exceção que, no nosso caso, é a classe FileNotFoundException. Sendo assim, o primeiro catch que iremos ter na nossa estrutura de tratamento de erro é justamente a versão genérica da classe FaultException, especificando a classe que fornece o complemento do erro (FileNotFoundException). Definir mais um bloco catch com apenas a classe FaultException também é uma boa prática, pois evitará que exceções não mapeadas no contrato do serviço possam prejudicar a sua aplicação. O código abaixo exibe como invocar o método dentro de uma estrutura de tratamento de erro:

using (ArquivosClient proxy = new ArquivosClient())
{
    try
    {
        Console.WriteLine(proxy.LerConteudo("ArquivoQueNaoExiste.txt"));
    }
    catch (FaultException<FileNotFoundException>)
    {
        Console.WriteLine("Arquivo não encontrado");
    }
    catch (FaultException)
    {
        Console.WriteLine("Problema na execução da operação");
    }
    catch (CommunicationException)
    {
        Console.WriteLine("Problema no serviço");
    }
    catch (Exception)
    {
        Console.WriteLine("Erro");
    }
}

Interceptando as Exceções

Tudo que vimos até o momento consiste na criação de uma exceção para que ela seja disparada e, finalmente, chegue até o cliente para ser notificado de que algo aconteceu, e permitir a ele tomar a melhor decisão em cima disso. Mas e se desejarmos capturar toda e qualquer exceção disparada pelo serviço, possibilitando um log de forma centralizada?

O WCF possui algumas extensões, que nos permite acoplar um determinado código durante a execução do serviço que garantirá que ele será executado, dando a chance de catalogarmos a exceção ou até mesmo customizarmos informações adicionais para ela, evitando que a todo momento se escreva código para tratar localmente (dentro do método) a exceção. Esta seção do artigo tem a responsabilidade de mostrar como implementar esta técnica.

O primeiro tipo que precisamos analisar é a interface IErrorHandler que está contida no namespace System.ServiceModel.Dispatcher. Essa interface possui apenas dois métodos: ProvideFault e HandleError. O primeiro deles, ProvideFault, é executado quando qualquer exceção é disparada durante a execução da operação e antes da execução voltar para o cliente. Neste momento, o WCF dá a oportunidade de transformar exceções que não foram previstas no contrato em FaultException<TDetail>, garantindo que o cliente consiga receber informações mais detalhadas a respeito do problema. Já o método HandleError possibilita que você faça o log da exceção que ocorreu. Como esse método será disparado depois que a execução já tenha voltado para o cliente, você poderá efetuar uma tarefa mais custosa, e você não deverá confiar no contexto da execução (OperationContext), pois ela não estará mais disponível. Esse método recebe como parâmetro um tipo Exception e, teoricamente, tudo o que você tem que fazer é o log da mesma.

Repare que o método HandleError deve retornar um valor booleano. Ao definir como False, ele permitirá que outros error handlers possam ser processados. Outro detalhe importante é que, se este método retornar True e o modo de gerenciamento de instância estiver definido como Single, o WCF não abortará a sessão (caso ela exista). O código abaixo exibe um exemplo mostrando como podemos proceder para implementar a interface IErrorHandler:

using System;
using System.IO;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;

public class ErrorHandling : IErrorHandler
{
    public bool HandleError(Exception error)
    {
        try
        {
            Log(error);
        }
        catch (Exception) { }

        return false;
    }

    public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
    {
        if (error is FileNotFoundException)
        {
            FaultException ex =
                new FaultException(
                    (FileNotFoundException)error,
                    new FaultReason("O arquivo informado não foi encontrado"),
                    new FaultCode("FileNotFound"));

            fault = Message.CreateMessage(version, ex.CreateMessageFault(), ex.Action);
        }
    }

    private static void Log(Exception error)
    {
        //efetuar o log
    }
}

Neste caso, estamos centralizando a promoção das exceções que são disparadas pela execução da operação dentro do método ProvideFault, transformando-as em FaultException<TDetail>. Já o método HandleError apenas faz o log da exceção lançada pela operação.

Somente a implementação por si só não funcionará. Você precisará acoplar a instância desta classe à execução do serviço. O WCF fornece uma interface chamada IServiceBehavior que, por sua vez, fornece um método chamado ApplyDispatchBehavior. Esse método fornece uma coleção de channel dispatchers que são utilizados pelo host. Para cada dispatcher há uma propriedade chamada ErrorHandlers que expõe uma coleção, e cada elemento desta coleção deve implementar a interface IErrorHandler. O código abaixo mostra como acoplar a classe recém criada à execução do dispatcher:

public class ErrorServiceBehavior : IServiceBehavior
{
    public void ApplyDispatchBehavior(ServiceDescription serviceDescription, 
        ServiceHostBase serviceHostBase)
    {
        foreach (ChannelDispatcher cd in serviceHostBase.ChannelDispatchers)
        {
            cd.ErrorHandlers.Add(new ErrorHandling());
        }
    }

    //outros métodos
}

Finalmente, apenas devemos acomodar este behavior à coleção de behaviors do host. Para isso, basta adicionarmos a instância da classe ErrorServiceBehavior à coleção de behaviors, exposta pela propriedade Behaviors do ServiceHost. O exemplo abaixo demonstra como efetuar isso:

host.Description.Behaviors.Add(new ErrorServiceBehavior());

A propriedade IncludeExceptionDetailInFaults

Em tempo de desenvolvimento, fazer todas essas configurações pode ser complicado, pois você ainda não conseguirá determinar quais exceções o teu serviço poderá disparar. Para efeito de testes ou até mesmo em ambiente de produção, podemos recorrer à uma propriedade chamada IncludeExceptionDetailInFaults que, como o próprio nome diz, encaminhará os detalhes da exceção para o cliente, independente se ela foi ou não contemplada nas fault contracts.

Essa configuração poderá ser feita diretamente na classe que representa o serviço, através do atributo ServiceBehaviorAttribute, via ServiceDebugBehavior ou, ainda, através do arquivo de configuração, adicionando um behavior do tipo serviceDebug. A última opção é a mais flexível, já que permitirá alterar essa configuração sem a necessidade de recompilar o serviço.

Conclusão: O artigo mostrou as principais técnicas para tratamento de erros dentro de serviços WCF, bem como o comportamento de cada uma delas. Podemos utilizar as estratégias mostradas aqui para tornar o monitoramento da aplicação mais eficaz, dar a chance ao desenvolvedor/administrador catalogar e visualizar as exceções que foram disparadas pelo respectivo serviço e, principalmente, a possibilidade de notificar o cliente de forma que ele consiga determinar o que exatamente ocorreu.

ErrorHandling.zip (183.91 kb)

WCF – Tipos de Mensagens

Tradicionalmente, em qualquer tipo de aplicação, podemos criar um método que faz alguma tarefa. Ao criá-lo, podemos consumí-lo na mesma aplicação ao até mesmo referenciar a classe em que ele está contido e também consumí-los nos mais variados projetos. Ao realizar a chamada para este método, devemos esperar a sua execução e, quando finalizada, damos continuidade na execução do programa.

Ao criar uma operação em um serviço WCF, ela também se comportará da mesma forma. Mas essa não é a única alternativa fornecida pelo WCF. Desconsiderando a possibilidade de chamadas assíncronas, temos três alternativas que podemos utilizar ao construir uma operação (método) de um serviço: request-reply, one-way e duplex (callbacks). O foco deste artigo é explorar tais alternativas e como elas influenciam na configuração e implementação e execução do serviço.

Request-Reply

Quando criamos um método que retorne alguma informação dentro de uma interface que será o contrato do serviço, podemos decorar este método com o atributo OperationContractAttribute, que dirá ao runtime do WCF que ele deverá ser publicado e, conseqüentemente, consumido pelos clientes que referenciarem o serviço. Com essa configuração básica, o método já estará baseado no padrão request-reply.

Como o próprio nome diz, ao invocar o método a partir de um cliente, uma mensagem será enviada ao serviço através do proxy, contendo as informações necessárias para a execução do método. Neste momento a mensagem viaja pela rede até o respectivo serviço; ao chegar do outro lado, o dispatcher abre a mensagem e efetivamente executa o método requirido. Ao finalizar a execução do mesmo, uma nova mensagem é gerada e devolvida como retorno para o mesmo cliente que a requisitou. Ao chegar do outro lado (cliente), o proxy abre a mensagem, captura o resultado (ou o erro) e devolve para a aplicação.

Durante toda a execução do método, a aplicação cliente ficará bloqueada enquanto o resultado não voltar ou até o momento em que o timeout seja atingido. Com exceção dos bindings NetPeerTcpBinding e NetMsmqBinding, todos suportam operações do tipo request-reply. Para um melhor entendimento a imagem abaixo ilustra o processo entre o cliente e o serviço quando um método baseado no padrão request-reply é executado:

Figura 1 – Modelo Request-Reply.

One-Way

Quando você tem operações que não retornam valores e, principalmente, o cliente não está interessado no sucesso ou falha da mesma, podemos recorrer à técnica chamada de one-way. Também conhecida como fire-and-forget, ela permite que o proxy envie a mensagem ao seu destino, sem esperar pela sua execução. Há um bloqueio mínimo do lado do cliente, que é apenas necessário para que o proxy mande a mensagem e o serviço a enfileire. A partir daí, o cliente está livre para dar continuidade na execução do programa.

Como a característica desde tipo de mensagem é não notificar o cliente de sucesso ou falha, qualquer exceção que ocorra durante a execução do método não notificará o cliente (salvo algumas exceções que veremos mais abaixo). Neste caso, podemos utilizar algumas funcionalidades fornecidas pelo binding que está sendo utilizado, como o uso de sessões confiáveis ou até mesmo a possibilidade da criação de um contrato de callback em conjunto com operações duplex que veremos mais tarde, ainda neste artigo. Através da imagem abaixo, podemos visualizar a mensagem viajando entre cliente e servidor, sem haver uma mensagem de resposta correlacionada:

Figura 2 – Modelo One-Way.

A criação do contrato para suportar este tipo de mensagem muda ligeiramente em relação ao formato tradicional e todos os bindings suportam este tipo de mensagem. Já que a idéia das operações do tipo one-way é não ter retorno, os métodos não devem retornar nada, ou seja, devem ser void. Qualquer tipo de dado que você coloque diferente disso em operações marcadas como one-way, uma exceção do tipo XXXXX será lançada. Como o padrão é request-reply, para dizermos ao WCF que a operação trata-se de uma operação one-way, devemos definir a propriedade IsOneWay, fornecida pelo atributo OperationContractAttribute, como True. O trecho de código abaixo ilustra esta configuração:

using System;
using System.ServiceModel;

[ServiceContract]
public interface ICliente
{
    [OperationContract(IsOneWay = true)]
    void Notificar(string email);
}

Se algum erro ocorrer durante a execução do serviço e o mesmo for configurado como PerCall (que não mantém estado/sessão) e, exposto via BasicHttpBinding ou WSHttpBinding (sem as opções de segurança e mensagens confiáveis habilitadas), o proxy do cliente não será afetado e, conseqüentemente, poderá fazer requisições subseqüentes; agora, se estiver sendo exposto através do WSHttpBinding com segurança, NetTcpBinding sem a opção de mensagens confiáveis habilitada ou através do binding NetNamedPipeBinding e uma exceção ocorrer durante o processamento da operação, isso afetará o proxy do lado do cliente, e ele não poderá mais executar operações a partir deste mesmo proxy, e o fechamento do mesmo não ocorrerá de forma segura. Finalmente, se esses últimos bindings estiverem com a opção de mensagens confiáveis habilitada, ele poderá continuar utilizando o mesmo proxy, mesmo que uma exceção ocorra.

Quando o serviço estiver exposto no modelo PerSession ou Single suportando sessão, fica um pouco mais difícil ter um controle sob as exceções que são disparadas. Se o serviço está exposto via NetTcpBinding ou NetNamedPipeBinding e um erro ocorrer, a sessão será encerrada, a instância do serviço será destruída e o proxy do cliente será afetado, não podendo fazer chamadas subseqüentes.

Duplex (Callbacks)

A terceira e última alternativa que temos no WCF é a possibilidade de efetuar callbacks. 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. A imagem abaixo ilustra o funcionamento deste tipo de mensagem:

Figura 3 – Modelo Duplex (callbacks).

Nem todos os bindings possibilitam esse tipo de comunicação. Os bindings NetTcpBinding e NetNamedPipeBinding fazem isso nativamente, devido à natureza dos protocolos utilizados. O protocolo HTTP não traz esse suporte, por se tratar de uma conexão que não mantém a ligação entre cliente e servidor. Para possibilitar que os callbacks funcionem a partir do HTTP, foi criado um binding chamado de WSDualHttpBinding que tem essa finalidade.

Quando fazemos uso do WSDualHttpBinding, o WCF utilizará um canal diferente para invocar o callback. Internamente, o que acaba sendo feito do lado do cliente – por parte do runtime do WCF – é a criação de um “endpoint” (incluindo uma porta disponível) para que o serviço relacionado consiga invocá-lo. Esse endereço temporário criado pelo WCF é registrado sob o Http.sys que é um componente de baixo nível e que faz parte do sistema de comunicação do Windows. Sendo assim, qualquer requisição que chegar para esse “endpoint”, o Http.sys a encaminhará para o runtime do WCF que, finalmente, irá disparar a classe que implementa a interface de callback (mais detalhes a seguir) que, por sua vez, foi fornecida pelo contrato do serviço. A imagem abaixo ilustra como é realizada a comunicação entre servidor e cliente a partir do WSDualHttpBinding:

Figura 4 – Funcionamento do WSDualHttpBinding.

Para implementarmos esse tipo de comunicação, além de definirmos a interface que servirá como o contrato do serviço, precisaremos também criar uma segunda interface que especificará o callback (isso também é chamado de contrato de callback). Essa interface irá conter o método que deverá ser implementado pelo cliente e que o WCF irá disparar quando você achar necessário. Ao contrário de uma interface de contrato, a interface de callback apenas decora os métodos com o atributo OperationContractAttribute. O código abaixo ilustra como devemos proceder para criá-la:

using System;
using System.ServiceModel;

public interface ICallback
{
    [OperationContract]
    void NotificacaoRealizada(string msg);
}

Apesar das operações de callback não requererem que sejam one-way, é uma boa prática, já que diminui a possibilidade de deadlocks. Depois que o contrato de callback está definido, precisamos associá-lo ao contrato do serviço. Para isso, há uma propriedade chamada CallbackContract fornecida pelo atributo ServiceContractAttribute, que espera um tipo Type, especificando o contrato de callback. Uma vez associado o callback ao contrato de serviço, o WSDL irá contemplá-lo, permitindo que o cliente possa implementar o callback e informá-lo durante a criação do proxy. O código abaixo ilustra essa associação:

using System;
using System.ServiceModel;

[ServiceContract(CallbackContract = typeof(ICallback))]
public interface ICliente
{
    [OperationContract]
    void Notificar(string email);
}

Será a execução do serviço que determinará quando invocar o callback. Para que isso seja possível, primeiramente precisamos obter o canal de comunicação entre o serviço e o cliente e, para isso, recorrer à classe OperationContext. Ela fornece um método genérico chamado GetCallbackChannel que retornará a instância do canal entre o serviço e o cliente. Caso o retorno não seja nulo, então quer dizer que o cliente implementou (e se interessa) pelo callback. A partir de agora, no momento que achar conveniente, você poderá invocar o método de callback que será disparado no cliente. O exemplo abaixo exibe como invocar o callback:

using System;
using System.ServiceModel;

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall
    , ConcurrencyMode = ConcurrencyMode.Reentrant)]
public class ServicoDeClientes : ICliente
{
    public void Notificar(string email)
    {
        bool houveProblemas = false;

        try
        {
            //faz a notificação
        }
        catch
        {
            houveProblemas = true;
        }
        finally
        {
            ICallback callback = 
                OperationContext.Current.GetCallbackChannel<ICallback>();

            string msg = 
                houveProblemas ? 
                "Houve problemas na notificação." :
                "Notificação realizada com sucesso.";

            if (callback != null)
                callback.NotificacaoRealizada(msg);
        }
    }
}

Como dito anteriormente, o callback será contemplado no WSDL e, como o contrato do serviço, a interface que representa o callback também será disponibilizada para a implementação do lado do cliente. Essa interface deve ser implementada em uma classe, colocando o código desejado dentro do(s) método(s) fornecido(s) pela interface de callback. Com a classe implementada, você deverá informá-la na criação (construtor) do proxy. Essa nova versão do construtor do proxy aceita uma instância da classe InstanceContext. Essa classe envolve e gerencia a instância da classe onde a interface de callback foi implementada. Neste momento, o runtime do WCF ficará monitorando para que, quando o callback for disparado, o proxy seja notificado e, conseqüentemente, invocará o respectivo método do lado do cliente. O código ilustra essa configuração:

using System;
using System.ServiceModel;

using (ClienteClient proxy = 
    new ClienteClient(new InstanceContext(new GerenciadorDeRetorno())))
{
    proxy.Notificar("israel@projetando.net");
    Console.ReadLine();
}

//....

public class GerenciadorDeRetorno : IClienteCallback
{
    public void NotificacaoRealizada(string msg)
    {
        Console.WriteLine(msg);
    }
}

Quando um callback faz parte do contrato, a instância da classe InstanceContext passa a ser obrigatória em qualquer uma das versões (overloads) do proxy. Um outro ponto importante é com relação ao tempo de vida do proxy pois, quando o callback acontecer, ele ainda deverá estar ativo para conseguir receber a notificação enviada pelo serviço.

Observação Importante: Quando você possibilita que callbacks sejam disparados pelo seu serviço, é importante que você se atente ao modelo de sincronização utilizado pelo mesmo pois, do contrário, você poderá enfrentar problemas de deadlocks. Caso você queira saber mais sobre os modos de gerenciamento de concorrência dentro do WCF e, principalmente, sobre o modo Reentrant, poderá recorrer à este artigo.

Conclusão: O artigo exibiu as três características possíveis que uma operação possa ser desenvolvida em WCF. Cada uma delas tem uma finalidade exclusiva que, se bem adotada, pode trazer grande melhoria em performance e também possibilitar uma maior interatividade entre cliente e serviço. Para finalizar, é importante dizer também que quando mencionado cliente neste artigo, podemos também estar nos referindo à outro serviço, algo que é perfeitamente possível.

TiposDeMensagens.zip (314.44 kb)

WCF – Sincronização

Ao expor um serviço para que ele seja consumido, devemos nos atentar à possibilidade deste serviço ser acessado simultaneamente. Isso ocorre quando múltiplas requisições (threads) tentam acessar o mesmo recurso ao mesmo tempo. A possibilidade de acessos simultâneos poderá acontecer dependendo do tipo de gerenciamento de instância escolhido para o serviço. A finalidade deste artigo é mostrar as três opções fornecidas pelo WCF para tratar a concorrência e, além disso, exibir algumas das várias técnicas de sincronização fornecidas pelo .NET Framework e que poderão ser utilizadas em conjunto com o WCF.

É importante dizer que os modos de concorrência aqui abordados não garantem a segurança ao acesso à algum recurso compartilhado. Isso fica sob responsabilidade do desenvolvedor que está desenvolvendo o serviço e, para isso, como falado acima, devemos recorrer às classes já existentes dentro do .NET Framework que permitem a sincronização e proteção à tais recursos em um ambiente multi-threading. Há alguns casos onde a combinação entre o modo de gerenciamento de instância com o modo de concorrência já garantem isso e veremos mais abaixo como aplicá-los.

Quando uma requisição chega para um serviço, uma thread é retirada do ThreadPool para servir à requisição, mais precisamente, uma thread de I/O. Quando múltiplas requisições chegam ao serviço e ele, por sua vez, suporta que múltiplas requisições o acessem, então precisamos saber como tratá-las. Um ponto importante aqui é que a concorrência poderá ou não existir dependendo do modo de gerenciamento de instância do serviço. Para recapitular, temos três possibilidades: PerSession, PerCall e Single.

No modo PerSession uma instância será criada e mantida enquanto durar o proxy. Caso o cliente utilize o mesmo proxy para realizar várias chamadas ao mesmo tempo, então poderá haver concorrência entre as requisições de um mesmo cliente; já no modelo PerCall tradicional a concorrência não existirá, pois para cada chamada de uma determinada operação uma nova instância do serviço será criada para atendê-la. Infelizmente o modelo PerCall não está totalmente seguro em relação aos problemas de concorrência pois, o mesmo poderá fazer o uso de um recurso compartilhado entre várias chamadas (como caching, variáveis estáticas, etc.). Finalmente, o modelo Single é o modo de gerenciamento de instância mais propício aos problemas, já que toda e qualquer requisição será direcionada para a mesma instância e, caso os recursos compartilhados não estejam devidamente assegurados, podemos ter problemas de deadlocks ou contenção.

Para tratar a concorrência o WCF fornece uma propriedade chamada ConcurrencyMode, que está definida no atributo ServiceBehaviorAttribute, ou seja, é uma característica do serviço e não do contrato. Essa propriedade aceita uma das três opções definidas no enumerador ConcurrencyMode, as quais estão listadas abaixo:

  • Single: O serviço poderá aceitar apenas uma chamada por vez, não aceitando re-entradas (utilizadas em callbacks). Para as mensagens que chegarem enquanto uma requisição está sendo processada, ele aguardará até que o processo corrente seja finalizado. Quando a opção não é especificada, este valor é definido como padrão.

  • Reentrant: Neste modelo, o serviço poderá aceitar apenas uma chamada por vez, assim como o Single, mas permitirá que re-entradas sejam realizadas (utilizadas em callbacks), evitando assim o deadlock.

  • Multiple: Este modelo suporta multi-threading. Isso possibilitará que múltiplas threads entrem ao mesmo tempo no serviço e, caso existam recursos compartilhados, precisará utilizar técnicas para garantir o acesso seguro à eles, evitando possíveis deadlocks.

O exemplo abaixo ilustra como devemos proceder para configurar uma das opções de gerenciamento de concorrência na classe que representa o serviço:

using System;
using System.ServiceModel;

[ServiceBehavior(
    InstanceContextMode = InstanceContextMode.PerSession
    , ConcurrencyMode = ConcurrencyMode.Reentrant)]
public class ServicoDeClientes : ICliente
{
    //implementação
}

Observação importante: Antes de prosseguir é importante que você tenha conhecimento em algumas funcionalidades disponibilizadas pelo .NET Framework para sincronização e proteção de recursos em um ambiente multi-threading. Algumas destas funcionalidades serão abordadas neste artigo, mas o seu funcionamento não será comentado profundamente, já que isso está fora do escopo do artigo. Para um melhor entendimento destas funcionalidades, você poderá consultar o Capítulo 14 deste artigo.

Single

Quando a concorrência de um serviço é definida como Single, o mesmo será bloqueado enquanto uma requisição está sendo atendida, impossibilitando que outras requisições executem ao mesmo tempo. Se elas existirem, elas serão enfileiradas aguardando que o processo finalize para que elas sejam processadas, na respectiva ordem que chegaram. Esse modelo é o mais seguro, já que você não precisará lidar com a proteção de recursos compartilhados, pois o próprio WCF garante que apenas uma única thread será processada por vez.

Uma vez que a requisição corrente é encerrada, o WCF desbloqueia o serviço, permitindo que a próxima mensagem da fila (caso exista) seja processada. Quando esta mensagem começar a ser processada, ela bloqueia novamente o serviço, mantendo este bloqueio até que o método seja finalizado. Esse modo de concorrência diminui os problemas mas, da mesma forma, diminui o throughput.

Baseando-se no modo Single de concorrência, quando o modelo de gerenciamento de instância da classe estiver definido como PerSession e alguma requisição seja realizada para o serviço através do mesmo proxy, essa segunda chamada deverá aguardar até que a primeira seja completamente finalizada. No modelo PerCall não há concorrência, já que sempre haverá uma instância exclusiva para cada requisição e não sendo compartilhada com nenhuma outra chamada. Finalmente, se o modo de gerenciamento de instância estiver também definido como Single, ou seja, uma única instância servindo todas as requisições, apenas uma única chamada será processada por vez, bloqueando todas as outras, colocando-as em uma fila e não tendo problemas com concorrência.

Reentrant

Antes de falar do modo Reentrant, é necessário entender uma funcionalidade que é fornecida pelo WCF e que é o grande alvo deste modo de gerenciamento de concorrência: callbacks. Os callbacks possibilitam a comunicação bidirecional entre o cliente e o serviço e também são referidos como comunicação duplex. Neste tipo de comunicação será necessário definir um callback para que o contrato possa, em um determinado momento, disparar um método pré-definido do lado do cliente. A criação deste tipo de mensagem está fora do escopo do artigo e assume que você já tenha o respectivo conhecimento. Caso ainda não possua, você pode consultar este artigo.

Este modo de tratamento de concorrência segue a mesma idéia do modelo anterior (Single), ou seja, apenas é permitido uma requisição ser processada por vez; entretanto, esse modo possui uma customização: podemos deixar momentaneamente o serviço para a realização de alguma tarefa externa (como a chamada para outro serviço) e, via callback, voltarmos seguramente ao mesmo serviço, continuando a execução do mesmo, sem causar deadlock. É importante dizer que durante esta “janela”, possíveis requisições que estiverem na fila, aguardando o processamento, poderão entrar no serviço (em sua respectiva ordem). Quando a thread que deixou o serviço para executar uma outra tarefa for finalizada, ela será recolocada na fila para ser processada, ou melhor, dar continuidade no processamento que já existia antes do callback.

Quando o serviço está exposto como PerCall e ele exige uma re-entrada (isso ocorre quando há callbacks), então será necessário definí-lo como Reentrant para não causar deadlock com ela mesma; a re-entrada não sofrerá problemas com bloqueios em relação às outras requisições, pois no modo PerCall a instância somente servirá àquela requisição. Já quando o modo de gerenciamento de instância estiver definido como PerSession e o de concorrência como Reentrant, o callback será disparado e, quando o controle voltar para o serviço e já existir alguma outra requisição sendo executada, o retorno do callback será bloqueado até que a requisição corrente seja finalizada. Essa característica permite aumentar o throughput para clientes que fazem chamadas multi-threading, permitindo que enquanto o callback seja processado, outra chamada para o serviço possa ser executada. Finalmente, com o modelo de gerenciamento de instância definido como Single, o callback será disparado e, quando o controle voltar, ele será enfileirado caso exista alguma requisição em processamento.

Um cuidado especial que se deve ter ao utilizar o modo Reentrant é o gerenciamento de estado dos membros internos utilizados pelo serviço. Como vimos anteriormente, uma vez que o callback for executado, outras chamadas poderão ocorrer em paralelo, mas há um detalhe: quando o serviço for executar o callback, é importante que ele mantenha o serviço em um estado consistente para atender as outras requisições; da mesma forma, quando o callback retornar, é necessário atualizar as informações utilizadas pelo método, pois durante a chamada externa possivelmente outras chamadas foram realizadas, podendo modificar as informações que ele está (estava) utilizando.

A instância do proxy que iniciou o processo deverá ficar ativa até que o callback seja disparado pelo serviço e, caso isso não aconteça, uma exceção do tipo ProtocolException será disparada. Além disso, é importante saber que o callback é sempre executado em uma thread diferente da thread que iniciou a chamada para o método. Isso quer dizer que, quando estamos utilizando esta técnica em aplicações Windows Forms, devemos nos atentar quando precisarmos acessar controles do formulário a partir do método de callback. Isso se deve ao fato de que os controles são criados em uma thread diferente (a mesma que iniciou a chamada para o método através do proxy), o que nos obriga à acessá-los a partir dela. Há algumas facilidades que o Windows Forms fornece em conjunto com o .NET Framework, mas não serão abordadas aqui.

Multiple

Utilizar o modo de concorrência como Multiple em conjunto com o modelo de gerenciamento de instância PerSession ou Single permitirá que uma mesma instância atenda à diversas requisições (threads), aumentando assim o throughput. O problema que existe nestes cenários é a possibilidade do serviço utilizar um recurso compartilhado entre essas chamadas, o que obrigará a proteger manualmente tais recursos, utilizando as técnicas de sincronização existentes. No modelo PerCall a opção de concorrência Multiple é irrelevante, já que neste modo haverá sempre uma instância servindo cada requisição.

Dependendo dos recursos que estão sendo utilizados pelo serviço, devemos protegê-los do acesso concorrente. Se estiver acessando algum recurso que já possua o devido tratamento, então não há com que se preocupar. Por outro lado, se o serviço mantém recursos que são acessados por múltiplas threads, como é o caso de membros internos da classe, então será necessário protegê-los manualmente. Caso você não faça isso, é grande a probabilidade de ocorrer deadlocks, race conditions e o conflito de informações durante o processamento.

Comparativo

Instância/Concorrência Single Reentrant Multiple
PerSession
  • Limita o throughput em chamadas concorrentes
  • Chamadas concorrentes podem somente ser processadas entre diferentes proxies
  • Não há necessidade de trabalhar com mecanismos de bloqueio
  • Aumenta o throughput em chamadas concorrentes
  • Permite acesso multi-thread quando o callback está sendo executado
  • É necessário ter cuidados especiais com o estado dos membros internos do serviço
  • Permite acesso multi-thread através de um mesmo proxy
  • Há necessidade de trabalhar com mecanismos de bloqueio
PerCall
  • Obterá um deadlock 
  • Irrelevante, já que há uma instância do serviço exclusiva
  • Irrelevante, mas tendo cuidados especiais quando acessar um recurso externo e compartilhado (variáveis estáticas)
Single
  • Limita o throughput
  • Garantia de que o código não será acessado por várias requisições ao mesmo tempo
  • Não há necessidade de trabalhar com mecanismos de bloqueio
  • Enquanto um callback estiver sendo executado, a próxima requisição (caso exista) será executada
  • Aumenta o throughput quando há chamadas enfileiradas
  • É necessário ter cuidados especiais com o estado dos membros internos do serviço
  • Não há bloqueio à nível de serviço, podendo múltiplas requisições acessar a mesma instância
  • Há necessidade de trabalhar com mecanismos de bloqueio
  • O throughput está condicionado aos bloqueios realizados


Conclusão:
Este artigo mostrou as características de cada um dos modos de gerenciamento de concorrência que existe dentro do WCF. Apesar da configuração ser extremamente básica, isso muda drasticamente o comportamento do serviço, principalmente quando combinado com algum dos modos de gerenciamento de instância. Além disso, a escolha do modo de concorrência influenciará em como você deve escrever a implementação do serviço, se atentando ou não aos possíveis bloqueios que possam acontecer quando tentar acessar recurso compartilhado.

WCF e a interoperabilidade COM

O WCF fornece diversos meios de interoperabilidade com tecnologias existentes e, entre elas, a possibilidade de consumir um serviço a partir de aplicações COM, como é o caso do Visual Basic 6.0, C ou mesmo através do ASP Clássico. Isso permitirá que aplicações desenvolvidas nessas tecnologias façam o uso de serviços construídos em WCF sem muito trabalho.

A criação do serviço e a forma como voce irá expor não mudará. Atualmente temos duas alternativas para possibilitar essa interoperabilidade: a primeira delas seria fazer o uso do utilitário svcutil.exe, criando o arquivo cs/vb representando o proxy e também o seu respectivo arquivo de configuração. Com isso, podemos criar um Assembly (atente-se ao atributo ComVisibleAttribute) gerenciado e, a partir daqui, voce poderá expor o tlb e consumí-lo em aplicações COM (para maiores detalhes sobre este processo, consulte este link). Utilizando esta técnica, será necessário registar o Assembly e colocar o arquivo de configuração gerado pelo svcutil.exe no mesmo diretório do *.exe, com o mesmo nome da aplicação acrescido de .config, como por exemplo: AplicacaoEmVB6.exe.config. A segunda possibilidade e mais simples, é acessar diretamente o serviço que está rodando, sem a necessidade de expor um Assembly gerenciado ou até mesmo registrar o componente dentro do Windows.

Para ambos os casos, a aplicação cliente fará o uso do service moniker. Este elemento está disponibilizado a partir da função GetObject que, por sua vez, retorna uma instancia do objeto especificado como parametro (ProgID) e que para serviços WCF, essa instancia representará o canal de comunicação, ou seja, o proxy. No caso de clientes COM que farão o uso de um serviço WCF, é comum especificar como parametro o endereço do serviço (HTTP, TCP, etc.), endereço do WSDL, o contrato e o binding a ser utilizado pela aplicação.

O código abaixo ilustra como consumir o serviço WCF a partir do ASP Clássico, utilizando a segunda alternativa de interoperabilidade comentado acima:

<%

    Dim proxy
   
    Set proxy = GetObject(“service:mexAddress=net.tcp://localhost:9292/mex, ” & _
        “address=net.tcp://localhost:9292/srv, ” & _
        “contract=IOperacao, contractNamespace=http://www.projetando.net, ” & _
        “binding=NetTcpBinding_IOperacao, bindingNamespace=http://tempuri.org/”)
   
    Response.Write(proxy.Adicionar(3, 34))

%>

Neste caso, o moniker utilizará os dados disponibilizados pelo WSDL para recuperar as informações necessárias e, possibilitar a chamada para o serviço, diferentemente do primeiro caso, onde disponibilizamos o proxy criado para o mundo COM através dos próprios recursos nativos do .NET Framework. Caso utilizarmos a primeira possibilidade de interoperabilidade, as únicas informações que devemos passar para a função GetObject são o endereço e o binding.