228 lines
13 KiB
C#
228 lines
13 KiB
C#
using adas_core.Application.Services.Interfaces;
|
|
using adas_core.Domain.Models.GroupedObservations;
|
|
using MongoDB.Bson;
|
|
using System.Collections.Concurrent;
|
|
|
|
namespace adas_core.Application.Services.Caching
|
|
{
|
|
/// <summary>
|
|
/// Implementación de caché en memoria con soporte de locking seguro
|
|
/// mediante LockManagerService + InMemoryLockProvider.
|
|
/// Compatible con la interfaz ICacheService incluyendo GetOrSet.
|
|
/// </summary>
|
|
public class CacheService(LockManagerService lockManager) : ICacheService
|
|
{
|
|
private readonly ConcurrentDictionary<string, object> _mem = new();
|
|
|
|
// HELPERS
|
|
/// <summary>
|
|
/// Builds a composite key for a grouped observation field, scoped to a specific patient.
|
|
/// </summary>
|
|
/// <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>
|
|
private static string BuildGroupedKey(GroupedField gf, ObjectId patientId)
|
|
=> $"GroupedObs:{patientId}:{gf.Name}";
|
|
|
|
|
|
// GET OR SET (string key)
|
|
/// <summary>
|
|
/// Retrieves an object from the in-memory cache by key, or creates and stores it using the provided factory if absent. Uses a fast path for cache hits and a lock-based path with a double-check to prevent duplicate creation across concurrent callers.
|
|
/// </summary>
|
|
/// <param name="key">The cache key used to look up and store the object.</param>
|
|
/// <param name="factory">The asynchronous factory function invoked to produce the object when it is not found in the cache.</param>
|
|
/// <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>
|
|
public async Task<T> GetOrSetObjectAsync<T>(
|
|
string key,
|
|
Func<Task<T>> factory,
|
|
TimeSpan? ttl = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
// FAST PATH
|
|
if (_mem.TryGetValue(key, out var existing))
|
|
return (T)existing;
|
|
|
|
// LOCKED PATH
|
|
return await lockManager.WithLockAsync(
|
|
$"getorset:{key}",
|
|
TimeSpan.FromSeconds(5),
|
|
async () =>
|
|
{
|
|
if (_mem.TryGetValue(key, out var again))
|
|
return (T)again;
|
|
|
|
var created = await factory();
|
|
|
|
if (created != null!)
|
|
_mem[key] = created;
|
|
|
|
return created!;
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asynchronously retrieves the cached string value associated with the specified key, or loads and stores it using the provided loader function if absent.
|
|
/// Delegates to <see cref="GetOrSetObjectAsync"/> to handle caching, honoring the optional TTL override for the cache entry.
|
|
/// </summary>
|
|
/// <param name="key">The cache key used to identify the stored string value.</param>
|
|
/// <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>
|
|
public async Task<string?> GetOrSetValueAsync(
|
|
string key,
|
|
Func<Task<string>> loader,
|
|
TimeSpan? ttlOverride = null)
|
|
{
|
|
|
|
var result = await GetOrSetObjectAsync(key, loader, ttlOverride);
|
|
return (string?)result;
|
|
|
|
}
|
|
|
|
|
|
// GET OR SET (GroupedField + patientId)
|
|
/// <summary>
|
|
/// Asynchronously retrieves a cached object associated with the given grouped field and patient, or creates and caches a new one using the supplied factory when no cached value exists.
|
|
/// The factory is only invoked when the cache does not contain a value for the key, and the produced value is stored in the cache only when it is not null.
|
|
/// </summary>
|
|
/// <param name="groupedField">The grouped field that contributes to the cache key.</param>
|
|
/// <param name="patientId">The patient identifier that contributes to the cache key.</param>
|
|
/// <param name="factory">The asynchronous factory used to build the object when no cached value is available.</param>
|
|
/// <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>
|
|
public async Task<T> GetOrSetObjectAsync<T>(
|
|
GroupedField groupedField,
|
|
ObjectId patientId,
|
|
Func<Task<T>> factory,
|
|
TimeSpan? ttl = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var key = BuildGroupedKey(groupedField, patientId);
|
|
|
|
if (_mem.TryGetValue(key, out var existing))
|
|
return (T)existing;
|
|
|
|
return await lockManager.WithLockAsync(
|
|
$"getorset:{key}",
|
|
TimeSpan.FromSeconds(5),
|
|
async () =>
|
|
{
|
|
if (_mem.TryGetValue(key, out var again))
|
|
return (T)again;
|
|
|
|
var created = await factory();
|
|
|
|
if (created != null!)
|
|
_mem[key] = created;
|
|
|
|
return created!;
|
|
},
|
|
cancellationToken);
|
|
}
|
|
|
|
|
|
// GET / SET
|
|
/// <summary>
|
|
/// Stores the specified value in the in-memory collection under the given key, overwriting any existing entry.
|
|
/// </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>
|
|
public void SetValue(string key, string value)
|
|
=> _mem[key] = value;
|
|
|
|
/// <summary>
|
|
/// Retrieves the string representation of the value associated with the specified key from the in-memory store.
|
|
/// Returns the value converted via <see cref="object.ToString"/> when the key is found, or <c>null</c> when the key is not present.
|
|
/// </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>
|
|
public string? GetValue(string key)
|
|
=> _mem.TryGetValue(key, out var v) ? v.ToString() : null;
|
|
|
|
/// <summary>
|
|
/// Asynchronously retrieves an object of type <typeparamref name="T"/> from the in-memory cache using the specified key.
|
|
/// Returns the stored value cast to <typeparamref name="T"/> if the key exists, or <c>default</c> (null for reference types) if the key is not found.
|
|
/// </summary>
|
|
/// <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>
|
|
public Task<T?> GetObjectAsync<T>(string key, bool updateExpiration = true)
|
|
{
|
|
return Task.FromResult(
|
|
_mem.TryGetValue(key, out var v) ? (T?)v : default
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asynchronously stores the specified object in the in-memory cache using the given key.
|
|
/// </summary>
|
|
/// <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>
|
|
public Task SetObjectAsync<T>(string key, T obj, bool updateExpiration = true)
|
|
{
|
|
_mem[key] = obj!;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves an object of type <typeparamref name="T"/> associated with the specified key by delegating to an overload that supports an update flag. The <paramref name="ttlOverride"/> parameter is accepted by this overload but is not forwarded to the underlying call.
|
|
/// </summary>
|
|
/// <param name="key">The identifier of the object to retrieve.</param>
|
|
/// <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>
|
|
public Task<T?> GetObjectAsync<T>(string key, TimeSpan? ttlOverride, bool upd)
|
|
=> GetObjectAsync<T>(key, upd);
|
|
|
|
/// <summary>
|
|
/// Asynchronously stores an object associated with the specified key, with an option to override its time-to-live. The <paramref name="ttlOverride"/> parameter is accepted but is not forwarded to the underlying storage call, so the effective time-to-live is determined elsewhere.
|
|
/// </summary>
|
|
/// <param name="key">The identifier used to store and later retrieve the object.</param>
|
|
/// <param name="obj">The object to store in the underlying store.</param>
|
|
/// <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>
|
|
public Task SetObjectAsync<T>(string key, T obj, TimeSpan? ttlOverride, bool upd)
|
|
=> SetObjectAsync(key, obj, upd);
|
|
|
|
|
|
// DELETE / CLEAN
|
|
/// <summary>
|
|
/// 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>
|
|
public Task DeleteObjectAsync(string key)
|
|
{
|
|
_mem.TryRemove(key, out _);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asynchronously deletes cache entries whose keys contain the specified pattern, where asterisk (*) characters in the pattern are treated as wildcards (stripped and matched as substrings). Returns the number of entries successfully removed.
|
|
/// </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>
|
|
public Task<long> DeleteByPatternAsync(string pattern)
|
|
{
|
|
var p = pattern.Replace("*", "");
|
|
var keys = _mem.Keys.Where(k => k.Contains(p)).ToList();
|
|
|
|
long removed = 0;
|
|
foreach (var k in keys)
|
|
if (_mem.TryRemove(k, out _))
|
|
removed++;
|
|
|
|
return Task.FromResult(removed);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all entries from the in-memory cache, removing any previously stored data.
|
|
/// </summary>
|
|
public void CleanCache() => _mem.Clear();
|
|
}
|
|
} |