WCF enables developers to customize the default exception reporting and propagation, and even provide for a hook for custom logging. This extensibility is applied per channel dispatcher, although you are more than likely to simply utilize it across all dispatchers.
To install your own error-handling extension, you need to provide the dispatchers with an implementation of the IErrorHandler
interface defined as:
public interface IErrorHandler { bool HandleError(Exception error); void ProvideFault(Exception error,MessageVersion version,ref Message fault); }
Any party can provide this implementation, but typically it will be provided either by the service itself or by the host. In fact, you can have multiple error-handling extensions chained together. You will see later in this section just how to install the extensions.
The ProvideFault( )
method of the extension object is called immediately after any exception is thrown by the service or any object on the call chain down from a service operation. WCF calls ProvideFault( )
before returning control to the client and before terminating the session (if present) and before disposing of the service instance (if required). Because ProvideFault( )
is called on the incoming call thread while the client it still blocked waiting for the operation to complete, you should avoid lengthy execution inside ProvideFault( )
.
ProvideFault( )
is called regardless of the type of exception thrown, be it a regular CLR exception, a fault, or a fault in the fault contract. The error
parameter is a reference to the exception just thrown. If ProvideFault( )
does nothing, the client will get an exception according to the fault contract (if any) and the exception type being thrown, as discussed previously in this chapter:
class MyErrorHandler : IErrorHandler { public bool HandleError(Exception error) {...} public void ProvideFault(Exception error,MessageVersion version, ref Message fault) { //Nothing here - exception will go up as usual } }
However, ProvideFault( )
can examine the error
parameter and either return it to the client as is, or ProvideFault( )
can provide an alternative fault. This alternative behavior will affect even exceptions that are in the fault contract. To provide an alternative fault, you need to use the CreateMessageFault( )
method of FaultException
to create an alternative fault message. If you are providing a new fault contract message, you must create a new detailing object, and you cannot reuse the original error
reference. You then provide the created fault message to the static CreateMessage( )
method of the Message
class:
public abstract class Message { public static Message CreateMessage(MessageVersion version, MessageFault fault,string action); //More members }
Note that you need to provide CreateMessage( )
with the action of the fault message used. This intricate sequence is demonstrated in Example 6-11.
Example 6-11. Creating an alternative fault
class MyErrorHandler : IErrorHandler { public bool HandleError(Exception error) {...} public void ProvideFault(Exception error,MessageVersion version, ref Message fault) { FaultException<int> faultException = new FaultException<int>(3); MessageFault messageFault =faultException.CreateMessageFault
( ); fault = Message.CreateMessage
(version,messageFault,faultException.Action); } }
In Example 6-11, the ProvideFault( )
method provides FaultException<int>
with a value of 3 as the fault thrown by the service, irrespective of the actual exception that was thrown.
The implementation of ProvideFault( )
can also set the fault
parameter to null
:
class MyErrorHandler : IErrorHandler
{
public bool HandleError(Exception error)
{...}
public void ProvideFault(Exception error,MessageVersion version,
ref Message fault)
{
fault =null
;//Suppress any faults in contract
}
}
Doing so will result with all exceptions propagated to the client as a FaultException
, even if the exceptions were according to the fault contract. Setting fault
to null
is an effective way of suppressing any fault contract that may be in place.
One possible use for ProvideFault( )
is a technique I call exception promotion. The service may use downstream objects. The objects could be called by a variety of services. In the interest of decoupling, these objects may very well be unaware of the particular fault contracts of the service calling them. In case of errors, the objects simply throw regular CLR exceptions. What the service could do is use an error-handling extension to examine the exception thrown. If that exception is of the type T
, where FaultException<T>
is part of the operation fault contract, the service could promote that exception to a full-fledged FaultException<T>
. For example, given this service contract:
interface IMyContract
{
[OperationContract]
[FaultContract(typeof(InvalidOperationException
))]
void MyMethod( );
}
if the downstream object throws an InvalidOperationException
, ProvideFault( )
will promote it to FaultException<InvalidOperationException>
, as shown in Example 6-12.
Example 6-12. Exception promotion
class MyErrorHandler : IErrorHandler
{
public bool HandleError(Exception error)
{...}
public void ProvideFault(Exception error,MessageVersion version,
ref Message fault)
{
if(error is InvalidOperationException)
{
FaultException<InvalidOperationException> faultException =
new FaultException<InvalidOperationException>(
new InvalidOperationException(error.Message));
MessageFault messageFault =faultException.CreateMessageFault( );
fault = Message.CreateMessage(version,messageFault,faultException.Action);
}
}
}
The problem with Example 6-12 is that the code is coupled to a specific fault contract, and it requires a lot of tedious work across all services to implement it, not to mention that any change to the fault contract will necessitate a change to the error extension.
Fortunately, you can automate exception promotion using my ErrorHandlerHelper
static class:
public static class ErrorHandlerHelper { public static void PromoteException(Type serviceType, Exception error, MessageVersion version, ref Message fault); //More members }
The ErrorHandlerHelper.PromoteException( )
requires the service type as a parameter. It will then use reflection to examine all the interfaces and operations on that service type, looking for fault contracts for the particular operation. It gets the faulted operation by parsing the error
object. PromoteException( )
will promote a CLR exception to a contracted fault if the exception type matches any one of the detailing types defined in the fault contracts for that operation.
Using ErrorHandlerHelper
, Example 6-12 can be reduced to one or two lines of code:
class MyErrorHandler : IErrorHandler { public bool HandleError(Exception error) {...} public void ProvideFault(Exception error,MessageVersion version, ref Message fault) { Type serviceType = ...; ErrorHandlerHelper.PromoteException(serviceType,error,version,ref fault); } }
The implementation of PromoteException( )
has little to do with WCF and as such is not listed in this chapter. Instead you can examine it as part of the source code available with this book. The implementation makes use of some advanced C# programming techniques such as generics and reflection, string parsing, anonymous methods, and late binding.
The HandleError( )
method of IErrorHandler
is defined as:
bool HandleError(Exception error);
HandleError( )
is called by WCF after control returns to the client. HandleError( )
is strictly for service-side use, and nothing it does affects the client in any way. HandleError( )
is called on a separate worker thread, not the thread that was used to process the service request (and the call to ProvideFault( )
). Having a separate thread used in the background enables you to perform lengthy processing, such as logging to a database without impeding the client.
Because you could have multiple error-handling extensions installed in a list, WCF also enables you to control whether extensions down the list should be used. If HandleError( )
returns false
, then WCF will continue to call HandleError( )
on the rest of the installed extensions. If HandleError( )
returns true
, WCF stops invoking the error-handling extensions. Obviously, most extensions should return false
.
The error
parameter of HandleError( )
is the original exception thrown. The classic use for HandleError( )
is logging and tracing, as shown in Example 6-13.
Example 6-13. Logging the error log to a logbook service
class MyErrorHandler : IErrorHandler { public bool HandleError(Exception error) { try { LogbookServiceClient proxy = new LogbookServiceClient( ); proxy.Log(...); proxy.Close( ); } catch {} finally { return false; } } public void ProvideFault(Exception error,MessageVersion version, ref Message fault) {...} }
The source code available with this book contains a standalone service called LogbookService
, dedicated to error logging. LogbookService
logs the errors into a SQL Server database. The service contract also provides operations for retrieving the entries in the logbook and clearing the logbook. The source code also contains a simple logbook viewer and management tool. In addition to error logging, LogbookService
allows you to log entries explicitly into the logbook independently of exceptions. The architecture of this framework is depicted in Figure 6-1.
You can automate error logging to LogbookService
using the LogError( )
method of my ErrorHandlerHelper
static class:
public static class ErrorHandlerHelper { public static void LogError(Exception error); //More members }
The error
parameter is simply the exception you wish to log. LogError( )
encapsulates the call to LogbookService
. For example, instead of Example 6-13, you can simply write a single line:
class MyErrorHandler : IErrorHandler
{
public bool HandleError(Exception error)
{ErrorHandlerHelper.LogError(error);
return false;
}
public void ProvideFault(Exception error,MessageVersion version,
ref Message fault)
{...}
}
In addition to the raw exception information, LogError( )
performs extensive parsing of the exception and other environment variables for a comprehensive record of the error and its related information.
Specifically, LogError( )
captures the following information:
Where the exception occurred (machine name and host process name)
The code where the exception took place (the assembly name, the filename, and the line number)
The type where the exception took place and the member being accessed
The date and time of the exception
The exception name and message
Implementing LogError( )
has little to do with WCF and therefore is not shown in this chapter. The code, however, makes extensive use of interesting .NET programming techniques such as string and exception parsing, along with obtaining the environment information. The error information is passed in a dedicated data contract to LogbookService
.
Every channel dispatcher in WCF offers a collection of error extensions:
public class ChannelDispatcher : ChannelDispatcherBase { public Collection<IErrorHandler> ErrorHandlers {get;} //More members }
Installing your own custom implementation of IErrorHandler
requires merely adding it to the desired dispatcher (usually all of them).
You must add the error extensions before the first call arrives to the service and yet after the collection of dispatchers is constructed by the host. This narrow window of opportunity exists after the host is initialized but not yet opened. To act in that window, the best solution is to treat error extensions as custom service behaviors, because the behaviors are given the opportunity to interact with the dispatchers at just the right time. As mentioned in Chapter 4, all service behaviors implement the IServiceBehavior
interface defined as:
public interface IServiceBehavior { void AddBindingParameters(ServiceDescription description, ServiceHostBase host, Collection<ServiceEndpoint> endpoints, BindingParameterCollection parameters); voidApplyDispatchBehavior
(ServiceDescription description,ServiceHostBase host
); void Validate(ServiceDescription description,ServiceHostBase host); }
The ApplyDispatchBehavior( )
method is your cue to add the error extension to the dispatchers. You can safely ignore all other methods of IServiceBehavior
and provide an empty implementation.
In ApplyDispatchBehavior( )
you need to access the collection of dispatchers available in the ChannelDispatchers
property of ServiceHostBase
:
public class ChannelDispatcherCollection : SynchronizedCollection<ChannelDispatcherBase> {} public abstract class ServiceHostBase : ... { public ChannelDispatcherCollection ChannelDispatchers {get;} //More members }
Each item in ChannelDispatchers
is of the type ChannelDispatcher
. You can add the implementation of IErrorHandler
to all dispatchers or just add it to specific dispatchers associated with a particular binding. Example 6-14 demonstrates adding an implementation of IErrorHandler
to all dispatchers of a service.
Example 6-14. Adding an error extension object
class MyErrorHandler : IErrorHandler
{...}
class MyService : IMyContract,IServiceBehavior
{
public void ApplyDispatchBehavior(ServiceDescription description,
ServiceHostBase host)
{
IErrorHandler handler = new MyErrorHandler( );
foreach(ChannelDispatcher dispatcher in host.ChannelDispatchers)
{dispatcher.ErrorHandlers.Add(handler);
}
}
public void Validate(...)
{}
public void AddBindingParameters(...)
{}
}
In Example 6-14, the service itself implements IServiceBehavior
. In ApplyDispatchBehavior( )
, the service obtains the dispatchers collection and adds an instance of the MyErrorHandler
class to each dispatcher. Instead of relying on an external class to implement IErrorHandler
, the service class itself can support IErrorHandler
directly, as shown in Example 6-15.
Example 6-15. Supporting IErrorHandler by the service class
class MyService : IMyContract,IServiceBehavior,IErrorHandler
{ public void ApplyDispatchBehavior(ServiceDescription description, ServiceHostBase host) { foreach(ChannelDispatcher dispatcher in host.ChannelDispatchers) { dispatcher.ErrorHandlers.Add(this
); } } public bool HandleError(Exception error) {...} public void ProvideFault(Exception error,MessageVersion version, ref Message fault) {...} //More members }
The problem both with Examples 6-14 and 6-15 is that they pollute the service class code with WCF plumbing. Instead of having the service focus on the business logic, it also has to wire up error extensions. Fortunately, you can provide the same plumbing declaratively using my ErrorHandlerBehaviorAttribute
, defined as:
public class ErrorHandlerBehaviorAttribute : Attribute,IErrorHandler, IServiceBehavior { protected Type ServiceType {get;set;} }
Applying the ErrorHandlerBehavior
attribute is straightforward:
[ErrorHandlerBehavior] class MyService : IMyContract {...}
The attribute installs itself as the error-handling extension. Its implementation uses ErrorHandlerHelper
to both automatically promote exceptions to fault contracts if required, and to automatically log the exception to LogbookService
. Example 6-16 lists the code for the ErrorHandlerBehavior
attribute.
Example 6-16. The ErrorHandlerBehavior attribute
[AttributeUsage(AttributeTargets.Class)]
public class ErrorHandlerBehaviorAttribute : Attribute,IServiceBehavior,
IErrorHandler
{
protected Type ServiceType
{get;set;}
void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription description,
ServiceHostBase host)
{
ServiceType = description.ServiceType;
foreach(ChannelDispatcher dispatcher in host.ChannelDispatchers)
{
dispatcher.ErrorHandlers.Add(this
);
}
}
bool IErrorHandler.HandleError(Exception error)
{
ErrorHandlerHelper.LogError(error);
return false;
}
void IErrorHandler.ProvideFault(Exception error,MessageVersion version,
ref Message fault)
{
ErrorHandlerHelper.PromoteException(ServiceType,error,version,ref fault);
}
void IServiceBehavior.Validate(...)
{}
void IServiceBehavior.AddBindingParameters(...)
{}
}
Note in Example 6-16 that ApplyDispatchBehavior( )
saves the service type in a protected property. The reason is that the call to ErrorHandlerHelper.PromoteException( )
in ProvideFault( )
requires the service type.
While the ErrorHandlerBehavior
attribute greatly simplifies the act of installing an error extension, it does require the service developer to apply the attribute. It would be nice if the host could add error extensions independently of whether or not the service provides one. However, due to the narrow timing window of installing the extension, having the host add such an extension requires multiple steps. First, you need to provide an error-handling extension type that supports both IServiceBehavior
and IErrorHandler
. The implementation of IServiceBehavior
will add the error extension to the dispatchers as shown previously. Next, derive a custom host class from ServiceHost
and override the OnOpening( )
method defined by the CommunicationObject
base class:
public abstract class CommunicationObject : ICommunicationObject
{
protected virtual void OnOpening( );
//More memebrs
}
public abstract class ServiceHostBase : CommunicationObject ,...
{...}
public classServiceHost
: ServiceHostBase,...
{...}
In OnOpening( )
you need to add the custom error-handling type to the collection of service behaviors in the service description. That behaviors collection was described in Chapters 1 and 4:
public class Collection<T> : IList<T>,...
{public void Add(T item);
//More members
}
public abstract class KeyedCollection<K,T> : Collection<T>
{...}
public class KeyedByTypeCollection<I> : KeyedCollection<Type,I>
{...}
public class ServiceDescription
{
public KeyedByTypeCollection<IServiceBehavior> Behaviors
{get;}
}
public abstract class ServiceHostBase : ...
{
public ServiceDescription Description
{get;}
//More members
}
This sequence of steps is already encapsulated and automated in ServiceHost<T>
:
public class ServiceHost<T> : ServiceHost { public void AddErrorHandler(IErrorHandler errorHandler); public void AddErrorHandler( ); //More members }
ServiceHost<T>
offers two overloaded versions of the AddErrorHandler( )
method. The one that takes an IErrorHandler
object will internally associate it with a behavior so that you could provide it with any class that supports just IErrorHandler
, not IServiceBehavior
:
class MyService : IMyContract {...} class MyErrorHandler : IErrorHandler {...} SerivceHost<MyService> host = new SerivceHost<MyService>( ); host.AddErrorHandler(new MyErrorHandler( )); host.Open( );
The AddErrorHandler( )
method that takes no parameters will install an error-handling extension that uses ErrorHandlerHelper
, just as if the service class was decorated with the ErrorHandlerBehavior
attribute:
class MyService : IMyContract {...} SerivceHost<MyService> host = new SerivceHost<MyService>( ); host.AddErrorHandler( ); host.Open( );
Actually, for this last example, ServiceHost<T>
does use internally an instance of the ErrorHandlerBehaviorAttribute
.
Example 6-17 shows the implementation of the AddErrorHandler( )
method.
Example 6-17. Implementing AddErrorHandler( )
public class ServiceHost<T> : ServiceHost { class ErrorHandlerBehavior : IServiceBehavior,IErrorHandler { IErrorHandler m_ErrorHandler; public ErrorHandlerBehavior(IErrorHandler errorHandler) { m_ErrorHandler = errorHandler; } void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription description, ServiceHostBase host) { foreach(ChannelDispatcher dispatcher in host.ChannelDispatchers) { dispatcher.ErrorHandlers.Add(this); } } bool IErrorHandler.HandleError(Exception error) { return m_ErrorHandler.HandleError(error); } void IErrorHandler.ProvideFault(Exception error,MessageVersion version, ref Message fault) { m_ErrorHandler.ProvideFault(error,version,ref fault); } //Rest of the implementation } List<IServiceBehavior> m_ErrorHandlers = new List<IServiceBehavior>( ); public void AddErrorHandler(IErrorHandler errorHandler) { if(State == CommunicationState.Opened) { throw new InvalidOperationException("Host is already opened"); } IServiceBehavior errorHandler = new ErrorHandlerBehavior(errorHandler); m_ErrorHandlers.Add(errorHandlerBehavior); } public void AddErrorHandler( ) { if(State == CommunicationState.Opened) { throw new InvalidOperationException("Host is already opened"); } IServiceBehavior errorHandler = new ErrorHandlerBehaviorAttribute( ); m_ErrorHandlers.Add(errorHandlerBehavior); } protected override void OnOpening( ) { foreach(IServiceBehavior behavior in m_ErrorHandlers) { Description.Behaviors.Add(behavior); } base.OnOpening( ); } //Rest of the implementation }
To avoid forcing the provided IErrorHandler
reference to also support IServiceBehavior
, ServiceHost<T>
defines a private nested class called ErrorHandlerBehavior
. ErrorHandlerBehavior
implements both IErrorHandler
and IServiceBehavior
. To construct ErrorHandlerBehavior
, you need to provide it with an implementation of IErrorHandler
. That implementation is saved for later use. The implementation of IServiceBehavior
adds the instance itself to the error-handler collection of all dispatchers. The implementation of IErrorHandler
simply delegates to the saved construction parameter. ServiceHost<T>
defines a list of IServiceBehavior
references in the m_ErrorHandlers
member variable. The AddErrorHandler( )
method that accepts an IErrorHandler
reference uses it to construct an instance of ErrorHandlerBehavior
and then adds it to m_ErrorHandlers
. The AddErrorHandler( )
method that takes no parameter uses an instance of the ErrorHandlerBehaviorAttribute
, because the attribute is merely a class that supports both IErrorHandler
and IServiceBehavior
. The attribute instance is also added to m_ErrorHandlers
. Finally, the OnOpening( )
method iterates over m_ErrorHandlers
, adding each behavior to the behavior collection.
The client-side callback object can also provide an implementation of IErrorHandler
for error handling. Compared with the service-error extensions, the main difference is that to install the callback extension, you need to use the IEndpointBehavior
interface defined as:
public interface IEndpointBehavior { void AddBindingParameters(ServiceEndpoint serviceEndpoint, BindingParameterCollection bindingParameters); voidApplyClientBehavior
(ServiceEndpoint serviceEndpoint, ClientRuntimebehavior
); void ApplyDispatchBehavior(ServiceEndpoint serviceEndpoint, EndpointDispatcher endpointDispatcher); void Validate(ServiceEndpoint serviceEndpoint); }
IEndpointBehavior
is the interface supported by all callback behaviors. The only relevant method for the purpose of installing an error extension is the ApplyClientBehavior( )
method, which lets you associate the error extension with the single dispatcher of the callback. The behavior
parameter is of the type ClientRuntime
, which offers the CallbackDispatchRuntime
property of the type DispatchRuntime
. The DispatchRuntime
class offers the ChannelDispatcher
with its collection of error handlers:
public sealed class ClientRuntime { public DispatchRuntime CallbackDispatchRuntime {get;} //More members } public sealed class DispatchRuntime { public ChannelDispatcher ChannelDispatcher {get;} //More members }
As with a service-side error-handling extension, you need to add to that collection your custom error-handling implementation of IErrorHandler
.
The callback object itself can implement IEndpointBehavior
, as shown in Example 6-18.
Example 6-18. Implementing IEndpointBehavior
class MyErrorHandler : IErrorHandler
{...}
class MyClient : IMyContractCallback,IEndpointBehavior
{
public void OnCallBack( )
{...}
void IEndpointBehavior.ApplyClientBehavior(ServiceEndpoint serviceEndpoint,
ClientRuntime behavior)
{
IErrorHandler handler = new MyErrorHandler( );behavior.CallbackDispatchRuntime.ChannelDispatcher.
ErrorHandlers.Add(handler);
}
void IEndpointBehavior.AddBindingParameters(...)
{}
void IEndpointBehavior.ApplyDispatchBehavior(...)
{}
void IEndpointBehavior.Validate(...)
{}
}
Instead of using an external class for implementing IErrorHandler
, the callback class itself can implement IErrorHandler
directly:
class MyClient : IMyContractCallback,IEndpointBehavior,IErrorHandler
{
public void OnCallBack( )
{...}
void IEndpointBehavior.ApplyClientBehavior(ServiceEndpoint serviceEndpoint,
ClientRuntime behavior)
{
behavior.CallbackDispatchRuntime.ChannelDispatcher.ErrorHandlers.Add(this
);
}
public bool HandleError(Exception error)
{...}
public void ProvideFault(Exception error,MessageVersion version,
ref Message fault)
{...}
}
To automate code such as in Example 6-18, CallbackErrorHandlerBehaviorAttribute
is defined as:
public class CallbackErrorHandlerBehaviorAttribute : ErrorHandlerBehaviorAttribute, IEndpointBehavior { public CallbackErrorHandlerBehaviorAttribute(Type clientType); }
The CallbackErrorHandlerBehavior
attribute derives from the service-side ErrorHandlerBehavior
attribute, and adds explicit implementation of IEndpointBehavior
. The attribute uses ErrorHandlerHelper
to promote and log the exception.
In addition, the attribute requires as a construction parameter the type of the callback it is applied on:
[CallbackErrorHandlerBehavior(typeof(MyClient))] class MyClient : IMyContractCallback { public void OnCallBack( ) {...} }
The type is required because there is no other way to get a hold of the callback type, which is required by ErrorHandlerHelper.PromoteException( )
.
The implementation of CallbackErrorHandlerBehaviorAttribute
is shown in Example 6-19.
Example 6-19. Implementing CallbackErrorHandlerBehavior attribute
public class CallbackErrorHandlerBehaviorAttribute : ErrorHandlerBehaviorAttribute,
IEndpointBehavior
{
public CallbackErrorHandlerBehaviorAttribute(Type clientType)
{ServiceType = clientType;
}
void IEndpointBehavior.ApplyClientBehavior(ServiceEndpoint serviceEndpoint,
ClientRuntime behavior)
{
behavior.CallbackDispatchRuntime.ChannelDispatcher.ErrorHandlers.Add(this
);
}
void IEndpointBehavior.AddBindingParameters(...)
{}
void IEndpointBehavior.ApplyDispatchBehavior(...)
{}
void IEndpointBehavior.Validate(...)
{}
}
Note in Example 6-19 how the provided callback client type is stored in the ServiceType
protected property, defined as protected in Example 6-16.
18.225.55.151