17

I'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<int>(() => _channel.Foo(snurl));
    }

    public string Bar(string snuh)
    {
        return MakeWcfCall<string>(() => _channel.Bar(snuh));
    }

    private static T MakeWcfCall<T>(Func<T> 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.

Bob Horn
  • 33,387
  • 34
  • 113
  • 219

6 Answers6

17

I 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 an IFoo channel, but in fact it is a IFoo to 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.

Jim
  • 4,910
  • 4
  • 32
  • 50
  • 1
    +1 I think this is exactly what I was looking for. Let me look into this, and if it works like I think it should, then this should be the accepted answer. Thanks! – Bob Horn Apr 22 '13 at 17:08
  • 2
    I got this working. I'm able to intercept WCF calls. Now I just need to implement the retry mechanism. Thanks again! – Bob Horn Apr 22 '13 at 18:48
  • @BobHorn, you're welcome! If you want to integrate into the whole WCF channel creation process you can derive from ChannelFactory and override CreateChannel. Inside CreateChannel, you would use your RealProxy derived class to create the channel proxy to return. – Jim Apr 22 '13 at 19:17
  • I've actually just done that. I may post my code here as a reference. Not sure if anyone would be interested, but it would probably help me later when I forget how I did this. :) – Bob Horn Apr 22 '13 at 19:19
  • 1
    Hello, The need that you have right here is basically how to implement a "cross-cutting" concern (retry, other example might be logging) Aspect Oriented programming solves that issue: https://en.wikipedia.org/wiki/Aspect-oriented_programming This is actually a very clean solution, let me suggest taking a look into Interception with Unity: http://msdn.microsoft.com/en-us/library/ff647107.aspx and Aspect Oriented Programming http://msdn.microsoft.com/en-us/magazine/gg490353.aspx If you have that concern in other areas of your code, and want to make those behaviors configurable. Cheers, – wacdany Apr 23 '13 at 09:01
  • 1
    any final solution with full source code sample working about it ? – Kiquenet Nov 12 '13 at 12:34
17

Note: 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<IPrestoService>(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<T> : ChannelFactory<T> 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<T>(this);
        return proxy.GetTransparentProxy() as T;
    }
}

