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 bookname
. First, the shopkeeper looks for the book on the bookshelf namedRedis
. 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 theRedis
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 thename
param.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!