Skip to content

API documentation (v 2.x)

hippy edited this page Dec 2, 2023 · 20 revisions

Quickstart

See QuickStart docs

LazyCache API documentation

Getting started

The best way to use LazyCache is through the GetOrAdd method. It uses generics to save you any type casting hassle and a Func to ensure you only call you cacheable delegate once.

// Create the cache
IAppCache cache = new CachingService();

// Get all products from the cache, or get from db and cache results
var products = cache.GetOrAdd("productService-getProducts", () => dbContext.Products.ToList());

Selecting a cache key

Each cache entry must have a unique key, where the key is unique to the underlying cache instance, which by default is the shared MemoryCache.Default.

Constructing a unique key is left to the developer, but a good best practice is to cache the results of a chosen methods call and adopt convention of className-methodName-argument1Value-argument2Value etc for the method you are adding caching to. Following this naming convention makes it easy to examine keys at run-time and establish the source and guarantees uniqueness.

For example

public class ProductService() {
...
    public Product GetProductById(int id){
        var key = string.Format("ProductService-GetProductById-{0}", id); //or use string interpolation
        return cache.GetOrAdd(key, () => dbContext.Products.Get(id));
    }
...
}

Change the cache duration - absolute expiration

var products = cache.GetOrAdd("all-products", () => dbContext.Products.ToList(), DateTimeOffset.Now.AddMinutes(5));

Change the default cache duration - 5 mins sliding expiration

var products = cache.GetOrAdd("all-products", () => dbContext.Products.ToList(), TimeSpan.FromMinutes(5));

Should I use GetOrAdd or GetOrAddAsync?

If the thing you are caching is fetched asynchronously, i.e. it returns a Task<Something> you should use GetOrAddAsync(). If the thing you are caching is fetched synchronously, i.e. it does not return as task, you should use GetOrAdd().

GetOrAdd an Async delegate/Task to the cache

Use GetOrAddAsync to cache tasks. In this example we demonstrate using lazy cache to cache an async Entity Framework query inside a webapi controller:

[HttpGet]
[Route("api/products")]
public async Task<Product> Get(int id)
{
    Func<Task<Product>> cacheableAsyncFunc = () => dbContext.Products.GetAsync(id);

    var cachedProducts = await cache.GetOrAddAsync($"ProductsController-Get-{id}", cacheableAsyncFunc);

    return cachedProducts;

    // Or just do it all in one line if you prefer
    // return await cache.GetOrAddAsync($"ProductsController-Get-{id}", () => dbContext.Products.GetAsync(id));
}

Add an item to the cache manually for the default duration (20 mins)

Most of the time all you need is GetOrAdd but sometimes you might need to force an item into the cache

// Get the cache (or use a DI container)
IAppCache cache = new CachingService();

// Get all products from db
var products = dbContext.Products.ToList();

// Add them to the cache so we can retrieve them up to 20 mins later
cache.Add("all-products", products);

Add an item to the cache for a custom duration (absolute expiration)

// Add products to the cache for at most one minute
cache.Add("all-products", products, DateTimeOffset.Now.AddMinutes(1));

Add an item to the cache for a custom duration (sliding expiration)

// Add products to the cache and keep them there as long as they 
// have been accessed in the last 5 minutes
cache.Add("all-products", products, new TimeSpan(0, 5, 0));

(Get or) add an item to the cache for duration defined when you generate the item to cache

cache.GetOrAdd("some-key", entry => {
    var thingWithAnExpiryDate = GetTheThingToCache();
    DateTimeOffset expiryDate = thingWithAnExpiryDate.Expires;
    // can set expiry date using a DateTimeOffset (or a TimeSpan with RelativeToNow variant)
    entry.AbsoluteExpiration = expiryDate;
    return thingWithAnExpiryDate;
});

Custom cache policies

Get notified when something is removed, set cache entry priorities and more by using a MemoryCacheEntryOptions

// Add products to the cache with an unremovable custom cache priority
// add a custom eviction callback
var options = new MemoryCacheEntryOptions(){ 
   Priority = CacheItemPriority.NeverRemove
};
options.RegisterPostEvictionCallback((key, value, reason, state) =>
{
    log.Write("Products removed from cache")
});            
cache.Add("all-products", products, options);

If you need the last version of your cached data, you can access it by as the second parameter of the callback delegate:

var options = new MemoryCacheEntryOptions
{
    AbsoluteExpiration = DateTimeOffset.Now.AddMilliseconds(100) // about to expire!
}.RegisterPostEvictionCallback(
    (key, value, reason, state) => Debug.Write("this value has just been removed: " + value.ToString())
);
cache.Add("all-products", products, options);

Caching items that should not expire

To cache a single item so that it is never removed from the cache:

// Add products to the cache with an unremovable custom cache priority
var options = new MemoryCacheEntryOptions(){ 
   Priority = CacheItemPriority.NeverRemove
};
cache.Add("all-products", products, options);

If you want to change the LazyCache defaults so that unless specified otherwise items are cached forever you can just set the default duration to a really big number:

var myCachingService = new CachingService(); //Better to use Dependency Injection and a singleton

