The past couple of days I've been struggling setting up proper exception handling in WCF and Silverlight. In WCF I wanted to handle exceptions in 1 place rather than having to try/catch in every single service method. I was able to accomplish this using behaviors and the IErrorHandler:

Here's the WCF code:

Services/Models/Constants.cs:

public class Constants
    {
        public const string FaultAction = "//ErrorHandler/FaultAction">http://ErrorHandler/FaultAction";
    }

 

Services/Interfaces/ICategoryService.cs: (sample WCF service interface)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using CouponJoe.Schemas;
using CouponJoe.Schemas.Results;

namespace CouponJoe.Services.Interfaces
{
    [ServiceContract]
    public interface ICategoryService
    {
        [OperationContract]
        [FaultContract(typeof(string), Action = Models.Constants.FaultAction)] 
        Category Add(Category category);
        [OperationContract]
        [FaultContract(typeof(string), Action = Models.Constants.FaultAction)] 
        Category Update(Category category);
        [OperationContract]
        [FaultContract(typeof(string), Action = Models.Constants.FaultAction)] 
        DeleteResult Delete(Category category);
        [OperationContract]
        [FaultContract(typeof(string), Action = Models.Constants.FaultAction)] 
        Category Get(int categoryId);
        [OperationContract]
        [FaultContract(typeof(string), Action = Models.Constants.FaultAction)] 
        Category GetByName(string categoryName);
        [OperationContract]
        [FaultContract(typeof(string), Action = Models.Constants.FaultAction)] 
        List<Category> Search();
    }
}

Services/Behaviors/ErrorHandler.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using CouponJoe.Services.Models;

namespace CouponJoe.Services.Behaviors
{
    public class ErrorHandler : ErrorHandlerBehavior, IErrorHandler, IServiceBehavior
    {

        public bool HandleError(Exception error)
        {
            //TODO: log exception.
            return true;
        }
        
        public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
        {
            string errorMessage = String.Empty;
            if (error.InnerException == null)
            {
                errorMessage = String.Format("{0}: {1}", error.GetType().FullName, error.Message);
               
            }
            else
            {
                errorMessage = String.Format("{0}: {1}", error.InnerException.GetType().FullName, error.InnerException.Message);
            }
            FaultException<string> faultException = new FaultException<string>(errorMessage, new FaultReason(errorMessage));
            MessageFault messageFault = faultException.CreateMessageFault();
            fault = Message.CreateMessage(version, messageFault, Models.Constants.FaultAction);
        }

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

        public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> serviceEndPoints, BindingParameterCollection bindingParameters)
        {
            return;
        }

        public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
        {
            //do nothing.
        }

    }
}

Services/Behaviors/ErrorHandlerBehavior.cs:

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

namespace CouponJoe.Services.Behaviors
{
    public class ErrorHandlerBehavior : BehaviorExtensionElement
    {
        public override Type BehaviorType
        {
            get
            {
                return typeof(ErrorHandler);
            }
        }

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

    }
}

Services/Behaviors/SilverlightFaultBehavior.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;

namespace CouponJoe.Services.Behaviors
{
    public class SilverlightFaultBehavior : BehaviorExtensionElement, IEndpointBehavior
    {

        public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
        {
            SilverlightFaultMessageInspector inspector = new SilverlightFaultMessageInspector();
            endpointDispatcher.DispatchRuntime.MessageInspectors.Add(inspector);
        }

        public class SilverlightFaultMessageInspector : IDispatchMessageInspector
        {
            public void BeforeSendReply(ref Message reply, object correlationState)
            {
                if (reply.IsFault)
                {
                    HttpResponseMessageProperty property = new HttpResponseMessageProperty();
                    // Here the response code is changed to 200.
                    property.StatusCode = System.Net.HttpStatusCode.OK;
                    reply.Properties[HttpResponseMessageProperty.Name] = property;
                }
            }

            public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
            {
                // Do nothing to the incoming message.
                return null;
            }
        }

