Sunday, February 27, 2011

WCF REST Exception handling deserializing with XmlSerializer

As an alternative to using the DataContractSerializer you can use the XmlSerializer in your WCF services. Assuming that you have a special need that cannot be covered by the DataContractSerializer here is a sample of how to catch and handle WCF REST faults on the client side. This provides an alternative to using the DataContractSerializer which is used in this post WCF REST Exception handlingThe WCF REST Exception handling post sets the scene for this post.

Dan Rigsbyb have a great blog posting where he compares the DataContractSerializer with the XmlSerializer, it is located here XmlSerializer vs DataContractSerializer: Serialization in Wcf.
The advantage of this version of the WcfRestExceptionHelper class is that it allows for somewhat simpler client code (compared to the version in  WCF REST Exception handling). 
To mix things up a little bit I have chosen to use the XmlSerializer instead of the DataContractSerializerWithout further ado.

WCF REST client code:

var factory = new ChannelFactory<IService1Wrapper>("Service1WrapperREST");var proxy = factory.CreateChannel();

try
{
 var serviceResult = proxy.GetProductById("1");

 // Do something with result
}
catch (Exception exceptionThrownByRestWcfCall)
{
 var serviceResult = WcfRestExceptionHelper<SampleItem[], SampleError>.HandleRestServiceError(exceptionThrownByRestWcfCall);

 // Do something with result, let higher levels in the callstack handle possible real exceptions
}
finally
{
 ((IDisposable)proxy).Dispose();
}

The WcfRestExceptionHelper class

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.ServiceModel;
using System.Text;
using System.Xml.Serialization;

namespace WcfRestClient.WcfErrors
{
 public static class WcfRestExceptionHelper<TServiceResult, TServiceFault> where TServiceFault : class
 {
  private static IDictionary<Type, XmlSerializer> cachedSerializers = new Dictionary<Type, XmlSerializer>();

  public static TServiceResult HandleRestServiceError(Exception exception)
  {
   if (exception == null) throw new ArgumentNullException("exception");

   // REST uses the HTTP procol status codes to communicate errors that happens on the service side.
   // This means if we have a teller service and you need to supply username and password to login
   // and you do not supply the password, a possible scenario is that you get a 400 - Bad request.
   // However it is still possible that the expected type is returned so it would have been possible 
   // to process the response - instead it will manifest as a ProtocolException on the client side.
   var protocolException = exception as ProtocolException;
   if (protocolException != null)
   {
    var webException = protocolException.InnerException as WebException;
    if (webException != null)
    {
     var responseStream = webException.Response.GetResponseStream();
     if (responseStream != null)
     {
      try
      {
       // Debugging code to be able to see the reponse in clear text
       //SeeResponseAsClearText(responseStream);

       // Try to deserialize the returned XML to the expected result type (TServiceResult)
       return (TServiceResult) GetSerializer(typeof(TServiceResult)).Deserialize(responseStream);
      }
      catch (InvalidOperationException serializationException)
      {
       // This happens if we try to deserialize the responseStream to type TServiceResult
       // when an error occured on the service side. An service side error serialized object 
       // is not deserializable into a TServiceResult

       // Reset responseStream to beginning and deserialize to a TServiceError instead
       responseStream.Seek(0, SeekOrigin.Begin);

       var serviceFault = (TServiceFault)GetSerializer(typeof(TServiceFault)).Deserialize(responseStream);
       throw new WcfRestServiceException<TServiceFault>() { ServiceFault = serviceFault };
      }
     }
    }
   }

   // Don't know how to handle this exception
   throw exception;
  }
  
  /// <summary>
  /// Based on the knowledge of how the XmlSerializer work, I found it safest to explicitly implement my own caching mechanism.
  /// 
  /// From MSDN:
  /// To increase performance, the XML serialization infrastructure dynamically generates assemblies to serialize and 
  /// deserialize specified types. The infrastructure finds and reuses those assemblies. This behavior occurs only when 
  /// using the following constructors:
  /// 
  /// XmlSerializer.XmlSerializer(Type)
  /// XmlSerializer.XmlSerializer(Type, String)
  /// 
  /// If you use any of the other constructors, multiple versions of the same assembly are generated and never unloaded, 
  /// which results in a memory leak and poor performance. The easiest solution is to use one of the previously mentioned 
  /// two constructors. Otherwise, you must cache the assemblies.
  /// 
  /// </summary>
  /// <param name="classSpecificSerializer"></param>
  /// <returns></returns>
  private static XmlSerializer GetSerializer(Type classSpecificSerializer)
  {
   if (!cachedSerializers.ContainsKey(classSpecificSerializer))
   {
    cachedSerializers.Add(classSpecificSerializer, new XmlSerializer(classSpecificSerializer));
   }
   return cachedSerializers[classSpecificSerializer];
  }

  /// <summary>
  /// Debugging helper method in case there are problems with the deserialization
  /// </summary>
  /// <param name="responseStream"></param>
  private static void SeeResponseAsClearText(Stream responseStream)
  {
   var responseStreamLength = responseStream.Length;
   var buffer = new byte[responseStreamLength];
   var x = responseStream.Read(buffer, 0, Convert.ToInt32(responseStreamLength));
   var enc = new UTF8Encoding();
   var response = enc.GetString(buffer);
   Debug.WriteLine(response);
   responseStream.Seek(0, SeekOrigin.Begin);
  }
 }
}
The generic exception that we can throw

using System;
namespace WcfRestClient.WcfErrors
{
 public class WcfRestServiceException<TServiceError> : Exception where TServiceError : class
 {
  public WcfRestServiceException() : base("An service error occured. Please inspect the ServiceError property for details.")
  {
  }
  
  public TServiceError ServiceFault { get; set; }
 }
}

1 comment:

Epoxy Flooring Thousand Oaks said...

This was loovely to read