Parser para CFFaultExceptions
Estou envolvido num projecto assente em .NET CF 3.5, onde utilizamos WCF sobre HTTP para recuperar informação de um servidor.
Se é verdade que a .NET CF 3.5 traz suporte a este novo modelo de comunicações, esse suporte é limitado, como podemos ver neste post do Andrew Arnott. Sendo suportado, quem já utilizou o WCF sobre HTTP para .NET CF, já se terá apercebido que esse suporte não é totalmente 'nativo', pois ao contrário do que acontece na full framework, para além das classes proxy, é sempre criado um ficheiro chamado CFClientBase.cs ou .vb, onde se baseia muito do suporte a WCF sobre HTTP pela .NET CF.
A limitação desse suporte chega ao ponto das FaultExcpetions, neste caso, CFFaultException, não serem expostas detalhadas. Se por exemplo tivermos uma chamada a um serviço dentro de um bloco Try/Catch, e este despoletar uma CFFaultException:
Try
...
Catch ex as CFFaultException
MsgBox(ex.Message)
End Try
O que obtemos na message box, é uma mensagem <not> muito </not> explícita: "CFFaultException".
Ao inspeccionar o objecto ex, chegamos a uma propriedade FaultString, do tipo string, onde encontramos um documento XML bem formado, e lá no meio, encontramos a causa da excepção. Esta inspecção visual até serve em ambiente de desenvolvimento, mas é importante poder recuperar essa informação programaticamente, pelo que se impõe um parser para este XML. Uma pesquisa com o Google a CFFaultException parser não produz resultados, colocando apenas CFFaultException, lá aparece uma página em italiano de um MVP, Andrea Boschin. Os meus conhecimentos da língua da Monica Bellucci são muito limitados - da língua da Monica em si, nulos mesmo - mas lá consegui chegar à conclusão que o tópico era o que me interessava - neste caso, a CFFaultException, não a Monica - ou seja, um parser para a CFFaultException em C#. Como este projecto está a ser desenvolvido em VB.net, recorri ao meu tradutor preferido de C# para VB.net, e completei o serviço corrigindo um ou outro erro:
Imports System.Text
Imports System.Xml.Serialization
Imports System.IO
Imports System.ServiceModel
<XmlRoot("Fault", Namespace:="http://schemas.xmlsoap.org/soap/envelope/")> _
Public Class ServiceFault
Private _faultCode, _faultString As String
Private _detail As List(Of ServiceFaultDetail)
Public Shared Empty As ServiceFault = New ServiceFault
Public Sub New()
MyBase.New()
Me.Detail = New List(Of ServiceFaultDetail)
End Sub
<XmlElement("faultcode", Namespace:="")> _
Public Property FaultCode() As String
Get
Return _faultCode
End Get
Set(ByVal value As String)
_faultCode = value
End Set
End Property
<XmlElement("faultstring", Namespace:="")> _
Public Property FaultString() As String
Get
Return _faultString
End Get
Set(ByVal value As String)
_faultString = value
End Set
End Property
<XmlArray("detail", Namespace:=""), _
XmlArrayItem(GetType(ServiceFaultDetail), Namespace:="http://schemas.datacontract.org/2004/07/System.ServiceModel")> _
Public Property Detail() As List(Of ServiceFaultDetail)
Get
Return _detail
End Get
Set(ByVal value As List(Of ServiceFaultDetail))
_detail = value
End Set
End Property
End Class
<XmlType(TypeName:="ExceptionDetail")> _
Public Class ServiceFaultDetail
Private _message, _stackTrace, _type As String
Public Sub New()
MyBase.New()
End Sub
<XmlElement("Message", Namespace:="http://schemas.datacontract.org/2004/07/System.ServiceModel")> _
Public Property Message() As String
Get
Return _message
End Get
Set(ByVal value As String)
_message = value
End Set
End Property
<XmlElement("StackTrace", Namespace:="http://schemas.datacontract.org/2004/07/System.ServiceModel")> _
Public Property StackTrace() As String
Get
Return _stackTrace
End Get
Set(ByVal value As String)
_stackTrace = value
End Set
End Property
<XmlElement("Type", Namespace:="http://schemas.datacontract.org/2004/07/System.ServiceModel")> _
Public Property Type() As String
Get
Return _type
End Get
Set(ByVal value As String)
_type = value
End Set
End Property
End Class
Public Class FaultSerializer
''' <summary>
''' Deserializes the specified fault.
''' </summary>
''' <param name="fault">The fault.</param>
''' <returns></returns>
Public Shared Function Deserialize(ByVal fault As CFFaultException) As ServiceFault
If (fault Is Nothing) Then
Throw New ArgumentNullException("fault", "fault cannot be null")
End If
If String.IsNullOrEmpty(fault.FaultMessage) Then
Return ServiceFault.Empty
End If
Dim xml As String = fault.FaultMessage
Dim reader As StringReader = New StringReader(xml)
Dim serializer As XmlSerializer = New XmlSerializer(GetType(ServiceFault), "http://schemas.xmlsoap.org/soap/envelope/")
Return CType(serializer.Deserialize(reader), ServiceFault)
End Function
''' <summary>
''' Serializes the specified fault.
''' </summary>
''' <param name="fault">The fault.</param>
''' <returns></returns>
Public Shared Function Serialize(ByVal fault As ServiceFault) As String
Dim builder As StringBuilder = New StringBuilder
Dim writer As StringWriter = New StringWriter(builder)
Dim ns As XmlSerializerNamespaces = New XmlSerializerNamespaces
ns.Add("s", "http://schemas.xmlsoap.org/soap/envelope/")
ns.Add("i", "http://www.w3.org/2001/XMLSchema-instance")
ns.Add("a", "http://schemas.microsoft.com/net/2005/12/windowscommunicationfoundation/dispatcher")
Dim serializer As XmlSerializer = New XmlSerializer(fault.GetType, "http://schemas.xmlsoap.org/soap/envelope/")
serializer.Serialize(writer, fault, ns)
Return builder.ToString
End Function
End Class
Assim, podemos mudar o nosso exemplo para mostrar, por ex., a propriedade .Message associada à excepção:
Try
...
Catch ex as CFFaultException
MsgBox(FaultSerializer.Deserialize(ex).Detail(0).Message)
End Try
Os agradecimentos vão para o Andrea Boshcin!
[Actualização]
O seguinte extension method permite obter de imediato a mensagem associada à excepção:
<Extension()> _
Public Function ExceptionMessage(ByVal exception As CFFaultException) As String
Return FaultSerializer.Deserialize(exception).FaultString
End Function
...
Try
...
Catch ex as CFFaultException
MsgBox(ex.ExceptionMessage)
End Try