One of the most tiresome (but important) things when developing web services is handling un-handled exceptions. A good design principle forces you to catch and cast relevant exceptions raised by your web methods into more meaningful SOAP exceptions. But exceptions will occur.

It is quite tedious to wrap each web method in a try/catch loop. This dictates a need for a common framework to handle unhandled web services exceptions. In this blog post, I will guide you through a step by step process for building one.

Summary:

  1. Extend SoapExtension class and override the ProcessMessage() method.
  2. In ProcessMessage(), add special handler code for message stage of SoapMessageStage.AfterSerialize.
  3. Modify the web service's web.config file and add a <soapExtensionTypes> node to <webServices> section.

Details:

Ok, now let's dig deeper into the code.

Step 1:

The first step in the whole process is to extend the SoapExtension class override the ProcessMessage() method.

using System.IO;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml;
using System;

public class TelspaceSoapExtension : SoapExtension
{
    private Stream originalStream;
    private Stream updatedStream;

    public override object GetInitializer(Type serviceType)
    {
        return null;
    }

    public override object GetInitializer
    (LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
    {
        return null;
    }

    public override void Initialize(object initializer)
    {
    }

    public override Stream ChainStream(Stream stream)
    {
        originalStream = stream;
        updatedStream = new MemoryStream();
        return updatedStream;
    }

    private void Transfer(Stream inStream, Stream outStream)
    {
        StreamReader sr = new StreamReader(inStream);
        StreamWriter sw = new StreamWriter(outStream);
        sw.Write(sr.ReadToEnd());
        sw.Flush();
    }

    public override void ProcessMessage(SoapMessage soapMessage)
    {
        switch (soapMessage.Stage)
        {
            case SoapMessageStage.BeforeDeserialize:
                Transfer(originalStream, updatedStream);
                updatedStream.Position = 0;
                break;

            case SoapMessageStage.AfterSerialize:
                if ((soapMessage.Exception != null))
                {
                    ExceptionProcessor processor = new ExceptionProcessor();

                    string details;

                    // handle our exception, and get the SOAP <detail> string
                    details = processor.HandleWebServiceException(soapMessage);

                    // read the entire SOAP message stream into a string
                    updatedStream.Position = 0;
                    TextReader tr = new StreamReader(updatedStream);

                    // insert our exception details into the string
                    string s = tr.ReadToEnd();
                    s = s.Replace("<detail />", details);

                    // overwrite the stream with our modified string
                    updatedStream = new MemoryStream();
                    TextWriter tw = new StreamWriter(updatedStream);
                    tw.Write(s);
                    tw.Flush();
                }

                updatedStream.Position = 0;
                Transfer(updatedStream, originalStream);
                break;
        }
    }

}

Step 2:

The next step is to handle the exception and get out meaningful details from the exception. For this purpose, let's dig deeper into the ExceptionProcessor class. This class has one public method: HandleWebServiceException(System.Web.Services.Protocols.SoapMessage sm). This method is called from our ProcessMessage()case SoapMessageStage.AfterSerialize.

Here is the code:

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Web.Services.Protocols;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Reflection;
using SASMTPLib;
using System.IO;

class ExceptionProcessor
{
    private string currentExceptionDetails;
    private string currentExceptionTypeName;

    private const string RootExceptionName = "System.Web.HttpUnhandledException";
    private const string RootWsExceptionName = "System.Web.Services.Protocols.SoapException";

    public string HandleWebServiceException(System.Web.Services.Protocols.SoapMessage sm)
    {
        HandleException(sm.Exception);
        XmlDocument doc = new XmlDocument();
        XmlNode detailNode = doc.CreateNode(XmlNodeType.Element, SoapException.DetailElementName.Name, SoapException.DetailElementName.Namespace);
        XmlNode typeNode = doc.CreateNode(XmlNodeType.Element, "ExceptionType", SoapException.DetailElementName.Namespace);
        typeNode.InnerText = currentExceptionTypeName;
        detailNode.AppendChild(typeNode);

        XmlNode messageNode = doc.CreateNode(XmlNodeType.Element, "ExceptionMessage", SoapException.DetailElementName.Namespace);
        messageNode.InnerText = sm.Exception.Message;
        detailNode.AppendChild(messageNode);

        XmlNode infoNode = doc.CreateNode(XmlNodeType.Element, "ExceptionInfo", SoapException.DetailElementName.Namespace);
        infoNode.InnerText = currentExceptionDetails;
        detailNode.AppendChild(infoNode);
        return detailNode.OuterXml.ToString();
    }

    private void HandleException(Exception ex)
    {
        try
        {
            currentExceptionDetails = NormalizeException(ex);
            currentExceptionTypeName = ex.GetType().FullName;

            // ignore root exceptions
            if (((currentExceptionTypeName == RootExceptionName) || (currentExceptionTypeName == RootWsExceptionName)))
            {
                if (ex.InnerException != null)
                {
                    currentExceptionTypeName = ex.InnerException.GetType().FullName;
                }
            }
        }
        catch (Exception e)
        {
            currentExceptionDetails = string.Format("Error \'{0}\' while generating exception string", e.Message );
        }

        ExceptionToEmail();
    }

    private string NormalizeException(Exception ex)
    {
        StringBuilder sb = new StringBuilder();

        if (ex.InnerException != null)
        {
            if (((ex.GetType().ToString() == RootExceptionName) || (ex.GetType().ToString() == RootWsExceptionName)))
            {
                return NormalizeException(ex.InnerException);
            }
            else
            {
                sb.Append(NormalizeException(ex.InnerException));
                sb.Append(Environment.NewLine);
                sb.Append("(Outer Exception)");
                sb.Append(Environment.NewLine);
            }
        }

        // get exception-specific information
        sb.Append("Exception Type:\t\t");
        try
        {
            sb.Append(ex.GetType().FullName);
        }
        catch (Exception e)
        {
            sb.Append(e.Message);
        }
        sb.Append(Environment.NewLine);

        sb.Append("Exception Message:\t\t");
        try
        {
            sb.Append(ex.Message);
        }
        catch (Exception e)
        {
            sb.Append(e.Message);
        }
        sb.Append(Environment.NewLine);

        sb.Append("Exception Target Method:\t");
        try
        {
            sb.Append(ex.TargetSite.Name);
        }
        catch (Exception e)
        {
            sb.Append(e.Message);
        }
        sb.Append(Environment.NewLine);

        sb.Append("Stack Trace:\t\t");
        sb.Append(Environment.NewLine);
        try
        {
            sb.Append(ex.StackTrace);
        }
        catch (Exception e)
        {
            sb.Append(e.Message);
        }
        sb.Append(Environment.NewLine);

        return sb.ToString();
    }

    private void ExceptionToEmail()
    {
        // Send e-mail code
    }

}

Step 3:

The final step is to modify web.config of the web services to include the following section (under <system.web> node)

<webServices>
  <soapExtensionTypes>
    <add type="TelspaceSoapExtension, ExceptionHandler" priority="1" group="High"/>
  </soapExtensionTypes>
</webServices>