Wednesday, 4 August 2010

Handling custom errors in WCF

[Download Full Solution]

In WCF the preferred way to handle custom error/exception states is through creating an object that can be serialized containing the fault information, and assigning it against an OperationContractAttribute decorated method using the FaultContractAttribute. Assigning a FaultContractAttribute provides an alternate object type to pass back to the client. The default action on the client is to raise a FaultException<MyCustomFault> which allows for the returned fault to be returned in the generic type.



The basic "hello world" application demonstrates purposefully raising the FaultException, and then intercepting it on the client. This is great for small applications, however in larger applications where the service author does not control all the possible exceptions that can be thrown this can become complicated and require wrapping all calls in a try/catch block to ensure that other exceptions aren't raised in the event of an uncaught exception slipping by (which throws a generic FaultException saying something bad happened on the server). This can be addressed on the server side by providing an IErrorHandler implementation which greatly simplifies error management by centralizing it as part of the WCF configuration rather than the service configuration.

To further extend on this we can unwrap (in the case of our example, without the stack trace) the exception that was thrown and allow it to propagate across the client making the WCF bridge nearly transparent to the client. This can have significant benefits when breaking apart an otherwise coupled application without requiring any behavioural changes by the calling/consuming code, or complex per-method conversion code. Our simple example will encode the Type information, and exception message details and hydrate them (where it can) on the client side.

Although this sample has been written in Visual Studio 2010, with a couple modifications it will function under .NET 3.0 (in Visual Studio 2005 if you are in a pinch) as this functionality has been around for quite some time. The following code examples start by defining the basic WCF service, and describing the console client before jumping into the server/client configuration changes necessary, and the gluing code to get this example up and running.

Step one is to setup a basic service and client. Here's the extra three methods added to IService1:

using System.Runtime.Serialization;
using System.ServiceModel;
using CA.NGommans.Wcf.CustomExceptions.Common;

namespace SampleService
{
    [ServiceContract]
    public interface IService1
    {
        #region CustomException demo

        [OperationContract]
        [FaultContract(typeof(CustomFault))]
        string WillThrowArgumentException();
        
        [OperationContract]
        [FaultContract(typeof(CustomFault))]
        string WillThrowSharedException();

        [OperationContract]
        [FaultContract(typeof(CustomFault))]
        string WillThrowSomeException();

        #endregion
    }
}
As you can see from the above we have our FaultContract specified as a CustomFault type. We will explain the CustomFault object a little further on. Below is the implementation for each of the above which will throw our exceptions.

using System;
using CA.NGommans.Wcf.CustomExceptions.Common;
using CA.NGommans.Wcf.CustomExceptions.Service;

namespace SampleService
{
    public class Service1 : IService1
    {
        #region CustomException demo

        public string WillThrowArgumentException()
        {
            throw new ArgumentException("Something bad happened.");
        }

        public string WillThrowSharedException()
        {
            throw new SharedException("A shared exception - something bad happened.");
        }

        public string WillThrowSomeException()
        {
            throw new SomeException("Some exception - something bad happened.");
        }

        #endregion
    }
}
We define "SomeException" in our Service project which is just an exception with the no parameter and string message parameter constructors directing to the base implementation provided by Exception.

In our shared library between the service and client we need to include our "Shared" exception, and of course our fault contract.

using System;
using System.Runtime.Serialization;

namespace CA.NGommans.Wcf.CustomExceptions.Common
{
    /// 
    /// Custom fault class for transporting exception data over the wire
    /// 
    [DataContract(Namespace = CustomFaultNamespace)]
    [Serializable]
    public class CustomFault
    {
        /// 
        /// Custom fault namespace
        /// 
        public const string CustomFaultNamespace = "http://CA.NGommans.Wcf.CustomExceptions/CustomFault";

        /// 
        /// Type of exception thrown
        /// 
        [DataMember]
        public string ExceptionType { get; set; }

        /// 
        /// Exception message
        /// 
        [DataMember]
        public string Message { get; set; }

        /// 
        /// Create a new custom fault. Setting message and exception type will need to be handled
        /// 
        public CustomFault()
        {
        }

