Implementing Exception Management, Part 2

Applies to: Windows Communication Foundation

Published: June 2011

Author: Alex Culp

Referenced Image

This topic contains the following sections.

  • REST Service
  • Exception Correlation

REST Service

After you have successfully logged the parameters and associated them with an exception, you need a way to retrieve the information from the database. One of the easiest is to use a WCF Representational State Transfer (REST) service. Place the hyperlink to the REST service in the exception's HelpLink property. This makes it simple to send out alerts, especially if using an email tool, such as the one provided by SCOM. The following code is a simple example of how to use REST to retrieve parameter data.

Visual C# REST API for Retrieving Parameter Data

     [ServiceContract]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public class GetOperationParametersService
{
    [OperationContract]
    [WebGet(BodyStyle = WebMessageBodyStyle.Bare, RequestFormat = WebMessageFormat.Xml,
        ResponseFormat = WebMessageFormat.Xml,
        UriTemplate = "/RequestID={requestID}")]
    XmlElement GetServiceRequests(string referenceId)
    {
        Guid referenceIdGuid;
        if (!Guid.TryParse(referenceId, out referenceIdGuid))
        {
            var invalidGuidElement = new XmlElement();
            invalidGuidElement.InnerText = "Invalid Guid";
            return invalidGuidElement;
        }
            
        var parameters = GetParameters(referenceIdGuid);
        var sb = new StringBuilder();
        using (var xmlWriter = XmlWriter.Create(sb))
        {
            xmlWriter.WriteStartElement("Requests");
            foreach (var s in parameters)
            {
                xmlWriter.WriteStartElement("Request");
                xmlWriter.WriteRaw(s);
                xmlWriter.WriteEndElement();
            }
            xmlWriter.WriteEndElement();
            xmlWriter.Flush();
        }
        var document = new XmlDocument();
        document.LoadXml(sb.ToString());
        return document.DocumentElement;
    }
 
    /// <summary>
    /// Gets the parameter data of the operation that failed.
    /// </summary>
    /// <param name="referenceID"></param>
    /// <returns></returns>
    public List<string> GetParameters(Guid referenceID)
    {
        const string PROCEDURE_NAME = "[dbo].[ups_GetOperationParameters]";
        var ret = new List<string>();
 
        var db = DatabaseFactory.CreateDatabase("LoggingDB");
 
        using (var cmd = db.GetStoredProcCommand(PROCEDURE_NAME))
        {
            cmd.Parameters.Add(new SqlParameter("@ReferenceID", referenceID));
            try
            {
                using (var reader = db.ExecuteReader(cmd))
                {
                    while (reader.Read())
                    {
                        ret.Add(reader.Get<string>("Request"));
                    }
                }
            }
            catch (Exception ex)
            {
                ExceptionPolicy.HandleException(ex, "MyErrorPolicy");
            }
        }
        return ret;
     }      
}

Visual Basic REST API for Retrieving Parameter Data

