=== Summary: 675 files | 7 generated | 484 fresh | 4605 untracked | 4268 adopted | 355 marked | 466 validated-ok | 2+0 stale (sig+body) | 0 skipped | 0 failed | elapsed 11:08:11.442 (40091.44s) ===
This commit is contained in:
@@ -14,6 +14,7 @@ namespace adas_core.Domain.Utils
|
||||
/// - 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>
|
||||
/// <!-- aidoc:v1 sig=d7c4909 -->
|
||||
public static class CacheKeyTtl
|
||||
{
|
||||
/// <summary>
|
||||
@@ -21,6 +22,7 @@ namespace adas_core.Domain.Utils
|
||||
/// </summary>
|
||||
/// <param name="seconds">The number of seconds to convert.</param>
|
||||
/// <returns>A <see cref="TimeSpan"/> representing <paramref name="seconds"/>, or <c>null</c> when <paramref name="seconds"/> is <c>null</c> or less than or equal to zero.</returns>
|
||||
/// <!-- aidoc:v1 sig=967178f -->
|
||||
private static TimeSpan? SecondsOrNull(int? seconds)
|
||||
=> seconds is > 0
|
||||
? TimeSpan.FromSeconds(seconds.Value)
|
||||
@@ -32,6 +34,7 @@ namespace adas_core.Domain.Utils
|
||||
/// <param name="selector">A function that selects the desired <see cref="int?"/> value from a <see cref="TtlSettings"/> instance.</param>
|
||||
/// <param name="s">The cache settings whose in-memory TTL configuration is used as the source.</param>
|
||||
/// <returns>The nullable integer value produced by applying <paramref name="selector"/> to the in-memory <see cref="TtlSettings"/>.</returns>
|
||||
/// <!-- aidoc:v1 sig=a263f69 -->
|
||||
private static int? FromInMemory(Func<TtlSettings, int?> selector, CacheSettings s)
|
||||
=> selector(s.InMemory.Ttl);
|
||||
|
||||
@@ -42,6 +45,7 @@ namespace adas_core.Domain.Utils
|
||||
/// <param name="selector">A function that derives a nullable integer from a <see cref="TtlSettings"/> instance, or null to short-circuit the operation.</param>
|
||||
/// <param name="s">The cache settings whose Redis TTL configuration will be inspected, or null to use a default <see cref="TtlSettings"/>.</param>
|
||||
/// <returns>The nullable integer produced by invoking the selector on the resolved <see cref="TtlSettings"/>, or null if the selector is null.</returns>
|
||||
/// <!-- aidoc:v1 sig=219e390 -->
|
||||
private static int? FromRedis(Func<TtlSettings, int?>? selector, CacheSettings? s)
|
||||
=> selector?.Invoke(s?.Redis.Ttl ?? new TtlSettings());
|
||||
|
||||
@@ -50,6 +54,7 @@ namespace adas_core.Domain.Utils
|
||||
/// </summary>
|
||||
/// <param name="s">The cache settings containing the in-memory TTL configuration, or <c>null</c>.</param>
|
||||
/// <returns>A <see cref="TimeSpan"/> representing the global in-memory TTL converted from seconds, or <c>null</c> if <paramref name="s"/> is <c>null</c> or the global seconds value is not set.</returns>
|
||||
/// <!-- aidoc:v1 sig=08c6cf7 -->
|
||||
private static TimeSpan? GlobalInMemory(CacheSettings? s)
|
||||
=> SecondsOrNull(s?.InMemory.Ttl.GlobalSeconds);
|
||||
|
||||
@@ -58,12 +63,19 @@ namespace adas_core.Domain.Utils
|
||||
/// </summary>
|
||||
/// <param name="s">The cache settings containing the Redis TTL configuration, or <c>null</c>.</param>
|
||||
/// <returns>A <see cref="TimeSpan"/> representing the global Redis TTL, or <c>null</c> if the configuration is unavailable.</returns>
|
||||
/// <!-- aidoc:v1 sig=3ee947c -->
|
||||
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>
|
||||
/// <!-- aidoc-review:v1 severity=medium kind=missing_param
|
||||
/// "Parameter 'settings' (CacheSettings?) is not documented." -->
|
||||
/// <!-- aidoc-review:v1 severity=medium kind=missing_param
|
||||
/// "Parameter 'entity' (CacheEnum.EntityType) is not documented." -->
|
||||
/// <!-- aidoc-review:v1 severity=medium kind=missing_returns
|
||||
/// "Return value (TimeSpan?) is not documented." -->
|
||||
public static TimeSpan? ResolveForEntity(CacheSettings? settings, CacheEnum.EntityType entity)
|
||||
{
|
||||
// Selección del backend según la entidad
|
||||
@@ -115,6 +127,7 @@ namespace adas_core.Domain.Utils
|
||||
/// <summary>
|
||||
/// Dada una clave, clasifica la entidad y resuelve el TTL para esa clave.
|
||||
/// </summary>
|
||||
/// <!-- aidoc:v1 sig=e7e1d98 body=1fb058f -->
|
||||
public static TimeSpan? ResolveForKey(CacheSettings settings, string key)
|
||||
{
|
||||
var entity = CacheKeyClassifier.Classify(key);
|
||||
@@ -129,6 +142,7 @@ namespace adas_core.Domain.Utils
|
||||
/// - Prefijos normalizados para que el CacheDispatcher clasifique el backend.
|
||||
/// - Overloads "KeyWithTtl" para devolver (key, ttl) en una llamada.
|
||||
/// </summary>
|
||||
/// <!-- aidoc:v1 sig=6c8a7c2 -->
|
||||
public static class CacheKeys
|
||||
{
|
||||
#region ConfigObservations
|
||||
@@ -137,6 +151,7 @@ namespace adas_core.Domain.Utils
|
||||
/// Returns the configuration key string "configObservations:all", typically used to identify a setting or cache entry related to all configuration observations.
|
||||
/// </summary>
|
||||
/// <returns>The string "configObservations:all".</returns>
|
||||
/// <!-- aidoc:v1 sig=588468f -->
|
||||
public static string ConfigObservationsAll()
|
||||
=> "configObservations:all";
|
||||
|
||||
@@ -163,6 +178,7 @@ namespace adas_core.Domain.Utils
|
||||
/// </summary>
|
||||
/// <param name="displayId">The identifier of the display whose base key is being generated.</param>
|
||||
/// <returns>A formatted key string combining the display section, the display identifier, and the "base" suffix.</returns>
|
||||
/// <!-- aidoc:v1 sig=c78d13f -->
|
||||
public static string DisplayBase(ObjectId displayId)
|
||||
=> $"configDisplays:display:{displayId}:base";
|
||||
|
||||
@@ -172,6 +188,7 @@ namespace adas_core.Domain.Utils
|
||||
/// <param name="settings">Optional cache settings used to resolve the TTL for display entities. When null, a default TTL is applied.</param>
|
||||
/// <param name="displayId">The identifier of the display used to compose the base cache key.</param>
|
||||
/// <returns>A tuple containing the generated cache key and the resolved TTL for the display entity.</returns>
|
||||
/// <!-- aidoc:v1 sig=506cbc3 body=cd54390 -->
|
||||
public static (string Key, TimeSpan? Ttl) DisplayBaseKeyWithTtl(
|
||||
CacheSettings? settings,
|
||||
ObjectId displayId)
|
||||
@@ -186,6 +203,7 @@ namespace adas_core.Domain.Utils
|
||||
/// </summary>
|
||||
/// <param name="displayId">The identifier of the display whose configuration key is being generated.</param>
|
||||
/// <returns>A formatted string key in the form "configDisplays:display:{displayId}:config" representing the display's configuration entry.</returns>
|
||||
/// <!-- aidoc:v1 sig=034d7aa -->
|
||||
public static string DisplayWithConfig(ObjectId displayId)
|
||||
=> $"configDisplays:display:{displayId}:config";
|
||||
|
||||
@@ -195,6 +213,7 @@ namespace adas_core.Domain.Utils
|
||||
/// <param name="settings">The cache settings used to resolve the TTL for the display entity; may be <c>null</c>.</param>
|
||||
/// <param name="displayId">The identifier of the display entity for which the cache key is generated.</param>
|
||||
/// <returns>A tuple containing the generated cache key and the resolved TTL, which may be <c>null</c> if no TTL is configured.</returns>
|
||||
/// <!-- aidoc:v1 sig=1334a3e body=0bbecb4 -->
|
||||
public static (string Key, TimeSpan? Ttl) DisplayWithConfigKeyWithTtl(
|
||||
CacheSettings? settings,
|
||||
ObjectId displayId)
|
||||
@@ -212,6 +231,7 @@ namespace adas_core.Domain.Utils
|
||||
/// </summary>
|
||||
/// <param name="pocId">The unique identifier of the Point of Care entity whose base reference key is being generated.</param>
|
||||
/// <returns>A formatted string that combines the "pointOfCare" prefix, the provided <paramref name="pocId"/>, and the "base" suffix to uniquely identify the base resource of the Point of Care.</returns>
|
||||
/// <!-- aidoc:v1 sig=76872a0 -->
|
||||
public static string PointOfCareBase(ObjectId pocId)
|
||||
=> $"pointOfCare:{pocId}:base";
|
||||
|
||||
@@ -221,6 +241,7 @@ namespace adas_core.Domain.Utils
|
||||
/// <param name="settings">The cache settings used by the TTL resolver. May be null, in which case the resolver falls back to its default behavior.</param>
|
||||
/// <param name="pocId">The identifier of the Point of Care entity used to generate the cache key.</param>
|
||||
/// <returns>A tuple containing the generated cache key and the resolved TTL, which may be null when no expiration is configured.</returns>
|
||||
/// <!-- aidoc:v1 sig=b4d58ed body=d0c1f0e -->
|
||||
public static (string Key, TimeSpan? Ttl) PointOfCareBaseKeyWithTtl(
|
||||
CacheSettings? settings, ObjectId pocId)
|
||||
{
|
||||
@@ -234,6 +255,7 @@ namespace adas_core.Domain.Utils
|
||||
/// </summary>
|
||||
/// <param name="pocId">The identifier of the point of care used to compose the cache key.</param>
|
||||
/// <returns>A formatted cache key string in the form <c>pointOfCare:{pocId}:withInfo</c>.</returns>
|
||||
/// <!-- aidoc:v1 sig=373f954 -->
|
||||
public static string PointOfCareWithInfoKeyWithTtl(ObjectId pocId)
|
||||
=> $"pointOfCare:{pocId}:withInfo";
|
||||
|
||||
@@ -243,6 +265,7 @@ namespace adas_core.Domain.Utils
|
||||
/// <param name="settings">Optional cache settings used to resolve the TTL for the point of care entity. May be <c>null</c>.</param>
|
||||
/// <param name="pocId">The identifier of the point of care used to build the cache key.</param>
|
||||
/// <returns>A tuple containing the generated cache key and the resolved TTL, which may be <c>null</c> when no TTL is configured.</returns>
|
||||
/// <!-- aidoc:v1 sig=e93cb06 body=e1c096f -->
|
||||
public static (string Key, TimeSpan? Ttl) PointOfCareWithInfoKeyWithTtl(
|
||||
CacheSettings? settings, ObjectId pocId)
|
||||
{
|
||||
@@ -261,6 +284,7 @@ namespace adas_core.Domain.Utils
|
||||
/// <param name="fieldNames">The collection of field names to include in the key, normalized via <c>Normalize</c>.</param>
|
||||
/// <param name="last">Optional maximum number of recent observations to consider; when <c>null</c>, defaults to <c>0</c>.</param>
|
||||
/// <returns>A formatted cache key string combining the patient id, normalized field names, and the last value.</returns>
|
||||
/// <!-- aidoc:v1 sig=6571377 -->
|
||||
public static string LatestObservations(ObjectId patientId, IEnumerable<string> fieldNames, int? last = null)
|
||||
=> $"patients:latestObs:{patientId}:{Normalize(fieldNames)}:{last ?? 0}";
|
||||
|
||||
@@ -273,6 +297,7 @@ namespace adas_core.Domain.Utils
|
||||
/// <param name="fieldNames">The collection of field names to include in the latest observations cache key.</param>
|
||||
/// <param name="last">The optional number of most recent observations to consider when building the cache key.</param>
|
||||
/// <returns>A tuple containing the generated cache key and the resolved TTL for the cache entry.</returns>
|
||||
/// <!-- aidoc:v1 sig=c42ebf0 body=17e9521 -->
|
||||
public static (string Key, TimeSpan? Ttl) LatestObservationsKeyWithTtl(
|
||||
CacheSettings? settings, ObjectId patientId, IEnumerable<string> fieldNames, int? last = null)
|
||||
{
|
||||
@@ -290,6 +315,7 @@ namespace adas_core.Domain.Utils
|
||||
/// <param name="patientId">The unique identifier of the patient whose latest pump observations are being requested.</param>
|
||||
/// <param name="last">The optional number of most recent pump observation entries to include; when null, zero is used as a fallback.</param>
|
||||
/// <returns>A formatted cache key string of the form <c>pumpObs:latest:{patientId}:{last}</c>, with <c>last</c> resolved to <c>0</c> when null.</returns>
|
||||
/// <!-- aidoc:v1 sig=b2d398b -->
|
||||
public static string LatestPumps(ObjectId patientId, int? last = null)
|
||||
=> $"pumpObs:latest:{patientId}:{last ?? 0}";
|
||||
|
||||
@@ -300,6 +326,7 @@ namespace adas_core.Domain.Utils
|
||||
/// <param name="patientId">The identifier of the patient whose latest pump observations are being cached.</param>
|
||||
/// <param name="last">The optional maximum number of latest pump entries to include in the cache key.</param>
|
||||
/// <returns>A tuple containing the generated cache key and the resolved TTL, which may be <see langword="null"/> if no TTL is configured.</returns>
|
||||
/// <!-- aidoc:v1 sig=480ebe1 body=28bba9f -->
|
||||
public static (string Key, TimeSpan? Ttl) LatestPumpsKeyWithTtl(
|
||||
CacheSettings settings, ObjectId patientId, int? last = null)
|
||||
{
|
||||
@@ -316,6 +343,7 @@ namespace adas_core.Domain.Utils
|
||||
/// </summary>
|
||||
/// <param name="patientId">The unique identifier of the patient whose appointments are being keyed.</param>
|
||||
/// <returns>A formatted cache key string combining the patient identifier and the current UTC date.</returns>
|
||||
/// <!-- aidoc:v1 sig=5ef1337 body=ac8edc0 -->
|
||||
public static string PatientAppointmentsToday(ObjectId patientId)
|
||||
{
|
||||
var dateKey = DateTime.UtcNow.ToString("yyyyMMdd");
|
||||
@@ -328,6 +356,7 @@ namespace adas_core.Domain.Utils
|
||||
/// <param name="settings">Optional cache settings used to resolve the TTL for the appointments entity.</param>
|
||||
/// <param name="patientId">The identifier of the patient whose appointments cache key is being generated.</param>
|
||||
/// <returns>A tuple containing the generated cache key and the resolved TTL, which may be null when no TTL is configured.</returns>
|
||||
/// <!-- aidoc:v1 sig=ff8616a body=e708272 -->
|
||||
public static (string Key, TimeSpan? Ttl) PatientAppointmentsTodayKeyWithTtl(
|
||||
CacheSettings? settings,
|
||||
ObjectId patientId)
|
||||
@@ -342,6 +371,7 @@ namespace adas_core.Domain.Utils
|
||||
/// </summary>
|
||||
/// <param name="pocId">The identifier of the Point of Contact whose appointments are being keyed.</param>
|
||||
/// <returns>A formatted cache key string combining the PoC identifier and today's UTC date in <c>yyyyMMdd</c> format.</returns>
|
||||
/// <!-- aidoc:v1 sig=86e2e65 body=2c278cf -->
|
||||
public static string PocAppointmentsToday(ObjectId pocId)
|
||||
{
|
||||
var dateKey = DateTime.UtcNow.ToString("yyyyMMdd");
|
||||
@@ -354,6 +384,7 @@ namespace adas_core.Domain.Utils
|
||||
/// <param name="settings">The cache settings used to resolve the TTL; may be <c>null</c> when no settings are supplied.</param>
|
||||
/// <param name="pocId">The identifier of the Point of Care whose today's appointments cache key is being generated.</param>
|
||||
/// <returns>A tuple containing the generated cache key and the resolved TTL as a nullable <see cref="TimeSpan"/>.</returns>
|
||||
/// <!-- aidoc:v1 sig=55982d2 body=03e4a32 -->
|
||||
public static (string Key, TimeSpan? Ttl) PocAppointmentsTodayKeyWithTtl(
|
||||
CacheSettings? settings,
|
||||
ObjectId pocId)
|
||||
@@ -372,6 +403,7 @@ namespace adas_core.Domain.Utils
|
||||
/// <param name="patientId">The unique identifier of the patient whose grouped observations are being addressed.</param>
|
||||
/// <param name="groupedFieldName">The name of the grouped field used to categorize the observations.</param>
|
||||
/// <returns>A formatted string in the pattern <c>groupedObs:{groupedFieldName}:patient:{patientId}</c>.</returns>
|
||||
/// <!-- aidoc:v1 sig=66bcd8e -->
|
||||
public static string GroupedObs(ObjectId patientId, string groupedFieldName)
|
||||
=> $"groupedObs:{groupedFieldName}:patient:{patientId}";
|
||||
|
||||
@@ -382,6 +414,7 @@ namespace adas_core.Domain.Utils
|
||||
/// <param name="patientId">The identifier of the patient whose grouped observations are being cached.</param>
|
||||
/// <param name="groupedFieldName">The name of the grouped field used to compose the cache key.</param>
|
||||
/// <returns>A tuple containing the generated cache key and the resolved TTL, which may be <c>null</c> when no expiry is configured.</returns>
|
||||
/// <!-- aidoc:v1 sig=2c64810 body=ce29e10 -->
|
||||
public static (string Key, TimeSpan? Ttl) GroupedObsKeyWithTtl(
|
||||
CacheSettings? settings, ObjectId patientId, string groupedFieldName)
|
||||
{
|
||||
@@ -398,6 +431,7 @@ namespace adas_core.Domain.Utils
|
||||
/// </summary>
|
||||
/// <param name="items">The collection of strings to normalize.</param>
|
||||
/// <returns>A pipe-delimited string of the cleaned and sorted values, or an empty string if no valid entries are supplied.</returns>
|
||||
/// <!-- aidoc:v1 sig=6dd057a -->
|
||||
public static string Normalize(IEnumerable<string> items)
|
||||
=> string.Join("|", items
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s))
|
||||
@@ -411,6 +445,7 @@ namespace adas_core.Domain.Utils
|
||||
/// Clasificador de claves basado en prefijos.
|
||||
/// Permite al CacheDispatcher seleccionar el backend adecuado (Redis, InMemory, None).
|
||||
/// </summary>
|
||||
/// <!-- aidoc:v1 sig=933c0c4 -->
|
||||
public static class CacheKeyClassifier
|
||||
{
|
||||
/// <summary>
|
||||
@@ -421,6 +456,7 @@ namespace adas_core.Domain.Utils
|
||||
/// </summary>
|
||||
/// <param name="key">The cache key to classify.</param>
|
||||
/// <returns>The <see cref="CacheEnum.EntityType"/> that matches the key's prefix, or <see cref="CacheEnum.EntityType.Unknown"/> when no prefix is recognized.</returns>
|
||||
/// <!-- aidoc:v1 sig=8dce21c body=135a132 -->
|
||||
public static CacheEnum.EntityType Classify(string key)
|
||||
{
|
||||
if (key.StartsWith("patients:", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -450,6 +486,7 @@ namespace adas_core.Domain.Utils
|
||||
/// <summary>
|
||||
/// Patrones para DeleteByPattern (Redis) u operaciones masivas por prefijo.
|
||||
/// </summary>
|
||||
/// <!-- aidoc:v1 sig=6408068 -->
|
||||
public static class CacheKeyPatterns
|
||||
{
|
||||
/// <summary>
|
||||
@@ -457,6 +494,7 @@ namespace adas_core.Domain.Utils
|
||||
/// </summary>
|
||||
/// <param name="type">The entity type for which a cache key pattern is being generated.</param>
|
||||
/// <returns>A string representing the cache key pattern associated with the given entity type, or "*" if the entity type is not recognized.</returns>
|
||||
/// <!-- aidoc:v1 sig=fc7e3c6 -->
|
||||
public static string ForEntity(CacheEnum.EntityType type)
|
||||
=> type switch
|
||||
{
|
||||
@@ -474,6 +512,7 @@ namespace adas_core.Domain.Utils
|
||||
/// </summary>
|
||||
/// <param name="prefix">The key prefix to match against.</param>
|
||||
/// <returns>A formatted string combining <paramref name="prefix"/> and ":*" used as a wildcard pattern.</returns>
|
||||
/// <!-- aidoc:v1 sig=5dfcb40 -->
|
||||
public static string ByPrefix(string prefix)
|
||||
=> $"{prefix}:*";
|
||||
|
||||
@@ -483,6 +522,7 @@ namespace adas_core.Domain.Utils
|
||||
/// <param name="prefix">The category or context prefix prepended to the pattern.</param>
|
||||
/// <param name="patientId">The patient identifier used to scope the wildcard pattern.</param>
|
||||
/// <returns>A formatted pattern string in the form <c>{prefix}:*:{patientId}*</c>.</returns>
|
||||
/// <!-- aidoc:v1 sig=1b8b063 -->
|
||||
public static string ByPatient(string prefix, string patientId)
|
||||
=> $"{prefix}:*:{patientId}*";
|
||||
|
||||
@@ -492,6 +532,7 @@ namespace adas_core.Domain.Utils
|
||||
/// <param name="prefix">The prefix segment to include at the beginning of the returned string.</param>
|
||||
/// <param name="date">The date segment to include at the end of the returned string.</param>
|
||||
/// <returns>A string formatted as <c>{prefix}:*:{date}</c>.</returns>
|
||||
/// <!-- aidoc:v1 sig=5bc6f05 -->
|
||||
public static string ByDate(string prefix, string date)
|
||||
=> $"{prefix}:*:{date}";
|
||||
}
|
||||
@@ -501,6 +542,7 @@ namespace adas_core.Domain.Utils
|
||||
/// 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>
|
||||
/// <!-- aidoc:v1 sig=9ba5b41 -->
|
||||
public static class CacheKeyInspector
|
||||
{
|
||||
private static readonly Regex _patientRegex =
|
||||
@@ -518,6 +560,7 @@ namespace adas_core.Domain.Utils
|
||||
/// </summary>
|
||||
/// <param name="key">The input string from which to extract the patient identifier.</param>
|
||||
/// <returns>The extracted patient identifier, or <c>null</c> if no match is found.</returns>
|
||||
/// <!-- aidoc:v1 sig=1aa2718 body=b3a8f76 -->
|
||||
public static string? ExtractPatientId(string key)
|
||||
{
|
||||
var match = _patientRegex.Match(key);
|
||||
@@ -529,6 +572,7 @@ namespace adas_core.Domain.Utils
|
||||
/// </summary>
|
||||
/// <param name="key">The grouped observation key to parse.</param>
|
||||
/// <returns>A tuple containing the extracted field name and patient identifier, or <c>(null, null)</c> if the key does not match the pattern.</returns>
|
||||
/// <!-- aidoc:v1 sig=3435318 body=f175ccc -->
|
||||
public static (string? Field, string? PatientId) ExtractGroupedObservationInfo(string key)
|
||||
{
|
||||
var match = _groupedRegex.Match(key);
|
||||
@@ -543,6 +587,7 @@ namespace adas_core.Domain.Utils
|
||||
/// </summary>
|
||||
/// <param name="key">The input string expected to contain a location and a date in <c>yyyyMMdd</c> format.</param>
|
||||
/// <returns>A tuple containing the extracted <c>Location</c> string and the parsed <c>Date</c>; either value may be <c>null</c> when extraction or parsing fails.</returns>
|
||||
/// <!-- aidoc:v1 sig=45672a9 body=450f3c1 -->
|
||||
public static (string? Location, DateTime? Date) ExtractAppointmentInfo(string key)
|
||||
{
|
||||
var match = _appointmentDayRegex.Match(key);
|
||||
|
||||
Reference in New Issue
Block a user