
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
dotnet5branch, 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.

- User requests a
userobject. - App server checks if we already have a user in the cache and return the object if present.
- App server makes a HTTP call to retrieve the list of users.
- Users service returns the users list to the app server.
- App server sends the users list to the distributed (Redis) cache.
- App server gets the cached version until it expires (TTL).
- 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.
| Technology | NuGet package | Notes |
|---|---|---|
| Distributed Memory Cache | - | Useful for development and testing, but it isn’t actually shared. |
| Distributed SQL Server Cache | Microsoft.Extensions.Caching.SqlServer | Uses SQL Server as the backing store. |
| Distributed Redis Cache | Microsoft.Extensions.Caching.StackExchangeRedis | Uses Redis through the StackExchange.Redis client. |
| Distributed NCache Cache | NCache.Microsoft.Extensions.Caching.OpenSource | Uses NCache as the backing store. |
Scaffolding a sample app
We will create an ASP.NET Core MVC app targeting .NET 10.
dotnet new mvc -n DistributedCache --framework net10.0
dotnet new sln --format sln
dotnet sln add DistributedCacheLet’s go ahead and add the Redis client package from NuGet.
dotnet add DistributedCache package Microsoft.Extensions.Caching.StackExchangeRedis --version 10.0.11Creating 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.yamlfile, so you can start Redis withdocker compose up -dfrom the project root.
docker run --name redis-cache -p 5002:6379 -d redis:8.8.1-alpineThis 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,
docker ps -aor 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 😅

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:
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:
"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:
- Get the cached user (if any) and display its email address
- A button to invoke a HTTP call and cache a list of users
- A button to clear the cache
The UI would look something like the following.

Let’s look at the main entry point of the actions, the HomeController class.
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
CachedUserServiceas a decorator aroundUserService. 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.
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);
}
}- Read and deserialize. Redis gives us a string, so a cache hit has to be deserialized back into the requested .NET type.
- Serialize and store. Values cross the cache boundary as JSON strings, while the caller still works with typed objects.
- 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.
docker exec -it redis-cache redis-cliThe sample stores a JSON string, not a Redis hash. Issue GET "DistributedCacheSample:users" to inspect it:
GET "DistributedCacheSample:users"
If you prefer a GUI, Redis Insight provides a visual representation of what the application saved.

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 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 👋