The WcfClientProxy: (This is where we intercept and retry.)

    public class WcfClientProxy<T> : RealProxy where T : class
    {
        private WcfChannelFactory<T> _channelFactory;

        public WcfClientProxy(WcfChannelFactory<T> 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:

enter image description here

Bob Horn
  • 33,387
  • 34
  • 113
  • 219
  • How do we manage the connection close/dispose in the consuming code block? Won't it lead to the memory leak issues if we do not close them? – Mani May 11 '17 at 12:55
  • I think this would work only for synchronous calls. In case of async calls to contract (Begin/End), an Exception would be catched only by the callback (End action); any idea to implement retries in this scenario? – Stefano Luoni Jun 28 '21 at 16:17
5

You can implement generic proxy for example using Castle. There is a good article here http://www.planetgeek.ch/2010/10/13/dynamic-proxy-for-wcf-with-castle-dynamicproxy/. This approach will give user object which implements interface and you will have one class responsible for comunication

Piotr Stapp
  • 19,392
  • 11
  • 68
  • 116
  • +1 This looks promising. Interception is what is needed here. Speaking of that, I've just come across RealProxy: http://msdn.microsoft.com/en-us/library/system.runtime.remoting.proxies.realproxy(v=vs.100).aspx. I need to check that out as well. – Bob Horn Apr 22 '13 at 16:41
  • If RealProxy gives you enought flexibility it is a good choice – Piotr Stapp Apr 22 '13 at 16:47
  • 1
    If you need an example of what Garath is proposing you can view my answer here: http://stackoverflow.com/questions/7085406/how-to-make-alternative-use-of-interfaces-for-wcf-contracts/12100784#12100784 In the implementation of Intercept you would add your retry logic there. In theory then if you use this pattern you would get retry for all your methods on your WCF service. – Phil Patterson Apr 22 '13 at 16:49
  • @PhilPatterson Nice! Thanks for the link. If RealProxy (.NET class) doesn't work, your link looks like the way to go. – Bob Horn Apr 22 '13 at 16:53
  • 1
    @BobHorn It appears you could create the same pattern with the RealProxy class (which would have the advantage of not taking an external dependency on Castle. For the type being passed in you will probably want to create a class or interface that exposes the WCF contract and the IClientChannel interface so that you have access to the underlying communication methods. At that point using DyanmicProxy or RealProxy would just be an implementation detail. Of course there are multiple ways to accomplish this, hopefully you find inspiration that leads to a method you are comfortable supporting. – Phil Patterson Apr 22 '13 at 16:59
0

Do not mess with generated code because, as you mentioned, it will be generated again so any customization will be overridden.

I see two ways:

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

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

Emond
  • 50,210
  • 11
  • 84
  • 115
  • I wasn't clear; there is no generated code. The proxy shown above is my own class. No proxy is generated as I'm dealing with a system that owns both ends of the wire and can reference the interface. – Bob Horn Apr 22 '13 at 16:15
0

go 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) -
    /// <summary>
    /// Event fired during Duplex callback.
    /// </summary>
    public static event EventHandler<CallBackEventArgs> CallBackEvent;

    public static event EventHandler<System.EventArgs> InstanceContextOpeningEvent;
    public static event EventHandler<System.EventArgs> InstanceContextOpenedEvent;
    public static event EventHandler<System.EventArgs> InstanceContextClosingEvent;
    public static event EventHandler<System.EventArgs> InstanceContextClosedEvent;
    public static event EventHandler<System.EventArgs> InstanceContextFaultedEvent;
    #endregion

    #region - Property(ies) -
    /// <summary>
    /// Interface extension designation.
    /// </summary>
    public string Name {
        get {
            return "Duplex Call Back Notification Integration Extension.";
        }
    }

    /// <summary>
    /// GUI Interface extension.
    /// </summary>
    public IUIExtension UIExtension {
        get {
            return null;
        }
    }
    #endregion

    #region - Constructor(s) / Finalizer(s) -
    /// <summary>
    /// Initializes a new instance of the DuplexCallBackNotificationIntegrationExtension class.
    /// </summary>
    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) -

    /// <summary>
    /// 
    /// </summary>
    void CallDuplexNotificationPusher() {
        var routine = Task.Factory.StartNew(() => DoWorkRoutine(cToken), cToken);
        cToken.Register(() => cancelNotification());
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="ct"></param>
    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);
            }
        }
    }

    /// <summary>
    /// 
    /// </summary>
    void cancelNotification() {
       DefaultLogger.DUPLEXLogger.Warn("Cancellation request made!!");
    }
    #endregion 

    #region - Public Method(s) -
    /// <summary>
    /// Fire OnCallBackEvent event and fill automatic-recording collection with newest 
    /// </summary>
    /// <param name="action"></param>
    public void SendNotification(Notification objNotification) {

        // Fire event callback.
        OnCallBackEvent(objNotification);
    }
    #endregion

    #region - Callback(s) -
    private void OnAsyncExecutionComplete(IAsyncResult result) {

    }
    #endregion
}
xPridex
  • 91
  • 1
  • 8
0

Just wrap all service calls in a function, taking a delegate that would execute the passed delegate the amount of time necessary

internal R ExecuteServiceMethod<I, R>(Func<I, R> serviceCall, string userName, string password) {

    //Note all clients have the name Manager, but this isn't a problem as they get resolved        
    //by type
    ChannelFactory<I> factory = new ChannelFactory<I>("Manager");
    factory.Credentials.UserName.UserName = userName;
    factory.Credentials.UserName.Password = password;

    I manager = factory.CreateChannel();
    //Wrap below in a retry loop
    return serviceCall.Invoke(manager);
}
Bob Horn
  • 33,387
  • 34
  • 113
  • 219
3dd
  • 2,520
  • 13
  • 20