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:
- Define a generic fault which encapsulates the real exception thrown by the server-side application.
- Create a WCF extension (server-side) that handles exceptions globally, bundles and stores it inside the generic fault.
- 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:
[DataContract]
public class PackedFault
{
public PackedFault(byte[] serializedFault) { this.Fault = serializedFault; }
[DataMember]
public byte[] Fault { get; set; }
}
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.
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:
public class ErrorHandler : IErrorHandler
{
public bool HandleError(Exception error)
{
return false;
}
public void ProvideFault(Exception error,
System.ServiceModel.Channels.MessageVersion version, ref System.ServiceModel.Channels.Message fault)
{
error.Data.Add("StackTrace", error.StackTrace);
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:
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);
}
public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
foreach (ServiceEndpoint se in serviceDescription.Endpoints)
{
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:
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:
<system.servicemodel>
<extensions>
<behaviorExtensions>
<add name="errorHandler"
type="WcfService1.ServiceModel.ErrorHandlerExtensionElement, WcfService1" />
</behaviorExtensions>
</extensions>
</system.servicemodel>
Service (server-side):
[ServiceContract]
public interface IWCFService
{
[FaultContract(typeof(PackedFault)), OperationContract]
void ThrowUndeclaredException();
}
public class WCFService : IWCFService
{
public void ThrowUndeclaredException()
{
throw new NotImplementedException();
}
}
Service invoke (client-side):
try
{
using (WCFServiceClient client = new WCFServiceClient())
client.ThrowUndeclaredException();
}
catch (FaultException<PackedFault> e)
{
Exception exc = BinarySerializer.Deserialize<Exception>(e.Detail.Fault);
throw exc;
}