Tratamento de exceções usando AOP

Publicado por Ricardo Ushisima
23/11/2011
Categoria:
Tags: , , , , , ,

Quem já usou WCF ou Web Services sabe que quando uma exceção sobe do serviço para o cliente, ela é convertida em FaultException ou SOAPExeption respectivamente com informações sobre a exceção original (alias, no WCF, o padrão é reportar um FaultException genérico sem informações sobre a exceção original). Esse comportamento é ruim para quem programa uma API de um serviço que pode estar local ou remoto e que usa exceções nas lógicas de negócio.

Na minha aplicação de exemplo, o Conversor de Moedas, se o serviço quiser reportar que uma das moedas usadas na conversão não existe, o jeito mais natural seria lançar uma exceção de negócio (CurrencyNotFoundException). Mas quando a aplicação é remota, essa exceção de negócio nunca chega ao cliente, obrigando o programador ler e intepretar a mensagem do SOAPException ou FaultException, ou mudar o serviço para retornar uma estrutura que contenha o valor da conversão ou a exceção de negócio. A última opção é a que mais vejo sendo aplicada por ai, mas tem a desvantagem de poluir a API de seu serviço. Cada serviço tem que ter seu proprio objeto de retorno que engloba a resposta de fato (se houver) mais informações sobre as exceções quando elas ocorrerem. Essa abordagem também polui o tratamento de exceções do lado do cliente que precisa tratar erros do serviço diferentemente dos demais erros da aplicação. Muito provavelmente o tratamento de exceções vai ficar no meio do fluxo normal da aplicação deixando o código um pouco mais dificil de ler e manter.

Veja o exemplo abaixo com a abordagem mais comum.

using System;
using System.ServiceModel;
using System.Runtime.Serialization;

namespace SpringTutorial
{
    [ServiceContract(Namespace="http://zbra.com.br/SpringTutorial")]
    public interface ICurrencyService
    {
        [OperationContract]
        [FaultContract(typeof(ServiceFaultDetail))]
        ConvertResult Convert(string from, string to, decimal value);
    }

    [DataContract(Namespace = "http://zbra.com.br/SpringTutorial")]
    public class ConvertResult
    {
        [DataMember]
        bool Success { get; set; }
        [DataMember]
        string ErrorMessage { get; set; }
        [DataMember]
        decimal Result { get; set; }
    }
}

Existe uma maneira mais elegante de lidar com esse problema de transporte de exceções do serviço remoto para o cliente. A estatégia é utilizar dois Advices AOP, um para converter as exceções em FaultException, e outro Advice para converter FaultException de volta para a exceção original. Assim, o cliente pode tratar as exceções de negócio da maneira usual (com try/catch).

Como sempre, vou me basear no meu serviço de conversão de moedas que venho utilizando como exemplo em meus artigos. Para esta demostração, vou alterar meu serviço de conversão para lançar uma exceção de negócio CurrencyNotFoundException indicando ao cliente que a moeda solicitada não esta cadastrada no sistema.

using System;
using System.Collections.Generic;
using System.ServiceModel;

namespace SpringTutorial.Core
{
    [ServiceBehavior(Namespace = "http://zbra.com.br/SpringTutorial", ConcurrencyMode = ConcurrencyMode.Multiple, InstanceContextMode = InstanceContextMode.Single)]
    public class CurrencyService : ICurrencyService
    {
        private readonly Dictionary<string, decimal> exchangeRates = new Dictionary<string, decimal>();

        public CurrencyService()
        {
            InitCurrencyExchangeRates();
        }

        private void InitCurrencyExchangeRates()
        {
            exchangeRates.Add("BRL", 1); //Base Rate
            exchangeRates.Add("USD", 1.68m);
            exchangeRates.Add("EUR", 2.24m);
        }

        public decimal Convert(string from, string to, decimal value)
        {
            decimal fromRate = GetRate(from);
            decimal toRate = GetRate(to);
            decimal baseValue = value * fromRate;
            return baseValue / toRate;
        }

        private decimal GetRate(string currency)
        {
            decimal rate;
            if (!exchangeRates.TryGetValue(currency, out rate))
            {
                throw new CurrencyNotFoundException("Cannot find exchange rate for currency " + currency);
            }
            return rate;
        }
    }
}

Para que essa exceção de negócio possa ser transportada para o cliente, primeiro, devemos declarar no contrato de serviço quais os possiveis ‘Faults’ que o serviço pode experimentar. O tipo que pode ser lançado não pode ser do tipo Exception pois este não pode ser serializado usando Data Contracts. No exemplo, vou criar uma classe generica que transporta informações de qualquer tipo de exceção (de negócio ou não) para o cliente (mas o correto seria apenas transportar exceções de negócio).

using System;
using System.ServiceModel;
using System.Runtime.Serialization;

namespace SpringTutorial
{
    [ServiceContract(Namespace="http://zbra.com.br/SpringTutorial")]
    public interface ICurrencyService
    {
        [OperationContract]
        [FaultContract(typeof(ServiceFaultDetail))]
        decimal Convert(string from, string to, decimal value);
    }
}

