diff --git a/DysonNetwork.Sphere/Connection/Handlers/MessageTypingHandler.cs b/DysonNetwork.Sphere/Connection/Handlers/MessageTypingHandler.cs
index 7bc43b8..11e4dd9 100644
--- a/DysonNetwork.Sphere/Connection/Handlers/MessageTypingHandler.cs
+++ b/DysonNetwork.Sphere/Connection/Handlers/MessageTypingHandler.cs
@@ -2,7 +2,6 @@ using System.Net.WebSockets;
using DysonNetwork.Sphere.Chat;
using DysonNetwork.Sphere.Storage;
using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Caching.Memory;
namespace DysonNetwork.Sphere.Connection.Handlers;
diff --git a/DysonNetwork.Sphere/Connection/WebSocketPacket.cs b/DysonNetwork.Sphere/Connection/WebSocketPacket.cs
index 09d6f68..1028311 100644
--- a/DysonNetwork.Sphere/Connection/WebSocketPacket.cs
+++ b/DysonNetwork.Sphere/Connection/WebSocketPacket.cs
@@ -61,7 +61,7 @@ public class WebSocketPacket
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower,
- }.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);;
+ }.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
var json = JsonSerializer.Serialize(this, jsonOpts);
return System.Text.Encoding.UTF8.GetBytes(json);
}
diff --git a/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj b/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj
index 6833c32..4d2effa 100644
--- a/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj
+++ b/DysonNetwork.Sphere/DysonNetwork.Sphere.csproj
@@ -40,7 +40,9 @@
+
+
diff --git a/DysonNetwork.Sphere/Permission/PermissionService.cs b/DysonNetwork.Sphere/Permission/PermissionService.cs
index 68c4424..45ba123 100644
--- a/DysonNetwork.Sphere/Permission/PermissionService.cs
+++ b/DysonNetwork.Sphere/Permission/PermissionService.cs
@@ -11,17 +11,17 @@ public class PermissionService(
ILogger logger)
{
private static readonly TimeSpan CacheExpiration = TimeSpan.FromMinutes(1);
-
+
private const string PermCacheKeyPrefix = "Perm_";
private const string PermGroupCacheKeyPrefix = "PermCacheGroup_";
private const string PermissionGroupPrefix = "PermGroup_";
- private static string _GetPermissionCacheKey(string actor, string area, string key) =>
+ private static string _GetPermissionCacheKey(string actor, string area, string key) =>
PermCacheKeyPrefix + actor + ":" + area + ":" + key;
- private static string _GetGroupsCacheKey(string actor) =>
+ private static string _GetGroupsCacheKey(string actor) =>
PermGroupCacheKeyPrefix + actor;
-
+
private static string _GetPermissionGroupKey(string actor) =>
PermissionGroupPrefix + actor;
@@ -68,8 +68,8 @@ public class PermissionService(
var result = permission is not null ? _DeserializePermissionValue(permission.Value) : default;
- await cache.SetWithGroupsAsync(cacheKey, result,
- new[] { _GetPermissionGroupKey(actor) },
+ await cache.SetWithGroupsAsync(cacheKey, result,
+ [_GetPermissionGroupKey(actor)],
CacheExpiration);
return result;
diff --git a/DysonNetwork.Sphere/Program.cs b/DysonNetwork.Sphere/Program.cs
index a96327f..4c16c3c 100644
--- a/DysonNetwork.Sphere/Program.cs
+++ b/DysonNetwork.Sphere/Program.cs
@@ -45,13 +45,12 @@ builder.WebHost.ConfigureKestrel(options => options.Limits.MaxRequestBodySize =
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
builder.Services.AddDbContext();
-builder.Services.AddSingleton(sp =>
+builder.Services.AddSingleton(_ =>
{
var connection = builder.Configuration.GetConnectionString("FastRetrieve")!;
return ConnectionMultiplexer.Connect(connection);
});
-
-builder.Services.AddScoped();
+builder.Services.AddSingleton();
builder.Services.AddHttpClient();
builder.Services.AddControllers().AddJsonOptions(options =>
diff --git a/DysonNetwork.Sphere/Sticker/StickerService.cs b/DysonNetwork.Sphere/Sticker/StickerService.cs
index 82491c8..abae50f 100644
--- a/DysonNetwork.Sphere/Sticker/StickerService.cs
+++ b/DysonNetwork.Sphere/Sticker/StickerService.cs
@@ -67,9 +67,7 @@ public class StickerService(AppDatabase db, FileService fs, ICacheService cache)
// Invalidate cache for all stickers in this pack
foreach (var sticker in stickers)
- {
- PurgeStickerCache(sticker);
- }
+ await PurgeStickerCache(sticker);
}
public async Task LookupStickerByIdentifierAsync(string identifier)
diff --git a/DysonNetwork.Sphere/Storage/CacheService.cs b/DysonNetwork.Sphere/Storage/CacheService.cs
index 61b0cfc..7a81cf1 100644
--- a/DysonNetwork.Sphere/Storage/CacheService.cs
+++ b/DysonNetwork.Sphere/Storage/CacheService.cs
@@ -1,4 +1,7 @@
-using System.Text.Json;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+using NodaTime;
+using NodaTime.Serialization.JsonNet;
using StackExchange.Redis;
namespace DysonNetwork.Sphere.Storage;
@@ -12,17 +15,17 @@ public interface IDistributedLock : IAsyncDisposable
/// The resource identifier this lock is protecting
///
string Resource { get; }
-
+
///
/// Unique identifier for this lock instance
///
string LockId { get; }
-
+
///
/// Extends the lock's expiration time
///
Task ExtendAsync(TimeSpan timeSpan);
-
+
///
/// Releases the lock immediately
///
@@ -35,32 +38,32 @@ public interface ICacheService
/// Sets a value in the cache with an optional expiration time
///
Task SetAsync(string key, T value, TimeSpan? expiry = null);
-
+
///
/// Gets a value from the cache
///
Task GetAsync(string key);
-
+
///
/// Removes a specific key from the cache
///
Task RemoveAsync(string key);
-
+
///
/// Adds a key to a group for group-based operations
///
Task AddToGroupAsync(string key, string group);
-
+
///
/// Removes all keys associated with a specific group
///
Task RemoveGroupAsync(string group);
-
+
///
/// Gets all keys belonging to a specific group
///
Task> GetGroupKeysAsync(string group);
-
+
///
/// Helper method to set a value in cache and associate it with multiple groups in one operation
///
@@ -71,7 +74,7 @@ public interface ICacheService
/// Optional expiration time for the cached item
/// True if the set operation was successful
Task SetWithGroupsAsync(string key, T value, IEnumerable? groups = null, TimeSpan? expiry = null);
-
+
///
/// Acquires a distributed lock on the specified resource
///
@@ -80,8 +83,9 @@ public interface ICacheService
/// How long to wait for the lock before giving up
/// How often to retry acquiring the lock during the wait time
/// A distributed lock instance if acquired, null otherwise
- Task AcquireLockAsync(string resource, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null);
-
+ Task AcquireLockAsync(string resource, TimeSpan expiry, TimeSpan? waitTime = null,
+ TimeSpan? retryInterval = null);
+
///
/// Executes an action with a distributed lock, ensuring the lock is properly released afterwards
///
@@ -91,8 +95,9 @@ public interface ICacheService
/// How long to wait for the lock before giving up
/// How often to retry acquiring the lock during the wait time
/// True if the lock was acquired and the action was executed, false otherwise
- Task ExecuteWithLockAsync(string resource, Func action, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null);
-
+ Task ExecuteWithLockAsync(string resource, Func action, TimeSpan expiry, TimeSpan? waitTime = null,
+ TimeSpan? retryInterval = null);
+
///
/// Executes a function with a distributed lock, ensuring the lock is properly released afterwards
///
@@ -103,31 +108,32 @@ public interface ICacheService
/// How long to wait for the lock before giving up
/// How often to retry acquiring the lock during the wait time
/// The result of the function if the lock was acquired, default(T) otherwise
- Task<(bool Acquired, T? Result)> ExecuteWithLockAsync(string resource, Func> func, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null);
+ Task<(bool Acquired, T? Result)> ExecuteWithLockAsync(string resource, Func> func, TimeSpan expiry,
+ TimeSpan? waitTime = null, TimeSpan? retryInterval = null);
}
public class RedisDistributedLock : IDistributedLock
{
private readonly IDatabase _database;
private bool _disposed;
-
+
private const string LockKeyPrefix = "Lock_";
-
+
public string Resource { get; }
public string LockId { get; }
-
+
internal RedisDistributedLock(IDatabase database, string resource, string lockId)
{
_database = database;
Resource = resource;
LockId = lockId;
}
-
+
public async Task ExtendAsync(TimeSpan timeSpan)
{
if (_disposed)
throw new ObjectDisposedException(nameof(RedisDistributedLock));
-
+
var script = @"
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('pexpire', KEYS[1], ARGV[2])
@@ -135,21 +141,21 @@ public class RedisDistributedLock : IDistributedLock
return 0
end
";
-
+
var result = await _database.ScriptEvaluateAsync(
script,
- new RedisKey[] { $"{LockKeyPrefix}{Resource}" },
- new RedisValue[] { LockId, (long)timeSpan.TotalMilliseconds }
+ [$"{LockKeyPrefix}{Resource}"],
+ [LockId, (long)timeSpan.TotalMilliseconds]
);
-
+
return (long)result! == 1;
}
-
+
public async Task ReleaseAsync()
{
if (_disposed)
return;
-
+
var script = @"
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
@@ -157,16 +163,16 @@ public class RedisDistributedLock : IDistributedLock
return 0
end
";
-
+
await _database.ScriptEvaluateAsync(
script,
- new RedisKey[] { $"{LockKeyPrefix}{Resource}" },
- new RedisValue[] { LockId }
+ [$"{LockKeyPrefix}{Resource}"],
+ [LockId]
);
-
+
_disposed = true;
}
-
+
public async ValueTask DisposeAsync()
{
await ReleaseAsync();
@@ -177,49 +183,54 @@ public class RedisDistributedLock : IDistributedLock
public class CacheServiceRedis : ICacheService
{
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
- private const string GroupKeyPrefix = "CacheGroup_";
- private const string LockKeyPrefix = "Lock_";
-
+ private const string GroupKeyPrefix = GlobalKeyPrefix + "cg:";
+ private const string LockKeyPrefix = GlobalKeyPrefix + "lock:";
+
public CacheServiceRedis(IConnectionMultiplexer redis)
{
var rds = redis ?? throw new ArgumentNullException(nameof(redis));
_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 SetAsync(string key, T value, TimeSpan? expiry = null)
{
if (string.IsNullOrEmpty(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);
}
-
+
public async Task GetAsync(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentException("Key cannot be null or empty", nameof(key));
-
+
var value = await _database.StringGetAsync(key);
-
+
if (value.IsNullOrEmpty)
return default;
-
- return JsonSerializer.Deserialize(value!, _serializerOptions);
+
+ return JsonConvert.DeserializeObject(value!, _serializerSettings);
}
-
+
public async Task RemoveAsync(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentException("Key cannot be null or empty", nameof(key));
-
+
// Before removing the key, find all groups it belongs to and remove it from them
var script = @"
local groups = redis.call('KEYS', ARGV[1])
@@ -228,128 +239,128 @@ public class CacheServiceRedis : ICacheService
end
return redis.call('DEL', ARGV[2])
";
-
+
var result = await _database.ScriptEvaluateAsync(
script,
- values: new RedisValue[] { $"{GroupKeyPrefix}*", key }
+ values: [$"{GroupKeyPrefix}*", key]
);
-
+
return (long)result! > 0;
}
-
+
public async Task AddToGroupAsync(string key, string group)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentException(@"Key cannot be null or empty.", nameof(key));
-
+
if (string.IsNullOrEmpty(group))
throw new ArgumentException(@"Group cannot be null or empty.", nameof(group));
-
+
var groupKey = $"{GroupKeyPrefix}{group}";
await _database.SetAddAsync(groupKey, key);
}
-
+
public async Task RemoveGroupAsync(string group)
{
if (string.IsNullOrEmpty(group))
throw new ArgumentException(@"Group cannot be null or empty.", nameof(group));
-
+
var groupKey = $"{GroupKeyPrefix}{group}";
-
+
// Get all keys in the group
var keys = await _database.SetMembersAsync(groupKey);
-
+
if (keys.Length > 0)
{
// Delete all the keys
var keysTasks = keys.Select(key => _database.KeyDeleteAsync(key.ToString()));
await Task.WhenAll(keysTasks);
}
-
+
// Delete the group itself
await _database.KeyDeleteAsync(groupKey);
}
-
+
public async Task> GetGroupKeysAsync(string group)
{
if (string.IsNullOrEmpty(group))
throw new ArgumentException(@"Group cannot be null or empty.", nameof(group));
-
+
var groupKey = $"{GroupKeyPrefix}{group}";
var members = await _database.SetMembersAsync(groupKey);
-
+
return members.Select(m => m.ToString());
}
-
- public async Task SetWithGroupsAsync(string key, T value, IEnumerable? groups = null, TimeSpan? expiry = null)
+
+ public async Task SetWithGroupsAsync(string key, T value, IEnumerable? 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);
-
+
// If successful and there are groups to associate, add the key to each group
- if (setResult && groups != null)
- {
- var groupsArray = groups.Where(g => !string.IsNullOrEmpty(g)).ToArray();
- if (groupsArray.Length > 0)
- {
- var tasks = groupsArray.Select(group => AddToGroupAsync(key, group));
- await Task.WhenAll(tasks);
- }
- }
-
+ if (!setResult || groups == null) return setResult;
+ var groupsArray = groups.Where(g => !string.IsNullOrEmpty(g)).ToArray();
+ if (groupsArray.Length <= 0) return setResult;
+ var tasks = groupsArray.Select(group => AddToGroupAsync(key, group));
+ await Task.WhenAll(tasks);
+
return setResult;
}
-
- public async Task AcquireLockAsync(string resource, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null)
+
+ public async Task AcquireLockAsync(string resource, TimeSpan expiry, TimeSpan? waitTime = null,
+ TimeSpan? retryInterval = null)
{
if (string.IsNullOrEmpty(resource))
throw new ArgumentException("Resource cannot be null or empty", nameof(resource));
-
+
var lockKey = $"{LockKeyPrefix}{resource}";
var lockId = Guid.NewGuid().ToString("N");
var waitTimeSpan = waitTime ?? TimeSpan.Zero;
var retryIntervalSpan = retryInterval ?? TimeSpan.FromMilliseconds(100);
-
+
var startTime = DateTime.UtcNow;
var acquired = false;
-
+
// Try to acquire the lock, retry until waitTime is exceeded
while (!acquired && (DateTime.UtcNow - startTime) < waitTimeSpan)
{
acquired = await _database.StringSetAsync(lockKey, lockId, expiry, When.NotExists);
-
+
if (!acquired)
{
await Task.Delay(retryIntervalSpan);
}
}
-
+
if (!acquired)
{
return null; // Could not acquire the lock within the wait time
}
-
+
return new RedisDistributedLock(_database, resource, lockId);
}
-
- public async Task ExecuteWithLockAsync(string resource, Func action, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null)
+
+ public async Task ExecuteWithLockAsync(string resource, Func action, TimeSpan expiry,
+ TimeSpan? waitTime = null, TimeSpan? retryInterval = null)
{
await using var lockObj = await AcquireLockAsync(resource, expiry, waitTime, retryInterval);
-
+
if (lockObj == null)
return false; // Could not acquire the lock
-
+
await action();
return true;
}
-
- public async Task<(bool Acquired, T? Result)> ExecuteWithLockAsync(string resource, Func> func, TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null)
+
+ public async Task<(bool Acquired, T? Result)> ExecuteWithLockAsync(string resource, Func> func,
+ TimeSpan expiry, TimeSpan? waitTime = null, TimeSpan? retryInterval = null)
{
await using var lockObj = await AcquireLockAsync(resource, expiry, waitTime, retryInterval);
-
+
if (lockObj == null)
return (false, default); // Could not acquire the lock
-
+
var result = await func();
return (true, result);
}