        /// 
        /// Creates a new custom fault. Extracts message and type from the provided exception
        /// 
        /// Exception to use         public CustomFault(Exception exception)         {             if (exception != null)             {                 ExceptionType = exception.GetType().AssemblyQualifiedName;                 Message = exception.Message;             }         }     }      ///      /// Our special SharedException type which can be accessed from both     /// server and client for the purposes of this demonstration     ///      public class SharedException : Exception     {         public SharedException() : base() { }         public SharedException(string message) : base(message) { }     } } 
The last little bit is our client, which is rather basic and calls all three messages (excluded here for simplicity). Each method is then wrapped in a try/catch block looking specifically for the type of exception that we want to have thrown which - in order of their definitions above are ArgumentException, SharedException, FaultException<CustomFault>. Now if your application consists of the three projects, with code as above you will get FaultExceptions for all three calls indicating that something bad happened on the server. Naturally, we want to have our exceptions pass through to the client with a bit more information, especially if we have some custom exception types that should be presented. Now let's fill in the middle-ground which requires the creation of a new behaviour set. First we need a BehaviorExtensionElement:
using System;
using System.ServiceModel.Configuration;

namespace CA.NGommans.Wcf.CustomExceptions.Common.Dispatch
{
    /// <summary>
    /// Exception behavior element for attaching exception behavior to WCF services and clients
    /// </summary>
    public class ExceptionBehaviorElement : BehaviorExtensionElement
    {
        /// <summary>
        /// Type of behavior to expose (<see cref="ExceptionBehavior"/>)
        /// </summary>
        public override Type BehaviorType
        {
            get { return typeof(ExceptionBehavior); }
        }

        /// <summary>
        /// Create an instance of our behavior
        /// </summary>
        /// <returns>Returns a new <see cref="ExceptionBehavior"/></returns>
        protected override object CreateBehavior()
        {
            return new ExceptionBehavior();
        }
    }
}
With the above we can now create our "ExceptionBehavior" as per below. Note the code has been compressed to include only altered contracts. All other required interface methods are completely blank.
using System.Collections.ObjectModel;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;

namespace CA.NGommans.Wcf.CustomExceptions.Common.Dispatch
{
    /// <summary>
    /// Server/Client behavior which will catch/encode exceptions, and decode them (less the stack) on the client side.
    /// </summary>
    internal class ExceptionBehavior : IEndpointBehavior, IContractBehavior, IServiceBehavior
    {
        void IEndpointBehavior.ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime runtime)
        {
            // Target for endpoint behaviors is WCF clients only
            this.ApplyClientBehavior(runtime);
        }

        void IContractBehavior.ApplyClientBehavior(ContractDescription contract, ServiceEndpoint endpoint, ClientRuntime runtime)
        {
            // Target for contract behaviors is WCF clients only
            this.ApplyClientBehavior(runtime);
        }

        void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription description, ServiceHostBase host)
        {
            // Target for service behaviors is our server side error handler
            ApplyExceptionBehavior(host);
        }

        /// <summary>
        /// Client message inspector which will track an exception and throw it instead of the FaultException class.
        /// </summary>
        /// <param name="runtime">WCF client runtime</param>
        private void ApplyClientBehavior(ClientRuntime runtime)
        {
            // Don’t add a message inspector if it already exists
            foreach (IClientMessageInspector inspector in runtime.MessageInspectors)
            {
                if (inspector is ExceptionMessageInspector)
                {
                    return;
                }
            }
            runtime.MessageInspectors.Add(new ExceptionMessageInspector());
        }

        /// <summary>
        /// Server exception behavior which will trap thrown exceptions and encode them into our <see cref="CustomFault"/>
        /// </summary>
        /// <param name="host">WCF service host</param>
        private void ApplyExceptionBehavior(ServiceHostBase host)
        {
            // Ensure we only add this once per channel
            foreach (ChannelDispatcher dispatcher in host.ChannelDispatchers)
            {
                bool addErrorHandler = true;
                foreach (IErrorHandler handler in dispatcher.ErrorHandlers)
                {
                    if (handler is ExceptionHandler)
                    {
                        addErrorHandler = false;
                        break;
                    }
                }

                if (addErrorHandler)
                {
                    dispatcher.ErrorHandlers.Add(new ExceptionHandler());
                }
            }
        }
    }
}
Now in order to satisfy the above we need the real bits of the puzzle now: our IErrorHandler, and our IClientMessageInspector:
using System;
using System.Reflection;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
using System.Xml;

namespace CA.NGommans.Wcf.CustomExceptions.Common.Dispatch
{
    /// <summary>
    /// Server side exception handler. This class will intercept exceptions and setup a message which
    /// encodes the
    /// </summary>
    internal class ExceptionHandler : IErrorHandler
    {
        #region IErrorHandler Members

        /// <summary>
        /// Since we are just serializing the type and the message we can support any type.
        /// </summary>
        /// <param name="exception">Exception to encode</param>
        /// <returns>Always returns true since we always encode the exception</returns>
        public bool HandleError(Exception exception)
        {
            return true;
        }

        /// <summary>
        /// Generate a fault
        /// </summary>
        /// <param name="exception">Exception to encode</param>
        /// <param name="version">Message version</param>
        /// <param name="message">Message reference (returned fault)</param>
        public void ProvideFault(Exception exception, MessageVersion version, ref Message message)
        {
            // Wraps the exception type and message into our "CustomFault" before raising it to the client
            FaultException<CustomFault> faultexception = new FaultException<CustomFault>(new CustomFault(exception));
            MessageFault fault = faultexception.CreateMessageFault();
            message = Message.CreateMessage(version, fault, CustomFault.CustomFaultNamespace);
        }

