Files
adas-core/adas-core.Application/Services/Caching/CacheDispatcher.cs
T

226 lines
14 KiB
C#

using adas_core.Application.Services.Interfaces;
using adas_core.Domain.Enums;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.GroupedObservations;
using adas_core.Domain.Utils;
using MongoDB.Bson;
namespace adas_core.Application.Services.Caching
{
/// <summary>
/// Orquestador de caché. Selecciona el backend (Redis, InMemory, None)
/// 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,
NoCacheService noop,
CacheSettings cacheSettings)
: ICacheService
{
// Selección de backend
/// <summary>
/// Selects the appropriate cache backend (Redis, in-memory, or no-op) for the given key by classifying the key into an entity type and resolving its configured cache mode.
/// Unknown entity types default to in-memory caching, and unrecognized modes fall back to the no-op cache service.
/// </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);
var mode = entity switch
{
CacheEnum.EntityType.Patients => cacheSettings.Patients,
CacheEnum.EntityType.Displays => cacheSettings.Displays,
CacheEnum.EntityType.PumpObservations => cacheSettings.PumpObservations,
CacheEnum.EntityType.Appointments => cacheSettings.Appointments,
CacheEnum.EntityType.GroupedObservations => cacheSettings.GroupedObservations,
_ => CacheEnum.Mode.Cache
};
return mode switch
{
CacheEnum.Mode.Redis => redis,
CacheEnum.Mode.Cache => memory,
_ => noop
};
}
// Para GroupedObservations generamos la misma clave compuesta que el resto de servicios,
// de modo que el clasificador y la política de TTL funcionen igual.
/// <summary>
/// Builds a composite key used to identify grouped observations for a specific patient.
/// </summary>
/// <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}";
/// <summary>
/// Selects the appropriate cache backend for the given grouped field and patient identifier by building a grouped key and resolving the backend through the key-based overload.
/// </summary>
/// <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));
// GetOrSet (KEY string)
/// <summary>
/// Asynchronously retrieves the object associated with the specified key from the selected backend, or sets it using the provided factory if it is not already cached.
/// </summary>
/// <param name="key">The cache key used to identify the object and to select the appropriate backend.</param>
/// <param name="factory">A delegate that asynchronously produces the value to store when the key is not present in the selected backend.</param>
/// <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,
TimeSpan? ttl = null,
CancellationToken cancellationToken = default)
=> SelectBackend(key).GetOrSetObjectAsync(key, factory, ttl, cancellationToken);
/// <summary>
/// Asynchronously retrieves the value associated with the specified key from the backend selected for that key,
/// or loads and stores it using the provided loader function if it is not already present.
/// Supports an optional time-to-live (TTL) for the cached entry, and the returned value may be null.
/// </summary>
/// <param name="key">The key used to identify the cached value and to select the appropriate backend.</param>
/// <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,
TimeSpan? ttl = null)
=> SelectBackend(key).GetOrSetValueAsync(key, loader, ttl);
// GetOrSet (GroupedField + PatientId)
/// <summary>
/// Asynchronously retrieves the object associated with the specified grouped field and patient, or creates and stores it using the provided factory if it does not exist.
/// The appropriate backend is selected based on the grouped field and patient identifier before the underlying get-or-set operation is performed.
/// </summary>
/// <typeparam name="T">The type of the object to retrieve or create.</typeparam>
/// <param name="groupedField">The grouped field that determines the target backend and identifies the cached object.</param>
/// <param name="patientId">The identifier of the patient whose object is being retrieved or created.</param>
/// <param name="factory">The asynchronous factory used to create the object when no cached value is available.</param>
/// <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,
Func<Task<T>> factory,
TimeSpan? ttl = null,
CancellationToken cancellationToken = default)
=> SelectBackend(groupedField, patientId)
.GetOrSetObjectAsync(groupedField, patientId, factory, ttl, cancellationToken);
// Set/Get básicos
/// <summary>
/// Sets the value associated with the specified key by selecting the appropriate backend for that key and delegating the assignment to it.
/// </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);
/// <summary>
/// Retrieves the value associated with the specified key by delegating the lookup to a backend selected for that key.
/// </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);
/// <summary>
/// Retrieves an object of type <typeparamref name="T"/> from the backend selected by the given key, with an option to trigger an update.
/// </summary>
/// <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);
/// <summary>
/// Asynchronously stores an object in the backend selected by the specified key.
/// </summary>
/// <typeparam name="T">The type of the object to store.</typeparam>
/// <param name="key">The key used to select the backend and identify the stored object.</param>
/// <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);
/// <summary>
/// Retrieves an object of the specified type from the backend selected by the given key, optionally applying a time-to-live and update behavior.
/// </summary>
/// <param name="key">The identifier used to select the backend and locate the stored object.</param>
/// <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);
/// <summary>
/// Asynchronously stores an object in the backend selected for the specified key, with an optional time-to-live and update flag.
/// </summary>
/// <param name="key">The key used to select the target backend and identify the object.</param>
/// <param name="obj">The object to store.</param>
/// <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);
/// <summary>
/// Asynchronously deletes the object identified by the specified key by delegating the operation to the backend selected for that key.
/// </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);
/// <summary>
/// Deletes entries matching the specified pattern from both Redis and in-memory storage, returning the total count of deleted entries.
/// </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();
redis.CleanCache();
}
}
}