Custom Error Behavior by implementing IErrorHandler at WCF Service

Today, I am going to show how to handle errors (handled or unhandled by WCF service) at the application level. This is very similar to the way error is handled in IHttpModule (or Global.asax) in ASP.Net. This way you can control the format of the FaultException.

First, let’s take a look at how operation contract can be defined with FaultContract attribute with a custom type ErrorInformation.

[OperationContract]
[FaultContract(typeof(ErrorInformation))]
long Add(long value1, long value2);

To explicitly control the behavior of the application when an exception is thrown, implement the IErrorHandler interface and add it to the Dispatcher’s ErrorHandlers property. IErrorHandler enables you to explicitly control the SOAP fault generated, decide whether to send it back to the client, and perform associated tasks, such as logging. Error handlers are called in the order in which they were added to the ErrorHandlers property. This is a direct quote from Microsoft.com.

So, let us create a CustomErrorBehavior class that implements IErrorHandler. Next, we need to hook this behavior to the ChannelDispatcher. We are going to add this CustomErrorBehavior to our existing ServiceBehavior. Of course, you need to use the ServiceBehavior as an attribute to the service implementation class. If you prefer to attach the ServiceBehavior via configuration, you will need to implement ServiceBehaviorExtensionElement.

Here is the quick 1-2-3-4 of what we are going to need to make the Error Behavior work-

1) Create a CustomErrorBehavior class that implements IErrorHandler.

2) All the supporting classes needed to support CustomErrorBehavior.

3) ServiceBehavior to attach the CustomErrorBehavior to DispatchRuntime. Then, you can simply add the ServiceBehavior as an attribute to the service implementation class.

4) If you prefer to attach the ServiceBehavior through configuration, you will need to implement ServiceBehaviorExtensionElement.

So, let’s jump on to the codes-

CustomErrorBehavior.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Dispatcher;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Globalization;

namespace www.aspnet4you.com
{
	internal class CustomErrorBehavior : IErrorHandler
	{
		public bool HandleError(Exception error)
		{
			return true;
		}

		public void ProvideFault(Exception error, System.ServiceModel.Channels.MessageVersion version, ref System.ServiceModel.Channels.Message fault)
		{
			FaultException<ErrorInformation> excFault = null;
			try
			{
				if (error is FaultException<ErrorInformation>)
				{
					excFault = (FaultException<ErrorInformation>)error;
				}
				else
				{
					ErrorInformation errInfo = new ErrorInformation();
					errInfo.Message = error.Message;
					errInfo.ErrorDetails = error.ToString();
					excFault = new FaultException<ErrorInformation>(errInfo, new FaultReason(errInfo.Reason.ToString()));
				}

				fault = Message.CreateMessage(version, excFault.Code, excFault.Reason.GetMatchingTranslation(CultureInfo.InvariantCulture).Text, excFault.Detail, excFault.Action);

			}
			catch (Exception exc)
			{
				try
				{
					ErrorInformation errInError = new ErrorInformation();
					errInError.ErrorSeverity ="Error";
					errInError.Reason = "Unhandled Exception in CustomErrorBehavior.";
					errInError.Message = exc.Message;
					errInError.ErrorDetails = exc.ToString();
				}
				catch { }
			}
		}
	}
}

ErrorInformation.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization;
using System.Xml.Serialization;
using System.IO;

namespace www.aspnet4you.com
{
	/// <summary>
	/// This class encapsulates all the information / data for the fault contracts.
	/// This information is used for error logging or tracking purposes.
	/// </summary>
    [Serializable, XmlRoot(Namespace = "http://www.aspnet4you.com/services")]
	public class ErrorInformation : ISerializable
	{
		private string m_Reason = "UnKnown"; //Define your own reason codes.
		private string m_Message = "An unexpected error occured while executing the service method.";
		private string m_ErrorDetails = string.Empty;
		private string m_ErrorSeverity = "Error"; //Define your own severity enum.


		/// <summary>
		/// Stacktrace for the original exception.
		/// </summary>
		public string ErrorDetails
		{
			get { return m_ErrorDetails; }
			set { m_ErrorDetails = value; }
		}

		/// <summary>
		/// Exception severity raised by service implementor. Default is ERROR.
		/// </summary>
		public string ErrorSeverity
		{
			get { return m_ErrorSeverity; }
			set { m_ErrorSeverity = value; }
		}

		/// <summary>
		/// Errorcode for the given exception.
		/// </summary>
		public string Reason
		{
			get { return m_Reason; }
			set { m_Reason = value; }
		}

		/// <summary>
		/// Error Message for the given exception or error.
		/// </summary>
		public string Message
		{
			get { return m_Message; }
			set { m_Message = value; }
		}
		
		/// <summary>
		/// Returns a string version of the object, with all the properties appended.
		/// </summary>
		/// <returns></returns>
		public override string ToString()
		{
            return SerializationHelper.Serialize<ErrorInformation>(this);
		}

		/// <summary>
		/// Non-default constructor for setting properties during instantiation.
		/// </summary>
		/// <param name="msg"></param>
		/// <param name="rsn"></param>
		/// <param name="errDetails"></param>
		/// <param name="severity"></param>
		public ErrorInformation(string msg, string rsn, string errDetails, string severity)
		{
			this.Message = msg;
			this.Reason = rsn;
			this.ErrorDetails = errDetails;
			this.ErrorSeverity = severity;
		}

