The past few evenings, I’ve tried to come up with something to shield exceptions in WCF. I am aware that there are quite some posts about this topic already, but I thought I’d share my findings/solution anyway. I’ve ran into a few issues with the approaches I found and learned something doing so…
I wanted this to be simple and yet flexible. The following bullets sum up the base requirements:
- No need to apply
FaultContractAttribute
to all operations - Allow the user to map exceptions to faults (type and content) in a straightforward manner
- No coupling whatsoever, no imposed library-use, exception or fault types
- Simple and selective fallback to standard WCF behavior
Test First
Let’s start with a test that illustrates the idea (or skip to the implementation). The scenario is as follows. We have a simple service contract with two operations, one throws a custom ValidationException
, the other a NotImplementedException
. I have told the exception shielding behavior to shield two types of exceptions:
ValidationException
: should map to a validation fault (including one simple property, but this could be anything, of course)Exception
: should set theExceptionDetail
, basically this comes down toIncludeExceptionDetailInFaults
Here are the types used to test the exception shielding:
[ServiceContract] [ShieldExceptions ( new[] { typeof(ValidationFault), typeof(ExceptionDetail) }, new[] { typeof(ValidationException), typeof(Exception) })] public interface ITestService { [OperationContract] void OperationOne(); [OperationContract] void OperationTwo(); } public class TestService : ITestService { public void OperationOne() { throw new ValidationException() { CustomProperty = "OperationOne finds the request invalid." }; } public void OperationTwo() { throw new System.NotImplementedException(); } } public class ValidationException : Exception { public string CustomProperty { get; set; } } [DataContract] public class ValidationFault { // .ctor maps exception to fault public ValidationFault(ValidationException exception) { FaultProperty = exception.CustomProperty; } [DataMember] public string FaultProperty { get; set; } }
Notice the use of ShieldExceptions
and the fact that there are no FaultContractAttributes
on the operations.
And here is one of the tests (I’ve skipped the rest to reduce bloat) to verify the type of exception thrown in the client thread.
[Test] [ExpectedException(typeof(FaultException<ValidationFault>))] public void Calling_operation_that_throws_shielded_validation_exception_should_throw_corresponding_fault_exception_on_client() { // Arrange var svc = ChannelFactory<ITestService>.CreateChannel(binding, new EndpointAddress(serviceUri)); // Act svc.OperationOne(); }
As the test fixture above illustrates, you simply need to add the attribute ShieldExceptions
with a list of exception types you wish to shield. For every exception you need to specify the corresponding fault (this is checked during validation of the behavior). The order in which you specify exception types should be from most specific to the least; being System.Exception
. If you don’t specify System.Exception
, you’ll get the standard WCF behavior for all exception you didn’t specify.
The mapping of exceptions to faults is simply done by the means of a constructor. The ShieldExceptions behavior will try to create a fault based on the .ctor. First it tries to find the .ctor with the specific exception type, then with a generic exception type and finally it falls back to the default .ctor.
Implementation
There are three parts to this solution: A custom IContractBehavior
implementation, a custom IErrorhandler
and an IClientMessageInspector
.
The ShieldExceptionsAttribute
does a few things. It validates if the specified fault and exception types make sense adds a custom error handler to the ChannelDispatcher
and sets the required ExceptionShieldingMessageInspector
. Besides adding all this behavior the most important thing the ShieldExceptionsAttribute
does is updating the service contract with possible faults. If you check out the generated WSDL, you’ll see that all fault information is included.
public class ShieldExceptionsAttribute : Attribute, IContractBehavior { private readonly Type[] knownFaultTypes; private readonly Type[] exceptionTypes; public ShieldExceptionsAttribute(Type[] knownFaultTypes, Type[] exceptionTypes) { this.knownFaultTypes = knownFaultTypes; this.exceptionTypes = exceptionTypes; } public void AddBindingParameters(ContractDescription contractDescription, ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { foreach (var op in contractDescription.Operations) foreach (var knownFaultType in knownFaultTypes) { // Add fault contract if it is not yet present if (!op.Faults.Any(f => f.DetailType == knownFaultType)) op.Faults.Add(new FaultDescription(knownFaultType.Name) { DetailType = knownFaultType, Name = knownFaultType.Name }); } } public void ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, ClientRuntime clientRuntime) { clientRuntime.MessageInspectors.Add(new ExceptionShieldingMessageInspector(knownFaultTypes)); } public void ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime) { dispatchRuntime.ChannelDispatcher.ErrorHandlers.Add(new ExceptionShieldingErrorHandler(knownFaultTypes, exceptionTypes)); } public void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint) { if(knownFaultTypes.Length != exceptionTypes.Length) throw new ArgumentException("The ShieldExceptions behavior needs a corresponding exception type for each possible fault to shield."); var badType = knownFaultTypes.FirstOrDefault(t => !t.IsDefined(typeof (DataContractAttribute), true)); if(badType != null) throw new ArgumentException(string.Format("The specified fault '{0}' is no data contract. Did you forget to decorate the class with the DataContractAttirbute attribute?", badType)); var badExceptionType = exceptionTypes.FirstOrDefault(t => t != typeof(Exception) && !t.IsSubclassOf(typeof(Exception))); if (badExceptionType != null) throw new ArgumentException(string.Format("The specified type '{0}' is not an Exception-derived type.", badExceptionType)); } }
Now, every time service operation code throws an exception, the ExceptionShieldingErrorHandler
gets called. This handler analyzes the thrown exception and creates the appropriate FaultMessage
that can travel back to the client.
public class ExceptionShieldingErrorHandler : IErrorHandler { private readonly Type[] knownFaultTypes; private readonly Type[] exceptionTypes; public ExceptionShieldingErrorHandler(Type[] knownFaultTypes, Type[] exceptionTypes) { this.knownFaultTypes = knownFaultTypes; this.exceptionTypes = exceptionTypes; } public bool HandleError(Exception error) { return true; // session should not be killed, or should it? } public void ProvideFault(Exception error, MessageVersion version, ref Message fault) { if (error is FaultException) return; var exceptionType = exceptionTypes.FirstOrDefault(t => error.GetType() == t || error.GetType().IsSubclassOf(t)); if(exceptionType != null) { var faultType = knownFaultTypes[Array.IndexOf(exceptionTypes, exceptionType)]; fault = Message.CreateMessage(version, CreateFaultException(faultType, error).CreateMessageFault(), faultType.Name); } } private static FaultException CreateFaultException(Type faultType, Exception exception) { var ctor = faultType.GetConstructor(new[] { exception.GetType() }) // .ctor with specific exception? ?? faultType.GetConstructor(new[] { typeof(Exception) }); // .ctor with generic exception? var detail = ctor != null ? ctor.Invoke(new[] { exception }) : faultType.GetConstructor(Type.EmptyTypes).Invoke(null); // fall back to default .ctor // Create generic fault exception with detail and reason return Activator.CreateInstance(typeof(FaultException<>).MakeGenericType(faultType), detail, "Unhandled exception has been shielded.") as FaultException; } }
The CreateFaultException
makes sure the most appropriate .ctor is called. This allows you to map exceptions to faults just by initializing the fault from the constructor.
The final piece of the puzzle is a client message inspector. The fact that I have a IClientMessageInspector
, is to work around one of the issues I encountered: When you do not specify a FaultContract attribute on the operation(s) in your ServiceContract, you will always get a non-generic (or should I say generic) FaultException
in stead of the expected FaultException<T>, holding the detail. That is, if you use a ChannelFactory
to create your proxies, which is something I tend to do whenever I can.
Generating proxies from the WSDL (without re-using types, that is) does not need this message inspection since the code will be based on the WSDL which contains all information.
Without the explicit FaultContractAttribute
, there’s only one thing we miss: the client proxy doesn’t know about the fault detail. The reply message will contain it, but the channel will simply ignore it. Using this simple message inspector we can easily ‘fake’ the exact same behavior we have with the FaultContractAttribute
: we read the detail ourselves.
public class ExceptionShieldingMessageInspector : IClientMessageInspector { private readonly Type[] knownFaultTypes; public ExceptionShieldingMessageInspector(Type[] knownFaultTypes) { this.knownFaultTypes = knownFaultTypes; } public object BeforeSendRequest(ref Message request, IClientChannel channel) { return null; // no correlation required } public void AfterReceiveReply(ref Message reply, object correlationState) { if (!reply.IsFault) return; var action = reply.Headers.Action; var faultType = knownFaultTypes.FirstOrDefault(t => t.Name == action); if (faultType != null) { var detail = ReadFaultDetail(MessageFault.CreateFault(reply, int.MaxValue), faultType); var exceptionType = typeof(FaultException<>).MakeGenericType(faultType); var faultException = Activator.CreateInstance(exceptionType, detail, "Server exception has been shielded.") as Exception; throw faultException; } } private static object ReadFaultDetail(MessageFault reply, Type faultType) { using (var reader = reply.GetReaderAtDetailContents()) { var serializer = new DataContractSerializer(faultType); return serializer.ReadObject(reader); } } }
