laitimes

C# .NET 8 - Create a caching service with a distributed cache

C# .NET 8 - Create a caching service with a distributed cache

introduce

A common way to speed up an application is to introduce caching. Often, the first option that comes to mind is to use MemoryCache (RAM) to hold some data for faster retrieval.

This method is suitable for monolithic applications. However, in a microservices solution, where each service can scale independently, using a local cache can break the stateless rules of the microservices architecture pattern.

Therefore, a better solution is to use distributed caching. In .NET, a common interaction with a distributed cache is through an interface called .

Distributed caching

When working in a .NET application, you can choose which implementation to use.

The implementations available in .NET 8 include:

  • Memory Cache
  • Redis
  • SQL Server
  • NCache
  • Azure CosmosDB

One of the most commonly used implementations is Redis. In the rest of this article, I'll use Redis for the implementation.

For more information, see the official documentation:

ASP.NET Distributed Cache in Core

Learn how to use ASP.NET Core Distributed Cache to improve application performance and scalability, especially in the cloud or...

learn.microsoft.com

Important parameters

When using a distributed cache, there are two important parameters to be aware of:

  • Sliding Expiration: The span of time during which a cached entry must be accessed before it can be evicted from the cache.
  • Absolute expiration time: The point in time at which a cached entry is evicted. By default, entries held in the cache do not expire.

For more information, see the official documentation:

CacheItemPolicy.AbsoluteExpiration 属性 (System.Runtime.Caching)

Gets or sets a value that indicates whether the cached item should be evicted at a specified point in time.

learn.microsoft.com

CacheItemPolicy.SlidingExpiration 属性 (System.Runtime.Caching)

Gets or sets a value that indicates whether a cache entry should be evicted if it hasn't been accessed in a given range...

learn.microsoft.com

Caching services

One way we can use it in our applications is to interact directly with the distributed cache when needed.

For example, if the service needs caching, we'll request an instance of . This method is possible, but it may not be optimal because it can lead to duplicate operations.

A preferable approach is to create a dedicated service for caching interactions and use it throughout your application. This centralizes cache management and helps avoid redundancy and improve maintainability.

Sample project

Here's a sample project, and I've prepared a sample of a caching service: at the end of the article

Project structure

The project has the following structure:

  • docker: Contains a Docker Compose file that contains the configured Redis containers.
  • src: contains the source code of the project.
  • test: contains the project test.

Docker Compose

以下是您将在 GitHub 上找到的 docker-compose 文件的内容:

version: '3'

services:

 redis-monitoring:
 image: redislabs/redisinsight:latest
 pull_policy: always
 ports:
 - '8001:8001'
 restart: unless-stopped
 networks:
 - default

 redis: 
 image: redis:latest
 pull_policy: always
 ports:
 - "6379:6379"
 restart: unless-stopped
 networks:
 - default

networks:
 default:
 driver: bridge
           

As you can see, the compose file is configured with two services:

  • redis: An instance of Redis.
  • RedisInsight: A container to help you interact with Redis. This container can be used for debugging purposes.

This Docker Compose setup is for development or testing purposes only. Note that Redis has changed its licensing policy.

You can find more information in this previous article:

Overview of the API project

The API project exposes a number of ways to interact with .

Here's an example of what will be displayed when you launch the app:

C# .NET 8 - Create a caching service with a distributed cache

Here's the code behind the API:

app.MapGet("/GetOrCreateAsync/{key}", async (string key,ICacheService cache) => 
{ 
return await cache.GetOrCreateAsync(key, () => Task.FromResult($"{nameof(cache.GetOrCreateAsync)} - Hello World")); 
}) 
.WithName("GetOrCreateAsync") 
.WithOpenApi(); 


app.MapGet("/GetOrDefault/{key}", async (string key, ICacheService cache) => 
{ 
return await cache.GetOrDefaultAsync(key, $"{nameof(cache.GetOrDefault)} - Hello World"); 
}) 
.WithName("GetOrDefault") 
.WithOpenApi(); 


