365 lines
14 KiB
C#
365 lines
14 KiB
C#
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);
|
|
}
|
|
}
|
|
} |