Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / programming / exceptions

How to Handle Faults in WCF without Declaring Them Explicitly

5.00/5 (5 votes)
16 Mar 2017CPOL2 min read 16.8K   106  
How to handle faults in WCF without declaring them explicitly

Introduction

In WCF, the exception handling between client and server requires a specification of the faults that each service might invoke. This is a good thing because it allows fault declarations inside the WSDL (enhancing the code-generation at client side) and it also works as documentation about validations that can interrupt the execution flow.

However, imagine the scenario where you have a large standalone .NET application with hundreds of services that processes complex business rules. Distributing this application in tiers tracing all possible exceptions that each service might invoke can be challenging as hell.

For this particular scenario, an effective way to handle this problem is to implement a WCF architecture that does not declare the faults explicitly. Obviously, this approach assumes that some other layer is handling the exceptions appropriately. Furthermore, as exceptions will not be specified in the WSDL, developers should consider to move those that travel between the client and server to a shared base project.

Known limitations:

This approach is recommended for scenarios where interoperability is not a requirement. The best practices for service references states that everything (operations and data contracts) that the client needs to invoke a service must be predefined in the WSDL. In this case, exceptions will not be defined in the interface. Thus, it creates a coupling between the client and the server.

Also, I recommend this approach for interprocess distribution (e.g. using Named Pipes) or intranet applications. For security purposes, sending real exceptions to the client in scenarios where the binding is unsafe, the code is not obfuscated and/or services are available via internet is not recommended. You do not want to provide details of your exceptions so easily.

Considering that, we can do the following:

  1. Define a generic fault which encapsulates the real exception thrown by the server-side application.
  2. Create a WCF extension (server-side) that handles exceptions globally, bundles and stores it inside the generic fault.
  3. Unpack the exception (client-side) that comes from the server.

You can also adapt this behavior to bundle not all, but only a specific subset of exceptions (e.g. that inherits a particular class or exceptions defined in one or more specific namespaces).

Define a fault to encapsulate exceptions:

C#
[DataContract]
public class PackedFault
{
   public PackedFault(byte[] serializedFault) { this.Fault = serializedFault; }

   [DataMember]
   public byte[] Fault { get; set; } //Real exception will be stored here.
}

Create a serializer to pack and unpack the exception:

This serializer must be declared in a shared project between the client and server. You can also use the XmlObjectSerializer instead of the BinaryFormatter, but this last one is known to achieve better performance.

C#
public class BinarySerializer
{
   public static byte[] Serialize(object obj)
   {
      using (System.IO.MemoryStream serializationStream = new System.IO.MemoryStream())
      {
         IFormatter formater = new BinaryFormatter();
         formater.Serialize(serializationStream, obj);
         return serializationStream.ToArray();
      }
   }

   public static TObj Deserialize<TObj>(byte[] data)
   {
      using (System.IO.MemoryStream serializationStream = new System.IO.MemoryStream(data, false))
      {
         IFormatter formater = new BinaryFormatter();
         return (TObj)formater.Deserialize(serializationStream);
      }
   }
}

Create an ErrorHandler responsible for centralizing exception handling on the server:

C#
public class ErrorHandler : IErrorHandler
{
   public bool HandleError(Exception error)
   {
      //TODO Register (log) the exception (e-mail, eventviewer, databases, whatever).
      //Because the HandleError method can be called from many different places
      //there are no guarantees made about which thread the method is called on.
      //Do not depend on HandleError method being called on the operation thread.

      return false; //return true if WCF should not abort the session
   }

   public void ProvideFault(Exception error,
   System.ServiceModel.Channels.MessageVersion version, ref System.ServiceModel.Channels.Message fault)
   {
      error.Data.Add("StackTrace", error.StackTrace); //For security purposes,
              //the client should not need to know the server stack trace.
              //But in case you want to preserve that, here is an approach.
      PackedFault pack = new PackedFault(BinarySerializer.Serialize(error));
      FaultException<PackedFault> packedFault = new FaultException<PackedFault>
      (pack, new FaultReason(error.Message), new FaultCode("Sender"));
      fault = Message.CreateMessage(version, packedFault.CreateMessageFault(), packedFault.Action);
   }
}

Create a WCF extension responsible for handling errors:

C#
public class ErrorHandlerServiceBehavior : IServiceBehavior
{
   public void AddBindingParameters(ServiceDescription serviceDescription,
      ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints,
      BindingParameterCollection bindingParameters)
   {
      return;
   }

   public void ApplyDispatchBehavior(ServiceDescription serviceDescription,
      ServiceHostBase serviceHostBase)
   {
      var errorHandler = new ErrorHandler();
      foreach (ChannelDispatcher chanDisp in serviceHostBase.ChannelDispatchers)
         chanDisp.ErrorHandlers.Add(errorHandler);
   }

   /// <summary>
   /// Validate if every OperationContract included in a
   /// ServiceContract related to this behavior declares PackedFault as FaultContract
   /// </summary>
   /// <param name="serviceDescription"/>;
   /// <param name="serviceHostBase"/>;
   public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
   {
      foreach (ServiceEndpoint se in serviceDescription.Endpoints)
      {
         // Must not examine any metadata endpoint.
         if (se.Contract.Name.Equals("IMetadataExchange") &&
         se.Contract.Namespace.Equals("http://schemas.microsoft.com/2006/04/mex"))
            continue;

         foreach (OperationDescription opDesc in se.Contract.Operations)
            if (opDesc.Faults.Count == 0 ||
            !opDesc.Faults.Any(fault => fault.DetailType.Equals(typeof(PackedFault))))
               throw new InvalidOperationException(
                  string.Format("{0} requires a FaultContractAttribute
                  (typeof({1})) in each operation contract. The \"{2}\"
                  operation contains no FaultContractAttribute.",
                  this.GetType().FullName, typeof(PackedFault).FullName, opDesc.Name));
      }
   }
}

Create an ExtensionElement to ease service configuration:

C#
public class ErrorHandlerExtensionElement : BehaviorExtensionElement
{
   public override Type BehaviorType { get { return typeof(ErrorHandlerServiceBehavior); } }
   protected override object CreateBehavior() { return new ErrorHandlerServiceBehavior(); }
}

Declare the extension as a behaviorExtension in the configuration file:

XML
<system.servicemodel>
<extensions>
    <behaviorExtensions>
        <add name="errorHandler" 
        type="WcfService1.ServiceModel.ErrorHandlerExtensionElement, WcfService1" />
    </behaviorExtensions>
</extensions>
</system.servicemodel>

Service (server-side):

C#
[ServiceContract]
public interface IWCFService
{
   [FaultContract(typeof(PackedFault)), OperationContract]
   void ThrowUndeclaredException();
}

public class WCFService : IWCFService
{
   public void ThrowUndeclaredException()
   {
      throw new NotImplementedException();
   }
}

Service invoke (client-side):

C#
try
{
   using (WCFServiceClient client = new WCFServiceClient())
      client.ThrowUndeclaredException();
}
catch (FaultException<PackedFault> e)
{
   Exception exc = BinarySerializer.Deserialize<Exception>(e.Detail.Fault);
   throw exc; //this is the real exception thrown by the server
}

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)