Error-Handling Extensions

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.

Providing a Fault

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

Using 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 FaultExceptions, 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.

Exception promotion

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.

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

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.

The logbook service and viewer

Figure 6-1. The logbook service and viewer

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.

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

   void ApplyDispatchBehavior(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 ErrorHandlerBehavior attribute

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.

The Host and Error Extensions

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.

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

The CallbackErrorHandlerBehavior attribute

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.222.185