Download Example Solution
Introduction
This is part 3 of my 3 part series on exception handling with the ESB Toolkit. In this last article I will explain how to create your own error messages when your service fails. That is, when an error occurs in one of you own components… how do you handle this?
I’m not the first person to look at this and I took some of the ideas that Paolo Salvatori blogged about here.
The Problem - Catching Exceptions
Here is the scenario I was working on when I discovered the problem. You can see that I have a BizTalk service. I have exposed this as a WCF service.
During my testing of this service, I wanted to check that it provided the desired response when an error occurred within my itinerary. I found that the response I got contained information that I didn’t want my consumers to receive. For instance, when a map failed, the full reason for failure was available to my consumer. This is pretty much standard BizTalk pipeline behavior. However, what I wanted to do is hide the details of the error from my consumers.
Itinerates 101
Before we continue, I just want to reiterate here that itinerates execute in BizTalk pipelines. So, when an error occurs in you itinerary there is no way to catch the exception and you can’t the do anything with it. To overcome this, we need to configure a hook into the adapter to get to the exception.
The Solution – WCF Error Handler
I took the advice from Paolo and created my own IErrorHandler. In it I take any errors and convert them to a SOAP Fault. I was looking for a generic solution and decided that this was best. From there, I extended Paolo’s idea and added the ability to execute a map in the error handler. Here’s how I got it working.
Step 1 – Create the Error Handler
I’m still using BizTalk 2009, Visual Studio 2008 and the ESB Toolkit 2.0 but I’m sure you can get this to work with 2010 and 2.1 respectively.
So, I have a plain old assembly that contains the following types.
The solution all starts with the ErrorHandlerBehaviorExtensionElement class. This is a configuration element that we will be referencing when we setup our receive port. I have added a custom property to this class so that we can configure an optional map to execute after the exception has been converted into a SOAP Fault.
The Extension Element provides an instance of the ErrorHandler class which implements both IServiceBehavior and IErrorHandler. The error handler will convert the exception into a SOAP Fault by using the ErrorBodyWriter. It will also execute a map if configured.
The ability to execute a map has been provided because the Error Handler is executed in the bowels of the WCF adapter. It is no longer in BizTalk or even a pipeline and, as such, we loose any ability to process the message via an itinerary or orchestration.
Implementing of the IErrorHandler is where the guts of the solution sits. Here is the interface of the IErrorHandler.
For the HandlError method, I just trace the error for debugging purposes. In the ProvideFault method is where we create our custom response message. Here is the code:
public bool HandleError(Exception error)
{
return true;
}
public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
try
{
fault = CreateErrorMessage(fault, "An unexpected error occurred", error);
}
catch (Exception exception)
{
fault = CreateErrorMessage(fault, "An unexpected error occured", error);
}
}
I use a private method called CreateErrorMessage that creates a response message for us. In it, I manually execute a custom BodyWriter to build my fault. I then pass the result from the BodyWriter to a map if needed.
private Message CreateErrorMessage(Message message, string errorMessage, Exception exception)
{
string action = null;
if (message != null &&
message.Headers != null)
action = message.Headers.Action;
string messageType = "http://schemas.xmlsoap.org/soap/envelope/#Fault";
StringBuilder stringBuilder = new StringBuilder();
using (XmlWriter xmlWriter = XmlWriter.Create(stringBuilder))
{
using (XmlDictionaryWriter xmlDictionaryWriter =
XmlDictionaryWriter.CreateDictionaryWriter(xmlWriter))
{
BodyWriter bodyWriter = new ErrorBodyWriter("soap:Receiver",
exception.Message,
action,
errorMessage,
exception);
bodyWriter.WriteBodyContents(xmlDictionaryWriter);
xmlDictionaryWriter.Flush();
}
}
if (!string.IsNullOrEmpty(Map))
{
using (MemoryStream streamIn =
new MemoryStream(Encoding.Unicode.GetBytes(stringBuilder.ToString())))
{
Stream streamOut = TransformStream(streamIn, Map, ref messageType);
using (StreamReader streamReader = new StreamReader(streamOut))
{
stringBuilder = new StringBuilder(streamReader.ReadToEnd());
}
}
}
TextReader textReader = new StringReader(stringBuilder.ToString());
Message replyMessage = Message.CreateMessage(MessageVersion.Default,
messageType,
XmlReader.Create(textReader));
return replyMessage;
}
If we have the details of a map, then the ErrorHandler attempts to execute it. Any errors that happen when executing the map are ignored. We use some of the runtime types that you get with BizTalk to perform the mapping. Here is the method that does this.
private Stream TransformStream(Stream streamIn, string mapName, ref string messageType)
{
try
{
Type type = Type.GetType(mapName);
if (type == null)
throw new Exception(string.Format("The type for {0} was not a valid map type.", mapName));
TransformMetaData transformMetaData = TransformMetaData.For(type);
SchemaMetadata schemaMetadataSource = transformMetaData.SourceSchemas[0];
string schemaName = schemaMetadataSource.SchemaName;
if (!string.Equals(messageType, schemaName, StringComparison.InvariantCultureIgnoreCase))
{
return streamIn;
}
SchemaMetadata schemaMetadataTarget = transformMetaData.TargetSchemas[0];
messageType = schemaMetadataTarget.SchemaName;
XPathDocument input = new XPathDocument(streamIn);
XslTransform transform = transformMetaData.Transform;
Stream streamOut = new MemoryStream();
transform.Transform(input, transformMetaData.ArgumentList, streamOut);
streamOut.Flush();
streamOut.Seek(0L, SeekOrigin.Begin);
return streamOut;
}
catch (Exception exception)
{
return streamIn;
}
}
I need to point out that our BodyWriter doesn’t produce the full SOAP Envelope as the BodyWriter in from the first article in this series does. This is because the error handler is only required to build the message body not the whole message envelope as we needed in part I.
Here is an example of the SOAP Fault that we will be creating:
<Fault SOAP:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/">
<faultcode>soap:Receiver</faultcode>
<faultstring>There was a failure executing the receive pipeline: "Microsoft.Practices.ESB.Itinerary.Pipelines.ItinerarySelectReceiveXml,Microsoft.Practices.ESB.Itinerary.Pipelines, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" Source: "ESB Dispatcher"
Receive Port: "TestReceivePort" URI: "/ESB.ExceptionHandling/ExceptionHandling.svc" Reason: Function 'ScriptNS0:GetCommonValue()' has failed.</faultstring>
<detail>
<ErrorMessage>An unexpected error occured</ErrorMessage>
<Exception>
<Message>There was a failure executing the receive pipeline: "Microsoft.Practices.ESB.Itinerary.Pipelines.ItinerarySelectReceiveXml, Microsoft.Practices.ESB.Itinerary.Pipelines, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" Source: "ESB Dispatcher" Receive Port: "TestReceivePort" URI: "/ESB.ExceptionHandling/ExceptionHandling.svc" Reason: Function 'ScriptNS0:GetCommonValue()' has failed.</Message>
<Type>Microsoft.BizTalk.Message.Interop.BTSException</Type>
</Exception>
</detail>
</Fault>
Step 2 – Deploying The Error Handler
To deploy, just compile the assembly and add it to the GAC. Then make the new behavior available to your service by adding it to the machine.config file like this.
<system.serviceModel>
<extensions>
<behaviorExtensions>
<add name="ESBErrorHandler"
type="ESB.ExceptionHandling.ServiceModel.Configuration.ErrorHandlerBehaviorExtensionElement,
ESB.ExceptionHandling.ServiceModel, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=a496c9267b296339" />
</behaviorExtensions>
</extensions>
</system.serviceModel>
Only adding this to the BizTalk config file means that it won’t be accessible to IIS or the BizTalk console.
Remember to add this to both the 32bit and 64bit machine.config files if you are running a 64bit OS.
Also, remember to restart IIS and all BizTalk hosts to pick up the config file changes.
Step 3 – Creating your BizTalk Application
The next step is to create a BizTalk application called ESB.ExceptionHandling.
I have taken the schema from part I of this series and, have created an example a map that I know will fail. I also use the SOAP to Response map I created in part I. The schema and maps have been deployed to BizTalk and will be executed by my itinerary.
My port configuration includes a WCF receive port called TestReceivePort with a receive location called TestReceiveLocation. The receive location uses the ItinerarySelectReceiveXml pipeline with the following settings:
Property Name
| Value
|
ItineraryFactKey
| Resolver.Itinerary
|
ResolverConnectionString
| ITINERARY:\\name=ESB.ExceptionHandling.Test
|
To make use of the custom error handler, I have used WCF-CustomIsolated for the receive location type. This has been configured to use wsHttpBinding and I have set security to None.
Then I add the custom behavior extension on the behaviors tab.
I configure the error handler to execute my SOAPFault_to_Response map from part I by entering the full type of the map.
ESB.ExceptionHandling.Maps.SOAPFault_to_Response, ESB.ExceptionHandling, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a496c9267b296339
Lastly, I make sure that the error handling includes the fault details.
I published the receive location to IIS using the WCF Publishing wizard and can now use soapUI to consume my service.
The rest is pretty much standard ESB service configuration.
Step 4 – Building your itinerary
Now we are ready to build our itinerary. Here is what mine looks like. It’s basically a pass-through service with a transform before the off-ramp.
The transform will fail because we will use my map that I know will fail. The off-ramp will never be reached so I’m not going to go into its configuration.
Once you have the itinerary, you can deploy it as normal and you’re ready to test.
Step 5 – Testing
As before, I use soapUI to test my service. The result I get when I call my service conforms to my response schema as below. The map generated an error that was converted to a SOAP Fault which, in turn,
was mapped to a response using the map I created in part I.
<ns0:Response xmlns:ns0="http://ESB.ExceptionHandling.ResponseMessage">
<ResponseElement1>An unexpected error occured</ResponseElement1>
<ResponseElement2>There was a failure executing the receive pipeline: "Microsoft.Practices.ESB.Itinerary.Pipelines.ItinerarySelectReceiveXml, Microsoft.Practices.ESB.Itinerary.Pipelines, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" Source: "ESB Dispatcher" Receive Port: "TestReceivePort" URI: "/ESB.ExceptionHandling/ExceptionHandling.svc" Reason: Function 'ScriptNS0:GetCommonValue()' has failed.</ResponseElement2>
<ResponseElement3/>
</ns0:Response></span>
Finally
This series of 3 parts gives you all the tools you need to manage and control the way your services react and create exceptions when consuming and publishing WCF services.
Although I have written these article with the ESB Toolkit in mind, some of the techniques I present can also be used when building services in non-ESB ways.
History
Version 1.0