Vous êtes sur la page 1sur 6

Safari IT Books Visual Studio Web Services System Programming Windows Safari IT Books

Programming Internet/Online Windows Programming Juval Löwy O'Reilly Media, Inc.


Programming WCF Services, 2nd Edition

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

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

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

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

6.1.2.1. 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");

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

Vous aimerez peut-être aussi