Re-Authorize Efficiently Using Polly And .NET HttpClientFactory in .NET 8
Hi Everyone!
This is continuation of the series of posts on Polly v8 and .NET 8.
In this series of posts, I will try to cover some of the new features of Polly v8 and .NET 8. Below are the topics I am planning to cover in this series :
Implementing Retry Strategy for HttpClientFactory using Polly(v8) and .NET 8
This Post - Re-Authorize Efficiently Using Polly And .NET HttpClientFactory in .NET 8
Implementing Timeout Strategy for HttpClientFactory using Polly(v8) and .NET 8
Implementing CircuitBreaker Strategy for HttpClientFactory using Polly(v8) and .NET 8
Implementing RateLimiter Strategy for HttpClientFactory using Polly(v8) and .NET 8
Implementing Multiple Strategy for HttpClientFactory using Polly(v8) and .NET 8
Implementing Telemetry for HttpClientFactory using Polly(v8) and .NET 8
In the last post, we have seen how to implement Retry Strategy for HttpClientFactory using Polly(v8) and .NET 8. In this post, we will see how to re-authorize efficiently using Polly and .NET HttpClientFactory in .NET 8.
Please note, This post is only for .NET 8. For Polly v8 along with .NET 6 and .NET 7 probably this post will help you however I have not tested it yet.
Also note, You can refer this post if you are not using Polly v8.
Setup
For demonstration purpose, I have created a .NET 8 Web API project so that we can inject fault in our API randomly.
Our API is very simple and it has only one endpoint to get the weather forecast. Below is the implementation of the same -
1 |
|
We have another console application which will call this API. Below is the implementation of the same -
1 |
|
Couple of things to note here -
-
We are using Microsoft.Extensions.Http.Resilience package to implement the retry strategy, which is a wrapper around Polly v8 and provides a way to implement Polly strategies for HttpClientFactory in .NET 8.
-
We are using AddResilienceHandler to add the retry strategies. We are using two different retry strategy here. First one is to retry 3 times with a delay of 10 seconds in case of any transient error. Second one is to retry 3 times with a delay of 10 seconds in case of Unauthorized error. Also, we are refreshing the Token in case of Unauthorized error.
-
We are using AddHttpMessageHandler to add the TokenRetrievalHandler to intercept the call and add the Token in the header while retrying.
-
We are using ICachedTokenService to get the Token from the memory cache and refresh it from IExternalTokenService when it is expired.
-
We are using IExternalTokenService to get the actual Token from external service like Auth0 and refresh it when it is expired.
-
We are using AddHttpClient to add the IWeatherForecast as HttpClient.
Explanation
Let’s start with the AccessToken record which will be used to store the Token -
1 |
|
Next, To simulate the scenario, we are generating a random Guid as Token from IExternalTokenService. Abstartion for the same is as below -
1 |
|
Below is the implementation of the same -
1 |
|
In real world, we will get the Token from external service like Auth0 / Azure Entra etc.
Next, we will create the abstraction for ICachedTokenService as below -
1 |
|
Below is the implementation of the same -
1 |
|
Here, couple of things to note -
-
We are using ValueTask for GetTokenAsync. This is because we know that the Hot path will be to get the Token from the memory cache and very few scenraio(e.g very first time when Downstream is getting hit) it will actualy call the External Auth Server. So, we are using ValueTask to avoid the overhead of allocating a new Task in the heap.
-
We are using SetAbsoluteExpiration and SetSlidingExpiration both to make sure we are not overwhelming by storing the unused Token in the memory cache. Also to avoid edge case when the Token is expired but not yet refreshed we are making sure it removed from the cache before it’s actual expiry time.
-
We are using ResilienceContext from Polly to store the Token in the context so that we can use it in TokenRetrievalHandler.
Next, we will write custom DelegationHandler to intercept the call and add the Token in the header while retrying. Below is the implementation of the same -
1 |
|
Things to note here -
-
We are using ResilienceContextPool from Polly to get the context and then get the Token from the context. If the Token is not available in the context then we are getting it from ICachedTokenService.
-
We are adding the Token in the AuthenticationHeader.
At last, we will create the abstraction for IWeatherForecast as below -
1 |
|
Below is the implementation of the same -
1 |
|
All the pieces are in place. Now, we just need to orchestrate the flow. Let’s revisit the Program.cs file where we are configuring the HttpClientFactory -
1 |
|
AddResilienceHandler is an extension method provided by Microsoft.Extensions.Http.Resilience package. It takes two parameters, first one is the name of the pipeline and second one is the action to configure the pipeline. It does not return IHttpClientBuilder instead it returns IHttpResiliencePipelineBuilder provides a way to configure the resilience pipeline.
Hence, we are keeping the reference of IHttpClientsBuilder in httpClientBuilder and then using it to configure the pipeline and adding the TokenRetrievalHandler to intercept the call and add the Token in the header while retrying.
Sequence Diagram
Below is the sequence diagram for the same -
sequenceDiagram title HttpClient Retry Resiliency While UnAuthorized actor HttpCient Note over HttpCient,Polly Delegation Handler: PostAsync/GetAsync etc.
Calls SendAsync inernally HttpCient->>+ Polly Delegation Handler: Calls SendAsync Polly Delegation Handler->>+Pipeline: Calls ExecuteOutcomeAsync Pipeline->>+Retry: Calls ExecuteCore Retry->>+Token Retrival Delegation Handler: Calls SendAsync alt Token is Available Token Retrival Delegation Handler->>+ResilienceContextPool : Calls TryGetValue ResilienceContextPool->>-Token Retrival Delegation Handler : Returns Token else Token is not Available Token Retrival Delegation Handler->>+Cached Token Service : Calls GetTokenAsync Cached Token Service->>-Token Retrival Delegation Handler : Returns Token end alt Token is Expired Cached Token Service->>+ External Token Service: Calls RefreshTokenAsync External Token Service->>-Cached Token Service: Returns Token else Token is not Expired Cached Token Service->>+ Cached Token Service: Returns Cached Token end Token Retrival Delegation Handler->>Token Retrival Delegation Handler : Sets Authentication Header Note over Retry,Downstream Service: Initial Attempt Retry->>+Downstream Service: Invoke Downstream Service->>-Retry: Unauthorized (for Demonstration Purpose Only, because it is very unlikely it will return Unauthorized in very first Invoke just after getting fresh token) Retry-->Retry:Sleeps alt When Token is Valid Note over Downstream Service,Retry: 1st Retry Attempt Retry->>+Downstream Service: Invoke Downstream Service->>-Retry: Returns Valid Response Retry->>+Pipeline: Returns Valid Response Pipeline->>+Polly Delegation Handler: Returns Valid Response Polly Delegation Handler->>+HttpCient: Returns Valid Response else When Token is not Valid Note over Downstream Service,Retry: 1st Retry Attempt Retry->>+Downstream Service: Invoke alt Token is Available Token Retrival Delegation Handler->>+ResilienceContextPool : Calls TryGetValue ResilienceContextPool->>-Token Retrival Delegation Handler : Returns Token else Token is not Available Token Retrival Delegation Handler->>+Cached Token Service : Calls GetTokenAsync Cached Token Service->>-Token Retrival Delegation Handler : Returns Token end alt Token is Expired Cached Token Service->>+ External Token Service: Calls RefreshTokenAsync External Token Service->>-Cached Token Service: Returns Token else Token is not Expired Cached Token Service->>+ Cached Token Service: Returns Cached Token end Downstream Service->>-Retry: Returns Valid Response Retry->>+Pipeline: Returns Valid Response Pipeline->>+Polly Delegation Handler: Returns Valid Response Polly Delegation Handler->>+HttpCient: Returns Valid Response end
Result
Finally, we are ready to run the application. Let’s run the Web API first and then Console Application.
In my case, below is the output of the Console -
and below is the output of the Web API -
Let’s try to understand the output -
-
Very first attempt is unsuccessful and it is returning 500 which can be seen from the Console output. Since we are capturing the Authorization header in the Web API we can see that the Token is also getting logged in the Web API output. In our case, it is Bearer 201bf10e73164ea288ddc21d58fa0964. This is will be handled by the first retry strategy which is to retry 3 times with a delay of 10 seconds in case of any transient error.
-
Second attempt is also unsuccessful and it is returning 401. In this case we could see the same Token is getting logged in the Web API output as expected.
-
Since in the second attempt we got 401 which is handled by the second retry strategy which is to retry 3 times with a delay of 10 seconds in case of Unauthorized error. This will refresh the Token and then retry the call. In our case, we could see that the Token is getting refreshed and the different Token is getting logged in the Web API output.In our case, it is Bearer a4b1af59a33e46db8251ff7bb5d4a5c4.
-
To keep it short, I will not go through the third attempt. But I hope you got the idea that in case of Unauthorized error, it will refresh the Token and then retry the call and in case of any other error it will retry the call without refreshing the Token.
-
Let’s jump to the last attempt which is successful and it is returning 200. Before retrying the call, it will refresh the Token as it got 401 in the previous attempt. In this case we could see that the different Token is getting logged in the Web API output as expected, which is Bearer 503b1c11dc66449cabf20fa04198c816.
Conclusion
IMO, it is now more easier to implement than the pervious version of Polly. But I did had a hard time to figure out how to add DelegationHandler while retrying, because the documentation is not very clear on that. I hope this post will help you to implement the same.
You can find the complete source code here