Automatically Converting Exceptions to WCF Faults
One of the annoyances in service design is that you have to dedicate lots of thinking to your error propagation strategy. Any application framework or even utility class should be well-defined with regard to exceptions flowing to the outside world; however, when services are concerned, this is a matter of utmost importance.
The reason for the distinction is the following simple notion: If you're dealing with an object, as the object's client you are very strongly coupled to its exception model. You are well-aware of the fact that exceptions can propagate from the object and you have the facilities for catching those exceptions and acting according. However, if you're dealing with a service, as the service's client you are decoupled from its exception model. In fact, you don't care what it uses internally (be it exceptions, Win32 error codes, HRESULTs or longjmp's), as long as there is a well-defined contract that exposes these errors externally.
In WCF, errors are exposed externally through the use of a fault contract. An operation can specify that certain kinds of faults can escape it - this is similar to an exception specification in C++ or in Java. While it is possible for standard .NET exceptions to cross service boundaries, a fault contract is the cleanest way that can be discovered in advance, as part of the service description.
The problem begins when you want to map .NET exceptions (which are the most straightforward way of dealing with application errors) to WCF faults. This basically means that your top-level service methods have to take the following form:
public void SomeOperation()
{
try
{
DoSomething();
DoSomethingElse();
DoSomethingElseEntirely();
}
catch (InvalidOperationException iopEx)
{
throw new FaultException<InvalidOperationFault>(iopEx.ToString());
}
catch (SecurityException)
{
throw new FaultException<SecurityFault>();
}
catch (Exception ex)
{
throw new FaultException(ex.Message);
}
}
Note that in this particular case, there is a well-defined mapping the application has established between particular types of exceptions to particular types of faults (the InvalidOperationFault and SecurityFault classes were made up for this specific purpose). However, this mapping is not automatic and therefore very error-prone. And you don't want your exception propagation to be error-prone, right?
I was looking for an automatic approach to this scenario, and just like with everything else, WCF has an extensibility point to provide exactly what we're looking for. There are two well-defined ways to provide fault messages corresponding to an exception. The first is through the use of a FaultConverter, which is what you use if you're authoring a custom channel. However, authoring a custom channel (or even binding element) for the sole purpose of translating exceptions to faults is an overkill.
The second approach is implementing the IErrorHandler interface and registering your object to the channel dispatcher's error handlers' collection. This can be done as a service behavior, in about 20 lines of code. Providing the mapping from exceptions to faults is not very difficult either:
public static class ExceptionToFaultConverter
{
private static Dictionary<Type, Delegate> _converters =
new Dictionary<Type, Delegate>();
public static void RegisterConverter<TException, TFault>(
Func<TException, TFault> converter)
{
_converters.Add(typeof(TException), converter);
}
public static object ConvertExceptionToFault(Exception ex)
{
Delegate converter;
if (!_converters.TryGetValue(ex.GetType(), out converter))
return null;
return converter.DynamicInvoke(ex);
}
}
With this simple framework in place, when we have a new exception type to map to a new fault contract, we can go ahead and do it:
ExceptionToFaultConverter.RegisterConverter<
InvalidOperationException, InvalidOperationFault>(
x => new InvalidOperationFault {ErrorDetails=x.ToString()});
We could also filter by the actual operation, the contract name, the fault contracts on the operation, or whatever we want really, because this is what the point of invocation looks like for our extension:
class MyErrorHandler : IErrorHandler
{
public bool HandleError(Exception error)
{
return false;
}
public void ProvideFault(Exception error, MessageVersion version,
ref Message fault)
{
object faultDetail =
ExceptionToFaultConverter.ConvertExceptionToFault(error);
//Find fault contracts for current operation.
OperationContext ctx = OperationContext.Current;
ServiceDescription hostDesc = ctx.Host.Description;
ServiceEndpoint endpoint =
hostDesc.Endpoints.Find(
ctx.IncomingMessageHeaders.To);
string operationName =
ctx.IncomingMessageHeaders.Action.Replace(
endpoint.Contract.Namespace +
endpoint.Contract.Name + "/",
"");
OperationDescription operation =
endpoint.Contract.Operations.Find(operationName);
foreach (FaultDescription faultDesc in operation.Faults)
{
if (faultDesc.DetailType.IsAssignableFrom(
faultDetail.GetType()))
{
//Found a match:
fault = Message.CreateMessage(
version,
FaultCode.CreateSenderFaultCode(
faultDesc.Name, faultDesc.Namespace),
faultDetail.ToString(),
faultDetail,
faultDesc.Action
);
break;
}
}
}
}
To register this error handler, we need a service behavior attached, like the following one:
class ErrorBehaviorAttribute : Attribute, IServiceBehavior
{
Type _handlerType;
public ErrorBehaviorAttribute(Type handlerType)
{
_handlerType = handlerType;
}
#region IServiceBehavior Members
public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
{
}
public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
IErrorHandler errorHandler = (IErrorHandler)
Activator.CreateInstance(_handlerType);
foreach (ChannelDispatcherBase chanDisp in serviceHostBase.ChannelDispatchers)
{
ChannelDispatcher disp = chanDisp as ChannelDispatcher;
disp.ErrorHandlers.Add(errorHandler);
}
}
public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
}
#endregion
}
And finally, we need to specify that behavior on our service:
[ErrorBehavior(typeof(MyErrorHandler))]
class ServiceImpl : IService
This gives us the automatic facilities for mapping .NET exceptions to WCF faults. This is a simplistic sample, but it shows what the possibilities really are.