WCF enables developers to customize the default exception reporting and propagation behavior, and even to provide for a hook for custom logging. This extensibility is applied per channel dispatcher (that is, per endpoint), although you are most 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 how to install the extensions later in this section.
The ProvideFault( )
method of the extension object
is called immediately after any unhandled exception is thrown by the service or any object
on the call chain downstream from a service operation. WCF calls ProvideFault( )
before returning control to the client, and before
terminating the session (if present) and disposing of the service instance (if required).
Because ProvideFault( )
is called on the incoming call
thread while the client is 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, an unlisted fault, or a fault listed
in the fault contract. The error
parameter is a
reference to the exception just thrown. If ProvideFault(
)
does nothing, the exception the client gets will be determined by 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
provide an alternative fault. This alternative behavior will affect even exceptions that
are in the fault contracts. 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-12.
Example 6-12. 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-12, 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 in all exceptions being propagated to the client as FaultException
s, even if the exceptions were listed in the
fault contracts. Setting fault
to null
is therefore an effective way of suppressing any fault
contracts that may be in place.
One possible use for ProvideFault( )
is a
technique I call exception promotion. A service may use downstream
objects, which 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. If a
downstream object throws an exception of type T
,
where FaultException<T>
is part of the
operation fault contract, by default the service will report that exception to the
client as an opaque FaultException
. What the service
could do instead 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 then promote that exception to a full-fledged FaultException<T>
. For example, given this service
contract:
[ServiceContract]
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 Exception promotion.
Example 6-13. 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 Exception promotion is that the code is coupled to a specific fault contract, and implementing it across all services requires a lot of tedious work—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( )
method
requires the service type as a parameter. It uses 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(
)
lets exceptions in the contract go up the call stack unaffected, but it
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
, Exception promotion 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, so it is not listed in this chapter. However, you can examine it
as part of the source code available with ServiceModelEx. The
implementation makes use of some advanced C# programming techniques, such as generics
and reflection, and generics 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. Calling in the
background enables you to perform lengthy processing, such as logging to a database
without impeding the client.
Because you can 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
, 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 for logging and tracing, as shown in Example 6-14.
Example 6-14. 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 in ServiceModelEx
contains a standalone service called LogbookManager
that is dedicated to error logging. LogbookManager
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.
ServiceModelEx also contains a simple logbook viewer and
management tool. In addition to error logging, LogbookManager
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 LogbookManager
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
LogbookManager
. For example, instead of the code in
Example 6-14, 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 capturing 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 if debug symbols are provided)
The type where the exception took place and the member being accessed
The date and time when the exception occurred
The exception name and message
Implementing LogError( )
has little to do with
WCF, so this method 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 to LogbookManager
in a dedicated data contract.
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, but
after the host constructs the collection of dispatchers. This narrow window of opportunity
exists after the host is initialized, but before it is 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 bindingParameters); voidApplyDispatchBehavior
(ServiceDescription description,ServiceHostBase host
); void Validate(ServiceDescription description,ServiceHostBase host); }
The ApplyDispatchBehavior( )
method is your cue to
add the error extensions to the dispatchers. You can safely ignore all other methods of
IServiceBehavior
and provide empty implementations
for them.
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-15 demonstrates adding an implementation of
IErrorHandler
to all of a service's
dispatchers.
Example 6-15. 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(...)
{}
//More members
}
In Example 6-15, 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-16.
Example 6-16. Service class supporting IErrorHandler
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 with Example 6-15 and Example 6-16 is that they pollute the service
class code with WCF plumbing; instead of focusing exclusively on the business logic, the
service 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 an error-handling extension. Its implementation
uses ErrorHandlerHelper
both to automatically promote
exceptions to fault contracts, if required, and to automatically log the exceptions to
LogbookManager
. The ErrorHandlerBehavior attribute lists the implementation of the
ErrorHandlerBehavior
attribute.
Example 6-17. 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 The ErrorHandlerBehavior attribute 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, the attribute 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 any. However, due to the
narrow timing window available for installing extensions, 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, you must 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 members
}
public abstract class ServiceHostBase : CommunicationObject ,...
{...}
public class ServiceHost
: 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 Chapter 1 and Chapter 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 you can provide it with any class that supports just IErrorHandler
, not IServiceBehavior
:
class MyService : IMyContract {...} class MyErrorHandler : IErrorHandler {...} ServiceHost<MyService> host = new ServiceHost<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 {...} ServiceHost<MyService> host = new ServiceHost<MyService>( ); host.AddErrorHandler( ); host.Open( );
Actually, for this last example, ServiceHost<T>
does internally use an instance of the ErrorHandlerBehavior
attribute.
Example 6-18 shows the implementation of
the AddErrorHandler( )
method.
Example 6-18. 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 errorHandlerBehavior = new ErrorHandlerBehavior(errorHandler); m_ErrorHandlers.Add(errorHandlerBehavior); } public void AddErrorHandler( ) { AddErrorHandler(new ErrorHandlerBehaviorAttribute( )); } 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 ErrorHandlerBehavior
attribute, because the attribute is
merely a class that supports IErrorHandler
. Finally,
the OnOpening( )
method iterates over m_ErrorHandlers
, adding each behavior to the behaviors
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 endpoint,
BindingParameterCollection bindingParameters);
void ApplyClientBehavior
(ServiceEndpoint endpoint,
ClientRuntime clientRuntime);
void ApplyDispatchBehavior(ServiceEndpoint endpoint,
EndpointDispatcher endpointDispatcher);
void Validate(ServiceEndpoint endpoint);
}
IEndpointBehavior
is the interface all callback
behaviors support. 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
endpoint. The clientRuntime
parameter is of the type
ClientRuntime
, which offers the CallbackDispatchRuntime
property of the type DispatchRuntime
. The DispatchRuntime
class offers the ChannelDispatcher
property, 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-19.
Example 6-19. Implementing IEndpointBehavior
class MyErrorHandler : IErrorHandler {...} class MyClient : IMyContractCallback,IEndpointBehavior { public void OnCallBack( ) {...} void IEndpointBehavior.ApplyClientBehavior(ServiceEndpoint serviceEndpoint, ClientRuntime clientRuntime) { IErrorHandler handler = new MyErrorHandler( ); clientRuntime.CallbackDispatchRuntime.ChannelDispatcher.ErrorHandlers. Add(handler); } void IEndpointBehavior.AddBindingParameters(...) {} void IEndpointBehavior.ApplyDispatchBehavior(...) {} void IEndpointBehavior.Validate(...) {} //More members }
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 clientRuntime)
{
clientRuntime.CallbackDispatchRuntime.ChannelDispatcher.ErrorHandlers.Add
(this
);
}
public bool HandleError(Exception error)
{...}
public void ProvideFault(Exception error,MessageVersion version,
ref Message fault)
{...}
//More members
}
Code such as that shown in Example 6-19 can be
automated with the CallbackErrorHandlerBehaviorAttribute
, 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 on which it is applied:
[CallbackErrorHandlerBehavior(typeof(MyClient))] class MyClient : IMyContractCallback { public void OnCallBack( ) {...} }
The type is required because there is no other way to get hold of the callback type,
which is required by ErrorHandlerHelper.PromoteException(
)
.
The implementation of the CallbackErrorHandlerBehavior
attribute is shown in Example 6-20.
Example 6-20. Implementing the CallbackErrorHandlerBehavior attribute
public class CallbackErrorHandlerBehaviorAttribute : ErrorHandlerBehaviorAttribute, IEndpointBehavior { public CallbackErrorHandlerBehaviorAttribute(Type clientType) {ServiceType = clientType;
} void IEndpointBehavior.ApplyClientBehavior(ServiceEndpoint serviceEndpoint, ClientRuntime clientRuntime) { clientRuntime.CallbackDispatchRuntime.ChannelDispatcher.ErrorHandlers.Add (this
); } void IEndpointBehavior.AddBindingParameters(...) {} void IEndpointBehavior.ApplyDispatchBehavior(...) {} void IEndpointBehavior.Validate(...) {} }
Note in Example 6-20 how the provided
callback client type is stored in the ServiceType
property, defined as protected in The ErrorHandlerBehavior attribute.
3.144.222.185