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.
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 FaultException
s:
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.
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.
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( )
;
}
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");
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.
18.220.202.209