Skip to content
reisenberger edited this page Dec 5, 2017 · 36 revisions

Cache (v5.4.0 onwards)

Purpose

To provide results from cache where available.

Premise: 'Some proportion of requests may be similar'

The Polly CachePolicy is an implementation of read-through cache, also known as the cache-aside pattern. Providing results from cache where possible reduces network traffic and overall call duration.

Retrieving a result from an in-memory cache can eliminate a downstream call entirely. A distributed cache can be used to provide a shared cache across upstream nodes, to retrieve values from a nearer-by network resource than the underlying called system might be, or where cache requirements exceed in-memory storage.

CachePolicy must be used in conjunction with an ISyncCacheProvider or IAsyncCacheProvider implementation from a separate Nuget package. For example, Polly.Caching.MemoryCache provides an in-memory cache implementation using the standard .NET Framework / .NET Core providers.

New cache providers are easy to implement against the ISyncCacheProvider or IAsyncCacheProvider interfaces.

Operation

  • The cache key to use is determined according to the supplied (or default) cache key strategy.
  • Where the cache holds a value under the corresponding key:
    • the delegate passed to .Execute(...) or similar is not called
    • the value from cache is returned instead.
  • Where the cache does not hold a result under the corresponding key:
    • the delegate passed to .Execute(...) or similar is called as usual
    • the retrieved value is put in the cache, using the configured time-to-live strategy
    • the retrieved value is returned.

Syntax

CachePolicy cache = Policy
  .Cache(ISyncCacheProvider cacheProvider
           , TimeSpan ttl | ITtlStrategy ttlStrategy
          [, ICacheKeyStrategy cacheKeyStrategy]
              [, Action<Context, string, Exception> onCacheError]
                   |
              [, Action<Context, string> onCacheGet
               , Action<Context, string> onCacheMiss
               , Action<Context, string> onCachePut
               , Action<Context, string, Exception> onCacheGetError 
               , Action<Context, string, Exception> onCachePutError]
           );

CachePolicy cache = Policy
  .CacheAsync(IAsyncCacheProvider cacheProvider
           , TimeSpan ttl | ITtlStrategy ttlStrategy
          [, ICacheKeyStrategy cacheKeyStrategy]
              [, Action<Context, string, Exception> onCacheError]
                   |
              [, Action<Context, string> onCacheGet
               , Action<Context, string> onCacheMiss
               , Action<Context, string> onCachePut
               , Action<Context, string, Exception> onCacheGetError 
               , Action<Context, string, Exception> onCachePutError]
           );

Parameters

cacheProvider

cacheProvider: The underlying cache provider to use.

CachePolicy must be used in conjunction with an ISyncCacheProvider or IAsyncCacheProvider implementation from a separate Nuget package.

For example, Polly.Caching.MemoryCache provides an in-memory cache implementation using the standard .NET Framework / .NET Core providers. These are in a separate nuget package because the target frameworks supported by cache providers are generally more restrictive than the targets of the core Polly package.

The same cacheProvider and CachePolicy instance may be used across multiple call sites.

ttl

TimeSpan ttl: Time-to-live (ttl) for the cache item, as a relative, non-sliding duration from the moment the item is put in the cache.

For example, if TimeSpan.FromMinutes(5) is passed, the cacheProvider should consider the item valid for 5 minutes.

ttlStrategy (alternative to ttl above)

ITtlStrategy ttlStrategy: offers ttl strategies beyond the simple TimeSpan ttl above.

RelativeTtl

RelativeTtl(TimeSpan ttl): equivalent to ttl above.

AbsoluteTtl

AbsoluteTtl(DateTimeOffset absoluteExpirationTime): indicates that the cacheProvider should make the cached item expire at the absolute time given.

SlidingTtl

SlidingTtl(TimeSpan slidingTtl): indicates that the cacheProvider should treat the cached item as having a sliding ttl of the specified timespan. For instance, if TimeSpan.FromMinutes(5) is passed, the cacheProvider should consider the item valid for a further 5 minutes, each time the cache item is touched.