A classe ServiceFaultDetail é bem simples neste exemplo, contendo apenas o tipo da exceção original e a mensagem da exceção. Esse é o conjunto minimo de informações necessário para reconstruir a exceção do lado do cliente.

using System;
using System.Runtime.Serialization;

namespace SpringTutorial
{
    [DataContract(Namespace = "http://zbra.com.br/SpringTutorial"), Serializable]
    public class ServiceFaultDetail
    {
        [DataMember]
        public string SourceQualifiedName { get; set; }
        [DataMember]
        public string Message { get; set; }
    }
}

O advice no lado do servidor é responsável por interceptar exceções lançadas por nosso serviço e convertê-las para FaultException, tipo compatível com o WCF e do mesmo tipo declarado no contrato do serviço.

using System;
using System.Reflection;
using System.ServiceModel;
using Spring.Aop;

namespace SpringTutorial.Util
{
    public class ServerSideExceptionAdvice : IThrowsAdvice
    {
        public void AfterThrowing(MethodInfo method, Object[] args, Object target, Exception ex)
        {
            ServiceFaultDetail detail = new ServiceFaultDetail();
            detail.SourceQualifiedName = ex.GetType().AssemblyQualifiedName;
            detail.Message = ex.Message;
            throw new FaultException<ServiceFaultDetail>(detail, new FaultReason(ex.Message));
        }
    }
}

Esse Advice é criado e configurado no Spring.xml abaixo:

<?xml version="1.0" encoding="utf-8" ?>
<objects xmlns="http://www.springframework.net" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://www.springframework.net http://www.springframework.net/xsd/spring-objects.xsd">

  <object id="currencyService" type="SpringTutorial.Core.CurrencyService, SpringTutorial.Core" singleton="false">
  </object>

  <object id="ServerSideFaultAdvice" type="SpringTutorial.Util.ServerSideExceptionAdvice, SpringTutorial.Util">
  </object>

  <object type="Spring.Aop.Framework.AutoProxy.ObjectNameAutoProxyCreator, Spring.Aop">
    <property name="ObjectNames">
      <list>
        <value>currencyService</value>
      </list>
    </property>
    <property name="InterceptorNames">
      <list>
        <value>ServerSideFaultAdvice</value>
      </list>
    </property>
  </object>

</objects>

Do lado cliente, o seguinte advice faz o trabalho inverso, convertendo um FaultException de volta para a exceção original.

using System;
using System.Reflection;
using System.ServiceModel;
using Spring.Aop;
using SpringTutorial;

namespace SpringTutorial.Util
{
    public class ClientSideExceptionAdvice : IThrowsAdvice
    {
        public void AfterThrowing(MethodInfo method, Object[] args, Object target, FaultException<ServiceFaultDetail> ex)
        {
            if (ex.Detail != null)
            {
                Exception serverException = null;
                try
                {
                    Type type = Type.GetType(ex.Detail.SourceQualifiedName);
                    if (type != null)
                    {
                        serverException = type.GetConstructor(new Type[] { typeof(string) }).Invoke(new object[] { ex.Detail.Message }) as Exception;
                    }
                }
                catch (Exception)
                {
                    //Ignore
                }
                if (serverException != null)
                {
                    throw serverException;
                }
            }
            // else the default behavior throws the original exception
        }
    }
}

Note que nessa conversão, o StackTrace da exceção original é perdido mas essa informação também pode ser transportada se for interessante para seu caso.

A implementação do cliente agora pode capturar a exceção de negócio e tratar de acordo.

using System;
using SpringTutorial;

namespace WcfClient
{
    public partial class _Default : System.Web.UI.Page
    {
        public ICurrencyService CurrencyService { get; set; }

        protected void Page_Load(object sender, EventArgs e)
        {
        }

        protected void ConvertButton_Click(object sender, EventArgs e)
        {
            try
            {
                string fromCurrency = FromCurrencyDropDownList.SelectedValue;
                string toCurrency = ToCurrencyDropDownList.SelectedValue;
                decimal amount = decimal.Parse(ValueTextBox.Text);
                decimal result = CurrencyService.Convert(fromCurrency, toCurrency, amount);
                ResultTextBox.Text = result.ToString();
            }
            catch (CurrencyNotFoundException ex)
            {
                ResultTextBox.Text = ex.Message;
            }
        }
    }
}

Nesta abordagem, o tratamento de exceções é feita da maneira tradicional usando try/catch, tal como se o serviço fosse local e não remoto. O fato de o serviço ser remoto não interfere na lógica de negócio do cliente. A API de seu serviço fica mais limpa, o retorno das funções mais claras e o serviço mais robusto com exceções de negócio bem definidas para o cliente. Para Web Services, a abordagem é bem similar. No SOAPException, ao invés de criar um objeto que contém os dados da exceção (ServiceFaultDetail), cria-se um XmlNode arbitrário com os detalhes da exceção.





Desenvolvido por hacklab/ com WordPress