Category Archives: Caching

Using Garnet

Garnet is a Redis (RESP) compatible cache from Microsoft Research, it’s used internally within Microsoft but as it’s a research project it’s possible the design etc. will change/evolve.

Not only is is Redis compatible, it’s written in C#, so ideal for .NET native environments. Check out the Garnet website for more information

I’ve shown code to interact from C#/.NET to Redis in the past, the same code will work with Garnet.

Here’s a Dockerfile to create an instance of Garnet

services:
  garnet:
    image: 'ghcr.io/microsoft/garnet'
    ulimits:
      memlock: -1
    container_name: garnet
    ports:
      - "6379:6379"

Using Valkey (a Redis compatible memory data store)

Valkey is an in-memory, high performance key/value store. It’s Redis compatible which means we can use the same protocols, clients etc.

We can run up an instance via docker using

docker run --name valkey-cache -d -p 6379:6379 valkey/valkey

The valkey client can be run from the instance using

docker exec -ti valkey-cache valkey-cli

As mentioned, we can use existing tools that work with Redis, so here’s a docker-compose.yaml to run up a Valkey instance along with redis-commander

services:
  valkey:
    image: valkey/valkey
    container_name: valkey
    ports:
      - "6379:6379"

  redis-commander:
    image: rediscommander/redis-commander:latest
    container_name: redis-commander
    ports:
      - "8080:8080"
    environment:
      REDIS_HOSTS: valkey:valkey:6379

Now we can use localhost:8080 and view/interact with our data store via a browser.

valkey-cli

From the valkey-cli we can run various commands to add a key/value, get it, delete it etc.

CommandDescription
KEYS '*'List all keys
SET mykey "Hello"add/set a key/value
GET mykeyget the value for the given key
EXISTS mykeyCheck if the key exists
DEL mykeyDelete the key/value
EXPIRE mykey 60Set an expire on the key (60 seconds in this example)
TTL mykeyChecks the time to live for a key

Output caching in ASP.NET core

Output caching can be used on your endpoints so that if the same request comes into your endpoint within an “cache expiry” time then the endpoint will not get called and the stored/cached response from a previous call will be returned.

To make that clearer the endpoint’s method will NOT be called, the response is cached and hence returned via the ASP.NET middleware.

This is particularly useful for static or semi-static context.

Out of the box, the OutputCache can handle caching for different query parameters and routes can be easily set up to handle caching.

I’m going to setup the output cache to use Redis

builder.AddRedisOutputCache("cache");

builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("ShortCache", builder => builder.Expire(TimeSpan.FromSeconds(10)));
});

Now from a minimal API endpoint we can apply output caching using CacheOutput as below

app.MapGet("/cached/{id}", (int id) => new
    {
        Message = $"Output Cache {id}",
        Timestamp = DateTime.UtcNow
    })
    .CacheOutput(c => c.VaryByValue(
        ctx => new KeyValuePair<string, string>("id", ctx.Request.RouteValues["id"]?.ToString() ?? string.Empty)));

Each unique id gets its own cached response.

The endpoint is only executed once per id within the cache duration.
– The VaryByValue method lets you define custom cache keys based on route values, headers, or query strings.
– Without this, /cache/1 and /cache/2 might share a cache entry or overwrite each other depending on the default key behavior.

app.MapGet("/cached-query", (int id) => new
    {
        Message = $"Output Cache {id}",
        Timestamp = DateTime.UtcNow
    })
    .CacheOutput();
app.UseOutputCache();

Exploring Akavache

Akavache is a key-value store/cache. It’s compatible with flavours of Windows including the desktop, WinRT and Windows Phone 8 as well as iOS, Mac and Android via Xamarin. It supports async await as well as RX observable.

Let’s code

We interact with Akavache via the BlobCache static class but before we do anything with Akavache we need to setup the application name. So at the first opportunity, such as when your applications starts you should supply Akavache with your application name.

BlobCache.ApplicationName = "MyApplicationName";

