DEV Community

loading...

A Tale of Two Caches: Redis and the cache helper

Bemn
Hong Konger | Web Dev | Software Eng | Search for a tech job
Originally published at bemnlam.github.io on ・5 min read

Background

Recently our team started a new project: a showcase page under our main website. The website is read-only and the content won’t change frequently so we can have an aggressive caching policy.

I built this MVC web app using .NET Core 3.1 and deploy it as an IIS sub-site under the main website (which is a .NET Framework web app running on the IIS).

Table of Content


Redis

Why?

We are using Redis because it is simple, fast and we are already using it across all the main websites.

How?

Here are some highlights:

1. NuGet packages

<PackageReference Include="StackExchange.Redis" Version="2.1.30" />
<PackageReference Include="StackExchange.Redis.Extensions.Core" Version="6.1.7" />
<PackageReference Include="StackExchange.Redis.Extensions.Newtonsoft" Version="6.1.7" />
<PackageReference Include="StackExchange.Redis.Extensions.AspNetCore" Version="6.1.7" />
Enter fullscreen mode Exit fullscreen mode

StackExchange.Redis.Extensions.Newtonsoft is optional. Start from .NET Core 3.0 the default Json serializer will be System.Text.Json. If you want to use Newtonsoft.Json then you will need this package in your project.

StackExchange.Redis.Extensions.Core and StackExchange.Redis.Extensions.AspNetCore are the useful package to connect/read/write Redis easier. Read this documentation for more details.

2. appsettings.json

A typical .NET Core project should have an appsettings.json. Add the following section:

{
  "Redis": {
    "AllowAdmin": false,
    "Ssl": false,
    "ConnectTimeout": 6000,
    "ConnectRetry": 2,
    "Database": 0,
    "Hosts": [
      {
        "Host": "my-secret-redis-host.com",
        "Port": "6379"
      }
    ]
  } 
}
Enter fullscreen mode Exit fullscreen mode

Here, my-secret-redis-host.com is the Redis host and We are using the database no. 0. You can set multiple hosts. You can see a detailed configuration here.

3. Startup.cs

Add the following code in ConfigureServices()

var redisConfiguration = Configuration.GetSection("Redis").Get<RedisConfiguration>();
services.AddStackExchangeRedisExtensions<NewtonsoftSerializer>(redisConfiguration);
Enter fullscreen mode Exit fullscreen mode

5. CacheService

I created a CacheService.cs to help me reading/writing data in Redis. In this service:

public CacheService(RedisConfiguration redisConfiguration, ILogger<RedisCacheConnectionPoolManager> poolLogger)
{
    try
    {
        var connectionPoolManager = new RedisCacheConnectionPoolManager(redisConfiguration, poolLogger);
        _redisClient = new RedisCacheClient(connectionPoolManager, serializer, redisConfiguration);
    }
    catch(Exception ex)
    {
        /* something wrong when connection to Redis servers. */
    }
    _cacheDuration = 300; // cache period in seconds
}
Enter fullscreen mode Exit fullscreen mode

We need a method to write data:

public async Task<bool> AddAsync(string key, object value)
{
    try
    {
        bool added = await _redisClient.GetDbFromConfiguration().AddAsync(key, value, DateTimeOffset.Now.AddSeconds(_cacheDuration));
        return added;
    }
    catch (Exception ex)
    {
        /* something wrong when writing data to Redis */
        return false;
    }
}
Enter fullscreen mode Exit fullscreen mode

And we need a method to get cached data:

public async Task<T> TryGetAsync<T>(string key)
{
    try
    {
        if(await _redisClient.GetDbFromConfiguration().ExistsAsync(key))
        {
            return await _redisClient.GetDbFromConfiguration().GetAsync<T>(key);
        }
        else
        {
            return default;
        }
    }
    catch(Exception ex)
    {
        /* something wrong when writing data to Redis */
        return default;
    }
}
Enter fullscreen mode Exit fullscreen mode

I intentionally name this method TryGetAsync() because the cache may not exist or already expired when you try to get it from Redis.

After that, let’s go back to Startup.cs and register this service in ConfigureService():

services.AddTransient<CacheService>();
Enter fullscreen mode Exit fullscreen mode

Remember to register this service after services.AddStackExchangeRedisExtensions().

5. Controller

Inject the CacheService to the controller:

public DemoController(CacheService cacheService)
{
    _cacheService = cacheService;
}

public async Task<IActionResult> Demo(string name)
{
    var cacheKey = $"DemoApp:{name}";

    // Try to get cached value from Redis.
    string cachedResult = await _cacheService.TryGetAsync<string>(cacheKey);
    if(default != cachedResult)
    {
        return View(cachedResult);
    }

    // Add a new entry to Redis before returning the message.
    var message = $"Hello, {name}";
    if(null != sections && sections.Any())
    {
        await _cacheService.AddAsync(cacheKey, message);
    }

    return View(message);
}
Enter fullscreen mode Exit fullscreen mode

Explain Like I’m Five :

You ask the shopkeeper in Demo bookstore do they have a specific book name. First, the shopkeeper looks for the book on the bookshelf named Redis. If he finds that book, he takes it out and gives it to you.

If your book does not exist in the Redis bookstore, he has to go out and buy that book for you(!). However, he buys 2 identical copies. He gives you one and puts the other one on the Redis bookshelf, just in case another customer want that book later.


Cache Tag Helper

The Cache Tag Helper is a tag that you can use in a .NET Core MVC app. Content encolsed by this <cache> tag will be cached in the internal cache provider.

Example

<cache expires-after="@TimeSpan.FromSeconds(60)" 
       vary-by-route="name" 
       vary-by-user="false">
    @System.DateTime.Now
</cache>
Enter fullscreen mode Exit fullscreen mode

Explaination

In the above example, some attributes is set in the <cache> tag:

  • expires-after: how long (in seconds) will this cache last for.
  • vary-by-route: different copy will be cached when the route has a different value in the nameparam.
  • vary-by-user: different user will see different cached copies.

How can I know if it is working?

You will see the value rendered in the above example won’t change for 60 seconds even System.DateTime.Now should show the current time.


Bonus: A note on @helper and other HTML helpers

In the old days we can define some @helper functions in the razor view and (re)use it in the view. It’s being removed since .NET Core 3.0 because the design of @helper function does not compatible with async Razor content anymore.

Successor of the HTML helpers?

You can use the Tag Helpers in ASP.NET Core. Yes, the <cache> Tag Helper is one of the built-in Tag Helpers in .NET Core.

In addition, you can use the PartialAsync() method to render the partial HTML markup asynchronously.

@await Html.PartialAsync("_PartialName")
Enter fullscreen mode Exit fullscreen mode

More references on the HTML helpers and Tag Helpers:

What happened to the @helper directive in Razor ?

Remove the @helper directive from Razor

ASP.NET Core 1.0: Goodbye HTML helpers and hello TagHelpers!

Discussion (0)