Section 6.3. Error-Handling Extensions


6.3. Error-Handling Extensions

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.

6.3.1. Providing a Fault

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( ).

6.3.1.1. Using 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.

6.3.1.2. Exception promotion

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.

6.3.2. Handling a Fault

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)    {...} } 

6.3.2.1. The Logbook service

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.

Figure 6-1. The LogbookService and viewer


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.

6.3.3. Installing Error-Handling Extensions

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);    void ApplyDispatchBehavior(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 } 

6.3.3.1. Error-Handling attribute

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.

6.3.4. Host and Error Extensions

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 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 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.

6.3.5. Callbacks and Error Extensions

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);    void ApplyClientBehavior(ServiceEndpoint serviceEndpoint,                             ClientRuntime behavior);    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)    {...} } 

6.3.5.1. Callback error-handling attribute

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.




Programming WCF Services
Programming WCF Services
ISBN: 0596526997
EAN: 2147483647
Year: 2004
Pages: 148
Authors: Juval Lowy

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net