Authoring a proactive Polly policy (Custom policies Part II)
In this article we'll build our first custom Polly policy: a proactive Policy to capture execution timings.
Polly polices fall into two categories: reactive (which react to faults) and non-reactive or proactive (which act on all executions). To author a policy which reacts to faults, see Part III: Authoring a reactive custom policy.
For background on custom policies and the Polly.Contrib, see Part I: Introducing custom Polly policies.
A final article in the series, Part IV, covers Custom policies for all execution types: sync and async, generic and non-generic.
Authoring a non-reactive custom policy: Capture execution timing
For simplicity, in this article 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:
AsyncTimingPolicy<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: Add your custom functionality
To implement custom functionality, you just code ... whatever you want ... around the await action(...)
statement.
We're going to calculate execution duration, so we'll use System.Diagnostics.Stopwatch
:
Hmm, we still need to let the user do something with that elapsed
value. To keep this blog post simple and illustrate some points about async policy implementation, we'll intentionally allow users to provide an async delegate as timingPublisher
. (For a production implementation, you might want instead to adopt the event pattern or IObservable<>
.)
We also make the timingPublisher
delegate take an input parameter Polly.Context
: this execution-scoped context travels with every Polly execution. Users can (optionally) use it to pass extra information in to the execution, to exchange information between different parts of the execution, or to capture information after execution (more info here and here). It also carries important metadata about the execution which can be useful for filtering. It's good practice to make Context
an input parameter to any delegate you let users configure on a policy.
Note [*] the use of .ConfigureAwait(continueOnCapturedContext)
on the extra await
we added. Whenever you code a new async policy, you should decorate all await
statements in this way, if you want your policy to play nicely with what Polly users expect. Best practice for libraries is to .ConfigureAwait(false)
. However, some specialist uses of Polly (such as Actors) require continuations to continue on the original context; so Polly provides this control bool to be used on all await
statements.
Step 4: Add configuration syntax
Finally, add some configuration syntax so that users can create instances of your policy!
You'll see the code above had a public constructor. There's nothing wrong with this, but by convention, Polly configuration syntax uses static factory methods (eg Policy.Timeout(30)
). So we would suggest amending the construction like this:
Let's take it for a spin!
Let's take the new AsyncTimingPolicy<TResult>
for a spin!
This is also an example of a quick way to test HttpClientFactory with Polly policies in a Console App.
Here's some example output:
Took 0.331 seconds retrieving https://www.google.com/
Took 0.146 seconds retrieving https://www.google.co.uk/
Took 0.121 seconds retrieving https://www.bbc.co.uk/
Of course, writing to the Console is the least imaginative use. The intention of this policy is to enable you to capture execution timing metrics to your favourite time-series db (Prometheus, InfluxDB, Azure Time Series, App-Metrics).
Where can I get the code?
The full source code for AsyncTimingPolicy<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 reactive custom policy
In the next part of this series, Part III, we'll look at authoring a reactive custom policy, to log any exception or fault and then rethrow or bubble it outwards. The new policy will allow you to inject custom logging into any position in a PolicyWrap
.
Finally, in Part IV, we'll look at how to author other forms of policy - synchronous policies and non-generic policies - in the most efficient manner.