Dynamic caching policy with Polly

Dynamic caching policy with Polly

Polly is great library for easily configuring different kinds of policies around code execution, be it retry, circuit breaker, caching or lots of other things. It's probably most often used around HTTP requests. If you haven't heard about it you definitely should check it out.

Recently I worked on configuring a caching policy, but only wanted to cache the returned models from an API call in certain cases depending on the values of the model received and that's what this blog post is about.

The challenge

If you just want to selectively cache for a specific response type, e.g. HttpResponseMessage, then that is actually not too difficult and you can read about how to do that in the following blog post, where the policy gets configured to only cache 200 OK responses.

It's basically just a ITtlStrategy that in certain situations returns a "time-to-live" of 0, meaning the item expires from the cache immediately.

Caching at the HttpResponseMessage level might not be the right thing though, and there are some thing to consider in that regard, like re-reading response streams, deserializing the response again etc. Instead I wanted to selectively cache the deserialized result model. Doing this for different models seemed too cumbersome, as I would have to create specific policies for each return type (because of generics, i.e. ResultTtl<T>).

The solution

The solution I came up with was to create a custom ITtlStrategy that would look for a specific interface and choose depending on that.

Here's the interface ICacheable:

public interface ICacheable
{
    /// <summary>
    /// Should return true if the object shouldn't be cached in its current state.
    /// </summary>
    bool DisallowCaching();
}

Any model can implement that interface and return a fitting value depending on it's current state.

For this interface to have any effect we need a ITtlStrategy, and here it is:

/// <summary>
/// Dynamic TTL strategy that doesn't cache the item if it implements
/// <see cref="ICacheable"/> interface and doesn't allow caching in its current state.
/// </summary>
public class CacheableFilterTtl : ITtlStrategy
{
    private static readonly ZeroTtl = new Ttl(TimeSpan.Zero);

    private readonly ITtlStrategy _innerStrategy;

    public CacheableFilterTtl(ITtlStrategy innerStrategy)
    {
        _innerStrategy = innerStrategy ?? throw new ArgumentNullException(nameof(innerStrategy));
    }

    public CacheableFilterTtl(TimeSpan ttl)
    {
        _innerStrategy = new RelativeTtl(ttl);
    }

    /// <inheritdoc />
    public Ttl GetTtl(Context context, object result)
    {

        if (result is ICacheable cacheable && cacheable.DisallowCaching())
        {
            return ZeroTtl;
        }

        return _innerStrategy.GetTtl(context, result);
    }
}

It's rather simple, really. It can either be created as wrapping another ITtlStrategy as a fallback strategy, or with a simple RelativeTtl based on a TimeSpan.

When figuring out the Ttl to return it looks at the result and if it implements ICacheable and does not allow caching then it returns a Ttl of 0. If the result is not of type ICacheable then it just uses the fallback strategy.

Creating the cache policy using the custom ITtlStrategy is very simple:

IAsyncCacheProvider cacheProvider = null; // Get from Service Provider
Policy.CacheAsync(cacheProvider, new CacheableFilterTtl(TimeSpan.FromHours(1));

This will create a cache policy with a relative time-to-live of 1 hour as the default, unless the result implements ICacheable and does not allow caching.