Wednesday 21 March 2012

Using an IErrorHandler IServiceBehavior in WCF

It is sometimes useful to be able to customise the error handling behaviour of a WCF web service, I have found myself having to do this on a number of occasions, it is easy to do but it is also easy to run foul of little gotchas such as what is logged when, so I am posting a very basic solution here as a starting point for future reference.


Generally this is used to log all exceptions and to explicitly convert exceptions to faults, the simple solution here only uses the basic FaultException, but can be easily extended to use FaultException<T>.


WCF provides the IErrorHandler interface for our implementation of this functionality, here is what I have found to work (note the delegates/Actions are not necesarry - I just prefer not to have to modify the ErrorHandler for new projects):

public class ErrorHandler : IErrorHandler
{
    private readonly Action<exception> LogException;
    private readonly Action<message> LogFault;

    public ErrorHandler(Action<exception> logException, Action<message> logFault)
    {
        LogException = logException;
        LogFault = logFault;
    }

    public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
    {
        if (error is FaultException) // Thrown by WCF - eg request deserialization problems, could also be explicitly thrown in code
        {
            LogFault(fault);
            return;
        }

        var faultCode = new FaultCode("UnknownFault");
        if (error is ArgumentOutOfRangeException)
        {
            faultCode = new FaultCode("ArgumentOutOfRange");
        }

        var action = OperationContext.Current.IncomingMessageHeaders.Action;
        fault = Message.CreateMessage(version, faultCode, error.Message, action);
        LogFault(fault);
    }

    public bool HandleError(Exception error)
    {
        // Logging of exceptions should occur here as all exceptions will hit HandleError, but some will not hit ProvideFault
        LogException(error);

        return false; // false allows other handlers to be called - if none return true the dispatcher aborts any session and aborts the InstanceContext if the InstanceContextMode is anything other than Single.
    }
}


Note that the error handler alone is not enough, we also need to create a  IServiceBehavior  that will add the  IErrorHandler :


public class ErrorHandlerBehavior : IServiceBehavior
{
    private readonly IErrorHandler handler;
    public ErrorHandlerBehavior(IErrorHandler errorHandler)
    {
        this.handler = errorHandler;
    }

    public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
    { }
    
    public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<serviceendpoint> endpoints, BindingParameterCollection bindingParameters)
    { }

    public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
    {
        foreach(ChannelDispatcher dispatcher in serviceHostBase.ChannelDispatchers)
        {
            dispatcher.ErrorHandlers.Add(handler);
        }
    }
}

The behaviour can then be added either through the web.config file or programatically to the System.ServiceModel.ServiceHost.Description.Behaviors.