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



<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


Ocelot: Caching


2024年8月31日 星期六

[ASP.NET Core] HTTP response JSON format

 ASP.NET Core    Newtonsoft.Json    System.Text.Json 


Configuration


The following codes in Startup.cs: ConfigureServices(IServiceCollection services) , show the difference of JSON serialization for HTTP response with Newtonsoft.Json and System.Text.Json.

System.Text.Json

The namespace had been included in runtime in .NET Core 3.1 and later versions.


Newtonsoft.Json

If we still want to use Newtonsoft.Json as the default serialization/deserialization package, install it and write as following. 



2023年9月9日 星期六

[Vim] Using GitHub Copilot

    GitHub Copilot    Vim    Bash 


Introduction


copilot.vim is the Vim plugin to let us use GitHub Copilot in Vim.


Prerequisites


Node.js
Vim 9.0.0185


Setup


First enable GitHub Copilot to your GitHub account here.

Install the plugin: copilot.vim and enable it by

:Copilot auth


It will ask you to enter a OTP in https://github.com/login/device.
After the authentication, type :Copilot setup and a message will show that you've logged in.

Now type :Copilot version to check the version and :Copilot enable to get started.


Features


Panel of Suggestion

Select a text which contains your prompt, and then use the following command to show a list of solutions.

:Copilot panel


Ghost Text auto-completion


GitHub Copilot will show the auto-complete text, code or scripts based on the context of current working file. The auto-complete text will be shown in grey color, type TAB to complete it. 


For more details and default hotkeys, see :h copilot or :Copilot help.
I recorded a video to show how to use it in Vim and bash Vi mode.





Options


Disable by file type

Sometimes you want to disable it by default for some file types, you can write an AutoCommand like this (to disable it on markdown file).  

autocmd! BufNewFile,BufRead *.md exe 'Copilot disable'


copilot.vim has the global variable: "g:copilot_filetypes" that we can tell it to enable/disable GitHub Copilot on certain file types. See :h g:copilot_filetypes for details. Here is an example in my vimrc.

let g:copilot_filetypes = { 'xml': v:false, 'markdown': v:false, 'log': v:false, }


Or you can disable it in all filetypes(*) and only enable it in some filetypes by...

let g:copilot_filetypes = { '*': v:false, 'python': v:true, }









2023年7月9日 星期日

[開箱] FILCOキーボード工房 Majestouch 3 NINJA Tenkeyless

   FILCO   Majestouch 3 NINJA 

 



原本想入手茶軸的 Majestouch 3 NINJA ,台灣也有代理商;但是被官網的 キーボード工房 版本燒到,所以最後還是從日本購入這把 キーボード工房 Majestouch 3 NINJA Tenkeyless

上色後的 FILCO NINJA 非常的美,也沒有正刻帶來的繁雜感,對於我來說是極具收藏價值的藝術品;當然品質還是維持一樣的高水準。

購入時也附了一些特典,就一併直接上圖做個記錄囉!

 










2023年3月14日 星期二

[Oracle] CONNECT BY and Recursive CTE for hierarchy data

  Oracle   CTE   Hierarchy   CONNECT BY

 

Problem


Assume that we have a table, EMPLOYEES, which include hierarchy data like this,

 



Each employee has a manager, and a manager also has his/her manager, except JB, who is the boss.

We are going to learn how to use the following 2 ways to traverse the hierarchy rows.

1.  START WITH...CONNECT BY...

2.  Recursive CTE

 

START WITH...CONNECT BY...


We can use Oracle's START WITH... CONNECT BY... clause to traverse hierarchal rows.

Pattern


SELECT columns
FROM table
START WITH {condition to find the root row}
CONNECT BY {condition to find next row};
\

 

l   START WITH specifies the root row of the hierarchy.

l   CONNECT BY specifies the relationship of parent and child rows.

l   PRIOR indicates the recursive condition to traverse all the rows.
e.q. PRIOR A = B means using current row's column A to match next row's column B. And A = PRIOR B means use current row's column B to match next row's column A.

 

SQL

The following SQL lists the top-down hierarchy by starting from JB that has no manager and traverse the records that have manager as JB, and so on.

Notice that the condition to find next row is PRIOR EMP_NO = MANAGER_NO, and that means we use EMP_NO to find(match) the next row by its MANAGER_NO.

 


