DEV Community

loading...
Cover image for Distributed Caching in ASP.NET Core with Redis

Distributed Caching in ASP.NET Core with Redis

sahan profile image Sahan Originally published at sahansera.dev on ・7 min read

About a year ago, I wrote a blog post on simple In-Memory Caching in ASP.NET Core with IMemoryCache. This article mainly introduced the concept of caching and how we can store stuff in the server’s memory for simple tasks. Today’s objective is to leverage the IDistributedCache to do some distributed caching so that we can horizontally scale out our web app.

For this specific tutorial, I will be using Redis as my cache provider. Redis is a battle-tested, fast memory cache that can store many types of objects. Redis is being used by giants such as Twitter, Github, Instagram, Stackoverflow, Medium, Airbnb etc.

💡 You can find the accompanying code for this blog post from here.

Here’s a snapshot of what we are going to be building.

distributed-caching-in-aspdotnet-core-with-redis-1.png

  1. User requests a user object.
  2. App server checks if we already have a user in the cache and return the object if present.
  3. App server makes an HTTP call to retrieve the list of users.
  4. Users service returns the users list to the app server.
  5. App server sends the users list to the distributed (Redis) cache.
  6. App server gets the cached version until it expires (TTL).
  7. User gets the cached user object.

The main reason why we call this a distributed cache is that it lives outside of our application server (as opposed to traditional in-memory caching) and we have the flexibility of scaling it horizontally (when operating in the cloud), if need be. Head over here to have a look at how this could be useful in enterprise applications.

The IDistributedCache interface provides us with a bunch of methods to manipulate your cache. And the actual implementation is specific to the technology we want to use. Here’s a summary of different ways you can do this.

Technology NuGet package Notes
Distributed Memory Cache - This is only recommended for dev and testing purposes. This is not an actual distributed cache.
Distributed SQL Server Cache Microsoft.Extensions.Caching.SqlServer Use SQL Server instance as a cache (locally or in cloud with Azure SQL Server).
Distributed Redis Cache Microsoft.Extensions.Caching.StackExchangeRedis Use Redis as a backing store (locally or in cloud with Azure Redis Cache)client package is Developed by peeps at StackExchange.
Distributed NCache Cache NCache.Microsoft.Extensions.Caching.OpenSource Wrapper around the NCache Distributed Cache

Scaffolding a sample app

We will create a Web MVC app in ASP.NET Core 5.

dotnet new mvc -n DistributedCache
dotnet new sln
dotnet sln add DistributedCache
Enter fullscreen mode Exit fullscreen mode

Let’s go ahead and add the Redis client package from NuGet.

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis --version 5.0.1
Enter fullscreen mode Exit fullscreen mode

Creating a Redis docker container

For this step, I assume that you have already installed Docker on your machine. It’s handy to have this so that you can spin up your own Redis container whenever you want for development purposes.

docker run --name redis-cache -p 5002:6379 -d redis
Enter fullscreen mode Exit fullscreen mode

We are telling docker to use the official redis image and spin up a container with the name redis-cache and bind port 6379 of the container to the port 5002 of your host machine. Why I chose port 5002 is that it might be open as it’s a less obvious port number.

If you haven’t got the Redis image locally, it will fetch that from the DockerHub and spin up a new container under the name redis-cache. Next let’s verify that our docker instance is up and running. You could do so with,

docker ps -a
Enter fullscreen mode Exit fullscreen mode

or alternatively with docker ps -a | grep redis-cache to filter our the output, if you have a bunch of containers running in the background like I do 😅

distributed-caching-in-aspdotnet-core-with-redis-2.png

Now that we have the Redis container up and running let’s configure our web app to use it.

Application Configuration

Startup.cs

Since we have already added the required NuGet package, we only need to register its service in our app’s DI container and tell it where to find our Redis instance.

// Register the RedisCache service
services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = Configuration.GetSection("Redis")["ConnectionString"];
});
Enter fullscreen mode Exit fullscreen mode

When we call AddStackExchangeRedisCache on the services object, it registers a singleton of RedisCache class against the IDistributedCache interface under the covers. This is what it looks like in the source:

// ...
services.Add(ServiceDescriptor.Singleton<IDistributedCache, RedisCache>());
// ..
Enter fullscreen mode Exit fullscreen mode

appsettings.json

Since we have the Docker instance up and running at port 5002, we can mention that for development settings.

"Redis": {
  "ConnectionString": "localhost:5002"
}
Enter fullscreen mode Exit fullscreen mode

I have brought across the service from my previous tutorial and added them to this project. You can find them under the Services folder. In fact, I have made the code to look more bit simpler as well.

Implementation

The functionality is pretty simple, and here’s what we going to do:

  1. Get the cached user (if any) and display its email address
  2. A button to invoke a HTTP call and cache a list of users
  3. A button to clear the cache

The UI would look something like the following.

distributed-caching-in-aspdotnet-core-with-redis-3.png

