=== Summary: 675 files | 7 generated | 484 fresh | 4605 untracked | 4268 adopted | 355 marked | 466 validated-ok | 2+0 stale (sig+body) | 0 skipped | 0 failed | elapsed 11:08:11.442 (40091.44s) ===
This commit is contained in:
@@ -12,6 +12,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// según CacheSettings y la entidad del key.
|
||||
/// Implementa ICacheService y delega en el backend elegido.
|
||||
/// </summary>
|
||||
/// <!-- aidoc:v1 sig=9f15289 -->
|
||||
public class CacheDispatcher(
|
||||
RedisService redis,
|
||||
CacheService memory,
|
||||
@@ -27,6 +28,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// </summary>
|
||||
/// <param name="key">The cache key used to determine the entity type and the corresponding cache backend.</param>
|
||||
/// <returns>The <see cref="ICacheService"/> instance that should handle caching for the supplied key.</returns>
|
||||
/// <!-- aidoc:v1 sig=26395d4 body=a1c4c59 -->
|
||||
private ICacheService SelectBackend(string key)
|
||||
{
|
||||
var entity = CacheKeyClassifier.Classify(key);
|
||||
@@ -56,6 +58,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="gf">The grouped field whose name contributes to the key.</param>
|
||||
/// <param name="patientId">The identifier of the patient associated with the grouped observation.</param>
|
||||
/// <returns>A formatted key string in the form <c>GroupedObs:{patientId}:{gf.Name}</c>.</returns>
|
||||
/// <!-- aidoc:v1 sig=72c65a6 -->
|
||||
private static string BuildGroupedKey(GroupedField gf, ObjectId patientId)
|
||||
=> $"GroupedObs:{patientId}:{gf.Name}";
|
||||
|
||||
@@ -65,6 +68,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="groupedField">The grouped field used to derive the cache key.</param>
|
||||
/// <param name="patientId">The patient identifier used to derive the cache key.</param>
|
||||
/// <returns>The <see cref="ICacheService"/> backend associated with the built grouped key.</returns>
|
||||
/// <!-- aidoc:v1 sig=2461014 -->
|
||||
private ICacheService SelectBackend(GroupedField groupedField, ObjectId patientId)
|
||||
=> SelectBackend(BuildGroupedKey(groupedField, patientId));
|
||||
|
||||
@@ -78,6 +82,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="ttl">An optional time-to-live duration for the cached object. If null, the backend's default expiration is applied.</param>
|
||||
/// <param name="cancellationToken">A token to observe while waiting for the operation to complete.</param>
|
||||
/// <returns>A task that represents the asynchronous operation, containing the retrieved or newly created object of type <typeparamref name="T"/>.</returns>
|
||||
/// <!-- aidoc:v1 sig=5407b21 -->
|
||||
public Task<T> GetOrSetObjectAsync<T>(
|
||||
string key,
|
||||
Func<Task<T>> factory,
|
||||
@@ -94,6 +99,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="loader">An asynchronous function that produces the value to cache when no existing entry is found.</param>
|
||||
/// <param name="ttl">An optional time-to-live duration after which the cached entry expires. If null, the backend's default TTL is used.</param>
|
||||
/// <returns>A task that represents the asynchronous operation, containing the cached or loaded string value, or null if no value could be obtained.</returns>
|
||||
/// <!-- aidoc:v1 sig=0bd9db4 -->
|
||||
public Task<string?> GetOrSetValueAsync(
|
||||
string key,
|
||||
Func<Task<string>> loader,
|
||||
@@ -113,6 +119,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="ttl">An optional time-to-live applied to the cached object. When <c>null</c>, the backend's default expiration is used.</param>
|
||||
/// <param name="cancellationToken">The token to observe for canceling the asynchronous operation.</param>
|
||||
/// <returns>A task that represents the asynchronous get-or-set operation, containing the retrieved or newly created object.</returns>
|
||||
/// <!-- aidoc:v1 sig=c950b98 -->
|
||||
public Task<T> GetOrSetObjectAsync<T>(
|
||||
GroupedField groupedField,
|
||||
ObjectId patientId,
|
||||
@@ -129,6 +136,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// </summary>
|
||||
/// <param name="key">The key used to select the backend and identify the value to set.</param>
|
||||
/// <param name="value">The value to associate with the specified key.</param>
|
||||
/// <!-- aidoc:v1 sig=3796865 -->
|
||||
public void SetValue(string key, string value)
|
||||
=> SelectBackend(key).SetValue(key, value);
|
||||
|
||||
@@ -137,6 +145,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// </summary>
|
||||
/// <param name="key">The key used to select the backend and retrieve the associated value.</param>
|
||||
/// <returns>The value associated with the key, or <c>null</c> if the selected backend returns no value.</returns>
|
||||
/// <!-- aidoc:v1 sig=f39f53d -->
|
||||
public string? GetValue(string key)
|
||||
=> SelectBackend(key).GetValue(key);
|
||||
|
||||
@@ -146,6 +155,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="key">The identifier used to select the appropriate backend and to look up the object.</param>
|
||||
/// <param name="upd">Indicates whether the underlying backend should perform an update during retrieval. Defaults to <c>true</c>.</param>
|
||||
/// <returns>A task that represents the asynchronous retrieval operation, containing the object of type <typeparamref name="T"/> or <c>null</c> if not found.</returns>
|
||||
/// <!-- aidoc:v1 sig=15709a1 -->
|
||||
public Task<T?> GetObjectAsync<T>(string key, bool upd = true)
|
||||
=> SelectBackend(key).GetObjectAsync<T>(key, upd);
|
||||
|
||||
@@ -157,6 +167,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="obj">The object to store in the selected backend.</param>
|
||||
/// <param name="upd">Indicates whether an update operation should be performed. Defaults to <c>true</c>.</param>
|
||||
/// <returns>A task that represents the asynchronous store operation.</returns>
|
||||
/// <!-- aidoc:v1 sig=9d84645 -->
|
||||
public Task SetObjectAsync<T>(string key, T obj, bool upd = true)
|
||||
=> SelectBackend(key).SetObjectAsync(key, obj, upd);
|
||||
|
||||
@@ -167,6 +178,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="ttl">An optional time-to-live applied to the object; if null, the backend's default is used.</param>
|
||||
/// <param name="upd">A flag indicating whether the retrieval should update the object's state (e.g., refresh expiration).</param>
|
||||
/// <returns>A task containing the deserialized object, or null if the object is not found in the selected backend.</returns>
|
||||
/// <!-- aidoc:v1 sig=4675112 -->
|
||||
public Task<T?> GetObjectAsync<T>(string key, TimeSpan? ttl, bool upd)
|
||||
=> SelectBackend(key).GetObjectAsync<T>(key, ttl, upd);
|
||||
|
||||
@@ -178,6 +190,8 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="ttl">The optional time-to-live duration for the stored object.</param>
|
||||
/// <param name="upd">Indicates whether to update an existing entry or create a new one.</param>
|
||||
/// <returns>A task that represents the asynchronous set operation.</returns>
|
||||
/// <!-- aidoc-review:v1 severity=medium kind=missing_param
|
||||
/// "Generic type parameter T (the type of the object being stored) is not documented" -->
|
||||
public Task SetObjectAsync<T>(string key, T obj, TimeSpan? ttl, bool upd)
|
||||
=> SelectBackend(key).SetObjectAsync(key, obj, ttl, upd);
|
||||
|
||||
@@ -186,6 +200,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// </summary>
|
||||
/// <param name="key">The identifier of the object to delete, also used to resolve the responsible backend.</param>
|
||||
/// <returns>A task that represents the asynchronous delete operation.</returns>
|
||||
/// <!-- aidoc:v1 sig=0dde5af -->
|
||||
public Task DeleteObjectAsync(string key)
|
||||
=> SelectBackend(key).DeleteObjectAsync(key);
|
||||
|
||||
@@ -194,12 +209,14 @@ namespace adas_core.Application.Services.Caching
|
||||
/// </summary>
|
||||
/// <param name="pattern">The pattern used to match entries for deletion in both storage backends.</param>
|
||||
/// <returns>The combined total number of entries deleted from Redis and in-memory storage.</returns>
|
||||
/// <!-- aidoc:v1 sig=fa6dca4 -->
|
||||
public async Task<long> DeleteByPatternAsync(string pattern)
|
||||
=> await redis.DeleteByPatternAsync(pattern) + await memory.DeleteByPatternAsync(pattern);
|
||||
|
||||
/// <summary>
|
||||
/// Clears all cached data from both the in-memory cache and the Redis cache, ensuring that stale entries are removed across all configured cache providers.
|
||||
/// </summary>
|
||||
/// <!-- aidoc:v1 sig=d5e5e8c body=c0791ef -->
|
||||
public void CleanCache()
|
||||
{
|
||||
memory.CleanCache();
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// mediante LockManagerService + InMemoryLockProvider.
|
||||
/// Compatible con la interfaz ICacheService incluyendo GetOrSet.
|
||||
/// </summary>
|
||||
/// <!-- aidoc:v1 sig=53daded -->
|
||||
public class CacheService(LockManagerService lockManager) : ICacheService
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, object> _mem = new();
|
||||
@@ -21,6 +22,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="gf">The grouped field whose name is included in the key.</param>
|
||||
/// <param name="patientId">The identifier of the patient the key is scoped to.</param>
|
||||
/// <returns>A formatted key string in the form <c>GroupedObs:{patientId}:{gf.Name}</c>.</returns>
|
||||
/// <!-- aidoc:v1 sig=72c65a6 -->
|
||||
private static string BuildGroupedKey(GroupedField gf, ObjectId patientId)
|
||||
=> $"GroupedObs:{patientId}:{gf.Name}";
|
||||
|
||||
@@ -34,6 +36,8 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="ttl">Optional time-to-live associated with the cached object.</param>
|
||||
/// <param name="cancellationToken">A token to observe for cancellation requests.</param>
|
||||
/// <returns>The cached or newly created object of type <typeparamref name="T"/>.</returns>
|
||||
/// <!-- aidoc-review:v1 severity=high kind=mentions_removed_behavior
|
||||
/// "The `ttl` parameter is documented as 'Optional time-to-live associated with the cached object', but the method body never uses `ttl`—the cache assignment `_mem[key] = created;` has no TTL/expiration handling, so readers will be misled into believing the TTL is honored." -->
|
||||
public async Task<T> GetOrSetObjectAsync<T>(
|
||||
string key,
|
||||
Func<Task<T>> factory,
|
||||
@@ -71,6 +75,8 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="loader">The asynchronous function invoked to load the value when no cached entry exists for the key.</param>
|
||||
/// <param name="ttlOverride">An optional time span that overrides the default time-to-live for the cached value.</param>
|
||||
/// <returns>The cached or newly loaded string value, or <c>null</c> when the underlying cache entry is absent or cannot be cast to a string.</returns>
|
||||
/// <!-- aidoc-review:v1 severity=low kind=wrong_returns
|
||||
/// "The <returns> text claims the method returns null 'when the underlying cache entry ... cannot be cast to a string', but the explicit cast (string?)result would throw InvalidCastException rather than yield null in that case." -->
|
||||
public async Task<string?> GetOrSetValueAsync(
|
||||
string key,
|
||||
Func<Task<string>> loader,
|
||||
@@ -94,6 +100,8 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="ttl">Optional time-to-live for the cached entry.</param>
|
||||
/// <param name="cancellationToken">Token used to cancel the asynchronous operation.</param>
|
||||
/// <returns>A task containing the cached or newly created object.</returns>
|
||||
/// <!-- aidoc-review:v1 severity=medium kind=wrong_param_role
|
||||
/// "The `ttl` parameter is documented as 'Optional time-to-live for the cached entry', but it is never referenced in the method body — no TTL is applied to the stored entry, and the lock timeout is hardcoded to 5 seconds. A reader would expect their TTL value to be honored, but it is effectively ignored." -->
|
||||
public async Task<T> GetOrSetObjectAsync<T>(
|
||||
GroupedField groupedField,
|
||||
ObjectId patientId,
|
||||
@@ -131,6 +139,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// </summary>
|
||||
/// <param name="key">The key that identifies where the value will be stored.</param>
|
||||
/// <param name="value">The value to associate with the specified key.</param>
|
||||
/// <!-- aidoc:v1 sig=f3491fe -->
|
||||
public void SetValue(string key, string value)
|
||||
=> _mem[key] = value;
|
||||
|
||||
@@ -140,6 +149,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// </summary>
|
||||
/// <param name="key">The key used to look up the value in the underlying store.</param>
|
||||
/// <returns>The string representation of the stored value if the key exists; otherwise, <c>null</c>.</returns>
|
||||
/// <!-- aidoc:v1 sig=7a34703 -->
|
||||
public string? GetValue(string key)
|
||||
=> _mem.TryGetValue(key, out var v) ? v.ToString() : null;
|
||||
|
||||
@@ -150,6 +160,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="key">The cache key used to look up the stored object.</param>
|
||||
/// <param name="updateExpiration">Indicates whether the entry's expiration should be refreshed on access. Not currently used by this implementation.</param>
|
||||
/// <returns>A <see cref="Task{T}"/> containing the cached value cast to <typeparamref name="T"/>, or <c>null</c> if no entry exists for the given key.</returns>
|
||||
/// <!-- aidoc:v1 sig=5046896 body=6902631 -->
|
||||
public Task<T?> GetObjectAsync<T>(string key, bool updateExpiration = true)
|
||||
{
|
||||
return Task.FromResult(
|
||||
@@ -163,6 +174,8 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="key">The cache key under which the object will be stored.</param>
|
||||
/// <param name="obj">The object to store in the cache.</param>
|
||||
/// <param name="updateExpiration">Indicates whether the cache entry's expiration should be refreshed.</param>
|
||||
/// <!-- aidoc-review:v1 severity=medium kind=mentions_removed_behavior
|
||||
/// "The updateExpiration parameter is documented as indicating whether the cache entry's expiration should be refreshed, but the method body never references this parameter, so the documented behavior does not occur." -->
|
||||
public Task SetObjectAsync<T>(string key, T obj, bool updateExpiration = true)
|
||||
{
|
||||
_mem[key] = obj!;
|
||||
@@ -176,6 +189,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="ttlOverride">An optional time-to-live override accepted by this overload but ignored when delegating to the underlying retrieval call.</param>
|
||||
/// <param name="upd">A flag indicating whether the retrieval should trigger an update on the stored object.</param>
|
||||
/// <returns>A task that represents the asynchronous operation, containing the retrieved object of type <typeparamref name="T"/> or <c>null</c> if no object is found for the given key.</returns>
|
||||
/// <!-- aidoc:v1 sig=773648d -->
|
||||
public Task<T?> GetObjectAsync<T>(string key, TimeSpan? ttlOverride, bool upd)
|
||||
=> GetObjectAsync<T>(key, upd);
|
||||
|
||||
@@ -187,6 +201,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="ttlOverride">An optional time-to-live override for the stored entry; not applied by this overload.</param>
|
||||
/// <param name="upd">A flag indicating whether the operation should update an existing entry.</param>
|
||||
/// <returns>A task that represents the asynchronous set operation.</returns>
|
||||
/// <!-- aidoc:v1 sig=874b6ce -->
|
||||
public Task SetObjectAsync<T>(string key, T obj, TimeSpan? ttlOverride, bool upd)
|
||||
=> SetObjectAsync(key, obj, upd);
|
||||
|
||||
@@ -196,6 +211,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// Asynchronously removes the object associated with the specified key from the in-memory store. The operation succeeds silently whether or not the key exists.
|
||||
/// </summary>
|
||||
/// <param name="key">The identifier of the object to delete.</param>
|
||||
/// <!-- aidoc:v1 sig=93555f0 body=00affc5 -->
|
||||
public Task DeleteObjectAsync(string key)
|
||||
{
|
||||
_mem.TryRemove(key, out _);
|
||||
@@ -207,6 +223,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// </summary>
|
||||
/// <param name="pattern">The pattern to match against cache keys. Asterisk (*) characters are removed and the remaining text is used as a substring match.</param>
|
||||
/// <returns>A task that represents the asynchronous operation, containing the count of entries that were removed.</returns>
|
||||
/// <!-- aidoc:v1 sig=a8cca2e body=81413d3 -->
|
||||
public Task<long> DeleteByPatternAsync(string pattern)
|
||||
{
|
||||
var p = pattern.Replace("*", "");
|
||||
@@ -223,6 +240,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <summary>
|
||||
/// Clears all entries from the in-memory cache, removing any previously stored data.
|
||||
/// </summary>
|
||||
/// <!-- aidoc:v1 sig=8762842 -->
|
||||
public void CleanCache() => _mem.Clear();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// Se basa en SemaphoreSlim y solo controla concurrencia DENTRO del proceso.
|
||||
/// Para CacheService (in-memory).
|
||||
/// </summary>
|
||||
/// <!-- aidoc:v1 sig=ea44018 -->
|
||||
public class InMemoryLockProvider : ILockProvider
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new(StringComparer.Ordinal);
|
||||
@@ -15,6 +16,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <summary>
|
||||
/// Crea u obtiene un semáforo asociado a la clave.
|
||||
/// </summary>
|
||||
/// <!-- aidoc:v1 sig=af8b85d body=29079e4 -->
|
||||
private SemaphoreSlim GetOrCreate(string key)
|
||||
{
|
||||
return _locks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
|
||||
@@ -23,6 +25,12 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <summary>
|
||||
/// Intenta adquirir el lock por clave.
|
||||
/// </summary>
|
||||
/// <!-- aidoc-review:v1 severity=medium kind=missing_param
|
||||
/// "Parameter 'key' is not documented" -->
|
||||
/// <!-- aidoc-review:v1 severity=medium kind=missing_param
|
||||
/// "Parameter 'timeout' is not documented" -->
|
||||
/// <!-- aidoc-review:v1 severity=medium kind=missing_returns
|
||||
/// "Return value (bool indicating acquisition success) is not documented" -->
|
||||
public async Task<bool> AcquireAsync(string key, TimeSpan timeout)
|
||||
{
|
||||
var sem = GetOrCreate(key);
|
||||
@@ -42,6 +50,8 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <summary>
|
||||
/// Libera el lock (si existe y no está ya liberado).
|
||||
/// </summary>
|
||||
/// <!-- aidoc-review:v1 severity=medium kind=missing_param
|
||||
/// "The 'key' parameter is not documented; only a <summary> is provided, with no <param name=\"key\"> tag describing the lock identifier." -->
|
||||
public Task ReleaseAsync(string key)
|
||||
{
|
||||
if (!_locks.TryGetValue(key, out var sem))
|
||||
|
||||
@@ -8,6 +8,10 @@ namespace adas_core.Application.Services.Caching
|
||||
/// CacheService lo usará para evitar condiciones de carrera en GetOrSet.
|
||||
/// Funciona igual para locks locales o distribuidos.
|
||||
/// </summary>
|
||||
/// <!-- aidoc-review:v1 severity=medium kind=missing_param
|
||||
/// "Constructor parameter 'logger' (ILogger<LockManagerService>) is not documented." -->
|
||||
/// <!-- aidoc-review:v1 severity=medium kind=missing_param
|
||||
/// "Constructor parameter 'provider' (ILockProvider) is not documented." -->
|
||||
public class LockManagerService(
|
||||
ILogger<LockManagerService> logger,
|
||||
ILockProvider provider)
|
||||
@@ -15,6 +19,18 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <summary>
|
||||
/// Ejecuta una función que devuelve un valor bajo un lock por clave.
|
||||
/// </summary>
|
||||
/// <!-- aidoc-review:v1 severity=medium kind=missing_param
|
||||
/// "Parameter 'key' is not documented" -->
|
||||
/// <!-- aidoc-review:v1 severity=medium kind=missing_param
|
||||
/// "Parameter 'timeout' is not documented" -->
|
||||
/// <!-- aidoc-review:v1 severity=medium kind=missing_param
|
||||
/// "Parameter 'action' is not documented" -->
|
||||
/// <!-- aidoc-review:v1 severity=medium kind=missing_param
|
||||
/// "Parameter 'cancellationToken' is not documented" -->
|
||||
/// <!-- aidoc-review:v1 severity=medium kind=missing_returns
|
||||
/// "Method returns Task<T> via action delegate, but no <returns> tag is present" -->
|
||||
/// <!-- aidoc-review:v1 severity=medium kind=missing_exception
|
||||
/// "TimeoutException is thrown when the lock cannot be acquired within the timeout, but no <exception> tag is present" -->
|
||||
public async Task<T> WithLockAsync<T>(
|
||||
string key,
|
||||
TimeSpan timeout,
|
||||
@@ -57,6 +73,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <summary>
|
||||
/// Version Task (sin valor).
|
||||
/// </summary>
|
||||
/// <!-- aidoc:v1 sig=3facc4e body=59bc0c3 -->
|
||||
public Task WithLockAsync(
|
||||
string key,
|
||||
TimeSpan timeout,
|
||||
@@ -77,6 +94,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <summary>
|
||||
/// Libera el lock y registra posibles errores sin interrumpir el flujo.
|
||||
/// </summary>
|
||||
/// <!-- aidoc:v1 sig=88f2f04 body=608f118 -->
|
||||
private async Task SafeReleaseAsync(string key)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// No almacena nada, no devuelve nada y no interfiere con el flujo.
|
||||
/// Se usa cuando el CacheMode es "None".
|
||||
/// </summary>
|
||||
/// <!-- aidoc:v1 sig=2b881c5 -->
|
||||
public class NoCacheService : ICacheService
|
||||
{
|
||||
/// <summary>
|
||||
@@ -16,6 +17,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// </summary>
|
||||
/// <param name="key">The identifier used to reference the value.</param>
|
||||
/// <param name="value">The value intended to be associated with the key.</param>
|
||||
/// <!-- aidoc:v1 sig=c91035b body=4448e1d -->
|
||||
public void SetValue(string key, string value)
|
||||
{
|
||||
// No hacer nada
|
||||
@@ -27,6 +29,8 @@ namespace adas_core.Application.Services.Caching
|
||||
/// </summary>
|
||||
/// <param name="key">The key used to look up the associated value.</param>
|
||||
/// <returns>The value associated with <paramref name="key"/>, or <c>null</c> if no value is found.</returns>
|
||||
/// <!-- aidoc-review:v1 severity=high kind=wrong_summary
|
||||
/// "The summary states the method 'retrieves' a value for the specified key, but the body unconditionally returns null and performs no retrieval/lookup logic. The <returns> tag also implies a non-null value path that the code cannot produce." -->
|
||||
public string? GetValue(string key)
|
||||
{
|
||||
return null;
|
||||
@@ -38,6 +42,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="key">The identifier of the object to retrieve.</param>
|
||||
/// <param name="updateExpiration">Indicates whether the expiration of the entry should be updated upon retrieval.</param>
|
||||
/// <returns>A <see cref="Task{T}"/> that represents the asynchronous retrieval, containing the object associated with the key or the default value of <typeparamref name="T"/> if not found.</returns>
|
||||
/// <!-- aidoc:v1 sig=5046896 body=772039d -->
|
||||
public Task<T?> GetObjectAsync<T>(string key, bool updateExpiration = true)
|
||||
{
|
||||
return Task.FromResult<T?>(default);
|
||||
@@ -50,6 +55,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="key">The unique identifier used to store and later retrieve the object.</param>
|
||||
/// <param name="obj">The object to store in the data store.</param>
|
||||
/// <param name="updateExpiration">Indicates whether the expiration time of the cached entry should be updated. Defaults to true.</param>
|
||||
/// <!-- aidoc:v1 sig=255c42a body=6805ef5 -->
|
||||
public Task SetObjectAsync<T>(string key, T obj, bool updateExpiration = true)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
@@ -63,6 +69,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="ttlOverride">An optional time-to-live value that, when provided, overrides the default expiration for the entry.</param>
|
||||
/// <param name="updateExpiration">Indicates whether the expiration of the entry should be refreshed upon a successful retrieval.</param>
|
||||
/// <returns>A <see cref="Task{T}"/> that represents the asynchronous operation, containing the retrieved object or <c>null</c> if no value is found.</returns>
|
||||
/// <!-- aidoc:v1 sig=efdd78f body=772039d -->
|
||||
public Task<T?> GetObjectAsync<T>(string key, TimeSpan? ttlOverride, bool updateExpiration)
|
||||
{
|
||||
return Task.FromResult<T?>(default);
|
||||
@@ -76,6 +83,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="ttlOverride">An optional time-to-live value that overrides the default expiration period; <c>null</c> uses the default.</param>
|
||||
/// <param name="updateExpiration">A value indicating whether the expiration time should be updated.</param>
|
||||
/// <returns>A task that represents the asynchronous set operation.</returns>
|
||||
/// <!-- aidoc:v1 sig=25d628c body=6805ef5 -->
|
||||
public Task SetObjectAsync<T>(string key, T obj, TimeSpan? ttlOverride, bool updateExpiration)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
@@ -87,6 +95,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// </summary>
|
||||
/// <param name="pattern">The pattern used to identify the items to delete.</param>
|
||||
/// <returns>A <see cref="Task{Int64}"/> representing the asynchronous operation, with a result of 0 indicating that no items were deleted.</returns>
|
||||
/// <!-- aidoc:v1 sig=a8cca2e body=14896de -->
|
||||
public Task<long> DeleteByPatternAsync(string pattern)
|
||||
{
|
||||
return Task.FromResult(0L);
|
||||
@@ -98,6 +107,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// </summary>
|
||||
/// <param name="key">The identifier of the object to delete.</param>
|
||||
/// <returns>A <see cref="Task"/> that represents the asynchronous delete operation.</returns>
|
||||
/// <!-- aidoc:v1 sig=93555f0 body=6805ef5 -->
|
||||
public Task DeleteObjectAsync(string key)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
@@ -106,6 +116,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <summary>
|
||||
/// Performs a cleanup operation on the cache. Currently, this method has no implementation and does not perform any cleanup actions.
|
||||
/// </summary>
|
||||
/// <!-- aidoc:v1 sig=d5e5e8c body=4448e1d -->
|
||||
public void CleanCache()
|
||||
{
|
||||
// Nada que limpiar
|
||||
@@ -142,6 +153,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="loader">The asynchronous function used to load the value.</param>
|
||||
/// <param name="ttl">An optional time-to-live duration for the value.</param>
|
||||
/// <returns>A task that represents the asynchronous operation, containing the loaded value as a nullable string.</returns>
|
||||
/// <!-- aidoc:v1 sig=02351fc body=f2ab08b -->
|
||||
public async Task<string?> GetOrSetValueAsync(
|
||||
string key,
|
||||
Func<Task<string>> loader,
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// Lock distribuido en Redis. Usa token por adquisición (owner)
|
||||
/// y liberación segura con script Lua: borra la key solo si el valor coincide.
|
||||
/// </summary>
|
||||
/// <!-- aidoc:v1 sig=db4fe1b -->
|
||||
public class RedisLockProvider(Func<IDatabase?> getDatabase) : ILockProvider
|
||||
{
|
||||
private readonly string _prefix = "lock:";
|
||||
@@ -31,6 +32,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="key">The identifier of the resource to lock.</param>
|
||||
/// <param name="timeout">The maximum duration to keep retrying before giving up.</param>
|
||||
/// <returns><c>true</c> if the lock was successfully acquired; otherwise, <c>false</c>.</returns>
|
||||
/// <!-- aidoc:v1 sig=4b91964 body=b8dd956 -->
|
||||
public async Task<bool> AcquireAsync(string key, TimeSpan timeout)
|
||||
{
|
||||
var redis = getDatabase();
|
||||
@@ -58,6 +60,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// Asynchronously releases the token associated with the specified key by removing it from the in-memory token store and executing a Lua release script against Redis. If the key is not found in the local store, or the Redis database is unavailable, the method returns without performing any further action.
|
||||
/// </summary>
|
||||
/// <param name="key">The identifier of the token to release.</param>
|
||||
/// <!-- aidoc:v1 sig=03497b0 body=db68b0e -->
|
||||
public async Task ReleaseAsync(string key)
|
||||
{
|
||||
if (!_tokens.TryRemove(key, out var token))
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <summary>
|
||||
/// Represents a Redis-based implementation of the <see cref="ICacheService"/> interface for caching operations.
|
||||
/// </summary>
|
||||
/// <!-- aidoc:v1 sig=a3787ed -->
|
||||
public class RedisService : ICacheService
|
||||
{
|
||||
private readonly ILogger<RedisService> _logger;
|
||||
@@ -59,6 +60,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="ttl">Optional time-to-live duration for the cached object. If null, the cache default is used.</param>
|
||||
/// <param name="cancellationToken">The token used to cancel the asynchronous operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation, containing the cached or newly created object.</returns>
|
||||
/// <!-- aidoc:v1 sig=df20281 body=dc4ffdf -->
|
||||
public async Task<T> GetOrSetObjectAsync<T>(
|
||||
string key,
|
||||
Func<Task<T>> factory,
|
||||
@@ -100,6 +102,8 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="loader">The asynchronous function invoked to produce the value when it is not present in the cache.</param>
|
||||
/// <param name="ttl">Optional time-to-live applied to the cached value; if not provided, the default caching policy is used.</param>
|
||||
/// <returns>The cached value when available, or the value produced by the loader when the cache is empty or Redis is unavailable.</returns>
|
||||
/// <!-- aidoc-review:v1 severity=medium kind=mentions_removed_behavior
|
||||
/// "The ttl parameter is documented as 'applied to the cached value', but the code never references or passes ttl to SetValue; the parameter is unused in the method body." -->
|
||||
public async Task<string?> GetOrSetValueAsync(
|
||||
string key,
|
||||
Func<Task<string>> loader,
|
||||
@@ -136,6 +140,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="gf">The grouped field whose name is used to identify the observation group.</param>
|
||||
/// <param name="patientId">The identifier of the patient the observation belongs to.</param>
|
||||
/// <returns>A formatted string key combining the <c>GroupedObs</c> prefix, the patient identifier, and the grouped field name.</returns>
|
||||
/// <!-- aidoc:v1 sig=72c65a6 -->
|
||||
private static string BuildGroupedKey(GroupedField gf, ObjectId patientId)
|
||||
=> $"GroupedObs:{patientId}:{gf.Name}";
|
||||
|
||||
@@ -148,6 +153,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="ttl">Optional time-to-live applied to the stored cache entry. If null, no expiration is set.</param>
|
||||
/// <param name="cancellationToken">Token used to cancel the distributed lock operation.</param>
|
||||
/// <returns>The cached object if present, otherwise the object produced by <paramref name="factory"/>.</returns>
|
||||
/// <!-- aidoc:v1 sig=f122a12 body=a3fec17 -->
|
||||
public async Task<T> GetOrSetObjectAsync<T>(
|
||||
GroupedField groupedField,
|
||||
ObjectId patientId,
|
||||
@@ -190,6 +196,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// </summary>
|
||||
/// <param name="key">The key under which the value will be stored.</param>
|
||||
/// <param name="value">The string value to persist.</param>
|
||||
/// <!-- aidoc:v1 sig=fb46470 -->
|
||||
public void SetValue(string key, string value)
|
||||
=> _database?.StringSet(key, value, GetEntityTtl(key), true);
|
||||
|
||||
@@ -198,6 +205,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// </summary>
|
||||
/// <param name="key">The identifier of the value to look up in the data store.</param>
|
||||
/// <returns>The stored string value, or <c>null</c> if the key does not exist or the data store is unavailable.</returns>
|
||||
/// <!-- aidoc:v1 sig=39e4be0 body=3910e79 -->
|
||||
public string? GetValue(string key)
|
||||
{
|
||||
var val = _database?.StringGet(key);
|
||||
@@ -214,6 +222,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="updateExpiration">When <c>true</c> (the default), resets the key's time-to-live to the configured entity TTL on a successful read, implementing sliding expiration.</param>
|
||||
/// <returns>A <see cref="Task{T}"/> containing the deserialized object, or <c>default</c> if Redis is unavailable or the key is missing/empty.</returns>
|
||||
/// <exception cref="Exception">Thrown when the stored JSON payload cannot be deserialized into <typeparamref name="T"/>; the original exception is wrapped and rethrown.</exception>
|
||||
/// <!-- aidoc:v1 sig=a7a96b7 body=6c885c8 -->
|
||||
public async Task<T?> GetObjectAsync<T>(string key, bool updateExpiration = true)
|
||||
{
|
||||
if (!_isRedisAvailable)
|
||||
@@ -248,6 +257,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="key">The key under which the object will be stored.</param>
|
||||
/// <param name="obj">The object to store.</param>
|
||||
/// <param name="updateExpiration">Indicates whether the expiration time of the entry should be updated.</param>
|
||||
/// <!-- aidoc:v1 sig=2dac8a4 -->
|
||||
public async Task SetObjectAsync<T>(
|
||||
string key,
|
||||
T obj,
|
||||
@@ -261,6 +271,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="obj">The object to serialize and persist to Redis.</param>
|
||||
/// <param name="ttlOverride">An optional time-to-live override; when null, the entity's default TTL is applied.</param>
|
||||
/// <param name="updateExpiration">Flag indicating whether the expiration should be updated.</param>
|
||||
/// <!-- aidoc:v1 sig=b181b67 body=11fd650 -->
|
||||
public async Task SetObjectAsync<T>(
|
||||
string key,
|
||||
T obj,
|
||||
@@ -283,6 +294,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// When the Redis backend is unavailable, the call is skipped silently as a no-op fallback.
|
||||
/// </summary>
|
||||
/// <param name="key">The unique identifier of the cached object to remove.</param>
|
||||
/// <!-- aidoc:v1 sig=884f4b6 body=00b8917 -->
|
||||
public async Task DeleteObjectAsync(string key)
|
||||
{
|
||||
if (_isRedisAvailable)
|
||||
@@ -295,6 +307,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// </summary>
|
||||
/// <param name="pattern">The pattern used to match Redis keys to be deleted.</param>
|
||||
/// <returns>The number of keys that were deleted.</returns>
|
||||
/// <!-- aidoc:v1 sig=691e7ee body=f96f2c9 -->
|
||||
public async Task<long> DeleteByPatternAsync(string pattern)
|
||||
{
|
||||
if (!_isRedisAvailable || _server == null)
|
||||
@@ -311,6 +324,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <summary>
|
||||
/// Clears all cached data by flushing the underlying server database. If the server instance is <see langword="null"/>, the call is safely skipped as a no-op.
|
||||
/// </summary>
|
||||
/// <!-- aidoc:v1 sig=f510ade -->
|
||||
public void CleanCache()
|
||||
=> _server?.FlushDatabase();
|
||||
|
||||
@@ -321,6 +335,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// </summary>
|
||||
/// <param name="key">The cache key used to classify the entity type and determine the applicable TTL.</param>
|
||||
/// <returns>A <see cref="TimeSpan"/> representing the configured TTL, or <c>null</c> if the resolved seconds value is not positive.</returns>
|
||||
/// <!-- aidoc:v1 sig=be2ec4a body=d7d015b -->
|
||||
private TimeSpan? GetEntityTtl(string key)
|
||||
{
|
||||
var entity = CacheKeyClassifier.Classify(key);
|
||||
@@ -341,6 +356,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// </summary>
|
||||
/// <param name="key">The key identifying the entity whose TTL presence is being checked.</param>
|
||||
/// <returns><c>true</c> if a TTL value is found for the specified key; otherwise, <c>false</c>.</returns>
|
||||
/// <!-- aidoc:v1 sig=9f4c827 -->
|
||||
private bool ShouldRenewTtl(string key)
|
||||
=> GetEntityTtl(key) != null;
|
||||
|
||||
@@ -349,6 +365,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <summary>
|
||||
/// Initializes the Redis connection for caching by connecting asynchronously, obtaining the database and server, and marking the connection as available on success. Returns early without establishing a connection when the configured Redis connection string is null, and logs any exception that occurs during initialization without rethrowing, leaving the connection marked as unavailable.
|
||||
/// </summary>
|
||||
/// <!-- aidoc:v1 sig=819c56e body=e2d6e2b -->
|
||||
private async Task InitializeRedisConnectionAsync()
|
||||
{
|
||||
_isRedisAvailable = false;
|
||||
@@ -377,6 +394,7 @@ namespace adas_core.Application.Services.Caching
|
||||
/// <param name="ttlOverride">An optional time-to-live override; not applied by this overload.</param>
|
||||
/// <param name="updateExpiration">When true, the expiration of the cached entry is refreshed on retrieval.</param>
|
||||
/// <returns>A task that resolves to the cached object, or null if no entry exists for the specified key.</returns>
|
||||
/// <!-- aidoc:v1 sig=dc7158d -->
|
||||
public Task<T?> GetObjectAsync<T>(string key, TimeSpan? ttlOverride, bool updateExpiration)
|
||||
=> GetObjectAsync<T>(key, updateExpiration);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user