Error Isolation and Decoupling

In traditional .NET programming, any unhandled exception (except ThreadAbortException) immediately terminated the process in which it occurred. While this is a very conservative behavior, it does not provide for proper fault isolation, which would enable the client to keep functioning even after the object blew up. Much the same way, after any unhandled error on the client side, the object would go down with the ship. Developers that did not like this had to provide for process isolation between the client and the object, which greatly complicated the programming model. That is not the WCF behavior, however. If a service call on behalf of one client causes an exception, it must not be allowed to take down the hosting process. Other clients accessing the service, or other services hosted by the same process, should not be affected. As a result, when an unhandled exception leaves the service scope, the dispatcher silently catches and handles it by serializing it in the returned message to the client. When the returned message reaches the proxy, the proxy throws an exception on the client side. This behavior provides every WCF service with process-level isolation. The client and service can share a process, and yet be completely isolated as far as errors. The only exceptions that will take down the host process are critical errors that blow up .NET itself, such as stack overflows. Fault isolation, however, is only one of three key error-decoupling features of WCF. The second is error masking, and the third is faulting the channel.

Error Masking

The client can actually encounter three types of errors when trying to invoke a service. The first type of error is a communication error, which may occur because of network unavailability, an incorrect address, the host process not running, and so on. Communication exceptions are manifested on the client side by a CommunicationException or a CommunicationException-derived class such as EndpointNotFoundException.

The second type of error the client might encounter is related to the state of the proxy and the channels. There are many such possible exceptions. For example, these errors may occur when the client is trying to access an already closed proxy, resulting in an ObjectDisposedException; when there is a mismatch between the contract and the binding security protection level resulting in an InvalidOperationException; when the client's credentials are denied by the service resulting in a SecurityNegotiationException in case of authentication failure, or SecurityAccessDeniedException in case of authorization failure; or when the transport session times out, resulting in a TimeoutException.

The third type of error is an error that originates in the execution of the service call itself, as a result of either the service throwing an exception, or the service calling another object or resource and having that internal call throw an exception.

As stated at the beginning of this chapter, it is a common illusion that clients care about errors or have anything meaningful to do when they occur. Any attempt to bake such capabilities into the client creates an inordinate degree of coupling between the client and the object, raising serious design questions. How could the client possibly know more about the error than the service, unless it is tightly coupled to it? What if the error originated several layers below the service—should the client be coupled to those low-level layers? Should the client try the call again? How often and how frequently? Should the client inform the user of the error? Is there a user?

All that the client cares about is that something went wrong. The best practice for most clients is to simply let the exception go up the call chain. The topmost client typically will catch the exception, not in order to handle it, but simply to prevent the application from shutting down abruptly. A well-designed client should never care about the actual error; WCF enforces this. In the interest of encapsulation and decoupling, by default all exceptions thrown on the service side always reach the client as FaultExceptions:

public class FaultException : CommunicationException
{...}

By having all service exceptions be indistinguishable from each other, WCF decouples the client from the service. The less the client knows about what happened on the service side, the more decoupled the interaction will be.

Channel Faulting

In traditional .NET programming, the client can catch the exception and keep calling the object. Consider this definition of a class and an interface:

interface IMyContract
{
   void MyMethod( );
}
class MyClass : IMyContract
{...}

If the client snuffs out the exception thrown by the object, it can call it again:

IMyContract obj = new MyClass( );
try
{
   obj.MyMethod( );
}
catch
{}
obj.MyMethod( );

