Files

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);
}
}
}