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
TResult
types - when using
PolicyWrap
to 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/bar
mismatch 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/bar
mismatch 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.StatusCode
values - a
CircuitBreakerPolicy<HttpResponseMessage>
handlingHttpResponseMessage.StatusCode
values - 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.