-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Non generic and generic policies
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 returningTResult
.
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.
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 Func
s 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>
are:
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<HttpResponseException>() // 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);
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.
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.
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>()
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).
TimeoutPolicy timeout = ... // (non-generic)
RetryPolicy<HttpResponseMessage> retry = ... // (generic)
CircuitBreakerPolicy<HttpResponseMessage> breaker = ... // (generic)
FallbackPolicy<HttpResponseMessage> fallback = ... // (generic)
// Polly permits this, mixing non-generic and generic policies into a generic PolicyWrap
PolicyWrap<HttpResponseMessage> combinedResilience =
fallback
.WrapAsync(breaker)
.WrapAsync(retry)
.WrapAsync(timeout);
For further information on combining policies, see the PolicyWrap wiki.
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:
-
ICircuitBreakerPolicy
groups what is common to non-generic and generic circuit breakers. -
ICircuitBreakerPolicy<TResult>
adds what is specific to the genericTResult
form -
ICircuitBreakerPolicy<TResult> : ICircuitBreakerPolicy
.
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>
.
- Home
- Polly RoadMap
- Contributing
- Transient fault handling and proactive resilience engineering
- Supported targets
- Retry
- Circuit Breaker
- Advanced Circuit Breaker
- Timeout
- Bulkhead
- Cache
- Rate-Limit
- Fallback
- PolicyWrap
- NoOp
- PolicyRegistry
- Polly and HttpClientFactory
- Asynchronous action execution
- Handling InnerExceptions and AggregateExceptions
- Statefulness of policies
- Keys and Context Data
- Non generic and generic policies
- Polly and interfaces
- Some policy patterns
- Debugging with Polly in Visual Studio
- Unit-testing with Polly
- Polly concept and architecture
- Polly v6 breaking changes
- Polly v7 breaking changes
- DISCUSSION PROPOSAL- Polly eventing and metrics architecture