2 minute read

When building integrations with REST services or remote systems, we frequently encounter intermittent failuresโ€”null responses, incomplete data, or unexpected exceptions. A single failed call is often not the real issue; the challenge arises when the retry mechanism does not actually re-execute the operation. This happens when we pass the result of a method rather than the method itself.

A reliable pattern to address this problem is to use a function (factory delegate). This ensures that each retry performs a fresh attempt, giving the operation a chance to succeed if the failure was temporary.

๐Ÿงฉ The Issue with Passing Pre-Evaluated Values

Consider the following pattern:

var feed = GetAllVersions(...);
var result = RetryUtils.WithRetry(feed);

In this scenario, the retry routine receives a final value. If the first fetch returned null, the retry mechanism simply handles the same null on every attempt. No fresh calls are made, and the retry becomes ineffective.

๐Ÿ› ๏ธ The Factory Delegate Solution

A better approach is to pass a function that contains the logic:

var result = RetryUtils.WithRetry(() => GetAllVersions(...));

This changes the behavior significantly. Each retry now calls the delegate, creating a new opportunity for success. Transient issues such as service delays or network interruptions can be naturally recovered.

๐Ÿ”„ How the Delegate Enables Real Retries

A factory delegate stores the operation without executing it:

Func<T> fetchOperation = () => PerformOperation();

This delegate is invoked inside the retry loop. As the retry count increases, each iteration triggers another execution, ensuring we are not reusing any failed result.

๐Ÿงช Simple Example of Retry Logic Using a Factory Delegate

public static T Retry<T>(Func<T> action, int maxRetries = 3)
{
    T lastResult = default;

    for (int i = 0; i <= maxRetries; i++)
    {
        try
        {
            lastResult = action();

            if (lastResult != null)
                return lastResult;
        }
        catch
        {
            // errors are tolerated within retry attempts
        }

        Thread.Sleep(1000);
    }

    return lastResult;
}

Example usage:

var feed = Retry(() => d.GetAllVersions<Document>(FeedGetOptionsFactory.Full.Value));

Each retry performs a new request, improving resilience during temporary failures.

๐Ÿ“˜ When This Pattern Helps

This pattern becomes highly effective when dealing with:

  • cloud platforms prone to intermittent latency,
  • APIs that occasionally return null or incomplete data,
  • operations that must be attempted multiple times before succeeding,
  • integrations with content management systems like Documentum or SharePoint.

By ensuring each retry repeats the operation, we reduce the risk of failure caused by unstable external services.

๐Ÿ Conclusion

Passing a function rather than a pre-evaluated result is a small shift with a significant impact. Factory delegates make retry logic reliable by ensuring every attempt performs genuine work. In C#, this approach strengthens the resilience of our integrations and improves overall system stability.

This technique is a valuable addition to our development toolkit, especially when building robust applications that interact with unpredictable external systems.

Leave a comment