DEV Community

Tim Udoma
Tim Udoma

Posted on • Edited on

Distributed Caching In ASP.Net Core Using Redis

Distributed caching is the concept of centralizing a cache such that it is used by multiple servers. During development, caches commonly reside in memory on the developer’s system. This presents a challenge when deploying to multiple servers in a farm. If the cache were to reside in the memory of each of these servers, there would be no way to map keys to values of previous requests, since requests are not guaranteed to end up on the same server in a load-balanced distributed environment.

A distributed cache resolves this problem by acting as a central memory store for caching. It ensures that stateful resources such as user sessions are “sticky”, that is, they are routed to the same server that handled the first request for a user session.

Distributed caching in ASP.Net Core makes use of the interface IDistributedCache.

To get started, this article makes some assumptions:

  • You have an editor installed (Visual Studio Code or Visual Studio)
  • You have Redis installed. You can find installation instructions here
  • You are running .Net Core 3.1

With this in mind, let’s dive in.

Create a new ASP.Net core API project.
Build the project.
Install the nuget package Microsoft.Extensions.Caching.StackExchangeRedis
Register the package in Startup.cs using the code below:

// Add Redis distributed cache
services.AddStackExchangeRedisCache(options => options.Configuration = this.Configuration.GetConnectionString("redisServerUrl"));

Enter fullscreen mode Exit fullscreen mode

This adds Redis to the application and exposes the IDistributedCache interface which can be consumed using dependency injection. Notice that it is at this point we pass our Redis server port number. It is good practice not to hard code this, but to set it as a configurable property via appsettings. Now head over to appsettings.json and add the Redis server URL under ConnectionStrings section.

"ConnectionStrings": {
    "redisServerUrl" :  "localhost:6379"
  },
Enter fullscreen mode Exit fullscreen mode

You can find your Redis port number by running redis-server command in CMD. By default, it is localhost:6379. Now Redis is setup and ready to be used in your application.

Head over to the WeatherForecastController and inject IDistributedCache via constructor injection.

public WeatherForecastController(ILogger<WeatherForecastController> logger, IDistributedCache cache)
{
    _logger = logger;
    _cache = cache;
}

Enter fullscreen mode Exit fullscreen mode

IDistributedCache interface ensures the presence of essential methods for working with a cache namely:
GetString() and GetStringAsync() for retrieving string values including JSON strings
Get() and GetAsync() for retrieving byte array values.
SetString() and SetStringAsync() for setting string values also including JSON strings
Set() and SetAsync() for setting byte array values.
In the Get action method of WeatherForecastController controller, check the cache for the specified cache key, if present, return the result from the cache rather than the DB.

// Check if content exists in cache
string cachedWeatherResult = await _cache.GetStringAsync("weatherResult");
if (cachedWeatherResult != null)
{
    return cachedWeatherResult;
}
Enter fullscreen mode Exit fullscreen mode

Else, process the request

var rng = new Random();
            var weatherResult = Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
    })
    .ToArray();
Enter fullscreen mode Exit fullscreen mode

save it to cache for subsequent requests.

string result = JsonConvert.SerializeObject(weatherResult);
await _cache.SetStringAsync("weatherResult", result);
Enter fullscreen mode Exit fullscreen mode

Cache Expiration

Given our setup, we run the risk of having stale data in our cache. To resolve this, we need to set the absolute expiration and sliding expiration times whenever we add an item to the cache.
Absolute expiration time defines the period a particular item can remain in the cache before it is removed. On the other hand, sliding expiration time refers to the period before a cache item is removed if unused. It is usually smaller than the Absolute expiration time.

Now let us implement this by refactoring our code to avoid repetition. We will add extension methods to IDistributedCache.

Create a new folder in the project called Extensions and add a class named CacheExtensions and add the code below:

using Microsoft.Extensions.Caching.Distributed;
Enter fullscreen mode Exit fullscreen mode
public static async Task<T> GetCacheValueAsync<T>(this IDistributedCache cache, string key) where T : class
{
    string result = await cache.GetStringAsync(key);
    if (String.IsNullOrEmpty(result))
    {
         return null;
    }
    var deserializedObj = JsonConvert.DeserializeObject<T>(result);
    return deserializedObj;
}

public static async Task SetCacheValueAsync<T>(this IDistributedCache cache, string key, T value) where T : class
{
      DistributedCacheEntryOptions cacheEntryOptions = new DistributedCacheEntryOptions();

      // Remove item from cache after duration
      cacheEntryOptions.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(60);

      // Remove item from cache if unsued for the duration
      cacheEntryOptions.SlidingExpiration = TimeSpan.FromSeconds(30);

      string result = value.ToJsonString();

      await cache.SetStringAsync(key, result);
}
Enter fullscreen mode Exit fullscreen mode

Note that SetCacheValueAsync uses an instance of DistributedCacheEntryOptions to set Absolute expiration time and sliding expiration time.

With these extension methods in place, refactor the WeatherForecastController to invoke them. The controller should look like this

public async Task<IEnumerable<WeatherForecast>> Get()
{
   // Check if content exists in cache
   WeatherForecast[] weatherResult = await _cache.GetCacheValueAsync<WeatherForecast[]>("weatherResult");
   if (weatherResult != null)
   {
        return weatherResult;
   }
   var rng = new Random();
    weatherResult = Enumerable.Range(1, 5).Select(index => new WeatherForecast
   {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
    })
        .ToArray();

    await _cache.SetCacheValueAsync("weatherResult", weatherResult); 
    return weatherResult;
}
Enter fullscreen mode Exit fullscreen mode

Build and run the application. Ensure Redis CLI is still running in CMD. Open Postman and navigate to the weather forecast URL. Note the time taken to service the request. This first request was not from the cache.

Send the request again and this time, the request time should be less than that of the first request. The data in the response should be from the cache. This can be verified by running the command keys * in Redis CLI. If your key is in the cache, congrats, you have been able to implement Redis successfully.

There you have it. Thanks for reading. If you learned something new from this article, please like and share.

Did you spot a typo, an error or want to contribute? Here's the repo on GitHub

Top comments (3)

Collapse
 
alexandernnanna profile image
Da Real Obyno

Nice article

Collapse
 
amiradeli profile image
amiradeli

Thank you for this post.
Actually, the return type of Get method is IEnumerable and there is a compilation error when we return cachedWeatherResult which is string.
How can cachedWeatherResult be converted to the correct return type?

Collapse
 
samtimberlan profile image
Tim Udoma

Hello Amiradelli,

The return method of Get method is a task of IEnumerable. To get the correct return type for cached result, the sample uses GetCacheValueAsync() method which does the conversion using JsonConvert deserialization.

Here is a link to the implementation on GitHub: github.com/samtimberlan/Distribute...