Implementing Exception Management, Part 2
Applies to: Windows Communication Foundation
Published: June 2011
Author: Alex Culp
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.
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