項次 | 書名 | 出版社 | 作者 |
---|---|---|---|
1 | 享受:那些微不足道的小事 | 雪儷叢 | 希薇亞・克拉拉絲特芙 |
2 | 間隙:寫給那些受折磨的你 | 時報 | 平路 |
3 | 大家說英語雜誌7月份 | 空中英語教室文摘雜誌社 | 空中英語教室文摘雜誌社 |
4 | The Adventures of Tom Sawyer (Oxford Bookworms Library Level 1) | Oxford | Mark Twain Retold by Nick Bullard, Mike Borfield |
5 | 以數學名為致上敬意吧!畢達哥拉斯篇 | 商周出版 | 劉子・巴菲爾德 |
6 | 以改變世界的數學圖鑑 | 商周出版 | 日本DK編輯部 |
7 | 我們的文明病,都是這樣養出來的 | 和平 | 英國DK編輯部 |
8 | 跟大師學創意力6-9 | 字畝文化 | 譯者:周宜芳 |
9 | 化學實驗開外掛 | 商周出版 | 東方王 |
10 | 完全圖解人工智慧:零基礎也OK!從NLP、圖像辨識到生成模型,現代人必修的53堂AI課 | 台灣東販 | 高橋憲渡、三川裕之、小西力也、武井大輔 |
11 | 寫給中學生看的AI課:AI生態系需要又理實具的未來人才(增訂版) | 三采 | 蔡宗翰 |
12 | 你願意,人生就會值得:焦慮者的情緒課 | 如何 | 蔡康永 |
13 | 生命中最大的寶藏就是你自己 stand by yourself | 天下 | 曾寶儀 |
14 | 藝術這樣看How art works | 小天下 | 莎拉・霍爾 Sarah Hull |
15 | 寫給年輕人的古典音樂小史【暢銷漫畫Q版】II:違罪民的貝多芬、搞笑的韓得不要不要、蕭幫30位偉大音的快樂指南 | 原點出版社 | 洪允杓 |
16 | 你的不快樂,是花了太多時間在乎,不在乎你的人和事 | 是日創意文化有限公司 | Peter Su |
17 | 外界的聲音只是參考;你不開心就不參考 | 英屬維京群島商高寶國際有限公司台灣分公司 | 志揚的貓頭鷹 |
18 | 我的同學是一隻熊 | 親子天下 | 張友漁 |
19 | 只是開玩笑,竟然變被告 | 三采 | 吉靜如 |
文章分類
2025年5月23日 星期五
2025 Summer Book List
2025年3月23日 星期日
[ASP.NET Core] Ocelot - Redis caching and custom Redis key
Redis Ocelot API Gateway
▌Introduction
Ocelot is the API Gateway framework for ASP.NET Core, it supports memory caching and the cache key contains a hash value by default. We’ll implement the Redis caching and use our own cache key generating strategy.
The full sample code is at KarateJB/AspNetCore.Profiler.Sample.
▌Install Package
StackExchange.Redis version 2.8.24
Ocelot and Ocelot.Provider.Polly version 23.2.2
<PackageReference Include="StackExchange.Redis" Version="2.8.24" />
<PackageReference Include="Ocelot" Version="23.2.2" />
<PackageReference Include="Ocelot.Provider.Polly" Version="23.2.2" />
▌Implement
First we have to enable Ocelot in Program.cs.
using Ocelot.Cache;
using Ocelot.Configuration.File;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Ocelot.Provider.Polly;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
// Add Ocelot configuration file
builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);
builder.Services.AddOcelot(builder.Configuration).AddPolly();
builder.Services.AddControllers();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
await app.UseOcelot();
app.MapControllers();
app.Run();
Then complete the ocelot.json configuration, see Ocelot:Configuration.
▋Ocelot’s caching implementation and interfaces
We can find the default caching implementation DefaultMemoryCache<T> in Ocelot source code; it implements the interface IOcelotCache<T>.
using Microsoft.Extensions.Caching.Memory;
namespace Ocelot.Cache;
public class DefaultMemoryCache<T> : IOcelotCache<T>
{
}
For the cache key generator, the implementation class is DefaultCacheKeyGenerator., which implements the interface ICacheKeyGenerator.
namespace Ocelot.Cache;
public class DefaultCacheKeyGenerator : ICacheKeyGenerator
{
}
Ocelot registers them by the following code in Ocelot source code “src\Ocelot\DependencyInjection\Features.cs”.
public static IServiceCollection AddOcelotCache(this IServiceCollection services) => services
.AddSingleton<IOcelotCache<Regex>, DefaultMemoryCache<Regex>>()
.AddSingleton<IOcelotCache<FileConfiguration>, DefaultMemoryCache<FileConfiguration>>()
.AddSingleton<IOcelotCache<CachedResponse>, DefaultMemoryCache<CachedResponse>>()
.AddSingleton<ICacheKeyGenerator, DefaultCacheKeyGenerator>()
.AddSingleton<ICacheOptionsCreator, CacheOptionsCreator>();
In ASP.NET Core Dependency Injection, we can add multiple implementation classes for a single interface into the service containers. If we don’t specify which one to use, the last injected implementation class will be the default one. However, Ocelot checks if the service container already has the custom implementation of the two interfaces, it won’t add its own implementations to the DI container to make sure that the our custom implementations are used.
Thus, all we have to do is create our custom Redis caching and Redis key generator classes(services) and implement the interfaces, then register our services into the DI container.
▋Implement IOcelotCache<CachedResponse>
Let’s create a new class RedisCacheStore that implements IOcelotCache<CachedResponse>.
namespace AspNetCore.Profiler.Gateway.Services
{
public class RedisCacheStore : IOcelotCache<CachedResponse>
{
private readonly ILogger _logger;
private readonly RedisSetting _redisSetting;
private readonly IDatabase? _redisDb;
private Func<string, string, string> RedisKey = (string region, string key) => $"{region}:{key}";
public RedisCacheStore(ILogger<RedisCacheStore> logger, IOptions<AppSettings> options)
{
_redisSetting = options?.Value?.Redis ?? throw new ArgumentNullException(nameof(RedisSetting));
// Initialize Redis connection
var redis = ConnectionMultiplexer.Connect(_redisSetting.ConnectionString);
_redisDb = redis.GetDatabase();
}
public void Add(string key, CachedResponse value, TimeSpan ttl, string region)
{
string redisKey = RedisKey(region, key);
if (value is not null && _redisDb != null)
{
var cacheValue = JsonConvert.SerializeObject(value);
_redisDb.StringSet(redisKey, cacheValue, expiry: ttl);
}
}
public void AddAndDelete(string key, CachedResponse value, TimeSpan ttl, string region)
{
Add(key, value, ttl, region);
}
public CachedResponse Get(string key, string region)
{
string redisKey = RedisKey(region, key);
RedisValue cachedData = _redisDb != null ? _redisDb.StringGet(redisKey) : RedisValue.Null;
if (!cachedData.IsNullOrEmpty)
{
return JsonConvert.DeserializeObject<CachedResponse>(cachedData);
}
return null;
}
public bool TryGetValue(string key, string region, out CachedResponse value)
{
value = Get(key, region);
return value != null;
}
public void ClearRegion(string region)
{
// Skip
}
}
}
The code has nothing special, but we have to know what “region” and “key” are, because they are related to the cache key management in Ocelot’s design.
To enable caching for an upstreaming routine, we add the following configuration in Ocelot.json (see Ocelot document).
"FileCacheOptions": {
"TtlSeconds": 3600,
"Region": "payment",
"EnableContentHashing": false
}
The Region’s value is for the parameter “region”. A region is a concept of grouping of cache keys. For example, we can put cache keys “user:aaa” and “vip-bbb” in the same group: “users”, and “users” is the region. When we want to clear all the caches in the same group, the “region” is specified to clear all the cache keys that were put into this region. I suggest reading Ocelot’s memory cache implementation DefaultMemoryCache<T> and you will find out how Ocelot manages the cache keys with “region”.
In Redis, we often set the Redis key in this format, e.g. “users:aaa” or “users:bbb” for grouping keys; that’s the same concept of region. The “region” here is “users”, and we can find all the Redis keys of the same region with pattern “users:*”.
The “key” is the MD5 hash of the request’s “HTTP method + URL” that is generated by the implementation (DefaultCacheKeyGenerator by default) of ICacheKeyGenerator. If we don’t implement our own cache key generator, our Redis key will be like this:
payment:5D828182FD57BC692F0055A01C27374F
The hash “5D828182FD57BC692F0055A01C27374F” is the “key”. The caching (read/write) won’t have any problem with the Redis key; however, if we want to delete the cache before it expires (manually or by another application), the hash will make it hard to delete the cache unless we know how to get the same hash value. Let’s implement our cache key generator at the next step.
▋Implement ICacheKeyGenerator
Before implementing our custom cache key generator, we have to think about the key pattern. Your Redis key may contain the information from the request’s URL parameter or from a HTTP header, etc. The way to generate a Redis key might be different by each upstreaming request or HTTP method.
The following sample code generates the “key” with the HTTP header “X-Redis-Key”.
public class RedisKeyGenerator : ICacheKeyGenerator
{
private const string RedisKeyHeaderName = "X-Redis-Key";
public async ValueTask<string> GenerateRequestCacheKey(DownstreamRequest downstreamRequest, DownstreamRoute downstreamRoute)
{
StringBuilder customRedisKey = await this.TryGenRedisKeyByHttpHeader(downstreamRequest);
if (customRedisKey.Length > 0)
{
string cacheKey = customRedisKey.ToString();
return cacheKey;
}
else
{
#region Official implementation
// You can copy the original Ocelot implementation here if the client doesn't give the HTTP header.
#endregion
}
}
private static Task<string> ReadContentAsync(DownstreamRequest downstream) => downstream.HasContent && downstream.Request?.Content != null
? downstream.Request.Content.ReadAsStringAsync() ?? Task.FromResult(string.Empty)
: Task.FromResult(string.Empty);
private Task<StringBuilder> TryGenRedisKeyByHttpHeader(DownstreamRequest downstreamRequest)
{
StringBuilder sbRedisKey = new();
var httpHeaderValues = downstreamRequest.Headers.FirstOrDefault(x => x.Key.Equals(RedisKeyHeaderName));
if (httpHeaderValues.Value != null && httpHeaderValues.Value.Any())
{
string headerValue = httpHeaderValues.Value.FirstOrDefault();
if (!string.IsNullOrEmpty(headerValue))
{
sbRedisKey.Append(headerValue);
}
}
return Task.FromResult(sbRedisKey);
}
}
Don’t forget that we also put the “region” into our Redis key pattern “region:key” in RedisCacheStore. While we set the “Region” with value: “payment” and the request’s HTTP header “X-Redis-Key” is a payment transaction ID: “dedcf2fb-c8e0-4965-a590-d674e2094304”, then the Redis key will be
payment:dedcf2fb-c8e0-4965-a590-d674e2094304
The cache key seems more readable, because the client application knows the payment transaction ID and it can delete the cache anytime by itself.
▋Register the new services
Now we can register our new services RedisKeyGenerator and RedisCacheStore into DI containers to let Ocelot use them instead of its default memory caching and cache key generator.
builder.Services.AddSingleton<ICacheKeyGenerator, RedisKeyGenerator>();
builder.Services.AddSingleton<IOcelotCache<CachedResponse>, RedisCacheStore>();
▋The Cache Value
The cache value by Ocelot contains:
Http status code
Http response’s headers
Http response body (in Base64)
localhost:6379> mget payment:dedcf2fb-c8e0-4965-a590-d674e2094304
1) {"StatusCode":200,"Headers":{"Date":["Sat, 22 Mar 2025 16:45:22 GMT"],"Server":["Kestrel"],"Transfer-Encoding":["chunked"]},"ContentHeaders":{"Content-Type":["application/json; charset=utf-8"]},"Body":"eyJpZCI6IjkyMDQxNThiLTJhZjEtNDUxNS1iMTk4LTJlNjk5MmRmMjA4NiIsIml0ZW0iOiJEREQiLCJhbW91bnQiOjEwMCwiY3JlYXRlT24iOiIyMDI1LTAzLTE5VDAxOjQxOjQzLjQ5MjM1MzIrMDA6MDAifQ==","ReasonPhrase":"OK"}
By storing the cache in Redis and using the custom Redis key, we can easily enhance and manage the caching, or trouble-shooting.
▌Reference