SELECT EMP_NO, NAME, MANAGER_NO
FROM EMPLOYEES
START WITH MANAGER_NO IS NULL
CONNECT BY PRIOR EMP_NO = MANAGER_NO;

 

Result:



 

And the following SQL lists the bottom-up hierarchy by from James and shows his reporting line.

Notice that the condition to find next row is EMP_NO = PRIOR MANAGER_NO, and that means we use MANAGER_NO to find(match) the next row by its EMP_NO.


SELECT EMP_NO, NAME, MANAGER_NO
FROM EMPLOYEES
START WITH NAME = 'James'
CONNECT BY EMP_NO = PRIOR MANAGER_NO;

 

Result:

 


CTE Recursive


We can do the same thing by using CTE recursive.

SQL


WITH t1(EMP_NO,NAME,MANAGER_NO) AS
(
    SELECT EMP_NO,NAME,MANAGER_NO FROM EMPLOYEES
    WHERE NAME = 'James'
    UNION ALL
    SELECT t2.EMP_NO, t2.NAME, t2.MANAGER_NO FROM EMPLOYEES t2, t1
    WHERE t2.EMP_NO = t1.MANAGER_NO
)
SELECT * FROM t1;

 

 

Result:

 


(Optional) Create a Function


Let's create a Function FindReportLine that returns the report line of an exployee by his/her name.


Function by START WITH... CONNECT BY...


CREATE OR REPLACE FUNCTION FindReportLine (
    in_name IN VARCHAR2
) RETURN SYS_REFCURSOR IS
    emp_cursor SYS_REFCURSOR;
BEGIN
    OPEN emp_cursor FOR
    SELECT EMP_NO, NAME, MANAGER_NO
    FROM EMPLOYEES
    START WITH NAME = in_name
    CONNECT BY EMP_NO = PRIOR MANAGER_NO;
    RETURN emp_cursor;
END;


Function by Recursive CTE


CREATE OR REPLACE FUNCTION FindReportLine (
    in_name IN VARCHAR2
) RETURN SYS_REFCURSOR IS
    emp_cursor SYS_REFCURSOR;
BEGIN
    OPEN emp_cursor FOR
    WITH t1(emp_no, name, manager_no) as (
        SELECT EMP_NO, NAME, MANAGER_NO
        FROM EMPLOYEES
        WHERE name = in_name
        UNION ALL
        SELECT t2.EMP_NO, t2.name, t2.MANAGER_NO
        FROM EMPLOYEES t2 INNER JOIN t1
        ON t2.EMP_NO = t1.MANAGER_NO
    )
    SELECT * FROM t1;
    RETURN emp_cursor;
END;


Use the Function

Since the function returns the rows as SYS_REFCURSOR, we can parse the result by XML.

XMLTABLE maps the result of an XQuery evaluation into relational rows and columns.


SELECT * FROM xmltable(
'/ROWSET/ROW'
PASSING xmltype(FindReportLine('James'))
columns
EMP_NO PATH 'EMP_NO',
NAME PATH 'NAME',
MANAGER_NO PATH 'MANAGER_NO'
);


The XMLSEQUENCE could also do the same thing, though it's deprecated.


SELECT
extractvalue(column_value,'/ROW/EMP_NO') EMP_NO,
extractvalue(column_value,'/ROW/NAME') NAME,
extractvalue(column_value,'/ROW/MANAGER_NO') MANAGER_NO
FROM TABLE(xmlsequence(FindReportLine('James')));


(Optional) Aggregate the Result

We can use LISTAGG function in Oracle to combine multiple rows into a single row with each value separated by a specified character or symbol. For example, the following SQL aggregates and show the names of the report line of James.


SELECT LISTAGG(NAME, ' / ') WITHIN GROUP ( ORDER BY ROWNUM ) AS REPORT_LINE
FROM
(
SELECT EMP_NO, NAME, MANAGER_NO
FROM EMPLOYEES
START WITH NAME = 'James'
CONNECT BY EMP_NO = PRIOR MANAGER_NO
);

The result will be "James / Jack / JB".


Reference


[Sql Server] Recursive CTE for hierarchy data

[PostgreSQL] Recursive CTE for hierarchy data