Tuesday, December 22, 2009

Convert Exceptions in WCF Services to Fault Exceptions a Client Can Use

This article will show you how to trap an exception that occurs within a WCF service but also inform the client application of the exception so it can be handled.  To do this we will make use of a custom faultException.

1. You need to create a fault contract for the type of fault that you want to be returned to the client via WCF.  Below I have created a ‘DBConcurrencyFault’ fault that tells the client when a database concurrency violation has occurred (e.g. 2 people tried to save the same record).

namespace Common.Services.Faults
{
    [DataContract]
    public class DBConcurrencyFault
    {
        [DataMember]
        public String Message { get; set; }

        [DataMember]
        public String ExceptionType { get; set; }
    }
}

2. Now we are going to create a new attribute that we can use to decorate our service classes with.  This attribute will trap any exception occurring in a WCF service and convert them into a specific fault exception.

namespace Common.Services
{
    public class WcfErrorHandler : Attribute, IErrorHandler, IServiceBehavior
    {
        public bool HandleError(Exception error)
        {
            return false;
        }
       
        public void ProvideFault(Exception error, System.ServiceModel.Channels.MessageVersion version, ref System.ServiceModel.Channels.Message fault)
        {
            if (fault != null)
                return;

            if (error.GetType() == typeof(System.Data.DBConcurrencyException)) 
            {
                // Handle Database record being written to by different users (Conncurrency Fault)
                DBConcurrencyFault databaseConcurrencyFault = new DBConcurrencyFault();
                databaseConcurrencyFault.Message = error.Message;
                databaseConcurrencyFault.ExceptionType = error.GetType().ToString();
                String faultReason = "Someone else has already saved this record.";
                FaultException<DBConcurrencyFault> faultException = new FaultException<DBConcurrencyFault>(databaseConcurrencyFault, faultReason);
                MessageFault messageFault = faultException.CreateMessageFault();
                fault = Message.CreateMessage(version, messageFault, faultException.Action);
            }
            else
            {
                // Handle all other errors as standard FaultExceptions
                FaultException faultException = new FaultException(error.Message);
                MessageFault messageFault = faultException.CreateMessageFault();
                fault = Message.CreateMessage(version, messageFault, faultException.Action);
            }
        }

        #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)
        {
            foreach (ChannelDispatcher disp in serviceHostBase.ChannelDispatchers)
            {
                disp.ErrorHandlers.Add(this);
            }
        }

        public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
        {           
        }
        #endregion
    }
}

Note: the ‘ProvideFault’ method contains code that determines if an exception of type ‘System.Data.DBConcurrencyException’ has occurred.  If one has it is converted into a ‘DBConcurrencyFault’ which is then returned to the calling client by the WCF service. 

3. Now you can either add the WcfErrorHandler attribute to all your wcf services, or you can as I have below define a base service class that all your service classes will inherit from.

namespace Common.Services
{
   [WcfErrorHandler()]
   public class BaseService
   {
       // Common service methods go here
   }
}

4. Have your service class inherit from the base service class. 

public class MileageClaimService : BaseService, IMileageClaimService
{
      // Service methods go here
}

5. Trap the fault exception from a web client and redirect the user to a tailored error page.
   5a.  For an Asp.Net web application you can add the exception handling code to the global.asax Application_Error method.

protected void Application_Error(object sender, EventArgs e) 

     Exception ex = Server.GetLastError().GetBaseException(); 

     if (ex.GetType() == typeof(System.ServiceModel.FaultException<Common.Services.Faults.DBConcurrencyFault>) ) 
     { 
        Server.ClearError(); 
        Response.Redirect("~/Errors/ErrorDbConcurrency.aspx"); 
     } 
}

  5b. From an Asp.Net MVC web application you just add a HandleError attribute to the controller with the type of DBConcurrencyFault and redirect to a specific view.

[HandleError(ExceptionType = typeof(System.ServiceModel.FaultException<Common.Services.Faults.DBConcurrencyFault>),
View = "ErrorDbConcurrency")]
[HandleError]
public class ClaimController : Controller
{
    // Controller methods go here
}

Note: There are 2 HandleError attributes, the first handles the DBConcurrencyFault and redirects to a custom error page.  The 2nd traps all other exceptions and redirects to the default error page.  The order of these 2 HandleError attributes is important.

1 comment:

  1. Thanks for the information! I couldn't figure out how to get the Server.GetLastError() to work with FaultException. You saved me a lot of time and headaches!

    ReplyDelete