Desenvolvimento - C#

WCF - Error Handling

Esse artigo abordará como devemos proceder para disparar erros, notificar o cliente e, como ele pode fazer para tratar os erros que ocorrem.

por Israel Aéce



function doClick(index, numTabs, id) { document.all("tab" + id, index).className = "tab"; for (var i=1; i Veja o exemplo.

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);
}
Imports System
Imports System.IO
Imports System.ServiceModel

<ServiceContract()> _
Public Interface IArquivos
    <OperationContract(), FaultContract(GetType(FileNotFoundException))> _
    Function LerConteudo(ByVal nomeDoArquivo As String) As String
End Interface
C# VB.NET

É 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);
        }
    }
}
Imports System
Imports System.IO
Imports System.ServiceModel

Public Class ServicoDeArquivos
    Implements IArquivos

    Public Function LerConteudo(ByVal nomeDoArquivo As String) As String _
        Implements IArquivos.LerConteudo

        Try
            Using sr As New StreamReader(nomeDoArquivo)
                Return sr.ReadToEnd()
            End Using
        Catch ex As FileNotFoundException
            Throw New FaultException(Of FileNotFoundException)(ex)
        End Try
    End Function
End Class
C# VB.NET

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"));
        }
    }
}
Imports System
Imports System.IO
Imports System.ServiceModel

Public Class ServicoDeArquivos
    Implements IArquivos

    Public Function LerConteudo(ByVal nomeDoArquivo As String) As String _
        Implements IArquivos.LerConteudo

        Try
            Using sr As New StreamReader(nomeDoArquivo)
                Return sr.ReadToEnd()
            End Using
        Catch ex As FileNotFoundException
            Throw New FaultException(Of FileNotFoundException)( _
                    ex, _
                    New FaultReason("O arquivo informado não foi encontrado"), _
                    New FaultCode("FileNotFound"))
        End Try
    End Function
End Class
C# VB.NET

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");
    }
}
Using proxy As New ArquivosClient()
    Try
        Console.WriteLine(proxy.LerConteudo("ArquivoQueNaoExiste.txt"))
    Catch ex As FaultException(Of FileNotFoundException)
        Console.WriteLine("Arquivo não encontrado")
    Catch ex As FaultException
        Console.WriteLine("Problema na execução da operação")
    Catch ex As CommunicationException
        Console.WriteLine("Problema no serviço")
    Catch ex As Exception
        Console.WriteLine("Erro")
    End Try
End Using
C# VB.NET

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
    }
}
Imports System
Imports System.IO
Imports System.ServiceModel
Imports System.ServiceModel.Channels
Imports System.ServiceModel.Dispatcher

Public Class ErrorHandling
    Implements IErrorHandler

    Public Function HandleError(ByVal [error] As Exception) As Boolean _
        Implements IErrorHandler.HandleError

        Try
            Log([error])
        Catch
        End Try

        Return False
    End Function

    Public Sub ProvideFault(ByVal [error] As Exception, _
		ByVal version As MessageVersion, _
        ByRef fault As Message) Implements IErrorHandler.ProvideFault

        If TypeOf ([error]) Is FileNotFoundException Then
            Dim ex As New FaultException(Of FileNotFoundException)( _
                DirectCast([error], FileNotFoundException), _
                New FaultReason("O arquivo informado não foi encontrado"), _
                New FaultCode("FileNotFound"))

            fault = Message.CreateMessage(version, ex.CreateMessageFault(), ex.Action)
        End If
    End Sub

    Private Shared Sub Log(ByVal [error] As Exception)
        "efetuar o log
    End Sub
End Class
C# VB.NET

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
}
Public Class ErrorServiceBehavior
    Implements IServiceBehavior

    Public Sub ApplyDispatchBehavior(ByVal serviceDescription As ServiceDescription, _
                                     ByVal serviceHostBase As ServiceHostBase) _
                                     Implements IServiceBehavior.ApplyDispatchBehavior
        For Each cd As ChannelDispatcher In serviceHostBase.ChannelDispatchers
            cd.ErrorHandlers.Add(New ErrorHandling())
        Next
    End Sub

    "outros métodos
End Class
C# VB.NET

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());
host.Description.Behaviors.Add(New ErrorServiceBehavior())
C# VB.NET

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.
Israel Aéce

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