在任务exception的情况下,根据用户input多次重试任务

在我的应用程序中的所有服务调用都是作为任务来实现的。当任务发生故障时,我需要向用户提供一个对话框,以重试上次操作失败。如果用户select重试,程序应该重试任务,否则程序的执行应该在loggingexception之后继续执行。任何人都已经对如何实现这个function有了一个高层次的想法?

更新5/2017

C#6exceptionfilter使得catch子句更简单:

  private static async Task<T> Retry<T>(Func<T> func, int retryCount) { while (true) { try { var result = await Task.Run(func); return result; } catch when (retryCount-- > 0){} } } 

和一个recursion版本:

  private static async Task<T> Retry<T>(Func<T> func, int retryCount) { try { var result = await Task.Run(func); return result; } catch when (retryCount-- > 0){} return await Retry(func, retryCount); } 

原版的

编写Retry函数的方法有很多种:您可以使用recursion或任务迭代。 希腊.NET用户组讨论了一段时间,然后讨论了完成这个的不同方法。
如果你使用F#,你也可以使用Async结构。 不幸的是,至less在Async CTP中不能使用async / await结构,因为编译器生成的代码并不像多个等待或catch块中的可能的重新抛出。

recursion版本可能是在C#中构build重试的最简单方法。 以下版本不使用解包并在重试之前添加可选延迟:

 private static Task<T> Retry<T>(Func<T> func, int retryCount, int delay, TaskCompletionSource<T> tcs = null) { if (tcs == null) tcs = new TaskCompletionSource<T>(); Task.Factory.StartNew(func).ContinueWith(_original => { if (_original.IsFaulted) { if (retryCount == 0) tcs.SetException(_original.Exception.InnerExceptions); else Task.Factory.StartNewDelayed(delay).ContinueWith(t => { Retry(func, retryCount - 1, delay,tcs); }); } else tcs.SetResult(_original.Result); }); return tcs.Task; } 

StartNewDelayed函数来自ParallelExtensionsExtras示例,并使用计时器在发生超时时触发TaskCompletionSource。

F#版本更简单一些:

 let retry (asyncComputation : Async<'T>) (retryCount : int) : Async<'T> = let rec retry' retryCount = async { try let! result = asyncComputation return result with exn -> if retryCount = 0 then return raise exn else return! retry' (retryCount - 1) } retry' retryCount 

不幸的是,在C#中使用async / await从Async CTP编写类似的东西是不可能的,因为编译器不喜欢在catch块中等待语句。 下面的尝试也失败了沉默,因为运行时不喜欢遇到exception后等待:

 private static async Task<T> Retry<T>(Func<T> func, int retryCount) { while (true) { try { var result = await TaskEx.Run(func); return result; } catch { if (retryCount == 0) throw; retryCount--; } } } 

至于询问用户,可以修改Retry来调用一个函数,询问用户并通过TaskCompletionSource返回一个任务,当用户回答时触发下一步,例如:

  private static Task<bool> AskUser() { var tcs = new TaskCompletionSource<bool>(); Task.Factory.StartNew(() => { Console.WriteLine(@"Error Occured, continue? Y\N"); var response = Console.ReadKey(); tcs.SetResult(response.KeyChar=='y'); }); return tcs.Task; } private static Task<T> RetryAsk<T>(Func<T> func, int retryCount, TaskCompletionSource<T> tcs = null) { if (tcs == null) tcs = new TaskCompletionSource<T>(); Task.Factory.StartNew(func).ContinueWith(_original => { if (_original.IsFaulted) { if (retryCount == 0) tcs.SetException(_original.Exception.InnerExceptions); else AskUser().ContinueWith(t => { if (t.Result) RetryAsk(func, retryCount - 1, tcs); }); } else tcs.SetResult(_original.Result); }); return tcs.Task; } 

通过所有的延续,你可以看到为什么一个asynchronous版本的重试是如此理想。

更新:

在Visual Studio 2012 Beta中,以下两个版本起作用:

带有while循环的版本:

  private static async Task<T> Retry<T>(Func<T> func, int retryCount) { while (true) { try { var result = await Task.Run(func); return result; } catch { if (retryCount == 0) throw; retryCount--; } } } 

和一个recursion版本:

  private static async Task<T> Retry<T>(Func<T> func, int retryCount) { try { var result = await Task.Run(func); return result; } catch { if (retryCount == 0) throw; } return await Retry(func, --retryCount); } 

这里是我已经testing和正在使用的Panagiotis Kanavos的出色版本。

它解决了一些对我来说很重要的事情:

  • 想要根据前面的尝试次数和当前尝试的exception情况决定是否重试
  • 不想依赖async (较less的环境限制)
  • 希望在出现故障的情况下生成Exception ,包括每次尝试的详细信息

 static Task<T> RetryWhile<T>( Func<int, Task<T>> func, Func<Exception, int, bool> shouldRetry ) { return RetryWhile<T>( func, shouldRetry, new TaskCompletionSource<T>(), 0, Enumerable.Empty<Exception>() ); } static Task<T> RetryWhile<T>( Func<int, Task<T>> func, Func<Exception, int, bool> shouldRetry, TaskCompletionSource<T> tcs, int previousAttempts, IEnumerable<Exception> previousExceptions ) { func( previousAttempts ).ContinueWith( antecedent => { if ( antecedent.IsFaulted ) { var antecedentException = antecedent.Exception; var allSoFar = previousExceptions .Concat( antecedentException.Flatten().InnerExceptions ); if ( shouldRetry( antecedentException, previousAttempts ) ) RetryWhile( func,shouldRetry,previousAttempts+1, tcs, allSoFar); else tcs.SetException( allLoggedExceptions ); } else tcs.SetResult( antecedent.Result ); }, TaskContinuationOptions.ExecuteSynchronously ); return tcs.Task; } 

在高层次上,我发现从你拥有的和你想要的function签名是有帮助的。

你有:

  • 一个给你一个任务的函数( Func<Task> )。 我们将使用这个函数,因为任务本身通常不是可重试的。
  • 确定整个任务是完成还是应该重试的函数( Func<Task, bool>

你要:

  • 一个全面的任务

所以你会有一个像这样的function:

 Task Retry(Func<Task> action, Func<Task, bool> shouldRetry); 

扩展function内部的做法,任务几乎有2个操作与他们,阅读他们的状态和ContinueWith 。 为了完成你自己的任务, TaskCompletionSource是一个很好的起点。 第一次尝试可能看起来像这样:

 //error checking var result = new TaskCompletionSource<object>(); action().ContinueWith((t) => { if (shouldRetry(t)) action(); else { if (t.IsFaulted) result.TrySetException(t.Exception); //and similar for Canceled and RunToCompletion } }); 

这里显而易见的问题是只有1次重试会发生。 为了解决这个问题,你需要为函数自己调用一个方法。 通常的做法是使用lambdaexpression式,如下所示:

 //error checking var result = new TaskCompletionSource<object>(); Func<Task, Task> retryRec = null; //declare, then assign retryRec = (t) => { if (shouldRetry(t)) return action().ContinueWith(retryRec).Unwrap(); else { if (t.IsFaulted) result.TrySetException(t.Exception); //and so on return result.Task; //need to return something } }; action().ContinueWith(retryRec); return result.Task;