This is a fundamental flaw of .NET as a platform. Exceptions, by their very nature, are for exceptional cases. Here, something totally unexpected and horrible has happened. How could the client possibly pretend otherwise? The object is hopelessly broken, and yet the client keeps using it. In classic .NET, developers that did not approve of this behavior had to maintain a flag in each object, set the flag before throwing an exception (or after catching any downstream exceptions), and check the flag inside any public method, refusing to use the object if it was called after an exception had been thrown. This, of course, is cumbersome and tedious. WCF automates this best practice. If the service has a transport session, any unhandled exceptions (save those derived from FaultException, as described next) fault the channel (the proxy's state is changed to CommunicationState.Faulted), thus preventing the client from using the proxy, and the object behind it, after an exception. In other words, for this service and proxy definition:

[ServiceContract]
interface IMyContract
{
   [OperationContract]
   void MyMethod( );
}
class MyClass : IMyContract
{...}

class MyContractClient : ClientBase<IMyContract>,IMyContract
{...}

the following client code results in a CommunicationObjectFaultedException:

IMyContract proxy = new MyContractClient( );
try
{
   proxy.MyMethod( );
}
catch
{}

//Throws CommunicationObjectFaultedException
proxy.MyMethod( );

The obvious conclusion is that the client should never try to use a WCF proxy after an exception. If there was a transport session, the client cannot even close the proxy.

Tip

If there is no transport-level session, the client can technically keep using the proxy after an exception, except again, it should not.

The only thing a client might safely do after an exception is to abort the proxy, perhaps to trigger tracing, or raise events for state changes in the proxy, or to prevent others from using the proxy (even if there was no transport session):

MyContractClient proxy = new MyContractClient( );
try
{
   proxy.MyMethod( );
}
catch
{
   proxy.Abort( );
}

Closing the proxy and the using statement

I recommend against relying on the using statement to close the proxy. The reason is that in the presence of a transport session, any service-side exception will fault the channel. Trying to dispose of the proxy when the channel is faulted throws a CommunicationObjectFaultedException, so code after the using statement will never get called, even if you catch all exceptions inside the using statement:

using(MyContractClient proxy = new MyContractClient( ))
{
   try
   {
      proxy.MyMethod( );
   }
   catch
   {}
}
Trace.WriteLine("This trace may never get called");

This reduces the readability of the code and may introduce defects, since the code will behave differently than most developers will expect. The only remedy is to encase the using statement itself in a try/catch statement:

try
{
   using(MyContractClient proxy = new MyContractClient( ))
   {
      try
      {
         proxy.MyMethod( );
      }
      catch
      {}
   }
}
catch
{}
Trace.WriteLine("This trace always gets called");

It is therefore far better to call Close( ). In the case of an exception, the exception will skip over the call to Close( ):

MyContractClient proxy = new MyContractClient( );
proxy.MyMethod( );
proxy.Close( );

You can, of course, catch the exception, but now the code is readable:

try
{
   MyContractClient proxy = new MyContractClient( );
   proxy.MyMethod( );
   proxy.Close( );
}
catch
{
   proxy.Abort( );
}
Trace.WriteLine("This trace always gets called");

Exceptions and instance management

When the service is configured as per-call or as sessionful (which mandates the use of a transport session), the client can never access the same instance after an exception occurs. With a per-call service this is, of course, always true, but with a sessionful service this is the result of faulting the channel and terminating the transport session. The one exception to the rule here is a singleton. When the client calls a singleton service and encounters an exception, the singleton instance is not terminated and continues running. If there was no transport session (or if the exception was a FaultException-derived class, as described next), the client can keep using the proxy to connect to the singleton object. Even if the channel is faulted, the client can create a new proxy instance and reconnect to the singleton.

In the case of a durable service, the DurableService attribute offers the UnknownExceptionAction property, defined as:

public enum UnknownExceptionAction
{
    TerminateInstance,
    AbortInstance
}

[AttributeUsage(AttributeTargets.Class)]
public sealed class DurableServiceAttribute : ...
{
   public UnknownExceptionAction UnknownExceptionAction
   {get;set;}
   //More members
}

UnknownExceptionAction defaults to UnknownExceptionAction.TerminateInstance, meaning that any unhandled exception will not only fault the channel but also remove the instance state from the store, thus terminating the workflow. This behavior is analogous to simply faulting the channel with a regular service, preventing future use of the object. The value UnknownExceptionAction.AbortInstance, on the other hand, terminates the channel to the client but keeps the state in the store. While any changes made to the instance are not persisted, this value is analogous to not faulting the channel in the case of a regular service.

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

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