There are several ways to combine two policies. Let me list all the different options for the sake of completeness. I will add recommendations for each: when to use it and when to avoid it
Branch on exception
ExceptionDispatchInfo edi = null;
int retryCounter = 0;
var combinedPolicy = Policy
.Handle<DivideByZeroException>()
.Or<StackOverflowException>()
.WaitAndRetry(CalculateSleep());
combinedPolicy.Execute(() => {
try
{
//User code...
throw new DivideByZeroException();
}
catch (Exception ex)
{
edi = ExceptionDispatchInfo.Capture(ex);
edi.Throw();
}
});
IEnumerable<TimeSpan> CalculateSleep()
{
while (true)
{
var wasItADivideByZero = edi.SourceException is DivideByZeroException;
var attempt = retryCounter++;
if (wasItADivideByZero)
{
if (attempt > 3) break;
yield return TimeSpan.FromSeconds(1);
}
yield return TimeSpan.FromSeconds(0);
}
}
Notes
- The
WaitAndRetry
does not have an overload where you don't have to specify the retryCount
but you can access the thrown exception inside the sleepDurations
- So, the built-in options support: either use
retryCount
+ sleepDurationProvider
(from where you can access the exception) or use sleepDurations
- That's why we have to use
ExceptionDispatchInfo
to capture the thrown exception and access it inside our custom CalculateSleep
method
Use it
- If you would have two policies with the same amount of maximum retry attempts but with different sleep and different trigger, like this
var combinedPolicy = Policy
.Handle<DivideByZeroException>()
.Or<StackOverflowException>()
.WaitAndRetry(3,
(_, ex, __) => TimeSpan.FromSeconds(ex is DivideByZeroException ? 1 : 2),
(_, __, ___, ____) => { });
Avoid it
- Do not re-invent the wheel if you have other alternatives
- Even though the above solution works like a charm it is complex and hard to maintain
Explicit continuation
policy1.Execute(() =>
{
policy2.Execute(() =>
{
//User code
});
});
OR
policy2.Execute(() =>
{
policy1.Execute(() =>
{
//User code
});
});
Notes
- The order does not matter here, because the two policies are independent
- So, if we would have a retry and a timeout policy then the order would matter
- Outer: Timeout; Inner: Retry >> Timeout is a global, overarching
- Outer: Retry; Inner: Timeout >> Timeout is local (so per attempt)
Use it
- If you have no more than two policies and the to-be-decorated code is extracted into a separate method, like this
policy1.Execute(policy2.Execute(UserCode()));
Avoid it
The Wrap
instance method
var combinedPolicy = policy1.Wrap(policy2);
combinedPolicy.Execute(UserCode());
OR
var combinedPolicy = policy2.Wrap(policy1);
combinedPolicy.Execute(UserCode());
Notes
- We could separate the policy definition from its execution
- The ordering does not matter here as well
- The
combinedPolicy
's type is PolicyWrap
, which implements the ISyncPolicy
interface as well
Use it
- When you want to separate the policy definition from its usage
- It is suitable for combining no more than two policies
Avoid it
- If you want to combine more than 3 then it looks ugly IMHO
var combinedPolicy = globalTimeout.Wrap(retry).Wrap(circuitBreaker).Wrap(localTimeout);
- If you have a policy for a
void
and another policy for TResult
then the system tries to act smart
- The type of the
combinedPolicy
is now PolicyWrap<string>
- This combination is may or may not be intentional
- If unintentional then it will not warn you about it
var policy1 = Policy.Handle<DivideByZeroException>().WaitAndRetry(3, i => TimeSpan.FromSeconds(1));
var policy2 = Policy<string>.Handle<StackOverflowException>().RetryForever();
var combinedPolicy = policy1.Wrap(policy2);
string? result = combinedPolicy.Execute(() => "dummy response");
The Wrap
static method
var combinedPolicy = Policy.Wrap(policy2, policy1);
OR
var combinedPolicy = Policy.Wrap(policy2, policy1);
Notes
- This static method can receive any number of
ISyncPolicy
or ISyncPolicy<TResult>
policies
Use it
- With this method you could not combine a
ISyncPolicy
and a ISyncPolicy<string>
policies because all policies should have the same type
var combinedPolicy = Policy.Wrap<HttpResponseMessage>(globalTimeout,retry,circuitBreaker,localTimeout);
Avoid it
- When you want to explicitly combine two or more policies where you have a
TResult
and one or more void
- Or multiple
TResult
and one or more void
- Please note: You can not combine two policies where you have
TResult1
and TResult2