An Elegant Exception-Handling Proxy Solution
Recover from proxy failures gracefully
October 29, 2009
RELATED: "WCF and Client Proxies" and "Proxies and Exception Handling"
Almost two years ago I wrote in this column about the issues client developers face managing proxy lifetime specifically with respect to handling uncaught exceptions, dealing with session timeouts, and recovering from faulted channels without inconveniencing the end user. Ultimately what you want is to show the user those exceptions they care about and suppress unnecessary exceptions if you can re-create the proxy to solve the problem.
Although Windows Communication Foundation (WCF) has evolved since then with many a new feature, the problem statement hasn't changed for proxies and exception-handling. Since I still receive many questions on this subject, in this article I'll discuss a more elegant and robust solution that I've generated and point you to a free add-in that you can use to automate generating exception-handling proxies. You'll be able to access the source code for both the exception-handling proxy solution and the add-in at wcfproxygenerator.codeplex.com. In summary, this article will briefly revisit the reason we need exception-handling proxies and describe the major moving parts of the proposed exception-handling proxy solution.
Proxies and Exceptions
Communication across service boundaries necessitates exception-handling logic to recover from failures. This is a given regardless of the technology used for remote communication. You might think you can wrap a try/catch around any logic that calls a service operation, and that would suffice. The following example illustrates this simple approach for a call to a service operation named DeleteItem():
private void DeleteTodoItem_Click(object sender, System.Windows.RoutedEventArgs e){try{TodoItem item = (TodoItem)((Button)sender).DataContext;_Proxy.DeleteItem(item.ID);item.PropertyChanged -= item_PropertyChanged;_TodoItems.Remove(item);}catch (Exception ex){MessageBox.Show(ex.ToString());}}
A transport session exists when you use named pipes or the TCP or HTTP protocol with reliable sessions or secure sessions. In the presence of a transport session, some interesting problems can arise with WCF proxies:
The service or its underlying channel could throw an uncaught CLR exception as DeleteItem() executes, which means the service and client channel are put into a Faulted state.
The session could have already timed out, which means that the call to DeleteItem() above will fail because the service channel is faulted.
Figure 1 illustrates the first scenario. Normally, the proxy makes a call to a service operation and receives a reply (1). If the service throws an uncaught exception it first faults the service channel, and then the client channel (2). At this point, once the client channel is faulted, the proxy can no longer be used to call the service (3). The only remedy is to create a new proxy and reestablish a new session with the service.
Figure 1: Uncaught CLR exceptions faulting the channel
Figure 2 illustrates the second scenario. When the proxy is in use, the session remains active (1). After the configured ReceiveTimeout is exceeded on the service binding configuration, the service channel times out and is put into a faulted state (2). Since this happens while the proxy is idle, the client channel doesn't know about the fault at the service. The proxy will attempt to call the service operation only to discover that the channel is faulted (3), which will also fault the client channel. Once faulted, the proxy can no longer make calls to the service, and once again the only recourse is to re-create the proxy and establish a new session with the service.
Figure 2: Timeout exceptions faulting the channel
Some variations on these scenarios exist:
One-way calls that fail will not fault the client channel. The client will not discover the error until the next two-way call.
If reliable sessions are enabled, the client channel will learn of the fault through acknowledgments, before the next call. That means the client channel will be put into a faulted state.
The user should not be left without a proxy, so the client application must re-create the proxy under these circumstances. The question is, when should the proxy be re-created, and which of these issues should the user even be aware of? Should the user know that the session timed out? Usually not so we need a way to suppress that type of error and create a new proxy silently. Should the user see exceptions thrown by the service? Some, yes. They should not see communication exceptions that can be recovered from, but they should see exceptions and faults that the service throws because of a failure during an operation.
Recovering from Exceptions: The Manual Way
To handle the scenarios discussed in the previous section, you can do a few things:
Handle the proxy's Faulted event to intercept the moment when a proxy is moved into a faulted state and re-create it so that it's ready for the next call.
Catch exceptions thrown when a service operation is called, and first check to see whether the exception is a communication exception or a fault exception. If it's a communication exception, re-create the proxy and try again. If the problem was not a permanent one (as in the service is down, or the service operation is throwing uncaught CLR exceptions instead of faults because it's a very bad citizen), then the retry may just succeed. If not, report the error to the user so they can see it.
Figure 3 shows the code to handle this in a Windows client (extra details omitted). The proxy is constructed when the window is loaded and the application subscribes to the proxy's Faulted event. If the proxy is faulted, the client calls Abort() to clean up resources, then re-creates the proxy, subscribing again to the Faulted event. When calls are made to service operations such as DeleteItem() an extra try catch is wrapped around the call to add some special handling. If a fault exception is thrown, it's reported to the user; if not, the client assumes that the proxy has been re-created already (since the call would have faulted the client channel) and another attempt is made to call DeleteItem(). If that fails, we have a bigger problem, and so it's reported to the user.
As for proxy cleanup, this is another issue since if the client channel is in a faulted state Dispose(),Close() will throw an exception. To gracefully close, you should catch the exception on Close() and call Abort() to clean up resources quietly.
If you had to wrap every call to a proxy in a special try catch that inspects the exceptions this way and retries the call where applicable, that would be pretty annoying. Furthermore, it's a lot to remember to handle the Faulted event, re-create the proxy on fault, call Abort() to clean up, add extra code to gracefully clean up the proxy, and so on. Worse, what happens when you add multithreaded clients to the mix? That's where the exception-handling proxy comes into play.
Exception-Handling Proxy Goals
The goals of an exception-handling proxy are to encapsulate the process of re-creating the proxy as needed in response to recoverable failures and to suppress superfluous exceptions from being presented to the user. The exception-handling proxy should retry calls in the face of communication exceptions to see whether the problem is recoverable and should re-create the proxy proactively if the client channel is faulted.
My version of such an exception-handling proxy is called ExceptionHandlingProxyBase, where is the service contract type. Just like typical proxies inherit ClientBase, exception-handling proxies should inherit ExceptionHandlingProxyBase, as follows:
public abstract class ExceptionHandlingProxyBase :ICommunicationObject, IDisposable where T : class{ }public class ExceptionHandlingProxy : ExceptionHandlingProxyBase,ITodoListService{ }
The base class provides the following reusable functionality:
initialization of the channel factory and channel
access to core channel features such as ClientCredentials
an implementation of ICommunicationObject to expose communication events
safe disposal without exceptions
invoke() functionality that uses reflection to call any service operation
proxy re-creation logic
multi-threading protection that allows clients to share the proxy for multiple concurrent service calls while protecting critical code sections during initialization, disposal, and proxy re-creation
In the next sections I'll describe key implementation details of the ExceptionHandlingProxyBase and the derived type implementation.
Inheriting ExceptionHandlingProxyBase
To implement an exception-handling proxy, you need only extend ExceptionHandlingProxyBase supplying the service contract to generic parameter and implementing the service contract on the derived type so that clients can call service operations through that type. Figure 4 shows the ITodoListService service contract implementation of ExceptionHandlingProxy. The derived type, ExceptionHandlingProxy supplies the appropriate constructors to initialize the proxy and calls down to the base type constructor. Other than that, each service operation need only call down to the base type Invoke() passing the name of the method to call and any parameters expected.
The following illustrates how the DeleteItem() call mentioned earlier would be implemented in the ExceptionHandlingProxy type:
public void DeleteItem(string id) {base.Invoke("DeleteItem", id);}
Since DeleteItem() has a void return, the version of Invoke() called in the base type is that with the void return:
protected void Invoke(string operationName, params object[] parameters){ }
On the other hand, GetItems() returns ObservableCollection and takes no parameters:
public ObservableCollection GetItems(){return base.Invoke< ObservableCollection< TodoItem>>("GetItems");}
In this case, the base type Invoke is called, where is the type of return expected:
protected TResult Invoke(string operationName, params object[] parameters){ }
So, to implement an exception-handling proxy, you need only derive from the base type implementation and call down to Invoke() with the appropriate parameters.
Invoking Operations
Invoke() is the heart of the ExceptionHandlingProxyBase implementation. It does the following:
Checks to see whether the proxy has been dispose; if so, it throws an exception since that would be an invalid use of the proxy. Of course, if your code cleans up properly, this should never happen.
Creates the proxy and opens the channel if this hasn't already been done.
It looks for the method to call based on the method name string parameter passed in. If the method name doesn't exist on the service contract (defined by ), then it throws an exception.
It calls the operation using reflection.
If the call throws an exception, the code checks to see whether the exception is a CommunicationException (not a FaultException) and if so tries the call again.
All other exceptions are rethrown.
If it fails again, it throws an exception.
Figure 5 shows the complete listing for Invoke(). Invoke() is similar, except that it doesn't return a value. Most of this code should make sense given the conversation thus far, but there are a few interesting things worth discussing, namely: how the channel is opened, how safe disposal works, how the proxy re-creation lock and other thread-safety measures are implemented, and why the retry attempt has some special code for Message types. I'll break down these proxy features in the next sections.
Retry and the Message Type
I discovered an interesting problem when testing the exception-handling proxy with Message types. As you may know, a Message type can only be read once which puts a bit of a damper in the proxy retry mechanism since the first attempt will read the message; thus retry becomes impossible. I thought about creating a message buffer before the first try, but that would mean for most successful attempts I'm buffering a possibly large message for no good reason. A more elegant solution to this problem seemed to be firing an event that would allow the client code to supply a copy of the unread message.
The client code would be responsible for creating a MessageBuffer type at some point:
MessageBuffer buffer = requestMessage.CreateBufferedCopy(int.MaxValue);The client code subscribes to RetryInvoke and, if the event is fired, supplies a copy of the unread message:_Proxy.RetryInvoke += new ExceptionHandlingProxyBase.RetryInvokeHandler(_Proxy_RetryInvoke);void _Proxy_RetryInvoke(out System.ServiceModel.Channels.Message unreadMessage){unreadMessage = buffer.CreateMessage();}
When the retry code is executed, if there is only one parameter, a Message type, RetryInvoke is fired to gather an unread message if possible, as Figure 6 shows. The truth is that this is probably a rare occurrence, and I encountered it in an example where the service acted as a router, but I wanted to ensure there was a solution for the Message type as well.
Multi-threading Protection
This is probably one of the more important features of the exception-handling proxy because it allows client applications to be multi-threaded. There are two locks defined: a channel lock to protect the inner channel during creation, re-creation, and disposal and a proxy re-creation lock to protect Invoke() functionality from being accessed by multiple threads while the proxy is being re-created due to a fault.
private object m_channelLock = new object();private ManualResetEvent m_proxyRecreationLock =new ManualResetEvent(true);protected int m_proxyRecreationLockWait = 1000;
Almost every function in the proxy uses the channel lock to protect access to the state of the class, including the channel. Figure 7 illustrates the use of the channel lock during proxy re-creation.
The proxy re-creation lock is used a little differently. Let's say multiple threads are using the proxy and the first thread to encounter a faulted channel does so on the first Invoke() call:
this.m_proxyRecreationLock.WaitOne(this.m_proxyRecreationLockWait); result =(TResult)methodInfo.Invoke(m_channel, parameters);
That thread will immediately hit the InnerChannel_Faulted() handler, which will reset the proxy re-creation lock, so that other threads will wait at the WaitOne() command (above) until the proxy is safely re-created and the proxy re-creation lock is signaled, as Figure 8 shows.
Even if it's possible that more than one thread gets past the proxy re-creation lock before the lock is reset, only one thread will have a chance to re-create the proxy at a time because of the channel lock. At the worst case, the proxy gets re-created more than once from whatever threads had already passed the proxy re-creation lock prior to the first thread handling the faulted channel.
Another important thing to note here is that Invoke() allows multiple concurrent threads to process as it should and blocks only while the proxy is being re-created in the face of failure.
Opening and Disposal
As with ClientBase, you may create a proxy and forget to call Open() before invoking your first service operation. You should call Open() first, but the exception-handling proxy will do it for you on Invoke() which leads to ensuring that the proxy has been created as well, as Figure 9 shows.
One of the really annoying things about closing a proxy normally is that if the channel is faulted Dispose() and Close() throw an exception. The exception-handling proxy provide a safe Dispose() that locks the disposal process and swallows exceptions on Close(), calling Abort() to clean up, as Figure 10 shows.
Cleanup can be overridden if derived types want to customize cleanup. The IsDisposed property is used throughout the exception-handling proxy to prevent multiple threads from using a disposed object. You'll see statements like this at the start of most methods:
if (this.IsDisposed)throw new InvalidOperationException("Cannot use disposed object.");
Duplex Implementation
The duplex implementation of the exception-handling proxy is ExceptionHandlingDuplexProxyBase, where is the service contract type and is the callback object type. The implementation is identical with the exception of two things: the callback object and how the Closed event is handled.
The callback object is provided in the constructor (as it is for ClientBase) and stored in this member:
private C m_CallbackObject = default(C);public C CallbackObject{get { return this.m_CallbackObject; }set { this.m_CallbackObject = value; }}
When the channel factory is created (on construction), the callback is supplied, for example:
m_duplexChannelFactory = new DuplexChannelFactory(this.m_CallbackObject, binding,remoteAddress);
But an interesting thing happens when a callback object throws an uncaught exception instead of a fault. The proxy doesn't go into a faulted state, it goes directly to the Close handler! As such, the Close handler behaves as the Faulted handler: It re-creates the proxy and resets the proxy re-creation lock to prevent other threads from calling Invoke(). A little gotcha happens along the way, however. The callback object can still be used, but the InstanceConext is no longer valid because the channel is gone. Thus, re-creating the proxy requires that we re-create the InstanceContext, as follows:
m_duplexChannel = m_duplexChannelFactory.CreateChannel(newInstanceContext(this.m_CallbackObject));
Other Features
The exception-handling proxy wrapper also provides these features:
It implements ICommunicationObject and exposes the communication events for clients to subscribe to.
It exposes typical ClientBase properties to access the Endpoint, Channel, State, and ClientCredentials the latter of which is key to working with secure channels.
By the time this article is printed, asynchronous support should also be added (see the CodePlex site).
Efficient Proxy Exception-Handling
Encapsulating exception-handling techniques like this is really important if you want to keep your client code clean and have a reliable proxy available at all times (barring any bigger issues). This article summarizes the core features, but you should definitely download the source code at wcfproxygenerator.codeplex.com and play with it yourself! In fact, the CodePlex site also has a proxy generator that you can install that automatically generates exception-handling proxies for you right from Visual Studio Add Service Reference! Check it out and enjoy!
Download the complete code for the Exception Handling WCF Proxy Generator discussed in this article at wcfproxygenerator.codeplex.com or on this site.
Figure 3: Suppressing unwanted proxy exceptions
public partial class MainWindow : Window{TodoListServiceClient _Proxy;private void window_Loaded(object sender, RoutedEventArgs e){_Proxy = new TodoListServiceClient("default");_Proxy.InnerChannel.Faulted += new EventHandler(InnerChannel_Faulted);}void InnerChannel_Faulted(object sender, EventArgs e){_Proxy.Abort();_Proxy = new TodoListServiceClient("default");_Proxy.Faulted += new EventHandler(InnerChannel_Faulted);}private void DeleteTodoItem_Click(object sender, System.Windows.RoutedEventArgs e){try{TodoItem item = (TodoItem)((Button)sender).DataContext;try{_Proxy.DeleteItem(item.ID);}catch (CommunicationException commEx){FaultException faultEx = commEx as FaultException;if (faultEx == null){_Proxy.DeleteItem(item.ID);}elsethrow;}}catch (Exception ex){MessageBox.Show(ex.ToString());}}private void window_Closing(object sender, CancelEventArgs e){try{_Proxy.Faulted -= InnerChannel_Faulted;_Proxy.Close();}catch{try{_Proxy.Abort();}catch { }}}}
Figure 4: ExceptionHandlingProxy implementation for ITodoListService
[ServiceContract(Namespace="http://wcfclientguidance.codeplex.com/2009/04")]public interface ITodoListService{[OperationContract]List GetItems();[OperationContract]string CreateItem(TodoItem item);[OperationContract]void UpdateItem(TodoItem item);[OperationContract]void DeleteItem(string id);}public class ExceptionHandlingProxy : ExceptionHandlingProxyBase,ITodoListService {public ExceptionHandlingProxy() :this("") {}public ExceptionHandlingProxy(string endpointConfigurationName) :base(endpointConfigurationName) {}public ExceptionHandlingProxy(string endpointConfigurationName, string remoteAddress) :base(endpointConfigurationName, remoteAddress) {}public ExceptionHandlingProxy(System.ServiceModel.Channels.Binding binding,System.ServiceModel.EndpointAddress remoteAddress) :base(binding, remoteAddress) {}publicSystem.Collections.ObjectModel.ObservableCollection GetItems() {returnbase.Invoke>("GetItems");}public string CreateItem(TodoList.WpfClient.ServiceReference1.TodoItem item) {return base.Invoke("CreateItem", item);}public void UpdateItem(TodoList.WpfClient.ServiceReference1.TodoItem item) {base.Invoke("UpdateItem", item);}public void DeleteItem(string id) {base.Invoke("DeleteItem", id);}}
Figure 5: Invoke() implementation
protected TResult Invoke(string operationName, params object[] parameters){if (this.IsDisposed)throw new InvalidOperationException("Cannot use disposed object.");this.Open();MethodInfo methodInfo = GetMethod(operationName);TResult result = default(TResult);try{this.m_proxyRecreationLock.WaitOne(this.m_proxyRecreationLockWait); result =(TResult)methodInfo.Invoke(m_channel, parameters);}catch (TargetInvocationException targetEx) // Invoke() always throws this type{CommunicationException commEx = targetEx.InnerException as CommunicationException;if (commEx == null){throw targetEx.InnerException; // not a communication exception, throw it}FaultException faultEx = commEx as FaultException;if (faultEx != null){throw targetEx.InnerException; // the service threw a fault, throw it}try{this.m_proxyRecreationLock.WaitOne(this.m_proxyRecreationLockWait);if (parameters.Length == 1 && parameters[0] is Message){Message unreadMessage;RetryInvoke(out unreadMessage);result = (TResult)methodInfo.Invoke(m_channel, new object[] { unreadMessage });// communication exception, retry once}elseresult = (TResult)methodInfo.Invoke(m_channel, parameters);// communication exception, retry once}catch (TargetInvocationException targetEx2){throw targetEx2.InnerException; // still failed, throw it}}return result;}
Figure 6: Using RetryInvoke to gather an unread message
if (parameters.Length == 1 && parameters[0] is Message){Message unreadMessage;RetryInvoke(out unreadMessage);result = (TResult)methodInfo.Invoke(m_channel, new object[] { unreadMessage });// communication exception, retry once}elseresult = (TResult)methodInfo.Invoke(m_channel, parameters); //
Figure 7: Using the channel lock during proxy re-creation
protected virtual void RecreateProxy(){lock (this.m_channelLock){if (this.IsDisposed) throw new InvalidOperationException("Cannot use disposed object.");CreateInnerChannel();if (AfterRecreateProxy != null)AfterRecreateProxy(this, null);}}private void CreateInnerChannel(){lock (this.m_channelLock){m_channel = m_channelFactory.CreateChannel();ICommunicationObject co = m_channel as ICommunicationObject;co.Faulted += InnerChannel_Faulted;co.Closed += InnerChannel_Closed;co.Closing += InnerChannel_Closing;co.Opened += InnerChannel_Opened;co.Opening += InnerChannel_Opening;}}
Figure 8: Resetting the proxy re-creation lock
private void InnerChannel_Faulted(object sender, EventArgs e){lock (m_channelLock){if (this.IsDisposed) throw new InvalidOperationException("Cannot use disposed object.");try{this.m_proxyRecreationLock.Reset();// will stop other threads from trying to Invoke() while re-creating the proxyif (this.Faulted != null)this.Faulted(sender, e);OnFaulted();}finally{this.m_proxyRecreationLock.Set();// will stop other threads from trying to Invoke() while re-creating the proxy}}}
Figure 9: Calling Open()
public void Open(){lock (this.m_channelLock){if (this.IsDisposed) throw new InvalidOperationException("Cannot use disposed object.");if (!this.IsOpened){EnsureProxy();((ICommunicationObject)m_channel).Open();this.IsOpened = true;}}}private void EnsureProxy(){lock (this.m_channelLock){if (!this.m_isProxyCreated){this.CreateProxy();}}}
Figure 10: Safely disposing of the communication object
public void Dispose(){lock (m_channelLock){Cleanup();this.IsDisposed = true;}}protected virtual void Cleanup(){try{ICommunicationObject co = (ICommunicationObject)m_channel;co.Closed -= InnerChannel_Closed;co.Closing -= InnerChannel_Closing;co.Faulted -= InnerChannel_Faulted;co.Opened -= InnerChannel_Opened;co.Opening -= InnerChannel_Opening;co.Close();}catch{try{ICommunicationObject co = (ICommunicationObject)m_channel;co.Abort();}catch { }}try{m_channelFactory.Close();}catch{try{m_channelFactory.Abort();}catch { }}}
Read more about:
MicrosoftAbout the Author
You May Also Like