There are several different caches that we can use on the BlobCache object. Each implements the IBlobCache interface.

  • The InMemory property is used for caching anything which is to be stored (as the name suggests) in memory and thus lost when the application shuts down.
  • The LocalMachine is used to store data that is not related to the user’s account.
  • The Secure is encrypted and is used to anything you want to store in a more secure manner.
  • Finally the UserAccount is used to store data on a per user basis and can be part of a roaming profile.

As stated on the Akavache readme.md, “on the desktop, your application’s data will be stored in the %AppData%\[ApplicationName] and %LocalAppData%\[ApplicationName]”.

Inserting data into the cache

There are several insertion methods (instance and extension methods) and overloads for each. I’m not going to go through each method and their overloads, instead let’s look at the insertion method you’ll most likely start with – inserting a single object.

BlobCache.UserAccount.InsertObject("someObject", new SomeObject());

We supply a key, in this case the string someObject followed by an object. We can optionally supply a DateTimeOffset or TimeSpan to use as an expiration time, i.e. when the data is ejected from the cache.

Ofcourse, if we insert more than one item with the same key, we’re simply going to overwrite the existing item, therefore updating the cache with the last inserted value.

Getting data from the cache

Once we’ve inserted items in the cache we’ll need a way to retrieve them. As can be seen from the InsertObject method we supply a key for our cached item. So we obviously use the same key to get the value from the cache. Akavache uses an asynchronous model for getting items from a cache so uses either an IObservable (the RX way) or getting values using async await. Here’s an example using the RX method

SomeObject result = null;
BlobCache.UserAccount.GetObject<SomeObject>("someObject")
   .Subscribe(i => result = i);

and here’s the same thing using async await

SomeObject result = await BlobCache.UserAccount.GetObject<SomeObject>("someObject")

Getting data which is currently not cached

Obviously we don’t always start an application with the items already in the cache. So a common requirement is to query the cache for a value, if it doesn’t exist then get it from somewhere and then place it in the cache. Akavache offers a GetOrFetchObject method which allows the developer to to do just this. First we try to get an item from the cache and if it does not exist in the cache we can supply a Func<Task<T>> or Func<IObservable<T>> to get the item from somewhere else and it’s then automatically added to the cache for us.

SomeObject result = null;
BlobCache.UserAccount.GetOrFetchObject(
   "someObject", 
   () => Task.Factory.StartNew(() => new SomeObject()))
   .Subscribe(o => result = o);

// or using async await

SomeObject result = await BlobCache.UserAccount
   .GetOrFetchObject("someObject", 
   () => Task.Factory.StartNew(() => new SomeObject()));

We can also pass an expiration DateTimeOffset to the above method.

Note: GetOrFetchObject and GetOrCreateObject appear to do the same thing, both calling GetOrFetchObject which takes a Func<IObservable>. The former simply converts a Task<T> to an and IObservable<T> and the latter converts a Func<T> to an IObservable<T> also.

An interesting other “get” method to look at is GetAndFetchLatest. This will attempt to get a value from the cache and at the same time will use the supplied Func to get the value from some alternate place. So for example, let’s say you’re going to get a value from a webservice. This will return the current cached version (if one exists) and at the same time try to get the current value from the webservice. Hence two items may be returned.

In use…

BlobCache.UserAccount.GetAndFetchLatest(
   "someObject", () => Task.Factory.StartNew(() => new SomeObject()))
   .Subscribe(i => result = i);

In the above code the result = i will be called twice (assuming we’ve already inserted an object with the key “someObject” into the cache). As you’d expect from the previous description of this method, the first call would contain cached “someObject” value and the second would be the new object we’ve created in the task.

Removing items from the cache

Apart from setting an expiration for items as they’re added to the cache we can also use the method Invalidate to remove specific key values from the cache or InvalidateAll to remove/clear all items from the cache.

For example

BlobCache.UserAccount.Invalidate("someObject");