myCachingService.DefaultCachePolicy = new CacheDefaults
{
  DefaultCacheDurationSeconds = int.MaxValue // 60+ years
}

Retrieve something from the cache manually

If you have already cached something you can retrieve it, without the need for any type checking or casting, using Get. If its not cached null will be returned.

// Get the cache
IAppCache cache = new CachingService();

// Get product from the cache based on a key
var product = cache.Get<Product>("product-with-id-1");

Deleting/removing/clearing something from the cache

If it is cached it will be removed, if not no error will be thrown

// Remove the products from the cache
cache.Remove("all-products");

Configuring the default cache duration of 20 minutes

// Change the default cache duration from 20 minutes to 3 minutes
var cache = new CachingService() { DefaultCachePolicy.DefaultCacheDurationSeconds = 60 * 3 };

Empty the entire cache

Generally this is a code smell - you should remove unwanted items by key. But if you really need to this can be achieved by disposing the implementation of ICacheProvider which is typically the MemoryCacheProvider and then creating a new instance of CachingService with a new provider instance.

Method 1 - Dispose

// Say I already have a cache that I want to dispose:
IAppCache cache = new CachingService();
 
// now I dispose the provider to clear the cache:
cache.CacheProvider.Dispose();

// Need to create a new provider to replace the disposed one
var provider = new MemoryCacheProvider(
                            new MemoryCache(
                               new MemoryCacheOptions()));

// create a new cache instance with the new provider
cache = new CachingService(provider);

Method 2 - Cancellation Tokens (could also be used to dispose a range of items)

// Say I already have a cache:
IAppCache cache = new CachingService();

// I need a cancellation token to link every single cache item to the same cancellation event
// this instance must be shared everywhere you add stuff to the cache - a singleton
var sharedExpiryTokenSource = new CancellationTokenSource(); // IDisposable!

// add first item to the cache and link to the global expiry token
var expireToken1 = new CancellationChangeToken(sharedExpiryTokenSource.Token);
var options1 = new MemoryCacheEntryOptions()
                   .AddExpirationToken(expireToken)
var product1 = cache.GetOrAdd($"Products-1", () => dbContext.Products.GetAsync(1), options1);

// add second item to the cache and link to the same expiry token
var options2 = new MemoryCacheEntryOptions()
                   .AddExpirationToken(new CancellationChangeToken(sharedExpiryTokenSource.Token))
var product2 = cache.GetOrAdd($"Products-2", () => dbContext.Products.GetAsync(2), options2);

// And now later on I can remove both products from the cache by cancelling on the shared token
sharedExpiryTokenSource.Cancel();

ExpirationMode & evicting items from the cache as soon as they expire (LazyCache 2.1 upwards)

There are 2 options to configure how LazyCache evicts items from the cache:

  • ExpirationMode.LazyExpiration (the default) which removes expired cache items when they are next accessed by key, if they have expired. This is Lazy and uses the least resources.
  • ExpirationMode.ImmediateEviction which uses a timer to remove items from the cache as soon as they expire (and so is more resource intensive)

To use immediate expiration:

var result= await cache.GetOrAddAsync(
    "someKey", 
    () => GetStuff(), 
    DateTimeOffset.UtcNow.AddSeconds(100), 
    ExpirationMode.ImmediateEviction);

And this can also be configured on the LazyCacheEntryOptions options object when more complex cache item config is required:

var result= await cache.GetOrAddAsync(
    "someKey", 
    entry => GetStuff(), 
    LazyCacheEntryOptions
      .WithImmediateAbsoluteExpiration(TimeSpan.FromSeconds(10))
      .RegisterPostEvictionCallback((key, value, reason, state) => Console.WriteLine("cache item immediately evicted on expiry!"))
  );

Using ImmediateEviction and RegisterPostEvictionCallback to refresh a cached item automatically

Sometimes you may want to proactively repopulate the cached items on a schedule before they are requested again. This can be achieved using the ImmediateEviction feature in combination with a callback as shown below:

var key = "someKey";
var refreshInterval = TimeSpan.FromSeconds(30);

// this Local Function is the Func whoose results we are caching
Product[] GetProducts()
{
    return ....
}

// this Local Function builds options that will trigger a refresh of the cache entry immediately on expiry
MemoryCacheEntryOptions GetOptions()
{
    //ensure the cache item expires exactly on 30s (and not lazily on the next access)
    var options = new LazyCacheEntryOptions()
        .SetAbsoluteExpiration(refreshInterval, ExpirationMode.ImmediateEviction);

    // as soon as it expires, re-add it to the cache
    options.RegisterPostEvictionCallback((keyEvicted, value, reason, state) =>
    {
        // dont re-add if running out of memory or it was forcibly removed
        if (reason == EvictionReason.Expired || reason == EvictionReason.TokenExpired) {
            sut.GetOrAdd(key, _ => GetProducts(), GetOptions()); //calls itself to get another set of options!
        }
    });
    return options;
}

// Get products and auto refresh them every 30s
var products sut.GetOrAdd(key, () => GetProducts(), GetOptions());