My custom BusinessRuleException is really simple; it just adds an extra property that can contain a DataSet and typically a DataSet with error information. The exception handlers will treat BusinessRueException with the DataSet property set specially by not serializing the exception but instead converting the DataSet to a diffgram. Why? Because a serialized exception requires that the types are available on the client which is something that is not wishful, because you don't want to share class between services. It goes without saying that it will also require the client to be a .NET client, and sure the Diffgram is easily handled by a .NET client, but it is at least also possible for a client of another environment to handle it too. For the conversion I'm using my Convert-class that I wrote about
here.
For the SoapServerExceptionHandler I just added an overloaded version of the SerializeException method, that looks like this:
public static XmlElement SerializeException(IRM.BusinessRuleException exception)
{
if (exception.DataSet == null) return SerializeException(exception, true);
XmlDocument dom = new XmlDocument();
XmlNode node = dom.AppendChild(dom.CreateElement("detail"));
XmlElement el = Convert.ToDiffGram(exception.DataSet);
node = node.AppendChild(dom.CreateElement(namespacePrefix, schemaName, detailNamespace));
node.InnerXml = exception.DataSet.GetXmlSchema();
node.RemoveChild(node.FirstChild); //Remove Xml Declaration
node.RemoveChild(node.FirstChild); //Remove Whitespace
node = dom.FirstChild;
node = node.AppendChild(dom.CreateElement(namespacePrefix, diffgramName, detailNamespace));
node.AppendChild(dom.ImportNode(el, true));
return dom.DocumentElement;
}
The code is straightforward and simply adds one node with schema information and another node with the diffgram. The schema is necessary if you want to create a DataSet on the client. This brings us to the client code:
public override Exception HandleException(Exception exception, string policyName, Guid handlingInstanceId)
{
SoapException soapEx = exception as SoapException;
if (soapEx == null) return exception;
if (soapEx.Detail == null) return exception;
if (soapEx.Detail.ChildNodes.Count < 2) return exception;
Exception ex = null;
XmlReader reader = null;
XmlWriter writer = null;
System.IO.MemoryStream stream = new System.IO.MemoryStream();
try
{
XmlNode node = null;
foreach (XmlNode childNode in soapEx.Detail.ChildNodes)
{
if (childNode.NodeType==XmlNodeType.Element)
{
node = childNode;
break;
}
}
if (node!=null)
{
if (node.LocalName=="SOAP-ENV" &&
node.NamespaceURI=="http://schemas.xmlsoap.org/soap/envelope/")
{
reader = new XmlNodeReader(node);
writer = new XmlTextWriter(stream, System.Text.Encoding.UTF8);
writer.WriteNode(reader, true);
writer.Flush();
stream.Position = 0;
SoapFormatter formatter = new SoapFormatter();
ex = formatter.Deserialize(stream) as Exception;
}
else if (node.LocalName==Xml.ExceptionSerializer.schemaName &&
node.NamespaceURI==Xml.ExceptionSerializer.detailNamespace)
{
reader = new XmlNodeReader(node);
System.Data.DataSet data = new System.Data.DataSet();
data.ReadXmlSchema(reader);
reader.Close();
while (node.NextSibling!=null)
{
node = node.NextSibling;
if (node.NodeType==XmlNodeType.Element)
break;
}
node = node.FirstChild; //not interested in the irm diffgram node
while (node!=null && node.NodeType!=XmlNodeType.Element)
node = node.NextSibling;
data.AcceptChanges();
if (node!=null)
IRM.Xml.Convert.ToDataSet((XmlElement)node, data);
ex = new IRM.BusinessRuleException(data);
}
}
}
catch (System.Runtime.Serialization.SerializationException serEx)
{
System.Diagnostics.Debug.WriteLine(serEx.Message);
}
finally
{
if (reader != null)
reader.Close();
if (writer != null)
writer.Close();
((IDisposable)stream).Dispose();
}
if (ex == null) ex = exception;
return ex;
}
If you compare this code with the one posted first, you will notice that I have added some more checks in the beginning and that I also uses a safer way to get to the nodes in the xml document. The logical differences is that I now check for a SOAP envelope (=a serialized exception) and then checking for my own nodes that will contain the dataset with error information. If it is a DataSet, I recreate the structure of it by reading the schema and then again using the Convert-class to fill the DataSet with the data. Finally I recreate a BusinessRuleException that is later returned to the exception handler logic of EntLib.
With this code in place I have been able to get the error information to propagate through several services up to the client. Before I had to catch the exception in each service just to parse the error information and reset it on the structure of that service and then pass it along. This way the code gets cleaner and faster.