Let’s look at the main entry point of the actions, the HomeController class.

HomeController.cs

public async Task<IActionResult> Index()
{
    var users = (await _cacheService.GetCachedUser())?.FirstOrDefault();
    return View(users);
}

public async Task<IActionResult> CacheUserAsync()
{
    var users = await _usersService.GetUsersAsync();
    var cacheEntry = users.First();
    return View(nameof(Index), cacheEntry);
}

public IActionResult CacheRemoveAsync()
{
    _cacheService.ClearCache();
    return RedirectToAction(nameof(Index));
}
Enter fullscreen mode Exit fullscreen mode

The code here is pretty self-explanatory, and we implement the 3 features we discussed in the Index, CacheUserAsync and CacheRemoveAsync actions.

💡 Tip: You would ideally want to decorate the UsersService class with CacheService by using a DI container such as Scrutor. You don’t want to write the plumbing code we have written here to emulate a similar thing as the default DI container doesn’t support the behaviour. Refer to Andrew Lock’s excellent article on this topic.

I’m going to skip all the other plumbing code and show you how we Get and Set values with the Redis cache. The real magic happens in the ICacheProvider class.

The code itself it pretty self-explanatory. In the GetFromCache method, we call the GetStringAsync with a given key (_Users in this case). It’s worth noting that we need to deserialise it to the type we want before returning it to the caller. Similarly, we serialise our users list and save it as a string in the Redis cache under the _Users key.

CacheProvider.cs

public class CacheProvider : ICacheProvider
{
    private readonly IDistributedCache _cache;

    public CacheProvider(IDistributedCache cache)
    {
        _cache = cache;
    }

    public async Task<T> GetFromCache<T>(string key) where T : class
    {
        var cachedResponse = await _cache.GetStringAsync(key);
        return cachedResponse == null ? null : JsonSerializer.Deserialize<T>(cachedResponse);
    }

    public async Task SetCache<T>(string key, T value, DistributedCacheEntryOptions options) where T : class
    {
        var response = JsonSerializer.Serialize(value);
        await _cache.SetStringAsync(key, response , options);
    }

    public async Task ClearCache(string key)
    {
        await _cache.RemoveAsync(key);
    }
}
Enter fullscreen mode Exit fullscreen mode

So what gets saved under the covers?

We can connect to the container and open up the redis-cli to see what’s inside. To do that, you could run the following command.

docker exec -it redis-cache redis-cli
Enter fullscreen mode Exit fullscreen mode

Once you are in, you could issue a hgetall _Users command to inspect what’s inside the hash that got saved in our request.

distributed-caching-in-aspdotnet-core-with-redis-5.png

If you like to use a GUI, here’s a nice representation of what our web app saved under the hood. I used RedisInsight tool for this.

distributed-caching-in-aspdotnet-core-with-redis-4.png

Demo

Here’s a working demo when you run the code from my repo:

As you can see, it will only fetch the users list only the first time we click the “Cache It” button. Every subsequent request will fetch the users list from the Redis cache and serve to our app. The cache expiry can be configured by setting a sliding window or an absolute expiry by passing in the configuration. In this demo, I have set a sliding expiry for 2 minutes.

Conclusion

In this article, we converted our previous In-Memory example to use the IDistributedCache interface provided by ASP.NET Core and used Redis as a backing store. This approach can be utilised to leverage cloud service such as Azure Redis Cache for use-cases such as response caching, session storage etc.

Hope you enjoyed this article and feel free to share your thoughts and feedback. Until next time 👋

References

  1. https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed?view=aspnetcore-5.0
  2. https://redis.io/commands
  3. https://aspnetcore.readthedocs.io/en/stable/performance/caching/distributed.html#using-a-redis-distributed-cache

Discussion (5)

pic
Editor guide
Collapse
turnerj profile image
James Turner

Nice article! I've been doing a bunch of caching work recently and one thing to keep in mind is distributed locking. Helps avoid unnecessary pressure on the systems that are filling the cache.

In Cache Tower, I use the presence of a key already being set to see whether I should call the factory method or not:

var hasLock = await Database.StringSetAsync(lockKey, RedisValue.EmptyString, expiry: Options.LockTimeout, when: When.NotExists);
Enter fullscreen mode Exit fullscreen mode

Once the new value is cached, I use the pub/sub feature of Redis to inform the instances of the application that the cache is refreshed. This then allows those waiting requests to just ask Redis for the new value rather than process it themselves.

As an aside, noticed you're from Adelaide too - Hi! 👋

Collapse
swimburger profile image
Niels Swimberghe

Great tutorial, thanks!

Collapse
sahan profile image
Sahan Author

Hey Niels! Thanks for the kind words 😊

Collapse
shaijut profile image
Shaiju T

Nice 😄, Keep on sharing.

Collapse
sahan profile image
Sahan Author

Thank you! Means a lot 😊