Caching in Polly and the HttpClientFactory

Full source code here.

Want to learn more about Polly? Check out my Pluralsight course on it.

Polly allows you to cache a response for reuse by a subsequent request, it supports both local an distributed caches, full information can be found here https://github.com/App-vNext/Polly/wiki/Cache.

The HttpClientFactory lets you define polices in startup.cs and apply them to HttpClient requests anywhere in your application. With most policies this makes it easier for you to apply a policy to a request, but with the cache policy it complicates matters.

With the release of HttpClientFactory there are now two ways to use the cache policy, one taking advantage of HttpClientFactory to apply the policy to requests, and the other bypassing it when executing requests.

Caching with the cache policy inside the HttpClientFactory

First let’s look at how to use the Cache Policy using PolicySelector method on the HttpClientFactory to apply the policy to the request, see this post for more info.

If you are working in Polly V6+ add Polly.Caching.Memory package to your project, this is important, don’t add Polly.Caching.MemoryCache that is for Polly V5.9 and earlier, and many an hour was lost figuring that out.

In Startup.cs add the following to the ConfigureServices method -

 1public void ConfigureServices(IServiceCollection services)
 2{
 3    services.AddMemoryCache();
 4    services.AddSingleton<IAsyncCacheProvider, MemoryCacheProvider>();
 5
 6    IPolicyRegistry<string> registry = services.AddPolicyRegistry();
 7
 8    services.AddHttpClient("RemoteServer", client =>
 9    {
10        client.BaseAddress = new Uri("http://localhost:5000/api/");
11        client.DefaultRequestHeaders.Add("Accept", "application/json");
12    }).AddPolicyHandlerFromRegistry(PolicySelector);

This sets up the in memory cache and policy registry, adding both to ServicesCollection. The cache policy is not added here, instead it is added in the Configure(..) method (thanks Andrew Lock for reminding me about this).

Next, add the HttpClientFactory and a method to pick the appropriate policy from the registry.

Inside the Configure(..) I setup the cache policy. The registry and cache provider are passed via dependency injection.

1public void Configure(IApplicationBuilder app, IHostingEnvironment env, 
2    IAsyncCacheProvider cacheProvider, IPolicyRegistry<string> registry)
3{
4    CachePolicy<HttpResponseMessage> cachePolicy = 
5        Policy.CacheAsync<HttpResponseMessage>(cacheProvider, TimeSpan.FromSeconds(30));
6    registry.Add("CachingPolicy", cachePolicy);

In the controller I have a constructor that takes one argument, the HttpClientFactory.

1public CatalogController(IHttpClientFactory httpClientFactory)
2{
3    _httpClientFactory = httpClientFactory;
4}

In the action method I get the HttpClient from the HttpClientFactory and specify the remote endpoint I want to call, but now I can’t use usual httpClient.GetAsync(..) because I can’t pass it a cache context key (i.e. the name of where I look for previously cached responses and store new responses). Instead I build a HttpRequestMessage, specifying the HttpMethod, Uri and using a Polly extension method I set the cache context.

1HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, 
2    new System.Uri(httpClient.BaseAddress + requestEndpoint));
3
4httpRequestMessage.SetPolicyExecutionContext(new Context($"GetInventoryById-{id}"));
5
6HttpResponseMessage response = await httpClient.SendAsync(httpRequestMessage);

This approach feels a bit awkward and not many people use the HttpRequestMessage to send requests to remote services.

Caching with the cache policy outside the HttpClientFactory

An alternative to the above is to add the cache to the registry, but not add the registry to the HttpClientFactory.

In this case the adding the HttpClientFactory to the services collection changes slightly -

1services.AddHttpClient("RemoteServer", client =>  
2{
3    client.BaseAddress = new Uri("http://localhost:5000/api/");
4    client.DefaultRequestHeaders.Add("Accept", "application/json");
5}); // no policy selector

In the controller we now pass in the registry as HttpClientFactory as constructor arguments.

1public StockMgtController(IHttpClientFactory httpClientFactory, IPolicyRegistry<string> policyRegistry) 
2{
3    _httpClientFactory = httpClientFactory;
4    _policyRegistry = policyRegistry;
5}

There are changes inside the action method also -

 1[HttpGet("{id}")]
 2public async Task<IActionResult> Get(int id)
 3{
 4    string requestEndpoint = $"inventory/{id}";
 5    var httpClient = _httpClientFactory.CreateClient("RemoteServer");
 6
 7    var cachePolicy = _policyRegistry.Get<CachePolicy<HttpResponseMessage>>("CachingPolicy");
 8    Context policyExecutionContext = new Context($"GetInventoryById-{id}");
 9
10    HttpResponseMessage response = await cachePolicy.ExecuteAsync(
11        context => httpClient.GetAsync(requestEndpoint), policyExecutionContext);
12	//snip..	

The httpClient is retrieved from the HttpClientFactory as before, but now the cachePolicy is taken from the policy registry, the Context is defined and then we use the cachePolicy.ExecuteAsync(..) method to make call the httpClient.GetAsync(..) method.

The other way

You might have noticed that there is another way of using the cache policy. You could use the HttpClientFactory and a PolicySelector method to apply most policies to your HttpClient requests, but not for the cache policy. I wouldn’t do this, it will confuse everyone.

Conclusion

The outcome of both is the same, I think the first approach is better, even though the call to the httpClient is a little more convoluted, it will be more consistent with how you call other policies in your application, there is also a chance the Polly team will add an extension method to make the use of HttpRequestMessage transparent to us developers.  
Full source code here.

comments powered by Disqus

Related