WCF Retry Proxy-Collection of common programming errors
Bob HornI’m struggling with trying to find the best way to implement WCF retries. I’m hoping to make the client experience as clean as possible. There are two approaches of which I’m aware (see below). My question is: “Is there a third approach that I’m missing? Maybe one that’s the generally accepted way of doing this?“
Approach #1: Create a proxy that implements the service interface. For each call to the proxy, implement retries.
public class Proxy : ISomeWcfServiceInterface { public int Foo(int snurl) { return MakeWcfCall(() => _channel.Foo(snurl)); } public string Bar(string snuh) { return MakeWcfCall(() => _channel.Bar(snuh)); } private static T MakeWcfCall(Func func) { // Invoke the func and implement retries. } }Approach #2: Change MakeWcfCall() (above) to public, and have the consuming code send the func directly.
What I don’t like about approach #1 is having to update the Proxy class every time the interface changes.
What I don’t like about approach #2 is the client having to wrap their call in a func.
Am I missing a better way?
EDIT
I’ve posted an answer here (see below), based on the accepted answer that pointed me in the right direction. I thought I’d share my code, in an answer, to help someone work through what I had to work through. Hope it helps.
JimI have done this very type of thing and I solved this problem using .net’s RealProxy class.
Using
RealProxy, you can create an actual proxy at runtime using your provided interface. From the calling code it is as if they are using anIFoochannel, but in fact it is aIFooto the proxy and then you get a chance to intercept the calls to any method, property, constructors, etc.Deriving from
RealProxy, you can then override the Invoke method to intercept calls to the WCF methods and handle CommunicationException, etc.
Garath
Bob HornNote: This shouldn’t be the accepted answer, but I wanted to post the solution in case it helps others. Jim’s answer pointed me in this direction.
First, the consuming code, showing how it works:
static void Main(string[] args) { var channelFactory = new WcfChannelFactory(new NetTcpBinding()); var endpointAddress = ConfigurationManager.AppSettings["endpointAddress"]; // The call to CreateChannel() actually returns a proxy that can intercept calls to the // service. This is done so that the proxy can retry on communication failures. IPrestoService prestoService = channelFactory.CreateChannel(new EndpointAddress(endpointAddress)); Console.WriteLine("Enter some information to echo to the Presto service:"); string message = Console.ReadLine(); string returnMessage = prestoService.Echo(message); Console.WriteLine("Presto responds: {0}", returnMessage); Console.WriteLine("Press any key to stop the program."); Console.ReadKey(); }The WcfChannelFactory:
public class WcfChannelFactory : ChannelFactory where T : class { public WcfChannelFactory(Binding binding) : base(binding) {} public T CreateBaseChannel() { return base.CreateChannel(this.Endpoint.Address, null); } public override T CreateChannel(EndpointAddress address, Uri via) { // This is where the magic happens. We don't really return a channel here; // we return WcfClientProxy.GetTransparentProxy(). That class will now // have the chance to intercept calls to the service. this.Endpoint.Address = address; var proxy = new WcfClientProxy(this); return proxy.GetTransparentProxy() as T; } }The WcfClientProxy: (This is where we intercept and retry.)
public class WcfClientProxy : RealProxy where T : class { private WcfChannelFactory _channelFactory; public WcfClientProxy(WcfChannelFactory channelFactory) : base(typeof(T)) { this._channelFactory = channelFactory; } public override IMessage Invoke(IMessage msg) { // When a service method gets called, we intercept it here and call it below with methodBase.Invoke(). var methodCall = msg as IMethodCallMessage; var methodBase = methodCall.MethodBase; // We can't call CreateChannel() because that creates an instance of this class, // and we'd end up with a stack overflow. So, call CreateBaseChannel() to get the // actual service. T wcfService = this._channelFactory.CreateBaseChannel(); try { var result = methodBase.Invoke(wcfService, methodCall.Args); return new ReturnMessage( result, // Operation result null, // Out arguments 0, // Out arguments count methodCall.LogicalCallContext, // Call context methodCall); // Original message } catch (FaultException) { // Need to specifically catch and rethrow FaultExceptions to bypass the CommunicationException catch. // This is needed to distinguish between Faults and underlying communication exceptions. throw; } catch (CommunicationException ex) { // Handle CommunicationException and implement retries here. throw new NotImplementedException(); } } }Sequence diagram of a call being intercepted by the proxy:


Erno de WeerdDo not mess with generated code because, as you mentioned, it will be generated again so any customization will be overridden.
I see two ways:
-
Write/generate a partial class for each proxy that contains the retry variation. This is messy because you will still have to adjust it when the proxy changes
-
Make a custom version of svcutil that allows you to generate a proxy that derives from a different base class and put the retry code in that base class. This is quite some work but it can be done and solves the issue in a robust way.
-
xPridexgo through approach 1, then wrape all context event (open, opened, faulted, …) into event to be exposed by your class proxy, once the communication is faulted then re-create the proxy or call some recursive method inside proxy class. i can share with you some wok i have just tested.
public class DuplexCallBackNotificationIntegrationExtension : IExtension, INotificationPusherCallback { #region - Field(s) - private static Timer _Timer = null; private static readonly object m_SyncRoot = new Object(); private static readonly Guid CMESchedulerApplicationID = Guid.NewGuid(); private static CancellationTokenSource cTokenSource = new CancellationTokenSource(); private static CancellationToken cToken = cTokenSource.Token; #endregion #region - Event(s) - /// /// Event fired during Duplex callback. /// public static event EventHandler CallBackEvent; public static event EventHandler InstanceContextOpeningEvent; public static event EventHandler InstanceContextOpenedEvent; public static event EventHandler InstanceContextClosingEvent; public static event EventHandler InstanceContextClosedEvent; public static event EventHandler InstanceContextFaultedEvent; #endregion #region - Property(ies) - /// /// Interface extension designation. /// public string Name { get { return "Duplex Call Back Notification Integration Extension."; } } /// /// GUI Interface extension. /// public IUIExtension UIExtension { get { return null; } } #endregion #region - Constructor(s) / Finalizer(s) - /// /// Initializes a new instance of the DuplexCallBackNotificationIntegrationExtension class. /// public DuplexCallBackNotificationIntegrationExtension() { CallDuplexNotificationPusher(); } #endregion #region - Delegate Invoker(s) - void ICommunicationObject_Opening(object sender, System.EventArgs e) { DefaultLogger.DUPLEXLogger.Info("context_Opening"); this.OnInstanceContextOpening(e); } void ICommunicationObject_Opened(object sender, System.EventArgs e) { DefaultLogger.DUPLEXLogger.Debug("context_Opened"); this.OnInstanceContextOpened(e); } void ICommunicationObject_Closing(object sender, System.EventArgs e) { DefaultLogger.DUPLEXLogger.Debug("context_Closing"); this.OnInstanceContextClosing(e); } void ICommunicationObject_Closed(object sender, System.EventArgs e) { DefaultLogger.DUPLEXLogger.Debug("context_Closed"); this.OnInstanceContextClosed(e); } void ICommunicationObject_Faulted(object sender, System.EventArgs e) { DefaultLogger.DUPLEXLogger.Error("context_Faulted"); this.OnInstanceContextFaulted(e); if (_Timer != null) { _Timer.Dispose(); } IChannel channel = sender as IChannel; if (channel != null) { channel.Abort(); channel.Close(); } DoWorkRoutine(cToken); } protected virtual void OnCallBackEvent(Notification objNotification) { if (CallBackEvent != null) { CallBackEvent(this, new CallBackEventArgs(objNotification)); } } protected virtual void OnInstanceContextOpening(System.EventArgs e) { if (InstanceContextOpeningEvent != null) { InstanceContextOpeningEvent(this, e); } } protected virtual void OnInstanceContextOpened(System.EventArgs e) { if (InstanceContextOpenedEvent != null) { InstanceContextOpenedEvent(this, e); } } protected virtual void OnInstanceContextClosing(System.EventArgs e) { if (InstanceContextClosingEvent != null) { InstanceContextClosingEvent(this, e); } } protected virtual void OnInstanceContextClosed(System.EventArgs e) { if (InstanceContextClosedEvent != null) { InstanceContextClosedEvent(this, e); } } protected virtual void OnInstanceContextFaulted(System.EventArgs e) { if (InstanceContextFaultedEvent != null) { InstanceContextFaultedEvent(this, e); } } #endregion #region - IDisposable Member(s) - #endregion #region - Private Method(s) - /// /// /// void CallDuplexNotificationPusher() { var routine = Task.Factory.StartNew(() => DoWorkRoutine(cToken), cToken); cToken.Register(() => cancelNotification()); } /// /// /// /// void DoWorkRoutine(CancellationToken ct) { lock (m_SyncRoot) { var context = new InstanceContext(this); var proxy = new NotificationPusherClient(context); ICommunicationObject communicationObject = proxy as ICommunicationObject; communicationObject.Opening += new System.EventHandler(ICommunicationObject_Opening); communicationObject.Opened += new System.EventHandler(ICommunicationObject_Opened); communicationObject.Faulted += new System.EventHandler(ICommunicationObject_Faulted); communicationObject.Closed += new System.EventHandler(ICommunicationObject_Closed); communicationObject.Closing += new System.EventHandler(ICommunicationObject_Closing); try { proxy.Subscribe(CMESchedulerApplicationID.ToString()); } catch (Exception ex) { Logger.HELogger.DefaultLogger.DUPLEXLogger.Error(ex); switch (communicationObject.State) { case CommunicationState.Faulted: proxy.Close(); break; default: break; } cTokenSource.Cancel(); cTokenSource.Dispose(); cTokenSource = new CancellationTokenSource(); cToken = cTokenSource.Token; CallDuplexNotificationPusher(); } bool KeepAliveCallBackEnabled = Properties.Settings.Default.KeepAliveCallBackEnabled; if (KeepAliveCallBackEnabled) { _Timer = new Timer(new TimerCallback(delegate(object item) { DefaultLogger.DUPLEXLogger.Debug(string.Format("._._._._._. New Iteration {0: yyyy MM dd hh mm ss ffff} ._._._._._.", DateTime.Now.ToUniversalTime().ToString())); DBNotificationPusherService.Acknowledgment reply = DBNotificationPusherService.Acknowledgment.NAK; try { reply = proxy.KeepAlive(); } catch (Exception ex) { DefaultLogger.DUPLEXLogger.Error(ex); switch (communicationObject.State) { case CommunicationState.Faulted: case CommunicationState.Closed: proxy.Abort(); ICommunicationObject_Faulted(null, null); break; default: break; } } DefaultLogger.DUPLEXLogger.Debug(string.Format("Acknowledgment = {0}.", reply.ToString())); _Timer.Change(Properties.Settings.Default.KeepAliveCallBackTimerInterval, Timeout.Infinite); }), null, Properties.Settings.Default.KeepAliveCallBackTimerInterval, Timeout.Infinite); } } } /// /// /// void cancelNotification() { DefaultLogger.DUPLEXLogger.Warn("Cancellation request made!!"); } #endregion #region - Public Method(s) - /// /// Fire OnCallBackEvent event and fill automatic-recording collection with newest /// /// public void SendNotification(Notification objNotification) { // Fire event callback. OnCallBackEvent(objNotification); } #endregion #region - Callback(s) - private void OnAsyncExecutionComplete(IAsyncResult result) { } #endregion }