Skip to content

Commit f92638e

Browse files
committed
Added support for memory cache and Redis cache
1 parent e080c49 commit f92638e

File tree

5 files changed

+202
-2
lines changed

5 files changed

+202
-2
lines changed

OpenBioCardServer/Controllers/Classic/ClassicUserController.cs

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
using System.Text.Json;
12
using Microsoft.AspNetCore.Mvc;
23
using Microsoft.EntityFrameworkCore;
4+
using Microsoft.Extensions.Caching.Distributed;
5+
using Microsoft.Extensions.Caching.Memory;
36
using OpenBioCardServer.Data;
47
using OpenBioCardServer.Models.DTOs.Classic;
58
using OpenBioCardServer.Services;
@@ -14,23 +17,82 @@ public class ClassicUserController : ControllerBase
1417
private readonly AppDbContext _context;
1518
private readonly ClassicAuthService _authService;
1619
private readonly ILogger<ClassicUserController> _logger;
20+
21+
// 缓存相关依赖
22+
private readonly IMemoryCache _memoryCache;
23+
private readonly IDistributedCache? _distributedCache;
24+
private readonly IConfiguration _configuration;
25+
26+
// 缓存配置字段
27+
private readonly bool _useRedis;
28+
private readonly int _expirationMinutes;
1729

1830
public ClassicUserController(
1931
AppDbContext context,
2032
ClassicAuthService authService,
21-
ILogger<ClassicUserController> logger)
33+
ILogger<ClassicUserController> logger,
34+
IMemoryCache memoryCache,
35+
IConfiguration configuration,
36+
IServiceProvider serviceProvider)
2237
{
2338
_context = context;
2439
_authService = authService;
2540
_logger = logger;
41+
_memoryCache = memoryCache;
42+
_configuration = configuration;
43+
44+
// 读取缓存配置
45+
_useRedis = configuration.GetValue<bool>("CacheSettings:UseRedis");
46+
_expirationMinutes = configuration.GetValue<int>("CacheSettings:ExpirationMinutes", 5);
47+
48+
// 如果启用了 Redis,尝试获取 IDistributedCache 服务
49+
if (_useRedis)
50+
{
51+
_distributedCache = serviceProvider.GetService<IDistributedCache>();
52+
}
2653
}
54+
55+
// 生成统一的 Cache Key
56+
private static string GetProfileCacheKey(string username) =>
57+
$"Profile:{username.Trim().ToLowerInvariant()}";
2758

2859
/// <summary>
2960
/// Get user profile (public endpoint)
3061
/// </summary>
3162
[HttpGet("{username}")]
3263
public async Task<IActionResult> GetProfile(string username)
3364
{
65+
string cacheKey = GetProfileCacheKey(username);
66+
ClassicProfile? cachedProfile = null;
67+
68+
// 读取缓存
69+
try
70+
{
71+
if (_useRedis && _distributedCache != null)
72+
{
73+
// Redis: 读取字符串并反序列化
74+
var jsonStr = await _distributedCache.GetStringAsync(cacheKey);
75+
if (!string.IsNullOrEmpty(jsonStr))
76+
{
77+
cachedProfile = JsonSerializer.Deserialize<ClassicProfile>(jsonStr);
78+
}
79+
}
80+
else
81+
{
82+
// Memory: 直接读取对象引用
83+
_memoryCache.TryGetValue(cacheKey, out cachedProfile);
84+
}
85+
if (cachedProfile != null)
86+
{
87+
return Ok(cachedProfile);
88+
}
89+
}
90+
catch (Exception ex)
91+
{
92+
_logger.LogWarning(ex, "Cache read failed for {Key}", cacheKey);
93+
}
94+
95+
3496
try
3597
{
3698
var profile = await _context.Profiles
@@ -50,6 +112,32 @@ public async Task<IActionResult> GetProfile(string username)
50112
}
51113

52114
var classicProfile = ClassicMapper.ToClassicProfile(profile);
115+
116+
// 写入缓存
117+
try
118+
{
119+
if (_useRedis && _distributedCache != null)
120+
{
121+
// Redis: 序列化为 JSON 存储
122+
var jsonStr = JsonSerializer.Serialize(classicProfile);
123+
await _distributedCache.SetStringAsync(cacheKey, jsonStr, new DistributedCacheEntryOptions
124+
{
125+
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_expirationMinutes)
126+
});
127+
}
128+
else
129+
{
130+
// Memory: 存储对象引用 (带 Size 限制)
131+
_memoryCache.Set(cacheKey, classicProfile, new MemoryCacheEntryOptions()
132+
.SetAbsoluteExpiration(TimeSpan.FromMinutes(_expirationMinutes))
133+
.SetSlidingExpiration(TimeSpan.FromMinutes(2))
134+
.SetSize(1));
135+
}
136+
}
137+
catch (Exception ex)
138+
{
139+
_logger.LogError(ex, "Cache write failed for {Key}", cacheKey);
140+
}
53141

54142
return Ok(classicProfile);
55143
}
@@ -166,6 +254,24 @@ await _context.GalleryItems
166254

167255
await _context.SaveChangesAsync();
168256
await transaction.CommitAsync();
257+
258+
// 清除缓存
259+
string cacheKey = GetProfileCacheKey(username);
260+
try
261+
{
262+
if (_useRedis && _distributedCache != null)
263+
{
264+
await _distributedCache.RemoveAsync(cacheKey);
265+
}
266+
else
267+
{
268+
_memoryCache.Remove(cacheKey);
269+
}
270+
}
271+
catch (Exception ex)
272+
{
273+
_logger.LogError(ex, "Cache removal failed for {Key}", cacheKey);
274+
}
169275

