🐛 Replace the serializer in cache service with newtonsoft json to solve JsonIgnore issue

This commit is contained in:
LittleSheep 2025-05-24 18:29:20 +08:00
parent 460ce62452
commit 445e5d3705
7 changed files with 117 additions and 108 deletions

View File

@ -2,7 +2,6 @@ using System.Net.WebSockets;
using DysonNetwork.Sphere.Chat; using DysonNetwork.Sphere.Chat;
using DysonNetwork.Sphere.Storage; using DysonNetwork.Sphere.Storage;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
namespace DysonNetwork.Sphere.Connection.Handlers; namespace DysonNetwork.Sphere.Connection.Handlers;

View File

@ -61,7 +61,7 @@ public class WebSocketPacket
{ {
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower, DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower,
}.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);; }.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
var json = JsonSerializer.Serialize(this, jsonOpts); var json = JsonSerializer.Serialize(this, jsonOpts);
return System.Text.Encoding.UTF8.GetBytes(json); return System.Text.Encoding.UTF8.GetBytes(json);
} }

View File

@ -40,7 +40,9 @@
<PackageReference Include="NetVips" Version="3.0.1"/> <PackageReference Include="NetVips" Version="3.0.1"/>
<PackageReference Include="NetVips.Native.linux-x64" Version="8.16.1"/> <PackageReference Include="NetVips.Native.linux-x64" Version="8.16.1"/>
<PackageReference Include="NetVips.Native.osx-arm64" Version="8.16.1"/> <PackageReference Include="NetVips.Native.osx-arm64" Version="8.16.1"/>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NodaTime" Version="3.2.2"/> <PackageReference Include="NodaTime" Version="3.2.2"/>
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" />
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/> <PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0"/> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0"/>

View File