        #endregion
    }

    /// <summary>
    /// Attached to a client this message inspector will intercept fault exceptions and rethrow those of
    /// type <see cref="CustomFault"/> as their unwrapped exception (less the stack trace).
    /// </summary>
    internal class ExceptionMessageInspector : IClientMessageInspector
    {
        #region IClientMessageInspector Members

        /// <summary>
        /// Intercept faulted replies and where they are of type "CustomFault" unwrap and raise
        /// the underlying exception (if known)
        /// </summary>
        /// <param name="reply">Message from server</param>
        /// <param name="correlationState">request/response state object</param>
        public void AfterReceiveReply(ref Message reply, object correlationState)
        {
            if (reply.IsFault)
            {
                // Copy message just in case we don't want to throw an exception
                MessageBuffer buffer = reply.CreateBufferedCopy(Int32.MaxValue);
                Message copy = buffer.CreateMessage();
                reply = buffer.CreateMessage();

                MessageFault fault = MessageFault.CreateFault(copy, int.MaxValue);
                if (fault.HasDetail)
                {
                    XmlDictionaryReader reader = fault.GetReaderAtDetailContents();
                    if (reader.Name == "CustomFault") // Although this works it's not ideal
                    {
                        CustomFault customFault = fault.GetDetail<CustomFault>();
                        if( customFault != null)
                        {
                            Exception result = null;

                            // Exception type must be a valid type string...
                            if( !string.IsNullOrEmpty(customFault.ExceptionType))
                            {
                                try
                                {
                                    // Try to get a constructor we can use which needs to have one parameter for "message"
                                    Type exceptionType = Type.GetType(customFault.ExceptionType);
                                    if (exceptionType != null)
                                    {
                                        ConstructorInfo ci = exceptionType.GetConstructor(new Type[] { typeof(string) });
                                        result = ci.Invoke(new object[] { customFault.Message }) as Exception;
                                    }
                                }
                                catch(Exception)
                                {
                                    // Do nothing
                                }
                            }

                            // Raise the exception if we could decode it
                            // Instead, you could raise a general exception if the type could not be resolved
                            if( result  != null )
                            {
                                throw result;
                            }
                        }
                    }
                }
            }
        }

        public object BeforeSendRequest(ref Message request, IClientChannel channel)
        {
            return null;
        }
        #endregion
    }
}
The above two pieces handle interception of exceptions, and the resulting translation of FaultException<CustomFault> back into the "real" exception class that was thrown (if possible). Now in order to implement either of these we need to extend both our service, and clients do utilize the above components. This can be achieved through the following examples which include the added behaviorExtension, and resulting "encodeExceptions" elements in both examples. Note that the client also requires that you define the behaviorConfiguration to use against the endpoint. Server:
<?xml version="1.0"?>
<configuration>
  <system.serviceModel>
    <!-- Add extension for our shared exception management behavior -->
    <extensions>
      <behaviorExtensions>
        <add name="encodeExceptions" type="CA.NGommans.Wcf.CustomExceptions.Common.Dispatch.ExceptionBehaviorElement, Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
      </behaviorExtensions>
    </extensions>
    <behaviors>
      <serviceBehaviors>
        <behavior>
...
          <!-- Exception encoding behavior -->
          <encodeExceptions />
        </behavior>
      </serviceBehaviors>
    </behaviors>
...
  </system.serviceModel>  
</configuration>
Client
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.serviceModel>
      <!-- Add extension for our shared exception management behavior -->
      <extensions>
        <behaviorExtensions>
          <add name="encodeExceptions" type="CA.NGommans.Wcf.CustomExceptions.Common.Dispatch.ExceptionBehaviorElement, Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
        </behaviorExtensions>
      </extensions>
...
        <client>
            <endpoint address="http://localhost:1236/Service1.svc" binding="basicHttpBinding"
                bindingConfiguration="BasicHttpBinding_IService1" contract="ServiceReference1.IService1"
                name="BasicHttpBinding_IService1" behaviorConfiguration="exceptions" />
        </client>
...
      <behaviors>
        <endpointBehaviors>
          <behavior name="exceptions">
            <encodeExceptions />
          </behavior>
        </endpointBehaviors>
      </behaviors>
    </system.serviceModel>
</configuration>
With the above in place, you can now have the exceptions fully translated. This is a very generic solution and can work for most exception types. The best solution however is to expand on this to provide better customization around exception management to transport any application specific exceptions over the wire with useful information to help developers in understanding what went wrong as it is not always clear from the message text. The full example is attached at the very top of the article (and could certainly save a bit of cut/paste if you are trying to get it up and running).

No comments:

Post a Comment