Quantcast
Channel: typesafe » WCF
Viewing all articles
Browse latest Browse all 7

Flexible WCF Exception Shielding

$
0
0

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 the ExceptionDetail, basically this comes down to IncludeExceptionDetailInFaults

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);
		}
	}
}


Viewing all articles
Browse latest Browse all 7

Trending Articles