如何通用化重试逻辑:指定重试/抛出异常类型并提升可读性
Great question! To make your retry logic flexible, reusable, and easy to read, we can extract it into a dedicated helper method that lets you explicitly specify which exceptions trigger retries and which should be thrown immediately. Here's a step-by-step solution:
Step 1: Build a Reusable Retry Helper Class
We’ll create a static helper with both synchronous and asynchronous versions (since async is common in modern apps) that encapsulates all retry logic. The method will accept configurable parameters for retries, delays, and exception types.
Synchronous Version
using System; using System.Collections.Generic; using System.Linq; using System.Threading; public static class RetryHelper { /// <summary> /// Executes an action with retry logic, allowing specification of fatal exceptions (no retry) and retryable exceptions. /// </summary> /// <param name="action">The action to execute.</param> /// <param name="maxRetries">Total number of retry attempts allowed (total runs = maxRetries + 1).</param> /// <param name="delayBetweenRetries">Time to wait between retry attempts.</param> /// <param name="fatalExceptions">Exception types that should be rethrown immediately without retrying.</param> /// <param name="retryableExceptions">Exception types that trigger a retry attempt.</param> /// <exception cref="ArgumentNullException">Thrown if the action is null.</exception> /// <exception cref="ArgumentOutOfRangeException">Thrown if maxRetries is negative.</exception> /// <exception cref="AggregateException">Thrown if the action fails after all retries.</exception> public static void ExecuteWithRetry( Action action, int maxRetries, TimeSpan delayBetweenRetries, IEnumerable<Type> fatalExceptions, IEnumerable<Type> retryableExceptions) { // Validate input parameters first to catch errors early if (action == null) throw new ArgumentNullException(nameof(action)); if (maxRetries < 0) throw new ArgumentOutOfRangeException(nameof(maxRetries), "Max retries cannot be negative."); var fatalExceptionTypes = fatalExceptions?.ToHashSet() ?? new HashSet<Type>(); var retryableExceptionTypes = retryableExceptions?.ToHashSet() ?? new HashSet<Type>(); int retryCount = 0; while (true) { try { action(); return; // Success—exit the loop immediately } catch (Exception ex) { // Prioritize fatal exceptions (rethrow without retry) if (IsFatalException(ex, fatalExceptionTypes)) { throw; } // Check if this exception should trigger a retry if (!IsRetryableException(ex, retryableExceptionTypes)) { throw; } // If all retries are used up, wrap the exception for context if (retryCount >= maxRetries) { throw new AggregateException( $"Action failed after {maxRetries + 1} attempts.", ex); } // Wait before retrying Thread.Sleep(delayBetweenRetries); retryCount++; } } } /// <summary> /// Overload: Retry on all exceptions except the specified fatal ones. /// </summary> public static void ExecuteWithRetry( Action action, int maxRetries, TimeSpan delayBetweenRetries, params Type[] fatalExceptions) { ExecuteWithRetry( action, maxRetries, delayBetweenRetries, fatalExceptions, new[] { typeof(Exception) }); } // Helper methods to clean up the main logic private static bool IsFatalException(Exception ex, HashSet<Type> fatalTypes) { return fatalTypes.Any(type => type.IsInstanceOfType(ex)); } private static bool IsRetryableException(Exception ex, HashSet<Type> retryableTypes) { return retryableTypes.Any(type => type.IsInstanceOfType(ex)); } }
Asynchronous Version (Optional but Useful)
Add this to the RetryHelper class if you work with async/await workflows:
using System.Threading.Tasks; public static async Task ExecuteWithRetryAsync( Func<Task> action, int maxRetries, TimeSpan delayBetweenRetries, IEnumerable<Type> fatalExceptions, IEnumerable<Type> retryableExceptions) { if (action == null) throw new ArgumentNullException(nameof(action)); if (maxRetries < 0) throw new ArgumentOutOfRangeException(nameof(maxRetries), "Max retries cannot be negative."); var fatalExceptionTypes = fatalExceptions?.ToHashSet() ?? new HashSet<Type>(); var retryableExceptionTypes = retryableExceptions?.ToHashSet() ?? new HashSet<Type>(); int retryCount = 0; while (true) { try { await action(); return; } catch (Exception ex) { if (IsFatalException(ex, fatalExceptionTypes)) { throw; } if (!IsRetryableException(ex, retryableExceptionTypes)) { throw; } if (retryCount >= maxRetries) { throw new AggregateException( $"Action failed after {maxRetries + 1} attempts.", ex); } await Task.Delay(delayBetweenRetries); retryCount++; } } } // Async overload for fatal exceptions only public static async Task ExecuteWithRetryAsync( Func<Task> action, int maxRetries, TimeSpan delayBetweenRetries, params Type[] fatalExceptions) { await ExecuteWithRetryAsync( action, maxRetries, delayBetweenRetries, fatalExceptions, new[] { typeof(Exception) }); }
Step 2: Simplify Your Original Code
Replace your messy retry loop with a clean, readable call to the helper. Using named parameters makes the intent crystal clear:
private const int RetrySegmentCount = 3; private const int SecondsBetweenRetry = 30; // Your original logic simplified RetryHelper.ExecuteWithRetry( action: ExecuteProcessThatMayThrow, maxRetries: RetrySegmentCount, delayBetweenRetries: TimeSpan.FromSeconds(SecondsBetweenRetry), fatalExceptions: new[] { typeof(NotSupportedException) });
If you want to retry only on specific exceptions (e.g., IOException and TimeoutException), use the full overload:
RetryHelper.ExecuteWithRetry( action: ExecuteProcessThatMayThrow, maxRetries: RetrySegmentCount, delayBetweenRetries: TimeSpan.FromSeconds(SecondsBetweenRetry), fatalExceptions: new[] { typeof(NotSupportedException) }, retryableExceptions: new[] { typeof(IOException), typeof(TimeoutException) });
Key Improvements
- Flexibility: Configure exactly which exceptions to retry on and which to fail fast for.
- Readability: Named parameters and XML comments make the code self-documenting.
- Reusability: The helper can be used across your entire codebase instead of duplicating retry logic.
- Robustness: Input validation catches errors early, and wrapping failures in
AggregateExceptionprovides context about retry attempts. - Async Support: Seamlessly integrates with modern async/await workflows.
内容的提问来源于stack exchange,提问作者CuriousDeveloper

