Skip to content
S
asp.net core

Distributed Caching in ASP.NET Core with Redis

Updated · originally #dotnet#aspnetcore#caching#distributed-systems7 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.

💡 This tutorial targets .NET 10 LTS and pins the local Redis image. The sample also moved off the external users API it used to call, which started requiring a key. The .NET 5 version is still on the dotnet5 branch, though that framework is out of support.

For this tutorial, I will use Redis as the cache provider. Redis is a fast in-memory data store and the IDistributedCache abstraction keeps most of our application code independent of that choice.

💡 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 a 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.

We call this a distributed cache because it lives outside the application process. Multiple application instances can read the same entries, and cached data can survive an application restart or deployment. A managed service such as Azure Managed Redis can provide the same backing store in Azure.

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.

TechnologyNuGet packageNotes
Distributed Memory Cache-Useful for development and testing, but it isn’t actually shared.
Distributed SQL Server CacheMicrosoft.Extensions.Caching.SqlServerUses SQL Server as the backing store.
Distributed Redis CacheMicrosoft.Extensions.Caching.StackExchangeRedisUses Redis through the StackExchange.Redis client.
Distributed NCache CacheNCache.Microsoft.Extensions.Caching.OpenSourceUses NCache as the backing store.

Scaffolding a sample app

We will create an ASP.NET Core MVC app targeting .NET 10.

Shell
dotnet new mvc -n DistributedCache --framework net10.0
dotnet new sln --format sln
dotnet sln add DistributedCache

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

Shell
dotnet add DistributedCache package Microsoft.Extensions.Caching.StackExchangeRedis --version 10.0.11

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.

💡 The repository includes a docker-compose.yaml file, so you can start Redis with docker compose up -d from the project root.

Shell
docker run --name redis-cache -p 5002:6379 -d redis:8.8.1-alpine

This uses the official Redis image and maps container port 6379 to port 5002 on the host.

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,

Shell
docker ps -a

or alternatively with docker ps -a | grep redis-cache to filter 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

The .NET 10 sample uses the minimal hosting model in Program.cs. Register the Redis implementation and read its connection string from configuration:

C#
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis")
        ?? throw new InvalidOperationException("Connection string 'Redis' is not configured.");
    options.InstanceName = "DistributedCacheSample:";
});

AddStackExchangeRedisCache registers the Redis implementation behind IDistributedCache. The rest of the application depends on that interface rather than creating a Redis client directly.

For local development, put the Docker connection in appsettings.Development.json:

JSON
"ConnectionStrings": {
  "Redis": "localhost:5002"
}

Use Secret Manager locally or a secure store in production when the connection string contains credentials.

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

C#
public async Task<IActionResult> Index()
{
    var user = (await _cacheUserService.GetCachedUserAsync()).FirstOrDefault();
    return View(user);
}

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

public async Task<IActionResult> CacheRemoveAsync()
{
    await _cacheUserService.ClearCacheAsync();
    return RedirectToAction(nameof(Index));
}

The three actions read the current cache entry, populate it through the decorated user service, and clear it. It is important to await the asynchronous clear operation before redirecting.

💡 The sample registers CachedUserService as a decorator around UserService. For larger applications, a library such as Scrutor can make decorator registration more concise.

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

C#
public class CacheProvider : ICacheProvider
{
    private readonly IDistributedCache _cache;

    public CacheProvider(IDistributedCache cache)
    {
        _cache = cache;
    }
    
    public async Task<T?> GetFromCacheAsync<T>(string key) where T : class
    {
        var cachedValue = await _cache.GetStringAsync(key);
        return cachedValue is null ? null : JsonSerializer.Deserialize<T>(cachedValue);
    }

    public Task SetCacheAsync<T>(
        string key,
        T value,
        DistributedCacheEntryOptions options) where T : class
    {
        var serializedValue = JsonSerializer.Serialize(value);
        return _cache.SetStringAsync(key, serializedValue, options);
    }

    public Task ClearCacheAsync(string key)
    {
        return _cache.RemoveAsync(key);
    }
}
  1. Read and deserialize. Redis gives us a string, so a cache hit has to be deserialized back into the requested .NET type.
  2. Serialize and store. Values cross the cache boundary as JSON strings, while the caller still works with typed objects.
  3. Remove the key. Clearing the cache maps directly to the asynchronous Redis-backed remove operation.

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.

Shell
docker exec -it redis-cache redis-cli

The sample stores a JSON string, not a Redis hash. Issue GET "DistributedCacheSample:users" to inspect it:

Plain text
GET "DistributedCacheSample:users"

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

If you prefer a GUI, Redis Insight provides a visual representation of what the application saved.

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

Demo

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

Distributed caching with Redis demo

As you can see, it will only fetch the users list only the first time we click the “Cache It” button. Every subsequent requests 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 configuration. In this demo I have set a sliding expiry for 2 minutes.

Conclusion

In this article, we converted the in-memory example to use ASP.NET Core’s IDistributedCache interface with Redis as the backing store. The same abstraction can be used with a managed service such as Azure Managed Redis for workloads including data caching and session storage.

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

References

  1. Distributed caching in ASP.NET Core
  2. Upgrade to a new .NET version
  3. .NET 10 breaking changes
  4. Redis commands

Stay in the loop

Practical engineering notes, without the inbox noise.

Notes on distributed systems, resilient software, and engineering in the real world - usually once or twice a month.

Unsubscribe anytime. See what you get, or prefer a feed? Subscribe via RSS.