Skip to content

Non generic and generic policies

martincostello edited this page Sep 28, 2023 · 25 revisions

Why does Polly offer both non-generic and generic policies?

ℹ️ This documentation describes the previous Polly v7 API. If you are using the new v8 API, please refer to pollydocs.org.

TL;DR Policy<TResult> policies generic-typed to TResult enable compile-time type-binding and intellisense:

  • when configuring policies to .Handle<TResult>() return values;
  • when passing results to delegate hooks;
  • when using Fallback<TResult>;
  • when using PolicyWrap to combine policies already strongly-typed to executions returning TResult.

Non-generic policies, Policy

Polly offers non-generic policies: RetryPolicy, CircuitBreakerPolicy (etc), each extending the base non-generic type Policy.

These offer void-returning .Execute(), and generic method overloads .Execute<TResult>(...):

public abstract class Policy // (much elided!)
{
    void          Execute(Action action);               // (and many similar overloads)
    TResult       Execute<TResult>(Func<TResult> func); // generic _method_ overload (and many similar)
    Task          ExecuteAsync(Func<Task> action);      // (and many similar overloads)
    Task<TResult> ExecuteAsync<TResult>(Func<Task<TResult>> func); // generic _method_ overload (and many similar)
}

This offers maximum flexibility of what can be executed through the policy, for simpler use cases.

For fault-handling policies such as retry or circuit-breaker, these non-generic policies can be used flexibly across return types, provided you are only handling exceptions with the policy, not results.

Non-reactive policies such as Bulkhead and Timeout also configure to this form by default.

Generic policies, Policy<TResult>

The generic method overloads on non-generic Policy (above) offer flexibility, but can't offer compile-time type-binding to anything beyond that .Execute<TResult>() generic method. This is limiting if we want to do anything more with TResult.

Once multiple features are being used referencing TResult, where compile-time binding makes sense, Polly's configuration syntax instead returns you generic policies RetryPolicy<TResult>, CircuitBreakerPolicy<TResult> (etc), each extending the base Policy<TResult>.

These execute Funcs strongly-typed to return TResult:

public abstract class Policy<TResult> // (much elided!)
{
    TResult       Execute(Func<TResult> func);            // (and many similar overloads)
    Task<TResult> ExecuteAsync(Func<Task<TResult>> func); // (and many similar overloads)
}

Features that drive the transition to Policy<TResult>

Features that drive the transition to Policy<TResult> are:

(1) Binding .HandleResult<TResult>(..) and .Execute<TResult>(..)

When a .HandleResult<TResult>(...) clause is used, the generic Policy<TResult> enforces compile-time type binding between the .HandleResult<TResult>(...) clause and .Execute<TResult>(...) calls made on the policy.

RetryPolicy<HttpResponseMessage> httpRetryPolicy = Policy
    .HandleResult<HttpResponseMessage> // executions will now be bound to HttpResponseMessage
        (r => r.StatusCode == HttpStatusCode.InternalServerError) // and you can handle return results directly with the policy
    .Or<HttpRequestException>() // as well as exceptions
    .WaitAndRetryAsync(new[] {
        TimeSpan.FromSeconds(1),
        TimeSpan.FromSeconds(2),
        TimeSpan.FromSeconds(4)
    });

HttpResponseMesage response = await httpRetryPolicy.ExecuteAsync(
    ct => httpClient.GetAsync(url, ct), // compile-time type-bound to HttpResponseMessage
    cancellationToken);

Why?

If strong-typing were not used, it would be possible to write (and compile) non-sensical code such as:

Policy
    .HandleResult<foo>(Func<foo, bool>)
    .Retry(2)
    .Execute<bar>(Func<bar>);

This was deemed unacceptable. If executing a Func<bar> on a foo-handling Policy was permitted, what to do?

  • If the foo/bar mismatch were to throw an exception, then why not enforce the type matching at compile time instead of leave it to a (harder to discover/debug) run-time failure?
  • If the foo/bar mismatch were to not throw an exception, it would have to be silently ignored. But this would carry the grave risk of leading users into a pit of failure. Unwittingly mismatching the .HandleResult<T>() type and the .Execute<T>() type would lead to silent loss of operation of the Policy. This could be particularly pernicious when refactoring - a slight wrong change and your Polly protection would be (silently) gone.

(2) TResult execution results passed to policy hooks

Typed policies Policy<TResult> permit type-binding of TResult-values passed to delegate hooks such as onRetry, onBreak, onFallback etc.