ContextualTtl

ContextualTtl: specifies that the execution should take the ttl from a property on the Context passed to execution, context[ContextualTtl.TimeSpanKey].

This allows you to define a central cache policy that will use varying ttls in different call sites, by placing the desired ttl on Polly's execution context. For example:

context[ContextualTtl.TimeSpanKey] = new Ttl(TimeSpan.FromMinutes(5), slidingExpiration: true);

Default cacheKeyStrategy (if omitted)

If no cacheKeyStrategy is specified, the cache key to use is taken as the ExecutionKey property on the execution Context, ie context.ExecutionKey. For example:

TResult result = await cache.ExecuteAsync(async () => await getFooAsync(), new Context("FooKey")); // "FooKey" is the cache key to use in this execution.

If context.ExecutionKey is not specified (no Context is passed to the execution, or context.ExecutionKey is not set), caching behaviour is ignored, and the underlying delegate passed to .Execute(...) (or similar) is called.

cacheKeyStrategy (optional)

Func<Context, string> cacheKeyStrategy: allows the specification of a custom strategy for using a more specific cache key in the execution. For instance, to cache items obtained through the execution by some guid:

 // configuration
 CachePolicy cache = Policy.CacheAsync(cacheProvider, TimeSpan.FromMinutes(5), context => context.ExecutionKey + context["guid"]);

 // usage, elsewhere
 Guid guid = ... // from somewhere
 Context policyExecutionContext = new Context("GetResource-");
 policyExecutionContext["guid"] = guid.ToString();
 TResult result = await cache.ExecuteAsync(async () => await getResourceAsync(guid), policyExecutionContext); // "Resource-SomeGuid" is the key used in this execution, if guid == SomeGuid.

ICacheKeyStrategy cacheKeyStrategy: is available as a parameter in some overloads, for more complex funcs.

Interacting with policy operation

onCacheGet

An optional onCacheGet delegate allows specific code to be executed (for example for logging), when a value is retrieved from cache.

onCacheMiss

An optional onCacheGet delegate allows specific code to be executed (for example for logging), when a cache-miss occurs (a value is not found in the cache for the given key).

onCachePut

An optional onCacheGet delegate allows specific code to be executed (for example for logging), after a value has been put to the cache.

onCacheError

An optional onCacheError delegate allows specific code to be executed (for example for logging), if any call to the underlying cacheProvider throws an exception.

The string passed is the cache key.

onCacheGetError

The alternative, optional onCacheGetError delegate is a more specific version of onCacheError, executed only if get calls to the underlying cacheProvider throw an exception.

onCachePutError

The alternative, optional onCachePutError delegate is a more specific version of onCacheError, executed only if put calls to the underlying cacheProvider throw an exception.

Throws

No exceptions due to caching operations are thrown. If the underlying cacheProvider throws an exception during a cache operation:

  • the exception is passed to the relevant onCacheError, onCacheGetError or onCachePutError delegate, if configured.
  • the execution continues. For example, if the underlying cacheProvider throws while checking if the cache contained a value for the given key, the execution treats this as a cache-miss, and calls the delegate passed to .Execute(...).

Usage recommendations

Placement of cache policy within PolicyWrap

See guidance on ordering the available policy types in a wrap. CachePolicy should be usually be placed outermost in a PolicyWrap, with only FallbackPolicy outside.

void executions

If an execution returning void is placed through a CachePolicy, caching operation is silently bypassed (there is no result to cache) rather than an exception thrown. This allows for a CachePolicy to be included in a PolicyWrap which might sometimes be used for TResult-returning executions, sometimes for void-returning, without exceptions being thrown.

Thread safety and policy reuse

Thread safety

The internal operation of CachePolicy is thread-safe: multiple calls may safely be placed concurrently through a policy instance (assuming the configured cacheProvider implementation is also thread-safe).

Policy reuse

CachePolicy instances may be re-used across multiple call sites.

cacheProvider instances may be re-used across multiple CachePolicys and call sites.