app.MapGet("/CreateAndSet/{key}", async (string key, ICacheService cache) => 
{ 
await cache.CreateAndSet(key, $"{nameof(cache.CreateAndSet)} - Hello World"); 
}) 
.WithName("CreateAndSet") 
.WithOpenApi(); 


app.MapDelete("/RemoveAsync", (string key, ICacheService cache) => 
{ 
 cache.RemoveAsync(key); 
}) 
.WithName("RemoveAsync") 
.WithOpenApi();
           

Service Registration

If no Redis configuration is provided, the service is configured to use in-memory implementation.

In an API project, you only need to use extensions.

builder.Services.AddServiceCache(builder.Configuration);
           

The details of the extension are as follows:

public static IServiceCollection AddServiceCache(
this IServiceCollection services,
IConfiguration configuration
 )
 {
 services
 .AddOptions<CacheOptions>()
 .Bind(configuration.GetSection("Cache"))
 .ValidateDataAnnotations();

if (!string.IsOrEmpty(configuration.GetSection("RedisCache:Configuration").Value))
 {
 services.AddStackExchangeRedisCache(options =>
 {
 configuration.Bind("RedisCache", options);
 });
 }
else
 {
 services.AddDistributedMemoryCache();
 }

 services.AddTransient<ICacheService, CacheService>();
return services;
 }
           

As you can see, it searches for a key named "Cache" in the application settings to try to initialize the object. This option contains the value of the global Sliding Expiration value.

ServiceCache 服务缓存

这是接口:CacheService

public interface ICacheService
{
Task CreateAndSet<T>(string key, T thing, int expirationMinutes = 0)
where T : class;
Task<T> CreateAndSetAsync<T>(string key, Func<Task<T>> createAsync, int expirationMinutes = 0);
Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> create, int expirationMinutes = 0);
Task<T> GetOrDefault<T>(string key);
Task<T> GetOrDefaultAsync<T>(string key, T defaultVal);
Task RemoveAsync(string key);
}
           

It provides some practical ways to interact with .

In particular, it provides different ways to retrieve data and set automatic defaults or create methods.

Let's take a look at an example:

public async Task<T> GetOrCreateAsync<T>(
string key,
Func<Task<T>> create,
int expirationMinutes = 0
 )
 {
var bytesResult = await _cache.GetAsync(key);

if (bytesResult?.Length > 0)
 {
using StreamReader reader = new(new MemoryStream(bytesResult));
using JsonTextReader jsonReader = new(reader);
JsonSerializer ser = new();
 ser.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
 ser.TypeNameHandling = TypeNameHandling.All;
 ser.StringEscapeHandling = StringEscapeHandling.EscapeNonAscii;

var result = ser.Deserialize<T>(jsonReader);
if (result != )
 {
return result;
 }
 }

return await this.CreateAndSetAsync<T>(key, create, expirationMinutes);
 }
           

GetOrDefault, on the other hand, does not attempt to initialize the distributed cache. If the key is not found, it will simply return the default value.

public async Task<T> GetOrDefault<T>(string key)
 {
var bytesResult = await _cache.GetAsync(key);

if (bytesResult?.Length > 0)
 {
using StreamReader reader = new(new MemoryStream(bytesResult));
using JsonTextReader jsonReader = new(reader);
JsonSerializer ser = new();
 ser.TypeNameHandling = TypeNameHandling.All;
 ser.StringEscapeHandling = StringEscapeHandling.EscapeNonAscii;

var result = ser.Deserialize<T>(jsonReader);
if (result != )
 {
return result;
 }
 }

return default;
 }
           

FusionCache

The method of creating a service to manage the distributed cache is useful for avoiding code duplication and having an abstraction layer. This may be suitable for simple scenarios, or if you want to have full control over your codebase or limit external influences.

For more complex scenarios, a cool library you can use is FusionCache, which is essentially a service cache on steroids that offers advanced elastic features and an optional distributed second-level cache.

Source code acquisition: Official account reply message [

code:72610

If you like my article, please give me a like! Thank you

Read on