        // The following methods are stubs and not relevant.

        public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
        {
        }

        public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
        {
        }

        public void Validate(ServiceEndpoint endpoint)
        {
        }

        public override System.Type BehaviorType
        {
            get { return typeof(SilverlightFaultBehavior); }
        }

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

    }

}

Services/Web.config:


<system.serviceModel>
    <extensions>
      <behaviorExtensions>
        <add name="ExceptionHandlingBehavior"
             type="CouponJoe.Services.Behaviors.ErrorHandlerBehavior, CouponJoe.Services, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
        <add name="SilverlightFaultBehavior"
             type="CouponJoe.Services.Behaviors.SilverlightFaultBehavior, CouponJoe.Services, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
      </behaviorExtensions>
    </extensions>
    <bindings>
      <basicHttpBinding>
        <binding name="basicBinding">
        </binding>
      </basicHttpBinding>
    </bindings>
    <services>
      <service behaviorConfiguration="CouponJoe.Services.ServiceBehavior" name="CouponJoe.Services.CategoryService">
        <endpoint address="" binding="basicHttpBinding" bindingConfiguration="basicBinding"
                  behaviorConfiguration="SilverlightFaultEndPointBehavior" contract="CouponJoe.Services.Interfaces.ICategoryService">
          <identity>
            <dns value="localhost" />
          </identity>
        </endpoint>
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
      </service>
    </services>
    <behaviors>
      <endpointBehaviors>
        <behavior name="SilverlightFaultEndPointBehavior">
          <SilverlightFaultBehavior/>
        </behavior>
      </endpointBehaviors>
      <serviceBehaviors>
        <behavior name="CouponJoe.Services.ServiceBehavior">
          <serviceMetadata httpGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="true" />
          <ExceptionHandlingBehavior/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>

We're doing 2 things here... First, we're adding an Exception Handling behavior into the WCF pipeline (using ErrorHandler.cs and ErrorHandlerBehavior.cs), which will pick up all Exceptions thrown in the WCF services. We can then package them up in FaultExceptions and send them down to the client. You'll see in the web.config how we plugged it in.

For a normal client this would be the end of it, but Silverlight is "special". Silverlight doesn't grab the true exception because when the service faults, it returns a message other than 200 ("OK"). So the second step is plugging in the Silverlight Fault behavior into the WCF pipeline, which modifies the message to still be 200 ("OK") even when an exception was thrown.

So now that we're getting a nice fault exception passed to SL, what do we do with it? If we're using SL2, the answer is "not much". SL2 has half-baked FaultException calsses that aren't much good. However, SL3 has resolved this. Yeah, but SL3 is beta, right? Yes it is, but there's a delivery date of July 10th so you may be able to rationalize upgrading.

After upgrading to SL3, you can then handle your exceptions globally in the code-behind of App.xaml:

private void Application_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e)
        {
            //what kind of exception have we caught?
            string errorMessage = String.Empty;
            if (e.ExceptionObject.InnerException != null && e.ExceptionObject.InnerException is FaultException)
            {//wcf exception.
                FaultException exc = e.ExceptionObject.InnerException as FaultException;
                errorMessage = exc.Reason.ToString();
            }
            else
            {//silverlight exception.
                errorMessage = e.ExceptionObject.Message;
                //TODO: log exception.
            }
            //handle exception so app doesn't crash.
            e.Handled = true;
            //show js error.
            Deployment.Current.Dispatcher.BeginInvoke(delegate { ReportErrorToDOM(e); });
            //redirect to error page.
            Navigation.Navigate(Pages.ERROR, errorMessage);
        }

We have accomplished a few things here. You now have global exception handling in both WCF and Silverlight rather than having to add try/catch blocks around everything. You are receiving the true WCF error message from your Silverlight app rather thangeneric communication faults, which can really help debugging, as otherwise you'll most likely have to debug the service while running your Silverlight app in order to figure out what's going on. 

Note: please don't do this in a production app, this is only useful for debugging purposes...