Skip to main content

A Tale of Two Caches: Redis and the cache helper

·884 words·5 mins
Bemn
Author
Bemn
Hong Konger.

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 Contents #


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" />

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"
      }
    ]
  } 
}

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);

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
}

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;
    }
}

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;
    }
}

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>();

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);
}

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>

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")

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!

Related

How to add a new Hugo blog post
·43 words·1 min
Go to blog folder
A Note on SSL Certificate
·566 words·3 mins
This is a note about the Linkedin learning course SSL Certificates for Web Developers.
Creating a User With Limited Privileges in Postgres
·236 words·2 mins
This post showing how to create a user with limited privileges in PostgreSQL.