Authoring a reactive Polly policy (Custom policies Part III)
In this article we'll build our first reactive custom Polly policy: a policy to log exceptions or fault-results.
Polly polices fall into two categories: reactive (which react to configured faults) and non-reactive / proactive (which act on all executions). To author a proactive policy, see Part II: Authoring a proactive custom policy.
For background on custom policies and the Polly.Contrib, see Part I: Introducing custom Polly policies.
If you've already read Part II of this article series, the initial steps to create the reactive policy here are similar to those for the proactive policy covered in Part II. For what's different, skip straight to Steps 3 onwards, where we make the implementation react to the faults the policy handles, and tie the policy to Polly's existing Policy.Handle<>(...)
configuration syntax.
The goal: a policy to log faults
Polly already has configurable delegates hooks such as onRetry
, onBreak
and onFallback
, which many users use for logging with the relevant policies. So why a new policy to log faults? And what exactly will the policy do?
The aim is that the policy will log any exception or fault and then just rethrow or bubble it outwards.
This makes it a flexible logging construct that can be injected into any position in a PolicyWrap
(users have been asking for this).
It also makes for a good blog post example :~)
Authoring a reactive custom policy: Log exceptions and fault-results
For simplicity, we'll assume we're developing a custom policy for use with Polly and HttpClientFactory. This means that all executions are async and return Task<HttpResponseMessage>
.
For this, we only need to implement the IAsyncPolicy<TResult>
form of the policy. For other forms, see Part IV of the series.
Step 1: Name your policy and extend the base class
The base class for async generic policies is, as you might expect, AsyncPolicy<TResult>
. So start by declaring:
By extending the base class, your policy is already plugged in to all the benefits of the existing Polly architecture: the execution-dispatch, the integration into PolicyWrap
.
The compiler or IDE will report:
AsyncLoggingPolicy<TResult> does not implement inherited abstract member AsyncPolicy<TResult>
.ImplementationAsync(Func<Context, CancellationToken, Task<TResult>>, Context, CancellationToken, bool)
Step 2: Add a 'no-op' implementation
Let's fulfil the abstract method with a stub (your IDE may have a tooltip or shortcut to help):
This method is where your policy defines its implementation!
Let's look at the parameters - these are the information passed when the user executes a call through the policy:
Func<Context, CancellationToken, Task<TResult>> action
represents the delegate executed through the policyContext context
is the context passed to executionCancellationToken cancellationToken
is (you guessed it) the cancellation token passed to execution- and
bool continueOnCapturedContext
, whether the user asked to continue afterawait
s on a captured context.
The parameters map directly to the calling code:
So, let's flesh out our policy to provide a basic 'no-op' implementation. This just executes the code passed to the policy 'as is', using the parameters the user passed to execute the action
. The parameter continueOnCapturedContext
controls continuation context after the await
:
That's it for a no-op implementation!
Aside: To make it easier for you to get started, Polly provides all this as a template starting point for a custom policy. To author your own custom policy, you can grab the template repo and copy/paste/rename the classes.
Step 3: Matching the faults the user configured
aka: Do what the user told you!
The first thing any reactive policy needs to do is honor the exceptions and results which the user configured the policy to handle. Remember, the user specifies this with syntax such as:
Polly wraps that configuration up for you in two properties which are available on the configured policy instance:
ExceptionPredicates
- any exceptions the user configured the policy to handleResultPredicates
- any results the user configured the policy to handle
These are predicates, so essentially the implementation wants to do something like ExceptionPredicates.Any(predicate => predicate(exception))
. The below skeleton for our reactive policy implementation shows the actual syntax:
Let's dive in:
At [1] we check whether a returned result matches any of our ResultPredicates
.
If an exception is thrown, we check for matches at [2]. What's with the syntax there? Why is handledException
different from exception
?
Well, certain call chains can result in messy AggregateException
s with InnerException
s, and writing predicates to match those InnerException
s (including if nested) can be messy. So Polly provides in-built .HandleInner<TException>()
syntax to help. This line of code matches those exceptions, whether the user has configured .HandleInner<>()
or straight .Handle<>()
or a mixture:
If Polly plucks a matching InnerException
out of a messy AggregateException
chain,
handledException
will be it. If Polly matches a plain non-inner exception, again, handledException
will be it.
Step 4: Add your custom functionality
aka: You hit the jackpot! Do your stuff ... then let's get outta here!
If your code hits [3] or [4], the policy has matched a result or exception it was configured to handle - code your custom logic for your policy here!
Finally, your custom policy has to decide how to exit, at [5] and [6].
To be clear, the policy could do anything you choose, as its exit - substitute a different result (as Fallback policy does); rethrow a different exception; or just bubble the result or exception it was handling.
And also, you can build any structure you like within ImplementationAsync(...)
- it doesn't have exactly to follow the form above. Polly's Retry policy, for example, as you can guess, builds a loop.
In the logging policy, after we've logged, we'll exit by just returning the matched result or rethrowing the matched exception.
Here's a (near-final) implementation:
At [c], the policy returns the result it just handled.
The lines marked [d] show some special syntax for rethrowing an exception preserving the original call stack, when Polly's predicates have worked their magic to match an InnerException
(as described earlier).
What about the lines marked [a] and [b], where we do the actual logging?
We opted to let the user provide [a] a Func<Context, ILogger>
to specify the logger. Why? Well, policies are typically (particularly with HttpClientFactory) configured in the StartUp
file. On the other hand, loggers in .NET Core local to the particular component are usually only resolved at the call site by DI - for instance, a Controller class might take an ILogger<FooController>
by constructor injection.
The Func<Context, ILogger> loggerProvider
allows the user to bridge this, by providing a Func
to select the logger at execution time. Polly.Context
is an execution-scoped context which travels with every Polly execution, and has dictionary-like semantics - users can attach any information they like to it, to pass into the execution. The worked example at the end of this blog post shows how we'll pass the local ILogger
instance in to the call in practice; and the Polly wiki covers the general technique here.
Note that the complexity here around obtaining an instance of ILogger
is a factor of the split between early-binding to create the policy on HttpClientFactory and .NET Core's preference for category-specific ILogger<TCategory>
resolved in a late-binding manner by DI, usually by constructor injection into the relevant component. This complexity is a feature of these dimensions of logging in .NET Core rather than Polly per se. An alternative, simpler approach would replace [a] with a simple ILogger
, and use the overloads described here to configure the policy with a fixed ILogger<TFixed>
at startup.
To log to the logger, we chose [b] an Action<ILogger, Context, DelegateResult<TResult>>
. You'll be getting the picture here: It's good practice to make Context
an input parameter to any delegate you let users configure on a policy. We can let the user attach some custom data to Context
which they want to log (or in your own custom policy, use Context
information to influence policy operation some other way). And, in-built, Context
carries metadata about the Polly execution which can be useful for logging - which Policy is in use, which PolicyWrap, and a CorrelationId for the execution.
Finally, DelegateResult<TResult>
is a discriminated union representing either:
DelegateResult<TResult>.Exception
- the exception the underlying execution threw (or null, if not)DelegateResult<TResult>.Result
- the result the underlying execution returned.
Step 5: Add configuration syntax
Finally, add some configuration syntax so that users can create instances of your policy!
The policy constructor will need to take the parameters:
However, for a reactive policy, users will configure the policy with syntax something like:
Or with HttpClientFactory:
How do we make .AsyncLog(...)
follow on from the various .Handle<>()
and .Or...()
clauses? The answer is that handle clauses such as:
return an instance of a special builder type, PolicyBuilder<TResult>
, which encapsulates the choices the user has made about exceptions and results to handle. So the .AsyncLog(...)
overload must be coded as an extension method on this class. Shown combined with the constructor, the pattern looks like this:
The policyBuilder
instance [i] should be passed down to Polly's AsyncPolicy<TResult>
base class constructor (as shown at [ii]), so that the ExceptionPredicates
and ResultPredicates
properties can be configured on the policy. These also support Polly's .ExecuteAndCaptureAsync(...)
syntax.
Let's take it for a spin!
Let's take the new AsyncLoggingPolicy<TResult>
for a spin!
The below example includes a number of classes which feature in the Github repo for this policy:
Program.cs
: example program configuring anAsyncLoggingPolicy<HttpResponseMessage>
usingHttpClientFactory
ContextExtensions.cs
: helper methods on thePolly.Context
class, to make selection ofILogger
at runtime easierFooClient.cs
: a typed client configured byHttpClientFactory
StubErroringDelegatingHandler.cs
: a delegating handler also configured byProgram.cs
withinFooClient
, to randomly generate errors - just to give theLoggingPolicy<T>
some errors for us to log in this demonstration!
Here's some example output:
info: System.Net.Http.HttpClient.FooClient.LogicalHandler[100]
Start processing HTTP request GET https://www.google.com/
info: ConsoleAppExampleForBlogPost.FooClient[0]
https://www.google.com: InternalServerError
info: ConsoleAppExampleForBlogPost.FooClient[0]
https://www.google.com: Exception of type 'System.Net.Http.HttpRequestException' was thrown.
info: ConsoleAppExampleForBlogPost.FooClient[0]
https://www.google.com: InternalServerError
info: ConsoleAppExampleForBlogPost.FooClient[0]
https://www.google.com: Exception of type 'System.Net.Http.HttpRequestException' was thrown.
info: ConsoleAppExampleForBlogPost.FooClient[0]
https://www.google.com: Exception of type 'System.Net.Http.HttpRequestException' was thrown.
info: System.Net.Http.HttpClient.FooClient.LogicalHandler[101]
End processing HTTP request after 47.3552ms - OK
Got result: Dummy content from the stub helper class.
Note that this output includes some default logging from HttpClient
as well as that provided by AsyncLoggingPolicy<TResult>
.
Where can I get the code?
The full source code for AsyncLoggingPolicy<TResult>
and the test console app is available in this Github repo.
Where next?
Grab the template
If you want to dive straight in to experimenting with a custom policy, just grab the template repo and start copy/paste/rename/extend-ing the classes.
Authoring a proactive custom policy
If you haven't already looked at it, Part II covers authoring a proactive custom policy. The example chosen captures execution timing for any execution through the policy.
Understanding other forms of Polly policy
Part IV looks at how to author other forms of policy - synchronous policies and non-generic policies - in the most efficient manner.