Why does Polly offer both non-generic and generic policies?
Background
Polly offers both non-generic policies Policy with a generic .Execute<TResult>(...) method, as well as generic policies Policy<TResult>. This article sets out to answer questions Polly users sometimes raise: Why do both exist? And why can’t I just use the generic .Execute<TResult>(...) method everywhere?
TL;DR Offering generic Policy<TResult> policies allows compile-time type-binding and intellisense for two key scenarios:
- when configuring policies to handle
TResulttypes - when using
PolicyWrapto combine policies for executions returningTResult.
Non-generic policies: Policy
The non-generic policy Policy can be used to execute delegates returning void.
Policy policy = // ... (non-generic)
policy.Execute(/* some Action */)
It also offers a generic method overload to execute delegates returning any TResult:
Policy policy = // ... (non-generic still)
TResult result = policy.Execute<TResult>(/* some Func<..., TResult> */)
Given the non-generic policies offer this generic execute method, why also the generic policies Policy<TResult>?
Generic policies: Policy<TResult>
The drawback with the generic method overload above, on the non-generic policy, is that its scope is limited to that method: it can’t offer compile-time type-binding to anything beyond that execute overload. And that means no type-binding to any other aspect of Policy or PolicyWrap configuration.
And multiple typed operations with no type-binding between them spells trouble.
To explore this in more detail, let’s look at some examples.
Binding .HandleResult<TResult>(...) and .Execute<TResult>(...)
Generic policies Policy<TResult> allow compile-time type binding between .HandleResult<TResult>(...) and the .Execute<TResult>(...) calls made on the policy. For example:
Policy<HttpResponseMessage> policy = Policy
.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.BadGateway)
.WaitAndRetryAsync(4, TimeSpan.FromSeconds(5));
HttpResponseMessage result = policy.ExecuteAsync(/* some Func<HttpResponseMessage> */);
Without this type-binding, it would be possible to write (and compile) non-sensical code such as:
Policy
.Handle<foo>(Func<foo, bool>)
.Retry(2)
.Execute<bar>(Func<bar>);
This was deemed unacceptable. If executing Func<bar> on a foo-handling Policy was compilable, how should it execute?
- If the
foo/barmismatch were to throw an exception, then why not enforce the type matching at compile time, rather than leave it to a run-time failure? - If the
foo/barmismatch were to not throw an exception, it would have to be silently ignored. (There’s no other meaningful option.) But this carries the grave risk of leading users into a pit of failure. Unwittingly mismatching the.Handle<>()type and the.Execute<>()type would lead to silent failure of the Policy. This could be particularly pernicious when refactoring - a slight wrong change and, poof, your Polly protection is (silently) gone.
Binding multiple Policy<TResult> instances into a PolicyWrap<TResult>
Generic policies Policy<TResult> also allow compile-time type-binding between different Policy<TResult> instances combined into a PolicyWrap<TResult>.
In a policy protecting an Http call, for example, you might create a PolicyWrap<HttpResponseMessage> combining:
- a
RetryPolicy<HttpResponseMessage>handling particularHttpResponseMessage.StatusCodevalues - a
CircuitBreakerPolicy<HttpResponseMessage>handlingHttpResponseMessage.StatusCodevalues - a
FallbackPolicy<HttpResponseMessage>.
The generic policies give you the intellisense and compile-time checking to combine these only correctly, just as when coding other generic functional monads such as Linq or Rx expressions.
The alternative - permitting policy1<Foo>.Wrap(policy2<Bar>) - implies the same problems around non-type-safe combinations discussed above.
Mixing non-generic and generic polices into a PolicyWrap<TResult>
A riff on the above is that you can however combine non-generic and generic policies in a PolicyWrap.
Returning to the preceding PolicyWrap<HttpResponseMessage>, you could additionally combine in a TimeoutPolicy. TimeoutPolicy doesn’t respond to results; it pre-empts them, when necessary. So it’s always non-generic. Polly therefore permits the following:
TimeoutPolicy timeout = // ...
RetryPolicy<HttpResponseMessage> retry = // ...
CircuitBreakerPolicy<HttpResponseMessage> breaker = // ...
FallbackPolicy<HttpResponseMessage> fallback = // ...
// Polly permits this, mixing non-generic and generic policies.
PolicyWrap<HttpResponseMessage> combinedResilience =
fallback
.Wrap(breaker)
.Wrap(retry)
.Wrap(timeout);
For further information on combining policies, see the PolicyWrap wiki.
Summary
Polly’s generic Policy<TResult> policies allows compile-time type-binding when configuring policies to handle TResult types, and when using PolicyWrap to combine policies handling TResult executions. This avoids the pitfalls of type-unsafe operations at runtime.
If however your policy needs don’t extend to .HandleResult() clauses or policies that are intrinsically typed - FallbackPolicy and CachePolicy - then non-generic policies with the flexible generic method Policy.Execute<TResult>(...) remain your friend.