HttpStatusCode[] httpStatusCodesWorthRetrying = {
   HttpStatusCode.RequestTimeout, // 408
   HttpStatusCode.InternalServerError, // 500
   HttpStatusCode.BadGateway, // 502
   HttpStatusCode.ServiceUnavailable, // 503
   HttpStatusCode.GatewayTimeout // 504
};
Retry<HttpResponseMessage> httpRetryPolicy = Policy
  .HandleResult<HttpResponseMessage>(r => httpStatusCodesWorthRetrying.Contains(r.StatusCode))
  .WaitAndRetryAsync(retryDelays,
    async (outcome, duration) => {
      await logger.LogError($"HttpCall failed with status code {outcome.Result.StatusCode}"); // outcome.Result is passed as HttpResponseMessage
    });

Without strongly-typed Policy<TResult>, outcomes would have to be passed as object and endure ugly casting back to TResult within the delegate hooks.

(3) Binding multiple Policy<TResult> instances into a PolicyWrap<TResult>

Generic policies Policy<TResult> also enable compile-time type-binding between different Policy<TResult> instances combined into a PolicyWrap<TResult>.

FallbackPolicy<HttpResponseMessage> fallback = // ...
RetryPolicy<HttpResponseMessage> retry = // ...
CircuitBreakerPolicy<HttpResponseMessage> breaker = // ...

PolicyWrap<HttpResponseMessage> combinedResilience =
    Policy.WrapAsync(fallback, retry, breaker); // compile-time type-bound

The generic policies give you the compile-time intellisense to only combine these correctly, just as when coding other generic functional monads such as in Linq or Rx. This is just as Linq does not allow you to write .Select<int>().Where<foo>().OrderBy<bar>()

Mixing non-generic and generic in a PolicyWrap<TResult>

You can combine non-generic Policy instances with generic Policy<TResult> into a resulting PolicyWrap<TResult>.

Take the preceding PolicyWrap<HttpResponseMessage> example. You could combine in a TimeoutPolicy (non-generic by default, as it doesn't handle results).

FallbackPolicy<HttpResponseMessage> fallback      = ... // (generic)
CircuitBreakerPolicy<HttpResponseMessage> breaker = ... // (generic)
RetryPolicy<HttpResponseMessage> retry            = ... // (generic)
TimeoutPolicy timeout                             = ... // (non-generic)

// Polly permits this, mixing non-generic and generic policies into a generic PolicyWrap
// provided the instance syntax (as below) is used to create the PolicyWrap.
PolicyWrap<HttpResponseMessage> combinedResilience =
    fallback
    .WrapAsync(breaker)
    .WrapAsync(retry)
    .WrapAsync(timeout);

For further information on combining policies, see the PolicyWrap wiki.

Policy<TResult> does not extend Policy

Policy<TResult> necessarily does not extend non-generic Policy. The whole raison-d'etre of Policy<TResult> is to restrict executions to delegates returning a single TResult type. Policy allows executions returning void, and multiple TResult types for a simpler feature set.

Instead, Polly's interfaces group what is common to non-generic and generic policies of the same policy type. For example:

At the base class level, IsPolicy is a marker interface, signifying 'this is some kind of policy'.
IsPolicy contains what is common to the base classes Policy and Policy<TResult>.

When you hold a non-generic policy but need a generic policy

Some policies naturally define in the non-generic form, as they are not reactive to results:

IAsyncPolicy timeout = Policy.TimeoutAsync(10);

Policy consumers may however sometimes require a generic policy. HttpClientFactory in ASP.NET Core 2.1, for example, configures policies for use with HttpClient calls. All these policies govern executions returning HttpResponseMessage, so the configuration overloads only accept generic policies IAsyncPolicy<HttpResponseMessage>.

From Polly v5.9.0, Polly offers helper extension methods to facilitate the conversion:

// non-generic ISyncPolicy -> ISyncPolicy<TResult>
ISyncPolicy timeout = Policy.Timeout(10);
ISyncPolicy<TResult> genericVersion = timeout.AsPolicy<TResult>();

// non-generic IAsyncPolicy -> IAsyncPolicy<TResult>
IAsyncPolicy timeout = Policy.TimeoutAsync(10);
IAsyncPolicy<HttpResponseMessage> httpTimeout = timeout.AsAsyncPolicy<HttpResponseMessage>();

Note that all policies can be defined natively in the generic form. For instance, the TimeoutPolicy above can be defined:

IAsyncPolicy<HttpResponseMessage> httpTimeout = Policy.TimeoutAsync<HttpResponseMessage>(10);

The helper method .AsAsyncPolicy<HttpResponseMessage>() exists simply for those times when you already have an IAsyncPolicy in hand.

Clone this wiki locally