@ -69,7 +69,7 @@ public class PermissionService(
var result = permission is not null ? _DeserializePermissionValue<T>(permission.Value) : default; var result = permission is not null ? _DeserializePermissionValue<T>(permission.Value) : default;
await cache.SetWithGroupsAsync(cacheKey, result, await cache.SetWithGroupsAsync(cacheKey, result,
new[] { _GetPermissionGroupKey(actor) }, [_GetPermissionGroupKey(actor)],
CacheExpiration); CacheExpiration);
return result; return result;

View File

@ -45,13 +45,12 @@ builder.WebHost.ConfigureKestrel(options => options.Limits.MaxRequestBodySize =
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
builder.Services.AddDbContext<AppDatabase>(); builder.Services.AddDbContext<AppDatabase>();
builder.Services.AddSingleton<IConnectionMultiplexer>(sp => builder.Services.AddSingleton<IConnectionMultiplexer>(_ =>
{ {
var connection = builder.Configuration.GetConnectionString("FastRetrieve")!; var connection = builder.Configuration.GetConnectionString("FastRetrieve")!;
return ConnectionMultiplexer.Connect(connection); return ConnectionMultiplexer.Connect(connection);
}); });
builder.Services.AddSingleton<ICacheService, CacheServiceRedis>();
builder.Services.AddScoped<ICacheService, CacheServiceRedis>();
builder.Services.AddHttpClient(); builder.Services.AddHttpClient();
builder.Services.AddControllers().AddJsonOptions(options => builder.Services.AddControllers().AddJsonOptions(options =>

View File

@ -67,9 +67,7 @@ public class StickerService(AppDatabase db, FileService fs, ICacheService cache)
// Invalidate cache for all stickers in this pack // Invalidate cache for all stickers in this pack
foreach (var sticker in stickers) foreach (var sticker in stickers)
{ await PurgeStickerCache(sticker);
PurgeStickerCache(sticker);
}
} }
public async Task<Sticker?> LookupStickerByIdentifierAsync(string identifier) public async Task<Sticker?> LookupStickerByIdentifierAsync(string identifier)

View File

@ -1,4 +1,7 @@
using System.Text.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using NodaTime;
using NodaTime.Serialization.JsonNet;
using StackExchange.Redis; using StackExchange.Redis;
namespace DysonNetwork.Sphere.Storage; namespace DysonNetwork.Sphere.Storage;
@ -80,7 +83,8 @@ public interface ICacheService
/// <param name="waitTime">How long to wait for the lock before giving up</param> /// <param name="waitTime">How long to wait for the lock before giving up</param>
/// <param name="retryInterval">How often to retry acquiring the lock during the wait time</param> /// <param name="retryInterval">How often to retry acquiring the lock during the wait time</param>
/// <returns>A distributed lock instance if acquired, null otherwise</returns> /// <returns>A distributed lock instance if acquired, null otherwise</returns>
Task<IDistributedLock?> AcquireLockAsync(string resource, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null); Task<IDistributedLock?> AcquireLockAsync(string resource, TimeSpan expiry, TimeSpan? waitTime = null,
TimeSpan? retryInterval = null);
/// <summary> /// <summary>
/// Executes an action with a distributed lock, ensuring the lock is properly released afterwards /// Executes an action with a distributed lock, ensuring the lock is properly released afterwards
@ -91,7 +95,8 @@ public interface ICacheService
/// <param name="waitTime">How long to wait for the lock before giving up</param> /// <param name="waitTime">How long to wait for the lock before giving up</param>
/// <param name="retryInterval">How often to retry acquiring the lock during the wait time</param> /// <param name="retryInterval">How often to retry acquiring the lock during the wait time</param>
/// <returns>True if the lock was acquired and the action was executed, false otherwise</returns> /// <returns>True if the lock was acquired and the action was executed, false otherwise</returns>
Task<bool> ExecuteWithLockAsync(string resource, Func<Task> action, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null); Task<bool> ExecuteWithLockAsync(string resource, Func<Task> action, TimeSpan expiry, TimeSpan? waitTime = null,
TimeSpan? retryInterval = null);
/// <summary> /// <summary>
/// Executes a function with a distributed lock, ensuring the lock is properly released afterwards /// Executes a function with a distributed lock, ensuring the lock is properly released afterwards
@ -103,7 +108,8 @@ public interface ICacheService
/// <param name="waitTime">How long to wait for the lock before giving up</param> /// <param name="waitTime">How long to wait for the lock before giving up</param>
/// <param name="retryInterval">How often to retry acquiring the lock during the wait time</param> /// <param name="retryInterval">How often to retry acquiring the lock during the wait time</param>
/// <returns>The result of the function if the lock was acquired, default(T) otherwise</returns> /// <returns>The result of the function if the lock was acquired, default(T) otherwise</returns>
Task<(bool Acquired, T? Result)> ExecuteWithLockAsync<T>(string resource, Func<Task<T>> func, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null); Task<(bool Acquired, T? Result)> ExecuteWithLockAsync<T>(string resource, Func<Task<T>> func, TimeSpan expiry,
TimeSpan? waitTime = null, TimeSpan? retryInterval = null);
} }
public class RedisDistributedLock : IDistributedLock public class RedisDistributedLock : IDistributedLock
@ -138,8 +144,8 @@ public class RedisDistributedLock : IDistributedLock
var result = await _database.ScriptEvaluateAsync( var result = await _database.ScriptEvaluateAsync(
script, script,
new RedisKey[] { $"{LockKeyPrefix}{Resource}" }, [$"{LockKeyPrefix}{Resource}"],
new RedisValue[] { LockId, (long)timeSpan.TotalMilliseconds } [LockId, (long)timeSpan.TotalMilliseconds]
); );
return (long)result! == 1; return (long)result! == 1;
@ -160,8 +166,8 @@ public class RedisDistributedLock : IDistributedLock
await _database.ScriptEvaluateAsync( await _database.ScriptEvaluateAsync(
script, script,
new RedisKey[] { $"{LockKeyPrefix}{Resource}" }, [$"{LockKeyPrefix}{Resource}"],
new RedisValue[] { LockId } [LockId]
); );
_disposed = true; _disposed = true;
@ -177,20 +183,25 @@ public class RedisDistributedLock : IDistributedLock
public class CacheServiceRedis : ICacheService public class CacheServiceRedis : ICacheService
{ {
private readonly IDatabase _database; private readonly IDatabase _database;
private readonly JsonSerializerOptions _serializerOptions; private readonly JsonSerializerSettings _serializerSettings;
// Global prefix for all cache keys
private const string GlobalKeyPrefix = "dyson:";
// Using prefixes for different types of keys // Using prefixes for different types of keys
private const string GroupKeyPrefix = "CacheGroup_"; private const string GroupKeyPrefix = GlobalKeyPrefix + "cg:";
private const string LockKeyPrefix = "Lock_"; private const string LockKeyPrefix = GlobalKeyPrefix + "lock:";
public CacheServiceRedis(IConnectionMultiplexer redis) public CacheServiceRedis(IConnectionMultiplexer redis)
{ {
var rds = redis ?? throw new ArgumentNullException(nameof(redis)); var rds = redis ?? throw new ArgumentNullException(nameof(redis));
_database = rds.GetDatabase(); _database = rds.GetDatabase();
_serializerOptions = new JsonSerializerOptions _serializerSettings = new JsonSerializerSettings
{ {
PropertyNameCaseInsensitive = true ContractResolver = new CamelCasePropertyNamesContractResolver(),
}; PreserveReferencesHandling = PreserveReferencesHandling.Objects,
NullValueHandling = NullValueHandling.Include,
}.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
} }
public async Task<bool> SetAsync<T>(string key, T value, TimeSpan? expiry = null) public async Task<bool> SetAsync<T>(string key, T value, TimeSpan? expiry = null)
@ -198,7 +209,7 @@ public class CacheServiceRedis : ICacheService
if (string.IsNullOrEmpty(key)) if (string.IsNullOrEmpty(key))
throw new ArgumentException("Key cannot be null or empty", nameof(key)); throw new ArgumentException("Key cannot be null or empty", nameof(key));
var serializedValue = JsonSerializer.Serialize(value, _serializerOptions); var serializedValue = JsonConvert.SerializeObject(value, _serializerSettings);
return await _database.StringSetAsync(key, serializedValue, expiry); return await _database.StringSetAsync(key, serializedValue, expiry);
} }
@ -212,7 +223,7 @@ public class CacheServiceRedis : ICacheService
if (value.IsNullOrEmpty) if (value.IsNullOrEmpty)
return default; return default;
return JsonSerializer.Deserialize<T>(value!, _serializerOptions); return JsonConvert.DeserializeObject<T>(value!, _serializerSettings);
} }
public async Task<bool> RemoveAsync(string key) public async Task<bool> RemoveAsync(string key)
@ -231,7 +242,7 @@ public class CacheServiceRedis : ICacheService
var result = await _database.ScriptEvaluateAsync( var result = await _database.ScriptEvaluateAsync(
script, script,
values: new RedisValue[] { $"{GroupKeyPrefix}*", key } values: [$"{GroupKeyPrefix}*", key]
); );
return (long)result! > 0; return (long)result! > 0;
@ -281,26 +292,24 @@ public class CacheServiceRedis : ICacheService
return members.Select(m => m.ToString()); return members.Select(m => m.ToString());
} }
public async Task<bool> SetWithGroupsAsync<T>(string key, T value, IEnumerable<string>? groups = null, TimeSpan? expiry = null) public async Task<bool> SetWithGroupsAsync<T>(string key, T value, IEnumerable<string>? groups = null,
TimeSpan? expiry = null)
{ {
// First set the value in the cache // First, set the value in the cache
var setResult = await SetAsync(key, value, expiry); var setResult = await SetAsync(key, value, expiry);
// If successful and there are groups to associate, add the key to each group // If successful and there are groups to associate, add the key to each group
if (setResult && groups != null) if (!setResult || groups == null) return setResult;
{
var groupsArray = groups.Where(g => !string.IsNullOrEmpty(g)).ToArray(); var groupsArray = groups.Where(g => !string.IsNullOrEmpty(g)).ToArray();
if (groupsArray.Length > 0) if (groupsArray.Length <= 0) return setResult;
{
var tasks = groupsArray.Select(group => AddToGroupAsync(key, group)); var tasks = groupsArray.Select(group => AddToGroupAsync(key, group));
await Task.WhenAll(tasks); await Task.WhenAll(tasks);
}
}
return setResult; return setResult;
} }
public async Task<IDistributedLock?> AcquireLockAsync(string resource, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null) public async Task<IDistributedLock?> AcquireLockAsync(string resource, TimeSpan expiry, TimeSpan? waitTime = null,
TimeSpan? retryInterval = null)
{ {
if (string.IsNullOrEmpty(resource)) if (string.IsNullOrEmpty(resource))
throw new ArgumentException("Resource cannot be null or empty", nameof(resource)); throw new ArgumentException("Resource cannot be null or empty", nameof(resource));
@ -332,7 +341,8 @@ public class CacheServiceRedis : ICacheService
return new RedisDistributedLock(_database, resource, lockId); return new RedisDistributedLock(_database, resource, lockId);
} }
public async Task<bool> ExecuteWithLockAsync(string resource, Func<Task> action, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null) public async Task<bool> ExecuteWithLockAsync(string resource, Func<Task> action, TimeSpan expiry,
TimeSpan? waitTime = null, TimeSpan? retryInterval = null)
{ {
await using var lockObj = await AcquireLockAsync(resource, expiry, waitTime, retryInterval); await using var lockObj = await AcquireLockAsync(resource, expiry, waitTime, retryInterval);
@ -343,7 +353,8 @@ public class CacheServiceRedis : ICacheService
return true; return true;
} }
public async Task<(bool Acquired, T? Result)> ExecuteWithLockAsync<T>(string resource, Func<Task<T>> func, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null) public async Task<(bool Acquired, T? Result)> ExecuteWithLockAsync<T>(string resource, Func<Task<T>> func,
TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null)
{ {
await using var lockObj = await AcquireLockAsync(resource, expiry, waitTime, retryInterval); await using var lockObj = await AcquireLockAsync(resource, expiry, waitTime, retryInterval);