<ServiceContract> _
<AspNetCompatibilityRequirements(RequirementsMode := AspNetCompatibilityRequirementsMode.Allowed)> _
<ServiceBehavior(InstanceContextMode := InstanceContextMode.PerCall)> _
Public Class GetOperationParametersService
     <OperationContract> _
     <WebGet(BodyStyle := WebMessageBodyStyle.Bare, RequestFormat := WebMessageFormat.Xml, ResponseFormat := WebMessageFormat.Xml, UriTemplate := "/RequestID={requestID}")> _
     Private Function GetServiceRequests(referenceId As String) As XmlElement
          Dim referenceIdGuid As Guid
          If Not Guid.TryParse(referenceId, referenceIdGuid) Then
               Dim invalidGuidElement = New XmlElement()
               invalidGuidElement.InnerText = "Invalid Guid"
               Return invalidGuidElement
          End If

          Dim parameters = GetParameters(referenceIdGuid)
          Dim sb = New StringBuilder()
          Using xmlWriter__1 = XmlWriter.Create(sb)
               xmlWriter__1.WriteStartElement("Requests")
               For Each s As var In parameters
                    xmlWriter__1.WriteStartElement("Request")
                    xmlWriter__1.WriteRaw(s)
                    xmlWriter__1.WriteEndElement()
               Next
               xmlWriter__1.WriteEndElement()
               xmlWriter__1.Flush()
          End Using
          Dim document = New XmlDocument()
          document.LoadXml(sb.ToString())
          Return document.DocumentElement
     End Function

     ''' <summary>
     ''' Gets the parameter data of the operation that failed.
     ''' </summary>
     ''' <param name="referenceID"></param>
     ''' <returns></returns>
     Public Function GetParameters(referenceID As Guid) As List(Of String)
          Const  PROCEDURE_NAME As String = "[dbo].[ups_GetOperationParameters]"
          Dim ret = New List(Of String)()

          Dim db = DatabaseFactory.CreateDatabase("LoggingDB")

          Using cmd = db.GetStoredProcCommand(PROCEDURE_NAME)
               cmd.Parameters.Add(New SqlParameter("@ReferenceID", referenceID))
               Try
                    Using reader = db.ExecuteReader(cmd)
                         While reader.Read()
                              ret.Add(reader.[Get](Of String)("Request"))
                         End While
                    End Using
               Catch ex As Exception
                    ExceptionPolicy.HandleException(ex, "MyErrorPolicy")
               End Try
          End Using
          Return ret
     End Function
End Class

Exception Correlation

In a typical enterprise implementation of a service-oriented architecture (SOA), there are services that call other services. If an error is raised in one service, then bubbles up to the next service, how do know if there were two errors or just one? In these cases, you need a mechanism that correlates the errors in each service, and that allows you to track an error if it travels through different services. Enterprise Library does not include this capability. Fortunately, it ships with the source code, so you can modify it if you need to. Correlation requires that every logged exception have a unique ID. If two services each log the same exception, then you can check to see if both exceptions have the same ID.

Extending System.Exception

One way to implement correlation is to create a new exception type that has a correlation ID property, and to make sure that you use this exception type in all of your services. However, this approach is impractical because you must wrap the exceptions yourself before you can log the error. Instead, create an extension method on the System.Exception class that can retrieve the correlation ID.

When an exception is sent to the Logging Application Block, the exception's Data property is passed to the application block in the ExtendedProperties property. You can take advantage of this feature by ensuring that the correlation identifier is included in the Data property of the exception before it is sent to the exception policy. To do this, create an extension method that retrieves the correlation ID. To account for the fact that the Data property is not serialized across SOAP boundaries, add a property to the fault contract that contains the correlation ID. The basic approach wraps the retrieval of the correlation ID in a recursive method. This method checks that the correlation ID of an inner exception is the same as the correlation ID of the outer exception. The following code shows how to implement the extension method.

Visual C# Recursive Exception Extension Method

     public static class ExceptionExtension
{
    /// <summary>
    /// Gets the reference ID, a unique identifier for the error. 
    /// </summary>
    /// <remarks>This function has a side effect of updating the data collection
    /// on each exception in the chain.</remarks>
    /// <returns></returns>
    public static Guid GetReferenceID(this Exception ex)
    {
        const string REF_ID = "CorrelationId";
        Guid ret;
 
        if (ex == null)
        {
            //Lowest level in recursive function, at no point did
            //this exception have a reference id, so create one here and bubble it
            //back up the call stack.
            return Guid.NewGuid();
        }
 
        if (ex.Data.Contains(REF_ID))
        {
            //Short cut, found the reference id, we are done no need to call
            //recursively to get the reference id.
            ret = (Guid)ex.Data[REF_ID];
        }
        else
        {
            if (ex is FaultException<ReceiverFaultDetail>)
            {
                ret = ((FaultException<ReceiverFaultDetail>)ex).Detail.ReferenceID;
            }
            else if (ex is FaultException<SenderFaultDetail>)
            {
                ret = ((FaultException<SenderFaultDetail>)ex).Detail.ReferenceID;
            }
            else
            {
                //Recursively call down the inner exceptions to see if we
                //have a reference id in one of the inner exceptions.
                ret = GetReferenceID(ex.InnerException);
            }
            //Add the referenceid to the data collection so it
            //won't have to call recursively again.
            ex.Data.Add(REF_ID, ret);
        }
        return ret;
    }
 }

Visual Basic Recursive Exception Extension Method

Public NotInheritable Class ExceptionExtension
     Private Sub New()
     End Sub
     ''' <summary>
     ''' Gets the reference ID, a unique identifier for the error. 
     ''' </summary>
     ''' <remarks>This function has a side effect of updating the data collection
     ''' on each exception in the chain.</remarks>
     ''' <returns></returns>
     <System.Runtime.CompilerServices.Extension> _
     Public Shared Function GetReferenceID(ex As Exception) As Guid
          Const  REF_ID As String = "CorrelationId"
          Dim ret As Guid

          If ex Is Nothing Then
               'Lowest level in recursive function, at no point did
               'this exception have a reference id, so create one here and bubble it
               'back up the call stack.
               Return Guid.NewGuid()
          End If

          If ex.Data.Contains(REF_ID) Then
               'Short cut, found the reference id, we are done no need to call
               'recursively to get the reference id.
               ret = DirectCast(ex.Data(REF_ID), Guid)
          Else
               If TypeOf ex Is FaultException(Of ReceiverFaultDetail) Then
                    ret = DirectCast(ex, FaultException(Of ReceiverFaultDetail)).Detail.ReferenceID
               ElseIf TypeOf ex Is FaultException(Of SenderFaultDetail) Then
                    ret = DirectCast(ex, FaultException(Of SenderFaultDetail)).Detail.ReferenceID
               Else
                    'Recursively call down the inner exceptions to see if we
                    'have a reference id in one of the inner exceptions.
                    ret = GetReferenceID(ex.InnerException)
               End If
               'Add the referenceid to the data collection so it
               'won't have to call recursively again.
               ex.Data.Add(REF_ID, ret)
          End If
          Return ret
     End Function
End Class

Configuring Enterprise Library to Capture Correlation IDs

After the correlation ID is stored in the exception's Data, you need to make sure that the information is captured. If you use the Logging Application Block to send information to a database, you can modify the FormattedDatabaseTraceListener to include the correlation ID. This modification requires that you recompile Enterprise Library, which does have its drawbacks. If you decide to go ahead with this approach, make sure that your modifications are included every time you upgrade to a new version of the Enterprise Library.

To modify the trace listener, install the Enterprise Library source. In Visual Studio, open EnterpriseLibrary.2010.sln, which is in \EntLib50Src\Blocks. Next, open FormattedDatabaseTraceListener.cs. The following figure illustrates the location of the file.

Referenced Image

Add the following code to the ExecuteWriteLogStoredProcedure method, which takes a LogEntry as a parameter, just before the ExecuteNonQuery call to the database.

if (logEntry.ExtendedProperties.ContainsKey("CorrelationId"))
{
    db.AddInParameter(cmd, "CorrelationId", DbType.Guid, logEntry.ExtendedProperties["CorrelationId"]);
}

You must also modify the Log table and the WriteLog stored procedure to add the CorrelationId property.

Finally, whenever you are making a call to the policy, you will need to retrieve the GUID. This will ensure that the correlation ID is in the Data property of the exception before sending it to the logging policy.

Previous article: Implementing Exception Management, Part 1

Continue on to the next article: Handling Database Errors