public static bool TryInvoke<TException, T>(Func<T> del, out T value, out IList<Exception> exceptions, int attempts = 3, IInterval interval = null) where TException : Exception { if (del == null) throw new ArgumentNullException("del"); if (attempts <= 0) throw new ArgumentOutOfRangeException("attempts"); exceptions = new List<Exception>(); for (int i = 0; i < attempts; i++) { try { value = del(); return true; } catch (TException ex) { exceptions.Add(ex); if (interval != null) interval.Sleep(); } } value = default(T); return false; }
The above generic implementation of the Retry Pattern also follows the structure of the Try-Parse Pattern in that if the invocation of the delegate fails on all of it's attempts then the "out" parameter for the return value of the delegate is set to it's default value and the method returns false. Conversely, if the delegate succeeds then the return value of the delegate is assigned to the "out" parameter and returns true - this means we can invoke delegates as follows:
int value; Func<int> calculateValue = null; if (Invoker.TryInvoke<int>(calculateValue, out value)) { //print value Console.WriteLine("Value: {0}", value); } else { //show error message to user Console.WriteLine("Error calculating value after X attempts"); }
It is also possible for us to gracefully retry on specific types of exceptions and throw exceptions on others. For example, I may expect a failed attempt whilst downloading some data from an external source, therefore I may want to retry ONLY when a DownloadException is thrown, all other exceptions should cause an unhandled exception to be thrown:
string data; Func<string> getData = null; IList<Exception> exceptions; try { if (Invoker.TryInvoke<DownloadException, string>(getData, out data, out exceptions)) { //succeeded } else { //gracefully handle exceptions } } catch (Exception ex) { //unexpected exception }
We may also want to run this logic asynchronously, for example when a user clicks a button on a GUI and requests to download some data from an external source:
Func<string> downloadData = () => { return new Downloader().Download(); }; var task = await Invoker.TryInvokeAsync<string>(downloadData); if (task.Success) { //invocation passed Console.WriteLine(task.Value); }
Note how the syntax of the TryInvokeAsync method has changed slightly in that it no longer contains an out parameter for the assigned value - async methods are unable to support out parameters.
The complete source code of the Retry Invoker can be found on GitHub.