		/// <summary>
		/// Default constructor for the ErrorInformation class.
		/// </summary>
		public ErrorInformation()
		{
		}

		/// <summary>
		/// Constructor with SerializationInfo and StreamingContext for custom serialization. This
		/// is very helpful for interoperatibility. This setp eleminates the Backing Field generation by
		/// the .net framework.
		/// </summary>
		/// <param name="info">SerializationInfo</param>
		/// <param name="context">StreamingContext</param>
		public ErrorInformation(SerializationInfo info, StreamingContext context):this(new CustomSerializationInfo(info), context)
		{
			
		}

		/// <summary>
        /// Constructor with CustomSerializationInfo and StreamingContext for custom serialization. This
		/// is useful when an element is not present in SerializationInfo (cause serializer
		/// to break with SerializationException) and we try to get it's value. Clients are not
		/// required to pass the optional elements and the serialization should not break.
		/// </summary>
        /// <param name="info">CustomSerializationInfo</param>
		/// <param name="context">StreamingContext</param>
        private ErrorInformation(CustomSerializationInfo info, StreamingContext context)
		{
			ErrorDetails = info.Get<string>("ErrorDetails");
			ErrorSeverity = info.Get<string>("ErrorSeverity");
			Reason = info.Get<string>("Reason");
			Message = info.Get<string>("Message");
		}

		/// <summary>
		/// Populates a SerializationInfo with the data needed to serialize the target object.
		/// </summary>
		/// <param name="info">SerializationInfo</param>
		/// <param name="context">StreamingContext</param>
		public void GetObjectData(SerializationInfo info, StreamingContext context)
		{
			info.AddValue("ErrorDetails", ErrorDetails);
			info.AddValue("ErrorSeverity", ErrorSeverity);
			info.AddValue("Reason", Reason);
			info.AddValue("Message", Message);
		}
	}
}

CustomSerializationInfo.cs:

using System.Runtime.Serialization;

namespace www.aspnet4you.com
{
	public class CustomSerializationInfo
	{
		private SerializationInfo info = null;

        public CustomSerializationInfo(SerializationInfo info)
		{
			this.info = info;
		}


		public T Get<T>(string Name)
		{
			T result = default(T);

			try
			{
				result = (T)this.info.GetValue(Name, typeof(T));
			}
			catch (SerializationException)
			{
				result = default(T);
			}

			return result;
		}

		public SerializationInfo GetSerializationInfo()
		{
			return this.info;
		}

	}
}

SerializationHelper.cs:

using System;
using System.Xml.Serialization;
using System.Text;
using System.Xml;
using System.IO;

namespace www.aspnet4you.com
{
	public class SerializationHelper
	{
        /// <summary>
        /// Generic serializer with UTF-16
        /// </summary>
        /// <typeparam name="T">Type</typeparam>
        /// <param name="obj">Object to be serialized</param>
        /// <returns></returns>
        public static string Serialize<T>(T obj)
        {
            if (obj == null)
            {
                return null;
            }

            XmlSerializer serializer = new XmlSerializer(typeof(T));
            using (StringWriter writer = new StringWriter())
            {
                serializer.Serialize(writer, obj);
                return writer.ToString();
            }
        }

        /// <summary>
        /// Generic deserializer with UTF-16
        /// </summary>
        /// <typeparam name="T">Type</typeparam>
        /// <param name="obj">Object to be deserialized</param>
        /// <returns></returns>
        public static T Deserialize<T>(string stringXml)
        {
            if (string.IsNullOrEmpty(stringXml) == true)
            {
                return default(T);
            }

            XmlSerializer serializer = new XmlSerializer(typeof(T));
            using (StringReader reader = new StringReader(stringXml))
            {
                return (T)serializer.Deserialize(reader);
            }
        }
	}
}

ServiceBehavior.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.ServiceModel;

namespace www.aspnet4you.com
{
	public class ServiceBehavior: Attribute, IServiceBehavior
	{
		public ServiceBehavior()
        {   
        }
		
		#region IServiceBehavior Members

		public void AddBindingParameters(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
		{
			
		}

		public void ApplyDispatchBehavior(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
		{
			foreach (ChannelDispatcherBase cdb in serviceHostBase.ChannelDispatchers)
			{
				ChannelDispatcher cd = cdb as ChannelDispatcher;

				if (cd != null)
				{
                    cd.ErrorHandlers.Add(new CustomErrorBehavior());
				}
			}
		}

		public void Validate(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
		{
			
		}

		#endregion IServiceBehavior Members
	}
}

ServiceBehaviorExtensionElement.cs (optional):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Configuration;

namespace www.aspnet4you.com
{
	public class ServiceBehaviorExtensionElement : BehaviorExtensionElement
	{
		#region BehaviorExtensionElement Optimized

		protected override object CreateBehavior()
		{
			return new ServiceBehavior();
		}

		public override Type BehaviorType
		{
			get { return typeof(ServiceBehavior); }
		}

		#endregion BehaviorExtensionElement Optimized

	}
}

Leave a Reply