When reusing policies, use differing ExecutionKey to specify cache key (if DefaultCacheKeyStrategy is used), and to distinguish different call-site usages within logging and metrics.

Implementing cache providers and serializers

Cache providers

ISyncCacheProvider

The ISyncCacheProvider interface required to implement new providers is very simple:

public interface ISyncCacheProvider
{
    object Get(String key);
    void Put(string key, object value, Ttl ttl);
}

public interface ISyncCacheProvider<TResult>
{
    TResult Get(String key);
    void Put(string key, TResult value, Ttl ttl);
}

If the cache provider can cache objects of any type, implement the non-generic ISyncCacheProvider. This provider will be able to be used in generic CachePolicy<TResult> instances, and also in non-generic CachePolicy instances on which executions with the generic .Execute<TResult>() method overload are made.

If the cache provider can only cache objects of a particular type (for example, some cache providers typically cache all items as strings), implement just the generic interface ISyncCacheProvider<string>.

IAsyncCacheProvider

The IAsyncCacheProvider interface is similarly straightforward to implement:

public interface IAsyncCacheProvider
{
    Task<object> GetAsync(String key, CancellationToken cancellationToken, bool continueOnCapturedContext);
    Task PutAsync(string key, object value, Ttl ttl, CancellationToken cancellationToken, bool continueOnCapturedContext);
}

public interface IAsyncCacheProvider<TResult>
{
    Task<TResult> GetAsync(String key, CancellationToken cancellationToken, bool continueOnCapturedContext);
    Task PutAsync(string key, TResult value, Ttl ttl, CancellationToken cancellationToken, bool continueOnCapturedContext);
}

The same comments regarding generic and non-generic versions as for ISyncCacheProvider apply.

Any await calls in the implementation should be decorated .ConfigureAwait(continueOnCapturedContext).

Creating and using serializers

Polly provides an interface ICacheItemSerializer<TResult, TSerialized> which again is very simple to implement:

public interface ICacheItemSerializer<TResult, TSerialized>
{
    TSerialized Serialize(TResult objectToSerialize);
    TResult Deserialize(TSerialized objectToDeserialize);
}

An ICacheItemSerializer<TResult, TSerialized> allows you to serialize items for placing in the cache, and deserialize again on retrieving from cache.

Some cache providers (such as Redis) store most items as specific types (eg string or byte[]), requiring you to serialize to store more complex types. For example, for Redis you might implement:

RedisCacheProvider<string>: ISyncCacheProvider<string>, IAsyncCacheProvider<string>

The above RedisCacheProvider<string> (as is) would only be usable in CachePolicy<string>. To use the cache for other result types, implement (for example using Newtonsoft.Json):

JsonSerializer<TResult, string> : ICacheItemSerializer<TResult, string>
{
    string Serialize(TResult objectToSerialize);
    TResult Deserialize(string objectToDeserialize);
}

An in-built extension method in Polly, .WithSerializer<TResult, TSerialized>(), then allows you to bridge from the native TResult of the execution to the serialized TSerialized type.

ISyncCacheProvider<TSerialized>.WithSerializer<TResult, TSerialized>() returns an ISyncCacheProvider<TResult> (which is expected by the configuration syntax for CachePolicy<TResult>), allowing you to bridge as follows:

ISyncCacheProvider<string> redisCacheProvider = ... // configured earlier
ICacheItemSerializer<TResult, string> jsonSerializer = ... // configured earlier

CachePolicy<TResult> cache = Policy.Cache<TResult>(
    redisCacheProvider.WithSerializer<TResult, string>(jsonSerializer), 
    TimeSpan.FromMinutes(5));

The above examples are based around serializing to string using Newtonsoft.Json as this is one of the most commonly understood serialization approaches. However, it is far from the only option. Redis, for example, can also store any kind of byte[] array as a Redis 'string'. For a fuller overview of serialization options available, see the Microsoft Patterns-and-Practices article on Caching, section Serialization considerations.

Clone this wiki locally