One of the things that the .Net compiler won’t warn developers about, is when another developer decides to add a new FaultException type and the client code isn’t updated to handle this new type of exception. The solution I’m demonstrating here is a generic solution to check for this, but implies that the client is going through a ChannelFactory and not a ClientBase implementation.
ChannelFactory implementations are usually better if there’s full ownership, in the institution, of service and clients. The share of the service contracts will allow Continuous Integration builds to fail if there was a breaking change made on the service that broke one or more of the consuming clients. You may argue that ChannelFactory implementations have the issue that if you change the service, with a non-breaking change, you need to re-test and re-deploy all your clients code: This isn’t exactly true, as if it is a non-breaking change, all the clients will continue to work even with a re-deploy of the service.
Default ChannelFactory Wrapper
The generic implementation depends on our default WcfService wrapper for a ChannelFactory. This could be abstracted through an interface that had the Channel getter on it, and make the generic method depend on the interface instead of the actual implementation.
I will provide here a simple implementation of the ChannelFactory wrapper:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class WcfService<T> : IDisposable where T : class | |
{ | |
private readonly object _lockObject = new object(); | |
private bool _disposed; | |
private ChannelFactory<T> _factory; | |
private T _channel; | |
internal WcfService() | |
{ | |
_disposed = false; | |
} | |
internal virtual T Channel | |
{ | |
get | |
{ | |
if (_disposed) | |
{ | |
throw new ObjectDisposedException("Resource WcfService<" + typeof(T) + "> has been disposed"); | |
} | |
lock (_lockObject) | |
{ | |
if (_factory == null) | |
{ | |
_factory = new ChannelFactory<T>("*"); // First qualifying endpoint from the config file | |
_channel = _factory.CreateChannel(); | |
} | |
} | |
return _channel; | |
} | |
} | |
public void Dispose() | |
{ | |
Dispose(true); | |
GC.SuppressFinalize(this); | |
} | |
internal void Dispose(bool disposing) | |
{ | |
if (_disposed) | |
{ | |
return; | |
} | |
if (!disposing) | |
{ | |
return; | |
} | |
lock (_lockObject) | |
{ | |
if (_channel != null) | |
{ | |
try | |
{ | |
((IClientChannel)_channel).Close(); | |
} | |
catch (Exception) | |
{ | |
((IClientChannel)_channel).Abort(); | |
} | |
} | |
if (_factory != null) | |
{ | |
try | |
{ | |
_factory.Close(); | |
} | |
catch (Exception) | |
{ | |
_factory.Abort(); | |
} | |
} | |
_channel = null; | |
_factory = null; | |
_disposed = true; | |
} | |
} | |
} |
Example of a client using the Wrapper
Here’s an example of code that we want to test, for a client that’s using the WcfService wrappe. The separation from the method that creates the WcfService wrapped in a using clause and the internal static one is just for testing purposes, just so we can inject a WcfService mock and assert against it. The client successfully wraps a FaultException into something meaningful for the consuming application.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class DocumentClient : IDocumentService | |
{ | |
public string InsertDocument(string documentClass, string filePath) | |
{ | |
using (var service = new WcfService<IDocumentService>()) | |
{ | |
return InsertDocument(documentClass, filePath, service); | |
} | |
} | |
internal static string InsertDocument(string documentClass, string filePath, WcfService<IDocumentService> service) | |
{ | |
try | |
{ | |
return service.Channel.InsertDocument(documentClass, filePath); | |
} | |
catch (FaultException<CALFault> ex) | |
{ | |
throw new DocumentCALException(ex); | |
} | |
catch (Exception ex) | |
{ | |
throw new ServiceUnavailableException(ex.Message, ex); | |
} | |
} | |
} |
The generic Fault contract checker
This implementation is using Moq as the Mocking framework and the code is dependent on it. It also provides signatures up to 4 exceptions that are expected, this is done with a Top-Down approach, where the signature with the most type parameters has the full implementation and the others just call the one that’s one higher level in the signature chain. To support this mind set, a special empty DummyException is declared to fill the gaps between Type Parameters in the different signatures.
Breaking down the code, what it is doing is creating a dynamic Expression Tree that we can wire in the Setup method of the client mock that will intercept calls with any type of parameter (It.IsAny). Then for each FaultContractAttribute that is decorating the service, instantiate it and wire everything so that the service method is setup to throw it. Finally call it, and check if it was caught and wrapped or if we are getting the original FaultException back.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static class ContractCheckerExtension | |
{ | |
public static string CheckFaultContractMapping<TContract, TEx1>(this MethodInfo method, Action<Mock<WcfService<TContract>>> action) | |
where TContract : class | |
where TEx1 : Exception | |
{ | |
return method.CheckFaultContractMapping<TContract, TEx1, DummyException>(action); | |
} | |
public static string CheckFaultContractMapping<TContract, TEx1, TEx2>(this MethodInfo method, Action<Mock<WcfService<TContract>>> action) | |
where TContract : class | |
where TEx1 : Exception | |
where TEx2 : Exception | |
{ | |
return method.CheckFaultContractMapping<TContract, TEx1, TEx2, DummyException>(action); | |
} | |
public static string CheckFaultContractMapping<TContract, TEx1, TEx2, TEx3>(this MethodInfo method, Action<Mock<WcfService<TContract>>> action) | |
where TContract : class | |
where TEx1 : Exception | |
where TEx2 : Exception | |
where TEx3 : Exception | |
{ | |
return method.CheckFaultContractMapping<TContract, TEx1, TEx2, TEx3, DummyException>(action); | |
} | |
public static string CheckFaultContractMapping<TContract, TEx1, TEx2, TEx3, TEx4>(this MethodInfo method, Action<Mock<WcfService<TContract>>> action) | |
where TContract : class | |
where TEx1 : Exception | |
where TEx2 : Exception | |
where TEx3 : Exception | |
where TEx4 : Exception | |
{ | |
// we're creating a lambda on the fly that will call on the target method | |
// with all parameters set to It.IsAny<[the type of the param]>. | |
var lambda = Expression.Lambda<Action<TContract>>( | |
Expression.Call( | |
Expression.Parameter(typeof (TContract)), | |
method, | |
CreateAnyParameters(method)), | |
Expression.Parameter(typeof (TContract))); | |
// for all the fault contract attributes that are decorating the method | |
foreach (var faultAttr in method.GetCustomAttributes(typeof(FaultContractAttribute), false).Cast<FaultContractAttribute>()) | |
{ | |
// create the specific exception that get's thrown by the fault contract | |
var faultDetail = Activator.CreateInstance(faultAttr.DetailType); | |
var faultExceptionType = typeof(FaultException<>).MakeGenericType(new[] { faultAttr.DetailType }); | |
var exception = (FaultException)Activator.CreateInstance(faultExceptionType, faultDetail); | |
// mock the WCF pipeline objects, channel and client | |
var mockChannel = new Mock<WcfService<TContract>>(); | |
var mockClient = new Mock<TContract>(); | |
// set the mocks | |
mockChannel.Setup(x => x.Channel) | |
.Returns(mockClient.Object); | |
mockClient.Setup(lambda) | |
.Throws(exception); | |
try | |
{ | |
// invoke the client, wrapped in an Action delegate | |
action(mockChannel); | |
} | |
catch (Exception ex) | |
{ | |
// if we get a targeted exception it's because the fault isn't being handled | |
// and we return with the type of the fault contract detail type that was caught | |
if (ex is TEx1 || ex is TEx2 || ex is TEx3 || ex is TEx4) | |
return faultAttr.DetailType.FullName; | |
// else soak all other exceptions because we are expecting them | |
} | |
} | |
return null; | |
} | |
private static IEnumerable<Expression> CreateAnyParameters(MethodInfo method) | |
{ | |
return method.GetParameters() | |
.Select(p => typeof (It).GetMethod("IsAny").MakeGenericMethod(p.ParameterType)) | |
.Select(a => Expression.Call(null, a)); | |
} | |
} | |
[Serializable] | |
public class DummyException : Exception | |
{ | |
} |
Here’s a sample of a unit test using the ContractChecker for the example client showed previously in the post:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[TestMethod] | |
public void Ensure_InsertDocument_FaultContracts_AreAllMapped() | |
{ | |
var targetOperation = typeof (IDocumentService).GetMethod( | |
"InsertDocument", | |
new[] | |
{ | |
typeof (string), | |
typeof (string) | |
}); | |
var result = targetOperation.CheckFaultContractMapping<IDocumentService, ServiceUnavailableException>( | |
m => DocumentClient.InsertDocument(string.Empty, string.Empty, m.Object)); | |
Assert.IsNull(result, "The type {0} used to detail a FaultContract isn't being properly handled on the Service client", result); | |
} |