Exceptions, Faults, and IErrorHandler
Every so often, someone posts a question about handling Faults in the MSDN WCF Forums (http://forums.microsoft.com/MSDN/ShowForum.aspx?ForumID=118&SiteID=1). The question follows the lines of “I get the whole FaultException<T>/FaultContractAttribute ‘thing’, but sometimes I just like System.Exception-derived classes over custom Faults. What should I do?” The reason is frequently “because my business logic doesn’t know about Faults, and it shouldn’t”. This is perfectly correct and the right way to think. So, what should this person do? My favorite response for this is to use a behavior plus an IErrorHandler to make things ‘just work’. What we are going to do is enhance the set of faults that we understand. This complete technique ONLY works well with proxies generated via svcutil or similar methods. My technique here will fail when used when sharing the ServiceContract definition between the message sender and receiver.
The first thing I do is define an IContractBehavior. This behavior will have two jobs:
1. Modify every OperationDescription on a given ContractDescription to add a new Fault. I do this little bit of work in the behavior’s Validate method (your last chance to update any Contract information!). This information will then be published to any generated metadata. This method will only work on a running service—generating a proxy via assembly reflection won’t tell the complete story L
2. Add an IErrorHandler to the endpoint’s ChannelDispatcher.ErrorHandlers collection. Here, the custom IErrorHandler will get a chance to turn ArgumentNullExceptions and its brethren into SOAP Faults.
Step 1 is accomplished by the contract behavior:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)]
class MyContractBehaviorAttribute : Attribute, IContractBehavior
{
public void Validate(ContractDescription contractDescription,
ServiceEndpoint endpoint)
{
foreach (OperationDescription od in contractDescription.Operations)
{
FaultDescription fd = new FaultDescription(
MyErrorHandler.ScottSeelyExceptionAction);
fd.DetailType = typeof (string);
fd.Name = MyErrorHandler.ScottSeelyExceptionName;
fd.Namespace = MyErrorHandler.ScottSeelyExceptionNamespace;
od.Faults.Add(fd);
}
}
public void ApplyDispatchBehavior(ContractDescription contractDescription,
ServiceEndpoint endpoint,
DispatchRuntime dispatchRuntime)
{
dispatchRuntime.ChannelDispatcher.ErrorHandlers.Add(
new MyErrorHandler());
}
public void ApplyClientBehavior(ContractDescription contractDescription,
ServiceEndpoint endpoint,
ClientRuntime clientRuntime)
{
}
public void AddBindingParameters(ContractDescription contractDescription,
ServiceEndpoint endpoint,
BindingParameterCollection bindingParameters)
{
}
}
Step 2 then looks like this:
class MyErrorHandler : IErrorHandler
{
internal const string ScottSeelyExceptionAction =
"http://scottseely.com/ScottSeelyFault";
internal const string ScottSeelyExceptionName =
"ScottSeelyFault";
internal const string ScottSeelyExceptionNamespace =
"http://scottseely.com/ScottSeelyFault/2007/08";
public bool HandleError(Exception error)
{
return typeof(ScottSeelyException).IsAssignableFrom(error.GetType());
}
public void ProvideFault(Exception error, MessageVersion version,
ref Message fault)
{
if (HandleError(error))
{
ScottSeelyException exception = (ScottSeelyException)error;
fault = Message.CreateMessage(version, FaultCode.
CreateReceiverFaultCode(ScottSeelyExceptionName,
ScottSeelyExceptionNamespace),
exception.Message, error.Message, ScottSeelyExceptionAction);
}
}
}
Of course, you can extend this to handle more types of exceptions and do other smart things, but I think you get the picture. And, since this is an implementation detail and not something the administrator will be deciding, I then decorate my ServiceContract like so:
[ServiceContract]
[MyContractBehaviorAttribute]
interface ISayHello
{
...
}