170276
_logger.LogInformation("Profile updated for user: {Username}", username);
171277

OpenBioCardServer/OpenBioCardServer.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616
<PrivateAssets>all</PrivateAssets>
1717
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1818
</PackageReference>
19+
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.1" />
20+
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.1" />
1921
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
2022
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
2123
<PackageReference Include="Newtonsoft.Json.Bson" Version="1.0.3" />
2224
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
2325
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
26+
<PackageReference Include="StackExchange.Redis" Version="2.10.1" />
2427
<PackageReference Include="System.Text.Json" Version="10.0.1" />
2528
</ItemGroup>
2629

OpenBioCardServer/Program.cs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
using OpenBioCardServer.Models.Enums;
1111
using OpenBioCardServer.Services;
1212
using OpenBioCardServer.Utilities;
13+
using Microsoft.AspNetCore.ResponseCompression;
14+
using System.IO.Compression;
1315

1416
namespace OpenBioCardServer;
1517

@@ -77,7 +79,65 @@ await context.HttpContext.Response.WriteAsJsonAsync(new
7779

7880
// Database configuration with validation
7981
builder.Services.AddDatabaseContext(builder.Configuration);
80-
82+
83+
// Cache Configuration (Memory & Redis)
84+
var cacheSection = builder.Configuration.GetSection("CacheSettings");
85+
var useRedis = cacheSection.GetValue<bool>("UseRedis");
86+
var cacheSizeLimit = cacheSection.GetValue<long?>("ProfileCacheSizeLimit") ?? 100;
87+
var redisInstanceName = cacheSection.GetValue<string>("InstanceName") ?? "OpenBioCard:";
88+
89+
// Configure Local Memory Cache (Always available, with OOM protection)
90+
builder.Services.AddMemoryCache(options =>
91+
{
92+
options.SizeLimit = cacheSizeLimit;
93+
options.CompactionPercentage = 0.2; // Free up 20% when limit is reached
94+
});
95+
96+
// Configure Distributed Cache (Redis) if enabled
97+
if (useRedis)
98+
{
99+
var redisConn = cacheSection.GetValue<string>("RedisConnectionString");
100+
if (!string.IsNullOrEmpty(redisConn))
101+
{
102+
builder.Services.AddStackExchangeRedisCache(options =>
103+
{
104+
options.Configuration = redisConn;
105+
options.InstanceName = redisInstanceName;
106+
});
107+
}
108+
}
109+
110+
// Response Compression Configuration
111+
var compressionSection = builder.Configuration.GetSection("CompressionSettings");
112+
var enableCompression = compressionSection.GetValue<bool>("Enabled", true); // Default to true
113+
114+
if (enableCompression)
115+
{
116+
var enableForHttps = compressionSection.GetValue<bool>("EnableForHttps", true);
117+
var compressionLevelStr = compressionSection.GetValue<string>("Level") ?? "Fastest";
118+
119+
// Parse Compression Level Enum
120+
if (!Enum.TryParse(compressionLevelStr, true, out CompressionLevel compressionLevel))
121+
{
122+
compressionLevel = CompressionLevel.Fastest;
123+
}
124+
125+
builder.Services.AddResponseCompression(options =>
126+
{
127+
options.EnableForHttps = enableForHttps;
128+
options.Providers.Add<BrotliCompressionProvider>();
129+
options.Providers.Add<GzipCompressionProvider>();
130+
// Optional: Add MIME types if needed, defaults are usually fine for JSON/Text
131+
});
132+
133+
// Configure Providers
134+
builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
135+
options.Level = compressionLevel);
136+
137+
builder.Services.Configure<GzipCompressionProviderOptions>(options =>
138+
options.Level = compressionLevel);
139+
}
140+
81141
builder.Services.AddControllers()
82142
.AddNewtonsoftJson(options =>
83143
{
@@ -243,6 +303,13 @@ await context.HttpContext.Response.WriteAsJsonAsync(new
243303
}
244304

245305
app.UseHttpsRedirection();
306+
307+
// Enable Response Compression Middleware
308+
if (enableCompression)
309+
{
310+
app.UseResponseCompression();
311+
}
312+
246313
app.UseRateLimiter();
247314
app.UseCors("AllowFrontend");
248315
app.UseAuthorization();

OpenBioCardServer/appsettings.Development.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@
1515
"http://localhost:8787"
1616
]
1717
},
18+
"CacheSettings": {
19+
"UseRedis": false,
20+
"RedisConnectionString": "localhost:6379",
21+
"InstanceName": "OpenBioCard:",
22+
"ProfileCacheSizeLimit": 200,
23+
"ExpirationMinutes": 5
24+
},
25+
"CompressionSettings": {
26+
"Enabled": true,
27+
"EnableForHttps": true,
28+
"Level": "Fastest"
29+
},
1830
"AuthSettings": {
1931
"RootUsername": "root",
2032
"RootPassword": "password"

OpenBioCardServer/appsettings.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@
1515
"https://example.com/"
1616
]
1717
},
18+
"CacheSettings": {
19+
"UseRedis": false,
20+
"RedisConnectionString": "localhost:6379",
21+
"InstanceName": "OpenBioCard:",
22+
"ProfileCacheSizeLimit": 300,
23+
"ExpirationMinutes": 30
24+
},
25+
"CompressionSettings": {
26+
"Enabled": true,
27+
"EnableForHttps": true,
28+
"Level": "Fastest"
29+
},
1830
"AuthSettings": {
1931
"RootUsername": "root",
2032
"RootPassword": "change_me_in_production_env_vars"

0 commit comments

Comments
 (0)