Creado apartir del commit b888de6c92056de294456f581a56e25e734d4ab7 de develop
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using MongoDB.Bson;
|
||||
using adas_core.Domain.Enums;
|
||||
using adas_core.Domain.Models.AppSettings;
|
||||
|
||||
namespace adas_core.Domain.Utils
|
||||
{
|
||||
|
||||
// TTL RESOLVER POR ENTIDAD / CLAVE
|
||||
/// <summary>
|
||||
/// Resuelve TTLs a partir de CacheSettings teniendo en cuenta:
|
||||
/// - El backend seleccionado por entidad (None | Cache | Redis)
|
||||
/// - El TTL de la entidad para ese backend
|
||||
/// - Caída a GlobalSeconds del backend si no hay TTL específico
|
||||
/// - Si el TTL resultante es null o menor o igual a 0 sin expiración
|
||||
/// </summary>
|
||||
public static class CacheKeyTtl
|
||||
{
|
||||
private static TimeSpan? SecondsOrNull(int? seconds)
|
||||
=> seconds is > 0
|
||||
? TimeSpan.FromSeconds(seconds.Value)
|
||||
: null;
|
||||
|
||||
private static int? FromInMemory(Func<TtlSettings, int?> selector, CacheSettings s)
|
||||
=> selector(s.InMemory.Ttl);
|
||||
|
||||
private static int? FromRedis(Func<TtlSettings, int?>? selector, CacheSettings? s)
|
||||
=> selector?.Invoke(s?.Redis.Ttl ?? new TtlSettings());
|
||||
|
||||
private static TimeSpan? GlobalInMemory(CacheSettings? s)
|
||||
=> SecondsOrNull(s?.InMemory.Ttl.GlobalSeconds);
|
||||
|
||||
private static TimeSpan? GlobalRedis(CacheSettings? s)
|
||||
=> SecondsOrNull(s?.Redis.Ttl.GlobalSeconds);
|
||||
|
||||
/// <summary>
|
||||
/// Resuelve TTL para una entidad concreta, respetando el backend configurado para dicha entidad.
|
||||
/// </summary>
|
||||
public static TimeSpan? ResolveForEntity(CacheSettings? settings, CacheEnum.EntityType entity)
|
||||
{
|
||||
// Selección del backend según la entidad
|
||||
if (settings == null) return null;
|
||||
|
||||
var mode = entity switch
|
||||
{
|
||||
CacheEnum.EntityType.Patients => settings.Patients,
|
||||
CacheEnum.EntityType.Displays => settings.Displays,
|
||||
CacheEnum.EntityType.PumpObservations => settings.PumpObservations,
|
||||
CacheEnum.EntityType.Appointments => settings.Appointments,
|
||||
CacheEnum.EntityType.PontOfCare => settings.PointOfCares,
|
||||
CacheEnum.EntityType.GroupedObservations => settings.GroupedObservations,
|
||||
CacheEnum.EntityType.PatientObservations => settings.PatientObservations,
|
||||
_ => CacheEnum.Mode.Cache
|
||||
};
|
||||
|
||||
if (!settings.IsEnabled || mode == CacheEnum.Mode.None)
|
||||
return null;
|
||||
|
||||
// Selector de TTL específico por entidad
|
||||
Func<TtlSettings, int?> selector = entity switch
|
||||
{
|
||||
CacheEnum.EntityType.Patients => x => x.PatientsSeconds,
|
||||
CacheEnum.EntityType.Displays => x => x.DisplaysSeconds,
|
||||
CacheEnum.EntityType.PumpObservations => x => x.PumpObservationsSeconds,
|
||||
CacheEnum.EntityType.Appointments => x => x.AppointmentsSeconds,
|
||||
CacheEnum.EntityType.PontOfCare => x => x.PointOfCaresSeconds,
|
||||
CacheEnum.EntityType.GroupedObservations => x => x.GroupedObservationsSeconds,
|
||||
CacheEnum.EntityType.PatientObservations => x => x.PatientObservationsSeconds,
|
||||
CacheEnum.EntityType.ConfigObservations => x => x.ConfigObservationsSeconds,
|
||||
_ => x => x.GlobalSeconds
|
||||
};
|
||||
|
||||
// Resolver en función del backend elegido para la entidad
|
||||
return mode switch
|
||||
{
|
||||
CacheEnum.Mode.Cache => SecondsOrNull(FromInMemory(selector, settings))
|
||||
?? GlobalInMemory(settings),
|
||||
|
||||
CacheEnum.Mode.Redis => SecondsOrNull(FromRedis(selector, settings))
|
||||
?? GlobalRedis(settings),
|
||||
|
||||
_ => null
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dada una clave, clasifica la entidad y resuelve el TTL para esa clave.
|
||||
/// </summary>
|
||||
public static TimeSpan? ResolveForKey(CacheSettings settings, string key)
|
||||
{
|
||||
var entity = CacheKeyClassifier.Classify(key);
|
||||
return ResolveForEntity(settings, entity);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// CACHE KEYS (GENERATION)
|
||||
/// <summary>
|
||||
/// Generador centralizado de claves de caché.
|
||||
/// - Prefijos normalizados para que el CacheDispatcher clasifique el backend.
|
||||
/// - Overloads "KeyWithTtl" para devolver (key, ttl) en una llamada.
|
||||
/// </summary>
|
||||
public static class CacheKeys
|
||||
{
|
||||
#region ConfigObservations
|
||||
|
||||
public static string ConfigObservationsAll()
|
||||
=> "configObservations:all";
|
||||
|
||||
public static (string Key, TimeSpan? Ttl) ConfigObservationsAllKeyWithTtl(
|
||||
CacheSettings? settings)
|
||||
{
|
||||
// lo cual usará CacheEnum.EntityType.ConfigObservations (debes añadirlo)
|
||||
var key = ConfigObservationsAll();
|
||||
var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.ConfigObservations);
|
||||
return (key, ttl);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region DisplayConfig
|
||||
public static string DisplayBase(ObjectId displayId)
|
||||
=> $"configDisplays:display:{displayId}:base";
|
||||
|
||||
public static (string Key, TimeSpan? Ttl) DisplayBaseKeyWithTtl(
|
||||
CacheSettings? settings,
|
||||
ObjectId displayId)
|
||||
{
|
||||
var key = DisplayBase(displayId);
|
||||
var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Displays);
|
||||
return (key, ttl);
|
||||
}
|
||||
|
||||
public static string DisplayWithConfig(ObjectId displayId)
|
||||
=> $"configDisplays:display:{displayId}:config";
|
||||
|
||||
public static (string Key, TimeSpan? Ttl) DisplayWithConfigKeyWithTtl(
|
||||
CacheSettings? settings,
|
||||
ObjectId displayId)
|
||||
{
|
||||
var key = DisplayWithConfig(displayId);
|
||||
var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Displays);
|
||||
return (key, ttl);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PointOfCare
|
||||
public static string PointOfCareBase(ObjectId pocId)
|
||||
=> $"pointOfCare:{pocId}:base";
|
||||
|
||||
public static (string Key, TimeSpan? Ttl) PointOfCareBaseKeyWithTtl(
|
||||
CacheSettings? settings, ObjectId pocId)
|
||||
{
|
||||
var key = PointOfCareBase(pocId);
|
||||
var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PontOfCare);
|
||||
return (key, ttl);
|
||||
}
|
||||
|
||||
public static string PointOfCareWithInfoKeyWithTtl(ObjectId pocId)
|
||||
=> $"pointOfCare:{pocId}:withInfo";
|
||||
|
||||
public static (string Key, TimeSpan? Ttl) PointOfCareWithInfoKeyWithTtl(
|
||||
CacheSettings? settings, ObjectId pocId)
|
||||
{
|
||||
var key = PointOfCareWithInfoKeyWithTtl(pocId);
|
||||
var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PontOfCare);
|
||||
return (key, ttl);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Observations
|
||||
public static string LatestObservations(ObjectId patientId, IEnumerable<string> fieldNames, int? last = null)
|
||||
=> $"patients:latestObs:{patientId}:{Normalize(fieldNames)}:{last ?? 0}";
|
||||
|
||||
public static (string Key, TimeSpan? Ttl) LatestObservationsKeyWithTtl(
|
||||
CacheSettings? settings, ObjectId patientId, IEnumerable<string> fieldNames, int? last = null)
|
||||
{
|
||||
var key = LatestObservations(patientId, fieldNames, last);
|
||||
var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PatientObservations);
|
||||
return (key, ttl);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PumpObservations
|
||||
public static string LatestPumps(ObjectId patientId, int? last = null)
|
||||
=> $"pumpObs:latest:{patientId}:{last ?? 0}";
|
||||
|
||||
public static (string Key, TimeSpan? Ttl) LatestPumpsKeyWithTtl(
|
||||
CacheSettings settings, ObjectId patientId, int? last = null)
|
||||
{
|
||||
var key = LatestPumps(patientId, last);
|
||||
var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PumpObservations);
|
||||
return (key, ttl);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Appointments
|
||||
public static string PatientAppointmentsToday(ObjectId patientId)
|
||||
{
|
||||
var dateKey = DateTime.UtcNow.ToString("yyyyMMdd");
|
||||
return $"appointments:patient:{patientId}:{dateKey}";
|
||||
}
|
||||
|
||||
public static (string Key, TimeSpan? Ttl) PatientAppointmentsTodayKeyWithTtl(
|
||||
CacheSettings? settings,
|
||||
ObjectId patientId)
|
||||
{
|
||||
var key = PatientAppointmentsToday(patientId);
|
||||
var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Appointments);
|
||||
return (key, ttl);
|
||||
}
|
||||
|
||||
public static string PocAppointmentsToday(ObjectId pocId)
|
||||
{
|
||||
var dateKey = DateTime.UtcNow.ToString("yyyyMMdd");
|
||||
return $"appointments:PoC:{pocId}:{dateKey}";
|
||||
}
|
||||
|
||||
public static (string Key, TimeSpan? Ttl) PocAppointmentsTodayKeyWithTtl(
|
||||
CacheSettings? settings,
|
||||
ObjectId pocId)
|
||||
{
|
||||
var key = PocAppointmentsToday(pocId);
|
||||
var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PontOfCare);
|
||||
return (key, ttl);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Grouped Observations (claves: "groupedObs:{field}:patient:{id}")
|
||||
public static string GroupedObs(ObjectId patientId, string groupedFieldName)
|
||||
=> $"groupedObs:{groupedFieldName}:patient:{patientId}";
|
||||
|
||||
public static (string Key, TimeSpan? Ttl) GroupedObsKeyWithTtl(
|
||||
CacheSettings? settings, ObjectId patientId, string groupedFieldName)
|
||||
{
|
||||
var key = GroupedObs(patientId, groupedFieldName);
|
||||
var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.GroupedObservations);
|
||||
return (key, ttl);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
//Helpers
|
||||
public static string Normalize(IEnumerable<string> items)
|
||||
=> string.Join("|", items
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s))
|
||||
.Select(s => s.Trim())
|
||||
.OrderBy(s => s, StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
|
||||
// CLASSIFIER
|
||||
/// <summary>
|
||||
/// Clasificador de claves basado en prefijos.
|
||||
/// Permite al CacheDispatcher seleccionar el backend adecuado (Redis, InMemory, None).
|
||||
/// </summary>
|
||||
public static class CacheKeyClassifier
|
||||
{
|
||||
public static CacheEnum.EntityType Classify(string key)
|
||||
{
|
||||
if (key.StartsWith("patients:", StringComparison.OrdinalIgnoreCase))
|
||||
return CacheEnum.EntityType.Patients;
|
||||
|
||||
if (key.StartsWith("displays:", StringComparison.OrdinalIgnoreCase) ||
|
||||
key.StartsWith("configDisplays:", StringComparison.OrdinalIgnoreCase))
|
||||
return CacheEnum.EntityType.Displays;
|
||||
|
||||
if (key.StartsWith("pumpObs:", StringComparison.OrdinalIgnoreCase))
|
||||
return CacheEnum.EntityType.PumpObservations;
|
||||
|
||||
if (key.StartsWith("appointments:", StringComparison.OrdinalIgnoreCase))
|
||||
return CacheEnum.EntityType.Appointments;
|
||||
|
||||
if (key.StartsWith("groupedObs:", StringComparison.OrdinalIgnoreCase))
|
||||
return CacheEnum.EntityType.GroupedObservations;
|
||||
|
||||
if (key.StartsWith("configObservations:", StringComparison.OrdinalIgnoreCase))
|
||||
return CacheEnum.EntityType.ConfigObservations;
|
||||
|
||||
return CacheEnum.EntityType.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
// PATTERNS
|
||||
/// <summary>
|
||||
/// Patrones para DeleteByPattern (Redis) u operaciones masivas por prefijo.
|
||||
/// </summary>
|
||||
public static class CacheKeyPatterns
|
||||
{
|
||||
public static string ForEntity(CacheEnum.EntityType type)
|
||||
=> type switch
|
||||
{
|
||||
CacheEnum.EntityType.Patients => "patients:*",
|
||||
CacheEnum.EntityType.Displays => "displays:*",
|
||||
CacheEnum.EntityType.PumpObservations => "pumpObs:*",
|
||||
CacheEnum.EntityType.Appointments => "appointments:*",
|
||||
CacheEnum.EntityType.GroupedObservations => "groupedObs:*",
|
||||
CacheEnum.EntityType.ConfigObservations => "configObservation:*",
|
||||
_ => "*"
|
||||
};
|
||||
|
||||
public static string ByPrefix(string prefix)
|
||||
=> $"{prefix}:*";
|
||||
|
||||
public static string ByPatient(string prefix, string patientId)
|
||||
=> $"{prefix}:*:{patientId}*";
|
||||
|
||||
public static string ByDate(string prefix, string date)
|
||||
=> $"{prefix}:*:{date}";
|
||||
}
|
||||
|
||||
// INSPECTOR (diagnóstico)
|
||||
/// <summary>
|
||||
/// Utilidad para extraer información estructurada útil para trazas.
|
||||
/// No usada por la lógica de caché, pero sí por logs y diagnósticos.
|
||||
/// </summary>
|
||||
public static class CacheKeyInspector
|
||||
{
|
||||
private static readonly Regex _patientRegex =
|
||||
new(@"patients:(?<id>[^:]+)", RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex _groupedRegex =
|
||||
new(@"groupedObs:(?<field>[^:]+):(?<id>[^:]+)", RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex _appointmentDayRegex =
|
||||
new(@"appointments:(?<location>.+):(?<date>\d{8})", RegexOptions.Compiled);
|
||||
|
||||
public static string? ExtractPatientId(string key)
|
||||
{
|
||||
var match = _patientRegex.Match(key);
|
||||
return match.Success ? match.Groups["id"].Value : null;
|
||||
}
|
||||
|
||||
public static (string? Field, string? PatientId) ExtractGroupedObservationInfo(string key)
|
||||
{
|
||||
var match = _groupedRegex.Match(key);
|
||||
return !match.Success
|
||||
? (null, null)
|
||||
: (match.Groups["field"].Value, match.Groups["id"].Value);
|
||||
}
|
||||
|
||||
public static (string? Location, DateTime? Date) ExtractAppointmentInfo(string key)
|
||||
{
|
||||
var match = _appointmentDayRegex.Match(key);
|
||||
if (!match.Success)
|
||||
return (null, null);
|
||||
|
||||
var location = match.Groups["location"].Value;
|
||||
var dateStr = match.Groups["date"].Value;
|
||||
|
||||
if (DateTime.TryParseExact(
|
||||
dateStr, "yyyyMMdd", null,
|
||||
System.Globalization.DateTimeStyles.None,
|
||||
out var date))
|
||||
return (location, date);
|
||||
|
||||
return (location, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user