rama creada apartir de master en j

This commit is contained in:
jrojas
2026-06-26 10:29:23 +02:00
parent 319fd3dfb0
commit c1517fda87
2810 changed files with 1927392 additions and 25392 deletions
+14 -4
View File
@@ -2,6 +2,12 @@
namespace adas_core.Domain.Utils;
/// <summary>
/// Provides a set of static utility methods for authentication-related operations.
/// </summary>
/// <remarks>
/// This class is sealed and cannot be inherited.
/// </remarks>
public sealed class AuthUtils
{
private LoginResponse _loginResponse = new();
@@ -37,11 +43,15 @@ public sealed class AuthUtils
private static AuthUtils InternalInstance { get; set; }
public static AuthUtils Instance => InternalInstance;
/// <summary>
/// Retrieves the current <see cref="LoginResponse"/> in a thread-safe manner by acquiring a lock on the underlying field.
/// </summary>
/// <returns>The current <see cref="LoginResponse"/> instance.</returns>
public LoginResponse GetLoginResponse()
{
lock (_loginResponse)
{
return _loginResponse;
lock (_loginResponse)
{
return _loginResponse;
}
}
}
}
+30 -16
View File
@@ -2,25 +2,39 @@
namespace adas_core.Domain.Utils;
/// <summary>
/// Provides static utility methods for working with BSON (Binary JSON) data.
/// </summary>
public static class BsonUtils
{
/// <summary>
/// Converts a <see cref="BsonValue"/> to a corresponding common language runtime (CLR) scalar object, supporting <see cref="int"/>, <see cref="long"/>, <see cref="double"/>, <see cref="DateTime"/> (normalized to UTC), and <see cref="string"/> values.
/// </summary>
/// <param name="val">The <see cref="BsonValue"/> instance to convert to a CLR object.</param>
/// <returns>The underlying CLR value when <paramref name="val"/> is an Int32, Int64, Double, valid DateTime (in UTC), or String; otherwise <see langword="null"/>.</returns>
public static object? ToObject(this BsonValue val)
{
if (val.IsInt32) return val.AsInt32;
if (val.IsInt64) return val.AsInt64;
if (val.IsDouble) return val.AsDouble;
if (val.IsValidDateTime) return val.ToUniversalTime();
if (val.IsString) return val.AsString;
return null;
}
{
if (val.IsInt32) return val.AsInt32;
if (val.IsInt64) return val.AsInt64;
if (val.IsDouble) return val.AsDouble;
if (val.IsValidDateTime) return val.ToUniversalTime();
if (val.IsString) return val.AsString;
return null;
}
/// <summary>
/// Gets the value associated with the specified key from the BSON document, returning <c>null</c> if the key is not found.
/// </summary>
/// <param name="doc">The BSON document to search for the key.</param>
/// <param name="key">The key of the value to retrieve.</param>
/// <returns>The <see cref="BsonValue"/> associated with <paramref name="key"/>, or <c>null</c> if the key does not exist.</returns>
public static BsonValue? Get(this BsonDocument doc, string key)
{
return doc.TryGetValue(key, out var value) ? value : null;
}
{
return doc.TryGetValue(key, out var value) ? value : null;
}
}
+338 -146
View File
@@ -16,22 +16,50 @@ namespace adas_core.Domain.Utils
/// </summary>
public static class CacheKeyTtl
{
/// <summary>
/// Converts a nullable number of seconds into a <see cref="TimeSpan"/>, returning <c>null</c> when the value is not greater than zero.
/// </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>
private static TimeSpan? SecondsOrNull(int? seconds)
=> seconds is > 0
? TimeSpan.FromSeconds(seconds.Value)
: null;
=> seconds is > 0
? TimeSpan.FromSeconds(seconds.Value)
: null;
/// <summary>
/// Extracts a value from the in-memory TTL settings of the provided cache configuration by applying the specified selector function.
/// </summary>
/// <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>
private static int? FromInMemory(Func<TtlSettings, int?> selector, CacheSettings s)
=> selector(s.InMemory.Ttl);
=> selector(s.InMemory.Ttl);
/// <summary>
/// Extracts an integer value from the Redis TTL settings of a <see cref="CacheSettings"/> instance using the provided selector function.
/// Falls back to a new <see cref="TtlSettings"/> when the settings or their Redis TTL configuration is null, and returns null when the selector itself is null.
/// </summary>
/// <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>
private static int? FromRedis(Func<TtlSettings, int?>? selector, CacheSettings? s)
=> selector?.Invoke(s?.Redis.Ttl ?? new TtlSettings());
=> selector?.Invoke(s?.Redis.Ttl ?? new TtlSettings());
/// <summary>
/// Retrieves the global in-memory cache time-to-live as a <see cref="TimeSpan"/>, or <c>null</c> when the cache settings or the global seconds value are not provided.
/// </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>
private static TimeSpan? GlobalInMemory(CacheSettings? s)
=> SecondsOrNull(s?.InMemory.Ttl.GlobalSeconds);
=> SecondsOrNull(s?.InMemory.Ttl.GlobalSeconds);
/// <summary>
/// Retrieves the global Redis TTL setting as a nullable <see cref="TimeSpan"/>, returning <c>null</c> when the cache settings, Redis section, or TTL seconds value is not provided.
/// </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>
private static TimeSpan? GlobalRedis(CacheSettings? s)
=> SecondsOrNull(s?.Redis.Ttl.GlobalSeconds);
=> SecondsOrNull(s?.Redis.Ttl.GlobalSeconds);
/// <summary>
/// Resuelve TTL para una entidad concreta, respetando el backend configurado para dicha entidad.
@@ -105,8 +133,12 @@ namespace adas_core.Domain.Utils
{
#region ConfigObservations
/// <summary>
/// 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>
public static string ConfigObservationsAll()
=> "configObservations:all";
=> "configObservations:all";
public static (string Key, TimeSpan? Ttl) ConfigObservationsAllKeyWithTtl(
CacheSettings? settings)
@@ -119,138 +151,251 @@ namespace adas_core.Domain.Utils
#endregion
#region DisplayConfig
/// <summary>
/// Builds the base configuration key used to reference a display entry.
/// </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>
public static string DisplayBase(ObjectId displayId)
=> $"configDisplays:display:{displayId}:base";
=> $"configDisplays:display:{displayId}:base";
/// <summary>
/// Builds a cache key for a display entity along with its resolved time-to-live (TTL) based on the provided cache settings.
/// </summary>
/// <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>
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);
}
CacheSettings? settings,
ObjectId displayId)
{
var key = DisplayBase(displayId);
var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Displays);
return (key, ttl);
}
/// <summary>
/// Builds a configuration display key for the specified display identifier, used to look up or cache the associated configuration entry.
/// </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>
public static string DisplayWithConfig(ObjectId displayId)
=> $"configDisplays:display:{displayId}:config";
=> $"configDisplays:display:{displayId}:config";
/// <summary>
/// Builds a cache key and resolves its associated time-to-live (TTL) for a display entity using the provided configuration settings.
/// </summary>
/// <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>
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);
}
CacheSettings? settings,
ObjectId displayId)
{
var key = DisplayWithConfig(displayId);
var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Displays);
return (key, ttl);
}
#endregion
#region PointOfCare
/// <summary>
/// Generates a formatted identifier string for a Point of Care base entity, used as a cache key or lookup reference in the format "pointOfCare:{pocId}:base".
/// </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>
public static string PointOfCareBase(ObjectId pocId)
=> $"pointOfCare:{pocId}:base";
=> $"pointOfCare:{pocId}:base";
/// <summary>
/// Builds a cache key for a Point of Care entity and resolves the associated time-to-live (TTL) from the supplied cache settings.
/// </summary>
/// <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>
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);
}
CacheSettings? settings, ObjectId pocId)
{
var key = PointOfCareBase(pocId);
var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PontOfCare);
return (key, ttl);
}
/// <summary>
/// Builds a cache key for a point of care entry that includes its associated information and is intended to be stored with a time-to-live (TTL).
/// </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>
public static string PointOfCareWithInfoKeyWithTtl(ObjectId pocId)
=> $"pointOfCare:{pocId}:withInfo";
=> $"pointOfCare:{pocId}:withInfo";
/// <summary>
/// Builds the cache key and resolves the associated time-to-live (TTL) for caching a point of care entry, including its related information.
/// </summary>
/// <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>
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);
}
CacheSettings? settings, ObjectId pocId)
{
var key = PointOfCareWithInfoKeyWithTtl(pocId);
var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PontOfCare);
return (key, ttl);
}
#endregion
#region Observations
/// <summary>
/// Builds a cache key for retrieving the latest observations of a patient, optionally scoped to specific fields and a trailing count.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose observations are being keyed.</param>
/// <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>
public static string LatestObservations(ObjectId patientId, IEnumerable<string> fieldNames, int? last = null)
=> $"patients:latestObs:{patientId}:{Normalize(fieldNames)}:{last ?? 0}";
=> $"patients:latestObs:{patientId}:{Normalize(fieldNames)}:{last ?? 0}";
/// <summary>
/// Generates a cache key for the latest patient observations along with its associated Time-To-Live (TTL) duration.
/// The TTL is resolved from the provided cache settings for the PatientObservations entity type.
/// </summary>
/// <param name="settings">The optional cache settings used to resolve the TTL for the cache key.</param>
/// <param name="patientId">The identifier of the patient whose latest observations are being queried.</param>
/// <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>
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);
}
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
/// <summary>
/// Builds a cache key for retrieving the latest pump observations associated with the specified patient, using an optional entry count that defaults to zero when not provided.
/// </summary>
/// <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>
public static string LatestPumps(ObjectId patientId, int? last = null)
=> $"pumpObs:latest:{patientId}:{last ?? 0}";
=> $"pumpObs:latest:{patientId}:{last ?? 0}";
/// <summary>
/// Builds the cache key and resolves the TTL for the latest pump observations of a patient, optionally limited to a specified number of entries.
/// </summary>
/// <param name="settings">The cache settings used to resolve the TTL for pump observation entries.</param>
/// <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>
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);
}
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
/// <summary>
/// Generates a cache key for a patient's appointments scheduled on the current UTC day.
/// </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>
public static string PatientAppointmentsToday(ObjectId patientId)
{
var dateKey = DateTime.UtcNow.ToString("yyyyMMdd");
return $"appointments:patient:{patientId}:{dateKey}";
}
{
var dateKey = DateTime.UtcNow.ToString("yyyyMMdd");
return $"appointments:patient:{patientId}:{dateKey}";
}
/// <summary>
/// Builds the cache key for a patient's appointments scheduled for today and resolves the associated time-to-live from the provided cache settings.
/// </summary>
/// <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>
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);
}
CacheSettings? settings,
ObjectId patientId)
{
var key = PatientAppointmentsToday(patientId);
var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Appointments);
return (key, ttl);
}
/// <summary>
/// Builds a cache key for the appointments associated with the specified Point of Contact for the current UTC day.
/// </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>
public static string PocAppointmentsToday(ObjectId pocId)
{
var dateKey = DateTime.UtcNow.ToString("yyyyMMdd");
return $"appointments:PoC:{pocId}:{dateKey}";
}
{
var dateKey = DateTime.UtcNow.ToString("yyyyMMdd");
return $"appointments:PoC:{pocId}:{dateKey}";
}
/// <summary>
/// Builds a cache key for today's appointments associated with a Point of Care and resolves its corresponding time-to-live (TTL) using the provided cache settings for the Point of Care entity type.
/// </summary>
/// <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>
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);
}
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}")
/// <summary>
/// Builds a formatted cache key for grouped observations associated with a specific patient, combining the grouped field name and the patient identifier.
/// </summary>
/// <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>
public static string GroupedObs(ObjectId patientId, string groupedFieldName)
=> $"groupedObs:{groupedFieldName}:patient:{patientId}";
=> $"groupedObs:{groupedFieldName}:patient:{patientId}";
/// <summary>
/// Builds a cache key for a patient's grouped observations and resolves the associated time-to-live (TTL) from the supplied cache settings.
/// </summary>
/// <param name="settings">The cache settings used to resolve the TTL; may be <c>null</c>, in which case a default TTL is applied.</param>
/// <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>
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);
}
CacheSettings? settings, ObjectId patientId, string groupedFieldName)
{
var key = GroupedObs(patientId, groupedFieldName);
var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.GroupedObservations);
return (key, ttl);
}
#endregion
//Helpers
/// <summary>
/// Normalizes a collection of strings by removing null or whitespace entries, trimming the remaining values, sorting them using ordinal (case-sensitive) comparison, and joining them with a pipe (|) delimiter to produce a canonical, comparable representation.
/// </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>
public static string Normalize(IEnumerable<string> items)
=> string.Join("|", items
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(s => s.Trim())
.OrderBy(s => s, StringComparer.Ordinal));
=> string.Join("|", items
.Where(s => !string.IsNullOrWhiteSpace(s))
.Select(s => s.Trim())
.OrderBy(s => s, StringComparer.Ordinal));
}
@@ -261,29 +406,37 @@ namespace adas_core.Domain.Utils
/// </summary>
public static class CacheKeyClassifier
{
/// <summary>
/// Classifies a cache key into the corresponding entity type by inspecting its prefix.
/// Recognized prefixes include <c>patients:</c>, <c>displays:</c>/<c>configDisplays:</c>, <c>pumpObs:</c>,
/// <c>appointments:</c>, <c>groupedObs:</c>, and <c>configObservations:</c> (case-insensitive). If no
/// known prefix matches, the method returns <see cref="CacheEnum.EntityType.Unknown"/>.
/// </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>
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;
}
{
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
@@ -292,26 +445,48 @@ namespace adas_core.Domain.Utils
/// </summary>
public static class CacheKeyPatterns
{
/// <summary>
/// Returns a cache key pattern corresponding to the specified entity type, mapping each supported entity (Patients, Displays, PumpObservations, Appointments, GroupedObservations, and ConfigObservations) to its dedicated cache key prefix. For any unrecognized entity type, the wildcard pattern "*" is returned as a fallback to target all cache entries.
/// </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>
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:*",
_ => "*"
};
=> 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:*",
_ => "*"
};
/// <summary>
/// Builds a wildcard search key by appending ":*" to the specified prefix, enabling prefix-based lookups over a hierarchical key namespace.
/// </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>
public static string ByPrefix(string prefix)
=> $"{prefix}:*";
=> $"{prefix}:*";
/// <summary>
/// Builds a wildcard search pattern prefixed by the given value and scoped to a specific patient identifier.
/// </summary>
/// <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>
public static string ByPatient(string prefix, string patientId)
=> $"{prefix}:*:{patientId}*";
=> $"{prefix}:*:{patientId}*";
/// <summary>
/// Builds a formatted string by combining the provided prefix and date with a <c>:*:</c> separator, typically used as a key or identifier pattern.
/// </summary>
/// <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>
public static string ByDate(string prefix, string date)
=> $"{prefix}:*:{date}";
=> $"{prefix}:*:{date}";
}
// INSPECTOR (diagnóstico)
@@ -330,36 +505,53 @@ namespace adas_core.Domain.Utils
private static readonly Regex _appointmentDayRegex =
new(@"appointments:(?<location>.+):(?<date>\d{8})", RegexOptions.Compiled);
/// <summary>
/// Extracts the patient identifier from the given key using a regular expression pattern.
/// Returns <c>null</c> if the key does not match the expected patient format.
/// </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>
public static string? ExtractPatientId(string key)
{
var match = _patientRegex.Match(key);
return match.Success ? match.Groups["id"].Value : null;
}
{
var match = _patientRegex.Match(key);
return match.Success ? match.Groups["id"].Value : null;
}
/// <summary>
/// Extracts the observation field name and patient identifier from a grouped observation key using a regular expression pattern. Returns a tuple of <c>null</c> values when the key does not match the expected grouped format.
/// </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>
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);
}
{
var match = _groupedRegex.Match(key);
return !match.Success
? (null, null)
: (match.Groups["field"].Value, match.Groups["id"].Value);
}
/// <summary>
/// Extracts appointment location and date information from the provided key by matching against a predefined appointment-day pattern.
/// Returns a null location and null date if the key does not match the expected pattern, and returns the matched location with a null date if the date portion cannot be parsed using the <c>yyyyMMdd</c> format.
/// </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>
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);
}
{
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);
}
}
}
@@ -2,6 +2,12 @@
namespace adas_core.Domain.Utils;
/// <summary>
/// Provides static extension methods for <see cref="CardConfig"/> to augment its functionality.
/// </summary>
/// <remarks>
/// This class is a static container for extension methods that extend the capabilities of the <see cref="CardConfig"/> type.
/// </remarks>
public static class CardConfigExtensions
{
// Método principal para extraer todos los nombres
+71 -26
View File
@@ -2,47 +2,92 @@
namespace adas_core.Domain.Utils;
/// <summary>
/// Provides static utility methods for working with collections.
/// </summary>
public static class CollectionsUtils
{
/// <summary>
/// Determines whether the specified list is null or contains no elements.
/// </summary>
/// <param name="value">The list to evaluate.</param>
/// <returns><see langword="true"/> if the list is null or empty; otherwise, <see langword="false"/>.</returns>
public static bool IsNullOrEmpty<T>(this List<T>? value)
{
return value == null || !value.Any();
}
{
return value == null || !value.Any();
}
/// <summary>
/// Determines whether the specified list is <see langword="null"/> or contains no elements.
/// </summary>
/// <param name="value">The list to evaluate.</param>
/// <returns><see langword="true"/> if the list is <see langword="null"/> or empty; otherwise, <see langword="false"/>.</returns>
public static bool IsEmptyOrNull<T>(List<T>? value)
{
return value == null || !value.Any();
}
{
return value == null || !value.Any();
}
/// <summary>
/// Determines whether the specified list contains at least one element, returning <c>false</c> when the list is <c>null</c> or empty.
/// </summary>
/// <param name="value">The list to evaluate. May be <c>null</c>.</param>
/// <returns><c>true</c> if <paramref name="value"/> is not <c>null</c> and contains one or more elements; otherwise, <c>false</c>.</returns>
public static bool HasElements<T>(List<T>? value)
{
return value != null && value.Any();
}
{
return value != null && value.Any();
}
/// <summary>
/// Determines whether the specified list is neither null nor empty by negating the result of <see cref="IsEmptyOrNull{T}(List{T})"/>.
/// </summary>
/// <param name="value">The list to evaluate.</param>
/// <returns><see langword="true"/> if the list is not null and contains at least one element; otherwise, <see langword="false"/>.</returns>
public static bool IsNotEmpty<T>(List<T> value)
{
return !IsEmptyOrNull(value);
}
{
return !IsEmptyOrNull(value);
}
/// <summary>
/// Returns the provided list as-is when it is not null, or an empty list when it is null, ensuring a non-null result for safe iteration.
/// </summary>
/// <param name="value">The list to evaluate; may be <see langword="null"/>.</param>
/// <returns>The original <paramref name="value"/> if it is not null; otherwise, an empty <see cref="List{T}"/>.</returns>
public static List<T> EmptyIfNull<T>(List<T>? value)
{
return value ?? [];
}
{
return value ?? [];
}
/// <summary>
/// Returns <c>null</c> when the specified list is null or contains no elements; otherwise, returns the list as-is.
/// </summary>
/// <param name="value">The list to evaluate.</param>
/// <returns>The original <paramref name="value"/> when it is not null and not empty; otherwise, <c>null</c>.</returns>
public static List<T>? NullIfEmpty<T>(List<T> value)
{
return IsEmptyOrNull(value) ? null : value;
}
{
return IsEmptyOrNull(value) ? null : value;
}
/// <summary>
/// Converts the specified collection into a strongly typed array of the given element type.
/// </summary>
/// <param name="collection">The source collection whose elements are copied into the new array.</param>
/// <param name="type">The element <see cref="Type"/> of the array to create.</param>
/// <returns>A new <see cref="Array"/> of the specified type containing the elements copied from the collection.</returns>
public static Array ToArray(ICollection collection, Type type)
{
var result = Array.CreateInstance(type, collection.Count);
collection.CopyTo(result, 0);
return result;
}
{
var result = Array.CreateInstance(type, collection.Count);
collection.CopyTo(result, 0);
return result;
}
/// <summary>
/// Returns the provided default list when the input list is null or empty; otherwise returns the input list unchanged.
/// </summary>
/// <param name="list">The list to evaluate for a null or empty state.</param>
/// <param name="defaultList">The fallback list returned when <paramref name="list"/> is null or empty.</param>
/// <returns>The original <paramref name="list"/> when it contains items; otherwise <paramref name="defaultList"/>.</returns>
public static List<string> IfEmptyOrNull(List<string> list, List<string> defaultList)
{
return IsEmptyOrNull(list) ? defaultList : list;
}
{
return IsEmptyOrNull(list) ? defaultList : list;
}
}
+22 -10
View File
@@ -2,19 +2,31 @@
namespace adas_core.Domain.Utils;
/// <summary>
/// Represents a comparable collection of key/value pairs that extends the standard dictionary functionality.
/// </summary>
/// <typeparam name="TKey">The type of the keys in the dictionary, constrained to non-nullable types.</typeparam>
/// <typeparam name="TValue">The type of the values in the dictionary, constrained to non-nullable types.</typeparam>
/// <remarks>
/// Inherits from <see cref="Dictionary{TKey, TValue}"/> and enforces that both keys and values are non-nullable.
/// </remarks>
public class ComparableDictionary<TKey, TValue> : Dictionary<TKey, TValue> where TKey : notnull where TValue : notnull
{
/// <summary>
/// Generates a hash code for the collection by concatenating the string representations of all key-value pairs, separated by underscores and double percent signs, and returning the hash code of the resulting string.
/// </summary>
/// <returns>An integer hash code derived from the concatenated key-value pairs of the collection.</returns>
public override int GetHashCode()
{
StringBuilder str = new();
foreach (var item in this)
{
str.Append(item.Key);
str.Append('_');
str.Append(item.Value);
str.Append("%%");
StringBuilder str = new();
foreach (var item in this)
{
str.Append(item.Key);
str.Append('_');
str.Append(item.Value);
str.Append("%%");
}
return str.ToString().GetHashCode();
}
return str.ToString().GetHashCode();
}
}
@@ -11,10 +11,20 @@ using JsonConvert = Newtonsoft.Json.JsonConvert;
namespace adas_core.Domain.Utils;
/// <summary>
/// Represents a serializer for complex object value types, deriving from <see cref="SerializerBase{T}"/> with <see cref="object"/> as the type argument.
/// </summary>
/// <remarks>
/// Declared as a partial class, allowing its definition to be split across multiple files.
/// </remarks>
public partial class ComplexObjectValueTypeSerializer : SerializerBase<object>
{
/// <summary>
/// Provides a compiled regular expression for matching MongoDB ObjectId references, such as <c>ObjectId("...")</c>, that wrap a 24-character hexadecimal identifier.
/// </summary>
/// <returns>A <see cref="Regex"/> that matches strings containing an <c>ObjectId</c> call with a 24-character hexadecimal value enclosed in delimiters.</returns>
[GeneratedRegex("ObjectId\\((.[a-f0-9]{24}.)\\)")]
private static partial Regex ObjectIdRegex();
private static partial Regex ObjectIdRegex();
public override object Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
@@ -152,138 +162,166 @@ public partial class ComplexObjectValueTypeSerializer : SerializerBase<object>
}
}
/// <summary>
/// Serializes a value of an unknown runtime type into BSON, handling common primitives, <see cref="OptionList"/>, <see cref="PatientAllergiesValue"/>, and their list variants directly, and using reflection to dispatch generic <see cref="ArraySerializer{T}"/> for arbitrary <see cref="ICollection"/> instances. For any other type, the value is converted to JSON, parsed into a <see cref="BsonDocument"/>, tagged with its full type name in the <c>_t</c> field, and then written out.
/// </summary>
/// <param name="context">The BSON serialization context that provides the writer used to emit the serialized output.</param>
/// <param name="args">Additional BSON serialization arguments passed through to the underlying serializers.</param>
/// <param name="value">The runtime value to serialize. A <c>null</c> value is written as a BSON null; all other values are dispatched based on their concrete type.</param>
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value)
{
switch (value)
{
case null:
context.Writer.WriteNull();
return;
case bool b:
BooleanSerializer.Instance.Serialize(context, args, b);
return;
case double b:
DoubleSerializer.Instance.Serialize(context, args, b);
return;
case string b:
StringSerializer.Instance.Serialize(context, args, b);
return;
case int b:
Int32Serializer.Instance.Serialize(context, args, b);
return;
case long b:
Int64Serializer.Instance.Serialize(context, args, b);
return;
case Decimal128 b:
new Decimal128Serializer().Serialize(context, args, b);
return;
case DateTime b:
new DateTimeSerializer().Serialize(context, args, b);
return;
case OptionList optionList:
BsonSerializer.Serialize(context.Writer, optionList.GetType(), optionList);
return;
case IList<OptionList> optionListList:
BsonSerializer.Serialize(context.Writer, optionListList.GetType(), optionListList);
return;
case PatientAllergiesValue pav:
BsonSerializer.Serialize(context.Writer, pav.GetType(), pav);
return;
case IList<PatientAllergiesValue> pavList:
BsonSerializer.Serialize(context.Writer, pavList.GetType(), pavList);
return;
case ICollection i:
var t = i.GetType().GetElementType() ?? typeof(object);
var serializerType = typeof(ArraySerializer<>).MakeGenericType(t);
var ctor = serializerType.GetConstructor([]);
var serializer = ctor?.Invoke([]);
var serializeMethod = serializerType.GetMethod("Serialize");
var array = CollectionsUtils.ToArray(i, t);
serializeMethod?.Invoke(serializer!, [context, args, array]);
return;
default:
var json = JsonConvert.SerializeObject(value);
var document = BsonDocument.Parse(json);
document["_t"] = value.GetType().FullName;
BsonSerializer.Serialize(context.Writer, typeof(BsonDocument), document);
return;
}
}
private Type? GetElementTypeFromBsonArray(BsonArray bsonArray)
{
if (bsonArray.Count == 1 && bsonArray.Values.Any(c => c.IsBsonNull))
return typeof(OptionList);
foreach (var element in bsonArray)
if (element is BsonDocument bsonDocument)
switch (value)
{
if (bsonDocument.Contains("_t"))
{
var t = bsonDocument["_t"].AsString;
if (t.Contains('.'))
{
t = $"adas-core_Domain.Models.{t}";
t = t.Replace("adas-core_Models", "adas-core_Domain.Models");
}
var type = Type.GetType(t);
if (type != null) return type;
}
else
{
var instance = BsonSerializer.Deserialize(bsonDocument, typeof(OptionList));
if (instance != null && bsonDocument.Contains("name")) return typeof(OptionList);
instance = BsonSerializer.Deserialize(bsonDocument, typeof(List<OptionList>));
if (instance != null)
return typeof(List<OptionList>);
instance = BsonSerializer.Deserialize(bsonDocument, typeof(PatientAllergiesValue));
if (instance != null)
return typeof(PatientAllergiesValue);
instance = BsonSerializer.Deserialize(bsonDocument, typeof(PatientDrainagesValue));
if (instance != null)
return typeof(PatientDrainagesValue);
instance = BsonSerializer.Deserialize(bsonDocument, typeof(PatientIntravenousLinesValue));
if (instance != null)
return typeof(PatientIntravenousLinesValue);
}
case null:
context.Writer.WriteNull();
return;
case bool b:
BooleanSerializer.Instance.Serialize(context, args, b);
return;
case double b:
DoubleSerializer.Instance.Serialize(context, args, b);
return;
case string b:
StringSerializer.Instance.Serialize(context, args, b);
return;
case int b:
Int32Serializer.Instance.Serialize(context, args, b);
return;
case long b:
Int64Serializer.Instance.Serialize(context, args, b);
return;
case Decimal128 b:
new Decimal128Serializer().Serialize(context, args, b);
return;
case DateTime b:
new DateTimeSerializer().Serialize(context, args, b);
return;
case OptionList optionList:
BsonSerializer.Serialize(context.Writer, optionList.GetType(), optionList);
return;
case IList<OptionList> optionListList:
BsonSerializer.Serialize(context.Writer, optionListList.GetType(), optionListList);
return;
case PatientAllergiesValue pav:
BsonSerializer.Serialize(context.Writer, pav.GetType(), pav);
return;
case IList<PatientAllergiesValue> pavList:
BsonSerializer.Serialize(context.Writer, pavList.GetType(), pavList);
return;
case ICollection i:
var t = i.GetType().GetElementType() ?? typeof(object);
var serializerType = typeof(ArraySerializer<>).MakeGenericType(t);
var ctor = serializerType.GetConstructor([]);
var serializer = ctor?.Invoke([]);
var serializeMethod = serializerType.GetMethod("Serialize");
var array = CollectionsUtils.ToArray(i, t);
serializeMethod?.Invoke(serializer!, [context, args, array]);
return;
default:
var json = JsonConvert.SerializeObject(value);
var document = BsonDocument.Parse(json);
document["_t"] = value.GetType().FullName;
BsonSerializer.Serialize(context.Writer, typeof(BsonDocument), document);
return;
}
}
return null;
}
/// <summary>
/// Determines the .NET <see cref="Type"/> represented by the elements of a BSON array, handling both null-valued single-element arrays and BSON documents that may carry a discriminator (<c>_t</c>) or match known domain types (such as <see cref="OptionList"/>, <see cref="List{OptionList}"/>, <see cref="PatientAllergiesValue"/>, <see cref="PatientDrainagesValue"/>, and <see cref="PatientIntravenousLinesValue"/>).
/// When the discriminator contains a dot, it is resolved as a type under the <c>adas-core_Domain.Models</c> namespace, with legacy <c>adas-core_Models</c> segments rewritten to the current namespace.
/// </summary>
/// <param name="bsonArray">The BSON array whose element type needs to be resolved.</param>
/// <returns>The resolved <see cref="Type"/> for the array elements, or <c>null</c> when no matching type can be determined.</returns>
private Type? GetElementTypeFromBsonArray(BsonArray bsonArray)
{
if (bsonArray.Count == 1 && bsonArray.Values.Any(c => c.IsBsonNull))
return typeof(OptionList);
foreach (var element in bsonArray)
if (element is BsonDocument bsonDocument)
{
if (bsonDocument.Contains("_t"))
{
var t = bsonDocument["_t"].AsString;
if (t.Contains('.'))
{
t = $"adas-core_Domain.Models.{t}";
t = t.Replace("adas-core_Models", "adas-core_Domain.Models");
}
var type = Type.GetType(t);
if (type != null) return type;
}
else
{
var instance = BsonSerializer.Deserialize(bsonDocument, typeof(OptionList));
if (instance != null && bsonDocument.Contains("name")) return typeof(OptionList);
instance = BsonSerializer.Deserialize(bsonDocument, typeof(List<OptionList>));
if (instance != null)
return typeof(List<OptionList>);
instance = BsonSerializer.Deserialize(bsonDocument, typeof(PatientAllergiesValue));
if (instance != null)
return typeof(PatientAllergiesValue);
instance = BsonSerializer.Deserialize(bsonDocument, typeof(PatientDrainagesValue));
if (instance != null)
return typeof(PatientDrainagesValue);
instance = BsonSerializer.Deserialize(bsonDocument, typeof(PatientIntravenousLinesValue));
if (instance != null)
return typeof(PatientIntravenousLinesValue);
}
}
return null;
}
/// <summary>
/// Provides serialization for <see cref="List{T}"/> instances while ignoring null elements.
/// </summary>
/// <typeparam name="T">The type of elements contained in the list being serialized.</typeparam>
public class NullIgnoringListSerializer<T> : SerializerBase<List<T>>
{
/// <summary>
/// Deserializes a BSON array into a <see cref="List{T}"/>, returning an empty list when the current BSON value is null and skipping any null elements encountered while reading the array.
/// </summary>
/// <param name="context">The BSON deserialization context providing the reader used to read the BSON data.</param>
/// <param name="args">Additional arguments that influence the deserialization process.</param>
/// <returns>A <see cref="List{T}"/> containing the deserialized elements, or an empty list if the BSON value is null.</returns>
public override List<T> Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var bsonType = context.Reader.GetCurrentBsonType();
if (bsonType == BsonType.Null)
{
context.Reader.ReadNull();
return [];
}
var list = new List<T>();
context.Reader.ReadStartArray();
while (context.Reader.ReadBsonType() != BsonType.EndOfDocument)
{
var element = BsonSerializer.Deserialize<T>(context.Reader);
if (element != null) list.Add(element);
}
context.Reader.ReadEndArray();
return list;
}
{
var bsonType = context.Reader.GetCurrentBsonType();
if (bsonType == BsonType.Null)
{
context.Reader.ReadNull();
return [];
}
var list = new List<T>();
context.Reader.ReadStartArray();
while (context.Reader.ReadBsonType() != BsonType.EndOfDocument)
{
var element = BsonSerializer.Deserialize<T>(context.Reader);
if (element != null) list.Add(element);
}
context.Reader.ReadEndArray();
return list;
}
/// <summary>
/// Serializes a <see cref="List{T}"/> as a BSON array, writing each element using its actual runtime type to support polymorphic serialization. Null elements are handled safely through the null-conditional type resolution.
/// </summary>
/// <param name="context">The BSON serialization context providing the writer used to emit the array and its elements.</param>
/// <param name="args">The BSON serialization arguments supplying additional options for the serialization process.</param>
/// <param name="value">The list of items to serialize as a BSON array.</param>
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, List<T> value)
{
context.Writer.WriteStartArray();
foreach (var item in value) BsonSerializer.Serialize(context.Writer, item?.GetType(), item);
context.Writer.WriteEndArray();
}
{
context.Writer.WriteStartArray();
foreach (var item in value) BsonSerializer.Serialize(context.Writer, item?.GetType(), item);
context.Writer.WriteEndArray();
}
}
// public class IgnoreEmptyStringSerializer : SerializerBase<string>
// {
+62 -30
View File
@@ -6,47 +6,79 @@ using MongoDB.Bson;
namespace adas_core.Domain.Utils;
/// <summary>
/// Provides a static container for cryptographic operations and utilities related to the Adas domain.
/// </summary>
public static class CryptoAdas
{
private static readonly Regex PassRegex = new("^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$");
/// <summary>
/// Computes the MD5 hash of the specified input string and returns it as an uppercase hexadecimal string.
/// The hash bytes are iterated in reverse order before being formatted.
/// </summary>
/// <param name="input">The string to hash. Its ASCII byte representation is used as the input to the MD5 algorithm.</param>
/// <returns>The MD5 hash of <paramref name="input"/> formatted as an uppercase hexadecimal string.</returns>
public static string CreateMd5(string input)
{
using var md5 = MD5.Create();
var inputBytes = Encoding.ASCII.GetBytes(input);
var hashBytes = md5.ComputeHash(inputBytes);
var sb = new StringBuilder();
for (var i = hashBytes.Length - 1; i >= 0; i--) sb.Append(hashBytes[i].ToString("X2"));
var res = sb.ToString();
return res;
}
{
using var md5 = MD5.Create();
var inputBytes = Encoding.ASCII.GetBytes(input);
var hashBytes = md5.ComputeHash(inputBytes);
var sb = new StringBuilder();
for (var i = hashBytes.Length - 1; i >= 0; i--) sb.Append(hashBytes[i].ToString("X2"));
var res = sb.ToString();
return res;
}
/// <summary>
/// Creates a deterministic MD5 hash that uniquely identifies a grouped field configuration for a specific patient.
/// Falls back to a single-element list containing the group's name (or empty string) when the <see cref="GroupedField.Names"/> collection is null or empty.
/// </summary>
/// <param name="gF">The grouped field whose names, max, since, regularity, and results are included in the hash input.</param>
/// <param name="patientId">The identifier of the patient whose data is being hashed.</param>
/// <returns>An MD5 hash string representing the combined patient and grouped field data.</returns>
public static string CreateMd5GroupedObs(GroupedField gF, ObjectId patientId)
{
var names = CollectionsUtils.IfEmptyOrNull(gF.Names, [gF.Name ?? string.Empty]);
return CreateMd5(patientId +
string.Join("+", names.Select(c => c.ToString())) +
gF.Max +
gF.Since +
gF.Regularity +
string.Join("+", gF.Result.Select(c => c.ToString())));
}
{
var names = CollectionsUtils.IfEmptyOrNull(gF.Names, [gF.Name ?? string.Empty]);
return CreateMd5(patientId +
string.Join("+", names.Select(c => c.ToString())) +
gF.Max +
gF.Since +
gF.Regularity +
string.Join("+", gF.Result.Select(c => c.ToString())));
}
/// <summary>
/// Creates a BCrypt hash from the provided input string, typically used for securely storing passwords.
/// </summary>
/// <param name="input">The plain text string to be hashed.</param>
/// <returns>A BCrypt hashed representation of the input string.</returns>
public static string CreateBCrypt(string input)
{
return BCrypt.Net.BCrypt.HashPassword(input);
}
{
return BCrypt.Net.BCrypt.HashPassword(input);
}
/// <summary>
/// Verifies that the provided plain text password matches the stored BCrypt password hash.
/// </summary>
/// <param name="loginPass">The plain text password entered by the user during login.</param>
/// <param name="passwordHash">The stored BCrypt hash to compare the password against.</param>
/// <returns><c>true</c> if the password matches the hash; otherwise, <c>false</c>.</returns>
public static bool VerifyPassword(string loginPass, string passwordHash)
{
return BCrypt.Net.BCrypt.Verify(loginPass, passwordHash);
}
{
return BCrypt.Net.BCrypt.Verify(loginPass, passwordHash);
}
/// <summary>
/// Determines whether the specified password meets the strong password criteria defined by the password regex pattern.
/// </summary>
/// <param name="password">The password string to evaluate against the strong password requirements.</param>
/// <returns><c>true</c> if the password matches the strong password pattern; otherwise, <c>false</c>.</returns>
public static bool IsStrongPassword(string password)
{
return PassRegex.IsMatch(password);
}
{
return PassRegex.IsMatch(password);
}
}
@@ -2,6 +2,12 @@
namespace adas_core.Domain.Utils;
/// <summary>
/// Provides extension methods for the <see cref="DetailConfigExtension"/> type.
/// </summary>
/// <remarks>
/// This is a static utility class that cannot be instantiated and is intended to extend the functionality of <see cref="DetailConfigExtension"/> through extension method definitions.
/// </remarks>
public static class DetailConfigExtension
{
// Método principal para iniciar la extracción
+37 -14
View File
@@ -1,24 +1,47 @@
namespace adas_core.Domain.Utils;
/// <summary>
/// Provides static extension methods to augment the functionality of dictionary types.
/// </summary>
public static class DictionaryEx
{
/// <summary>
/// Retrieves the value associated with the specified key from the dictionary, or returns the provided default value if the key is not found.
/// </summary>
/// <param name="dict">The dictionary to search for the key.</param>
/// <param name="key">The key whose associated value should be returned.</param>
/// <param name="defaultValue">The value to return when the key is not present in the dictionary. Defaults to the default value of <typeparamref name="TV"/>.</param>
/// <returns>The value associated with the key if found; otherwise, <paramref name="defaultValue"/>.</returns>
public static TV? GetValue<TK, TV>(this IDictionary<TK, TV> dict, TK key, TV? defaultValue = default)
{
return dict.TryGetValue(key, out var value) ? value : defaultValue;
}
{
return dict.TryGetValue(key, out var value) ? value : defaultValue;
}
/// <summary>
/// Converts a dictionary into a formatted string by joining each key-value pair with a key separator and concatenating all pairs with an item separator.
/// </summary>
/// <param name="dict">The source dictionary whose entries will be converted to a string.</param>
/// <param name="itemSeparator">The separator placed between each formatted key-value pair in the resulting string.</param>
/// <param name="keySeparator">The separator placed between a key and its corresponding value within each pair.</param>
/// <returns>A single string containing all dictionary entries formatted and joined using the specified separators.</returns>
public static string ToListString<TK, TV>(this IDictionary<TK, TV> dict, string itemSeparator = ", ",
string keySeparator = ": ")
{
var s = dict.Keys.Select(key => key + keySeparator + dict[key]).ToList();
return string.Join(itemSeparator, s);
}
string keySeparator = ": ")
{
var s = dict.Keys.Select(key => key + keySeparator + dict[key]).ToList();
return string.Join(itemSeparator, s);
}
/// <summary>
/// Attempts to retrieve the value associated with the specified key from the dictionary, returning null if the key is not found.
/// </summary>
/// <param name="dictionary">The dictionary to search for the key.</param>
/// <param name="key">The key whose associated value should be retrieved.</param>
/// <returns>The value associated with the key, or null if the key was not found in the dictionary.</returns>
public static TValue? TryGetAndReturn<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key)
where TKey : notnull where TValue : class
{
if (!dictionary.TryGetValue(key, out var retValue)) retValue = null;
return retValue;
}
where TKey : notnull where TValue : class
{
if (!dictionary.TryGetValue(key, out var retValue)) retValue = null;
return retValue;
}
}
+20 -11
View File
@@ -3,18 +3,27 @@ using System.Reflection;
namespace adas_core.Domain.Utils;
/// <summary>
/// Provides static utility methods for working with <see cref="System.Enum"/> types.
/// </summary>
public static class EnumUtils
{
/// <summary>
/// Retrieves the human-readable description for an enumeration value by inspecting its <see cref="DescriptionAttribute"/>.
/// Falls back to the enum's string representation when no description attribute is defined on the member.
/// </summary>
/// <param name="value">The enumeration value whose description should be obtained.</param>
/// <returns>The description text from the <see cref="DescriptionAttribute"/> if present; otherwise, the string representation of the enum value.</returns>
public static string GetDescription(Enum value)
{
var enumMember = value.GetType().GetMember(value.ToString()).FirstOrDefault();
var descriptionAttribute =
enumMember == null
? null
: enumMember.GetCustomAttribute(typeof(DescriptionAttribute)) as DescriptionAttribute;
return
descriptionAttribute == null
? value.ToString()
: descriptionAttribute.Description;
}
{
var enumMember = value.GetType().GetMember(value.ToString()).FirstOrDefault();
var descriptionAttribute =
enumMember == null
? null
: enumMember.GetCustomAttribute(typeof(DescriptionAttribute)) as DescriptionAttribute;
return
descriptionAttribute == null
? value.ToString()
: descriptionAttribute.Description;
}
}
+41 -25
View File
@@ -4,40 +4,56 @@ public sealed class EquatableDictionary<TKey, TValue>
: Dictionary<TKey, TValue>, IEquatable<ComparableDictionary<TKey, TValue>>
where TKey : notnull where TValue : notnull
{
/// <summary>
/// Determines whether the current dictionary is equal to another <see cref="ComparableDictionary{TKey, TValue}"/> by comparing their counts and key-value pairs.
/// Returns <c>false</c> if the other dictionary is <c>null</c>, has a different count, is missing any key present in this dictionary, or contains a different value for any shared key.
/// </summary>
/// <param name="other">The dictionary to compare against this instance.</param>
/// <returns><c>true</c> if both dictionaries contain the same keys with equal values; otherwise, <c>false</c>.</returns>
public bool Equals(ComparableDictionary<TKey, TValue>? other)
{
if (other is null) return false;
if (Count != other.Count) return false;
foreach (var pair in this)
{
if (!other.TryGetValue(pair.Key, out var otherValue)) return false;
if (!EqualityComparer<TValue>.Default.Equals(pair.Value, otherValue)) return false;
if (other is null) return false;
if (Count != other.Count) return false;
foreach (var pair in this)
{
if (!other.TryGetValue(pair.Key, out var otherValue)) return false;
if (!EqualityComparer<TValue>.Default.Equals(pair.Value, otherValue)) return false;
}
return true;
}
return true;
}
//private readonly Dictionary<TKey, TValue> dictionary = new();
/// <summary>
/// Determines whether the current instance is equal to the specified object by attempting to cast it to a <see cref="ComparableDictionary{TKey, TValue}"/> and delegating to the typed equality comparison. Returns <c>false</c> when the supplied object is not a <see cref="ComparableDictionary{TKey, TValue}"/>.
/// </summary>
/// <param name="other">The object to compare with the current instance.</param>
/// <returns><c>true</c> if <paramref name="other"/> is a <see cref="ComparableDictionary{TKey, TValue}"/> and is equal to the current instance; otherwise, <c>false</c>.</returns>
public override bool Equals(object? other)
{
return Equals(other as ComparableDictionary<TKey, TValue>);
}
public override int GetHashCode()
{
var hash = 0;
foreach (var pair in this)
{
var miniHash = 17;
miniHash = miniHash * 31 +
EqualityComparer<TKey>.Default.GetHashCode(pair.Key);
miniHash = miniHash * 31 +
EqualityComparer<TValue>.Default.GetHashCode(pair.Value);
hash ^= miniHash;
return Equals(other as ComparableDictionary<TKey, TValue>);
}
return hash;
}
/// <summary>
/// Computes a hash code for the collection by combining the hash codes of each key-value pair.
/// Each pair contributes a hash derived from its key and value, and the per-pair hashes are combined using XOR.
/// </summary>
/// <returns>An integer hash code that represents the contents of the collection.</returns>
public override int GetHashCode()
{
var hash = 0;
foreach (var pair in this)
{
var miniHash = 17;
miniHash = miniHash * 31 +
EqualityComparer<TKey>.Default.GetHashCode(pair.Key);
miniHash = miniHash * 31 +
EqualityComparer<TValue>.Default.GetHashCode(pair.Value);
hash ^= miniHash;
}
return hash;
}
// Implementation of IDictionary<,> which just delegates to the dictionary
}
+15 -7
View File
@@ -1,15 +1,23 @@
namespace adas_core.Domain.Utils;
/// <summary>
/// Provides static access to global application data and configuration values.
/// </summary>
public static class GlobalData
{
public static Dictionary<string, object> Data = new();
/// <summary>
/// Adds or updates a key-value pair in the data store. If the key already exists, its value is replaced; otherwise, a new entry is created.
/// </summary>
/// <param name="key">The key used to identify the data entry in the store.</param>
/// <param name="value">The value to associate with the specified key.</param>
public static void AddData(string key, object value)
{
var exists = Data.ContainsKey(key);
if (exists)
Data[key] = value;
else
Data.Add(key, value);
}
{
var exists = Data.ContainsKey(key);
if (exists)
Data[key] = value;
else
Data.Add(key, value);
}
}
+3
View File
@@ -3,6 +3,9 @@ using Microsoft.Extensions.Logging;
namespace adas_core.Domain.Utils;
/// <summary>
/// Provides static utility methods for working with HL7 (Health Level 7) data formats and messages.
/// </summary>
public static class Hl7Utils
{
/// <summary>
@@ -2,7 +2,19 @@
public interface IMappingUtils
{
/// <summary>
/// Searches for an entity by its code within the specified category and returns its type, name, and group.
/// </summary>
/// <param name="code">The code used to look up the entity.</param>
/// <param name="category">The category used to filter the search.</param>
/// <returns>A nullable tuple containing the type, name, and group of the found entity, or <c>null</c> if no match is found.</returns>
(string type, string name, string group)? SearchByCode(object code, string category);
/// <summary>
/// Retrieves the complexity value associated with the specified name, optionally applying a weight.
/// </summary>
/// <param name="name">The name used to look up the complexity value.</param>
/// <param name="peso">An optional weight value to apply to the complexity calculation.</param>
/// <returns>The complexity value as an integer.</returns>
int GetComplexityValue(string name, double? peso = null);
// void ConvertStringToDoubleInMapping();
+17 -7
View File
@@ -3,14 +3,24 @@ using Newtonsoft.Json.Bson;
namespace adas_core.Domain.Utils;
/// <summary>
/// Provides extension methods for JSON-related operations.
/// </summary>
public static class JsonExtensions
{
/// <summary>
/// Reads JSON content from the specified input file and writes it as BSON to the output file.
/// By default, uses <see cref="FileMode.CreateNew"/>, which requires the output file to not already exist.
/// </summary>
/// <param name="inputPath">The path to the source JSON file to read from.</param>
/// <param name="outputPath">The path to the destination BSON file to write to.</param>
/// <param name="fileMode">The file mode used to open the output file. Defaults to <see cref="FileMode.CreateNew"/>.</param>
public static void CopyToBson(string inputPath, string outputPath, FileMode fileMode = FileMode.CreateNew)
{
using var textReader = File.OpenText(inputPath);
using var jsonReader = new JsonTextReader(textReader);
using var oFileStream = new FileStream(outputPath, fileMode);
using var dataWriter = new BsonDataWriter(oFileStream);
dataWriter.WriteToken(jsonReader);
}
{
using var textReader = File.OpenText(inputPath);
using var jsonReader = new JsonTextReader(textReader);
using var oFileStream = new FileStream(outputPath, fileMode);
using var dataWriter = new BsonDataWriter(oFileStream);
dataWriter.WriteToken(jsonReader);
}
}
+63 -42
View File
@@ -6,56 +6,77 @@ using Microsoft.IdentityModel.Tokens;
namespace adas_core.Domain.Utils;
/// <summary>
/// Serves as an abstract base class for providing helper functionality related to JSON Web Token (JWT) operations.
/// </summary>
/// <remarks>
/// This class is intended to be inherited by concrete implementations that define specific JWT processing behaviors.
/// </remarks>
public abstract class JwtHelper
{
/// <summary>
/// Creates a new JWT security token for the specified user, signing it with the provided secret using HMAC-SHA256.
/// The token includes standard subject, name, name identifier, and unique token identifier (JTI) claims, and optionally merges any additional claims supplied.
/// </summary>
/// <param name="username">The subject identifier used to populate the token's Sub, Name, and NameIdentifier claims.</param>
/// <param name="secret">The symmetric secret used to derive the signing key for the token.</param>
/// <param name="issuer">The issuer (iss claim) to associate with the token.</param>
/// <param name="audience">The audience (aud claim) to associate with the token.</param>
/// <param name="expiration">The token lifetime in minutes, added to the current UTC time to compute the expiration date.</param>
/// <param name="additionalClaims">Optional extra claims to merge into the token alongside the standard claims. If null, only the default claims are included.</param>
/// <returns>A <see cref="JwtSecurityToken"/> signed with HMAC-SHA256 and configured with the supplied issuer, audience, expiration, and claims.</returns>
public static JwtSecurityToken GetJwtToken(
string username,
string secret,
string issuer,
string audience,
int expiration,
Claim[]? additionalClaims = null
)
{
var claims = new[]
string username,
string secret,
string issuer,
string audience,
int expiration,
Claim[]? additionalClaims = null
)
{
new Claim(JwtRegisteredClaimNames.Sub, username),
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.NameIdentifier, username),
// this guarantees the token is unique
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
if (additionalClaims != null)
{
var claimList = new List<Claim>(claims);
claimList.AddRange(additionalClaims);
claims = claimList.ToArray();
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, username),
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.NameIdentifier, username),
// this guarantees the token is unique
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
if (additionalClaims != null)
{
var claimList = new List<Claim>(claims);
claimList.AddRange(additionalClaims);
claims = claimList.ToArray();
}
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var expireDate = DateTime.UtcNow.AddMinutes(expiration);
return new JwtSecurityToken(
issuer,
audience,
expires: expireDate,
claims: claims,
signingCredentials: creds
);
}
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var expireDate = DateTime.UtcNow.AddMinutes(expiration);
return new JwtSecurityToken(
issuer,
audience,
expires: expireDate,
claims: claims,
signingCredentials: creds
);
}
/// <summary>
/// Generates a cryptographically secure refresh token by producing 64 random bytes using <see cref="RandomNumberGenerator"/> and returning the value as a Base64-encoded string.
/// </summary>
/// <returns>A Base64-encoded string representation of a 64-byte cryptographically random sequence suitable for use as a refresh token.</returns>
public static string GenerateRefreshToken()
{
var randomNumber = new byte[64];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
{
var randomNumber = new byte[64];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
/// <summary>
/// return username from claim
+36 -28
View File
@@ -23,35 +23,43 @@ public static class Mapper<T>
);
}
/// <summary>
/// Maps properties from an <see cref="ExpandoObject"/> source to a strongly-typed destination object using a pre-defined property map.
/// Property lookups are case-insensitive. Null values are only assigned to reference types or <see cref="Nullable{T}"/> types; assigning null to a non-nullable value type throws. When a source value's type does not match the destination property's type, the value is converted using <see cref="Convert.ToDouble(object)"/>.
/// </summary>
/// <param name="source">The source <see cref="ExpandoObject"/> whose key-value pairs will be mapped onto the destination.</param>
/// <param name="destination">The target object that will receive the mapped property values.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="source"/> or <paramref name="destination"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when a source value is null but the corresponding destination property is a value type that is not <see cref="Nullable{T}"/>.</exception>
public static void Map(ExpandoObject source, T destination)
{
// Might as well take care of null references early.
if (source == null) throw new ArgumentNullException(nameof(source));
if (destination == null) throw new ArgumentNullException(nameof(destination));
// By iterating the KeyValuePair<string, object> of
// source we can avoid manually searching the keys of
// source as we see in your original code.
foreach (var kv in source)
if (PropertyMap.TryGetValue(kv.Key.ToLower(), out var p))
{
var propType = p.PropertyType;
if (kv.Value == null)
{
// Might as well take care of null references early.
if (source == null) throw new ArgumentNullException(nameof(source));
if (destination == null) throw new ArgumentNullException(nameof(destination));
// By iterating the KeyValuePair<string, object> of
// source we can avoid manually searching the keys of
// source as we see in your original code.
foreach (var kv in source)
if (PropertyMap.TryGetValue(kv.Key.ToLower(), out var p))
{
if (propType is { IsByRef: false } && propType.Name != "Nullable`1")
// Throw if type is a value type
// but not Nullable<>
throw new ArgumentException("not nullable");
var propType = p.PropertyType;
if (kv.Value == null)
{
if (propType is { IsByRef: false } && propType.Name != "Nullable`1")
// Throw if type is a value type
// but not Nullable<>
throw new ArgumentException("not nullable");
}
else if (kv.Value.GetType() != propType)
{
// You could make this a bit less strict
// but I don't recommend it.
p.SetValue(destination, Convert.ToDouble(kv.Value), null);
}
p.SetValue(destination, kv.Value, null);
}
else if (kv.Value.GetType() != propType)
{
// You could make this a bit less strict
// but I don't recommend it.
p.SetValue(destination, Convert.ToDouble(kv.Value), null);
}
p.SetValue(destination, kv.Value, null);
}
}
}
}
+73 -60
View File
@@ -5,6 +5,9 @@ using Microsoft.Extensions.Options;
namespace adas_core.Domain.Utils;
/// <summary>
/// Provides a sealed implementation of mapping utilities, serving as the concrete type for the <see cref="IMappingUtils"/> contract.
/// </summary>
public sealed class MappingUtils : IMappingUtils
{
private readonly List<MappingInterventions>? _cccData;
@@ -117,69 +120,79 @@ public sealed class MappingUtils : IMappingUtils
}
/// <summary>
/// Retrieves a complexity value from the CCC data mapping by matching the provided <paramref name="name"/>.
/// If a single value entry exists for the name, its initial value is returned as an integer; if multiple ranges are defined, the value contributed by the range containing the optional <paramref name="peso"/> is returned. Returns 0 when no matching name is found or when the data is not available.
/// </summary>
/// <param name="name">The name of the entry to look up in the CCC data.</param>
/// <param name="peso">Optional weight used to select the matching range when multiple values are defined; when null, range-based lookup is skipped.</param>
/// <returns>The matched initial value (for single-entry entries) or the contributed value (for range-based entries); returns 0 if the name is not found or no data is available.</returns>
public int GetComplexityValue(string name, double? peso = null)
{
ConvertStringToDoubleInMapping();
var data = _cccData;
if (data == null) return 0; // No encontro ningun nombre que conincida con el dado
foreach (var dato in data)
if (name.Equals(dato.Name))
{
if (dato.Value is
{
Count: 1
}) //value es una lista, si contiene un solo elemento retornaremos el initial value de ese elemento
return Convert.ToInt32(dato.Value[0].InitialValue);
if (dato.Value is { Count: > 1 } && peso != null)
foreach (var rango in dato.Value)
if (peso >= (double?)rango.InitialValue && peso <= (double?)rango.FinalValue)
return rango.ValueContributed;
}
return 0; // No encontro ningun nombre que conincida con el dado
}
private void ConvertStringToDoubleInMapping()
{
// esto es para garantizar que el punto (".") sea reconocido como separador decimal
var culture = CultureInfo.InvariantCulture;
if (_isTransformedValue)
{
}
else if (_cccData != null)
{
foreach (var mapping in _cccData)
{
// Recorrer la lista Codes
foreach (var code in mapping.Codes)
ConvertStringToDoubleInMapping();
var data = _cccData;
if (data == null) return 0; // No encontro ningun nombre que conincida con el dado
foreach (var dato in data)
if (name.Equals(dato.Name))
{
if (code.InitialValue is string initialValueString &&
double.TryParse(initialValueString, culture, out var initialValueDouble))
code.InitialValue = initialValueDouble; // Convertir y guardar como double
if (code.FinalValue is string finalValueString &&
double.TryParse(finalValueString, culture, out var finalValueDouble))
code.FinalValue = finalValueDouble; // Convertir y guardar como double
if (dato.Value is
{
Count: 1
}) //value es una lista, si contiene un solo elemento retornaremos el initial value de ese elemento
return Convert.ToInt32(dato.Value[0].InitialValue);
if (dato.Value is { Count: > 1 } && peso != null)
foreach (var rango in dato.Value)
if (peso >= (double?)rango.InitialValue && peso <= (double?)rango.FinalValue)
return rango.ValueContributed;
}
// Recorrer la lista Value (si no es nula)
if (mapping.Value != null)
foreach (var value in mapping.Value)
{
if (value.InitialValue is string initialValueString &&
double.TryParse(initialValueString, culture, out var initialValueDouble))
value.InitialValue = initialValueDouble; // Convertir y guardar como double
if (value.FinalValue is string finalValueString &&
double.TryParse(finalValueString, culture, out var finalValueDouble))
value.FinalValue = finalValueDouble; // Convertir y guardar como double
}
} //Fin ford
_isTransformedValue = true;
return 0; // No encontro ningun nombre que conincida con el dado
}
/// <summary>
/// Converts string representations of numeric values in the <c>_cccData</c> mapping to <see cref="double"/> using the invariant culture, ensuring the period (".") is recognized as the decimal separator. The method only runs when the data has not already been transformed (<c>_isTransformedValue</c> is false) and when <c>_cccData</c> is not null, iterating through each mapping's <c>Codes</c> and <c>Value</c> entries to parse and replace their <c>InitialValue</c> and <c>FinalValue</c> when they are strings. After processing, it marks the transformation as completed by setting <c>_isTransformedValue</c> to true.
/// </summary>
private void ConvertStringToDoubleInMapping()
{
// esto es para garantizar que el punto (".") sea reconocido como separador decimal
var culture = CultureInfo.InvariantCulture;
if (_isTransformedValue)
{
}
else if (_cccData != null)
{
foreach (var mapping in _cccData)
{
// Recorrer la lista Codes
foreach (var code in mapping.Codes)
{
if (code.InitialValue is string initialValueString &&
double.TryParse(initialValueString, culture, out var initialValueDouble))
code.InitialValue = initialValueDouble; // Convertir y guardar como double
if (code.FinalValue is string finalValueString &&
double.TryParse(finalValueString, culture, out var finalValueDouble))
code.FinalValue = finalValueDouble; // Convertir y guardar como double
}
// Recorrer la lista Value (si no es nula)
if (mapping.Value != null)
foreach (var value in mapping.Value)
{
if (value.InitialValue is string initialValueString &&
double.TryParse(initialValueString, culture, out var initialValueDouble))
value.InitialValue = initialValueDouble; // Convertir y guardar como double
if (value.FinalValue is string finalValueString &&
double.TryParse(finalValueString, culture, out var finalValueDouble))
value.FinalValue = finalValueDouble; // Convertir y guardar como double
}
} //Fin ford
_isTransformedValue = true;
}
}
}
}
+42 -21
View File
@@ -2,6 +2,9 @@
namespace adas_core.Domain.Utils;
/// <summary>
/// Provides static utility methods for handling media-related operations.
/// </summary>
public static class MediaUtils
{
public static readonly Dictionary<string, string> Mapping = new(StringComparer.InvariantCultureIgnoreCase)
@@ -89,32 +92,50 @@ public static class MediaUtils
{ ".wmv", "video/x-ms-wmv" }
};
/// <summary>
/// Determines the MIME type associated with the file extension of the specified path.
/// Performs a case-insensitive lookup against a known extension mapping and falls back to <c>application/octet-stream</c> when the extension is not recognized.
/// </summary>
/// <param name="filePath">The file path whose extension is used to resolve the MIME type.</param>
/// <returns>The corresponding MIME type string if the extension is found in the mapping; otherwise, the default <c>application/octet-stream</c> value.</returns>
public static string GetMimeType(string filePath)
{
var extension = Path.GetExtension(filePath).ToLowerInvariant();
return Mapping.TryGetValue(extension, out var mimeType) ? mimeType : "application/octet-stream";
}
{
var extension = Path.GetExtension(filePath).ToLowerInvariant();
return Mapping.TryGetValue(extension, out var mimeType) ? mimeType : "application/octet-stream";
}
/// <summary>
/// Determines whether the specified file path corresponds to a supported video file by comparing its extension against a predefined list of video formats (e.g., .mp4, .avi, .mkv, .mov).
/// The comparison is case-insensitive.
/// </summary>
/// <param name="filePath">The path of the file to evaluate.</param>
/// <returns><c>true</c> if the file extension matches one of the supported video extensions; otherwise, <c>false</c>.</returns>
public static bool IsVideoFile(string filePath)
{
// Extensiones de video soportadas
var videoExtensions = new[]
{ ".mp4", ".avi", ".mkv", ".mpeg", ".ogv", ".webm", ".3gp", ".3g2", ".mov", ".wmv", ".m3u8" };
return videoExtensions.Contains(Path.GetExtension(filePath), StringComparer.InvariantCultureIgnoreCase);
}
{
// Extensiones de video soportadas
var videoExtensions = new[]
{ ".mp4", ".avi", ".mkv", ".mpeg", ".ogv", ".webm", ".3gp", ".3g2", ".mov", ".wmv", ".m3u8" };
return videoExtensions.Contains(Path.GetExtension(filePath), StringComparer.InvariantCultureIgnoreCase);
}
/// <summary>
/// Retrieves the duration of the video at the specified path, expressed in milliseconds.
/// Returns null if the duration cannot be read, with the underlying error logged to the console.
/// </summary>
/// <param name="videoPath">The file path of the video whose duration should be retrieved.</param>
/// <returns>The video duration in milliseconds, or null if an error occurs while reading the file.</returns>
public static long? GetVideoDuration(string videoPath)
{
try
{
var file = File.Create(videoPath);
var duration = file.Properties.Duration;
return (long)duration.TotalMilliseconds;
try
{
var file = File.Create(videoPath);
var duration = file.Properties.Duration;
return (long)duration.TotalMilliseconds;
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
return null;
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
return null;
}
}
}
+29 -16
View File
@@ -1,24 +1,37 @@
namespace adas_core.Domain.Utils;
/// <summary>
/// Provides a collection of static utility methods for performing common numeric operations.
/// </summary>
public static class NumberUtils
{
/// <summary>
/// Determines whether the specified object is an instance of any built-in numeric type, including signed and unsigned integral types as well as floating-point and decimal types.
/// </summary>
/// <param name="value">The object to evaluate.</param>
/// <returns><c>true</c> if <paramref name="value"/> is one of the supported numeric types (sbyte, byte, short, ushort, int, uint, long, ulong, float, double, or decimal); otherwise, <c>false</c>.</returns>
public static bool IsNumber(this object value)
{
return value is sbyte
|| value is byte
|| value is short
|| value is ushort
|| value is int
|| value is uint
|| value is long
|| value is ulong
|| value is float
|| value is double
|| value is decimal;
}
{
return value is sbyte
|| value is byte
|| value is short
|| value is ushort
|| value is int
|| value is uint
|| value is long
|| value is ulong
|| value is float
|| value is double
|| value is decimal;
}
/// <summary>
/// Converts the specified object to a double-precision floating-point number using the underlying <see cref="Convert.ToDouble(object)"/> conversion.
/// </summary>
/// <param name="value">The object to convert to a <see cref="double"/>.</param>
/// <returns>A <see cref="double"/> that represents the converted value.</returns>
public static double ToDouble(this object value)
{
return Convert.ToDouble(value);
}
{
return Convert.ToDouble(value);
}
}
+52 -27
View File
@@ -3,39 +3,64 @@ using Newtonsoft.Json;
namespace adas_core.Domain.Utils;
/// <summary>
/// Provides a JSON converter for serializing and deserializing <see cref="ObjectId"/> values.
/// </summary>
/// <remarks>
/// Inherits from <see cref="JsonConverter"/> to customize JSON representation for the type it targets.
/// </remarks>
public class ObjectIdConverter : JsonConverter
{
/// <summary>
/// Serializes an <see cref="ObjectId"/> instance to JSON by writing its string representation; if the value is not an <see cref="ObjectId"/>, writes a JSON null instead.
/// </summary>
/// <param name="writer">The <see cref="JsonWriter"/> used to emit the JSON output.</param>
/// <param name="value">The value to serialize, expected to be an <see cref="ObjectId"/> instance.</param>
/// <param name="serializer">The <see cref="JsonSerializer"/> invoking this converter.</param>
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value is ObjectId objectId)
// Serializa el ObjectId como un string
writer.WriteValue(objectId.ToString());
else
writer.WriteNull();
}
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue,
JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.String)
{
var stringValue = reader.Value as string;
// Verifica si el valor es el valor predeterminado de ObjectId
if (!string.IsNullOrEmpty(stringValue) &&
stringValue != "000000000000000000000000" &&
ObjectId.TryParse(stringValue, out var oid))
return oid;
if (value is ObjectId objectId)
// Serializa el ObjectId como un string
writer.WriteValue(objectId.ToString());
else
writer.WriteNull();
}
// Nullables reciben null, no-null vuelven Empty
if (objectType == typeof(ObjectId))
return ObjectId.Empty;
// Retorna null o ObjectId.Empty para manejar el valor predeterminado de ObjectId
return null;
}
/// <summary>
/// Reads a JSON value and converts it to an <see cref="ObjectId"/> instance. When the token is a non-empty string different from the default ObjectId representation and parses successfully, the parsed value is returned; otherwise the method falls back to <see cref="ObjectId.Empty"/> for non-nullable target types or <c>null</c> for nullable target types.
/// </summary>
/// <param name="reader">The JSON reader positioned on the value to deserialize.</param>
/// <param name="objectType">The target type expected by the deserializer, used to decide between returning <see cref="ObjectId.Empty"/> or <c>null</c> when the value cannot be parsed.</param>
/// <param name="existingValue">The existing value being populated, passed through from the deserializer.</param>
/// <param name="serializer">The JSON serializer invoking this converter.</param>
/// <returns>An <see cref="ObjectId"/> instance when the value can be parsed or when the target type is non-nullable; <c>null</c> when the target type is nullable and no valid value is found.</returns>
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue,
JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.String)
{
var stringValue = reader.Value as string;
// Verifica si el valor es el valor predeterminado de ObjectId
if (!string.IsNullOrEmpty(stringValue) &&
stringValue != "000000000000000000000000" &&
ObjectId.TryParse(stringValue, out var oid))
return oid;
}
// Nullables reciben null, no-null vuelven Empty
if (objectType == typeof(ObjectId))
return ObjectId.Empty;
// Retorna null o ObjectId.Empty para manejar el valor predeterminado de ObjectId
return null;
}
/// <summary>
/// Determines whether the converter can convert the specified type by checking if the type is assignable from <see cref="ObjectId"/>.
/// </summary>
/// <param name="objectType">The type to evaluate for compatibility with the converter.</param>
/// <returns><see langword="true"/> if <paramref name="objectType"/> can be assigned from <see cref="ObjectId"/>; otherwise, <see langword="false"/>.</returns>
public override bool CanConvert(Type objectType)
{
return typeof(ObjectId).IsAssignableFrom(objectType);
}
{
return typeof(ObjectId).IsAssignableFrom(objectType);
}
}
+24 -15
View File
@@ -2,24 +2,33 @@
namespace adas_core.Domain.Utils;
/// <summary>
/// Provides static utility methods for common object-related operations.
/// </summary>
public static class ObjectUtils
{
/// <summary>
/// Converts an object to a string representation showing its type name and non-null property values in the format "TypeName[Property1: Value1,Property2: Value2]".
/// Returns "[OBJ NULL]" when the object is null, and skips properties whose values are null.
/// </summary>
/// <param name="obj">The object to convert to a string. Can be null.</param>
/// <returns>A string containing the type name followed by the object's non-null property values and their names.</returns>
public static string ToStr(this object? obj)
{
if (obj == null) return "[OBJ NULL]";
var type = obj.GetType();
//if (type == null) return "[OBJ TYPE NULL]";
StringBuilder sb = new();
sb.Append(type.Name + "[");
foreach (var property in obj.GetType().GetProperties())
{
var value = obj.GetType().GetProperty(property.Name)?.GetValue(obj);
if (value == null) continue;
sb.Append($"{property.Name}: {value},");
if (obj == null) return "[OBJ NULL]";
var type = obj.GetType();
//if (type == null) return "[OBJ TYPE NULL]";
StringBuilder sb = new();
sb.Append(type.Name + "[");
foreach (var property in obj.GetType().GetProperties())
{
var value = obj.GetType().GetProperty(property.Name)?.GetValue(obj);
if (value == null) continue;
sb.Append($"{property.Name}: {value},");
}
sb.Append(']');
return sb.ToString();
}
sb.Append(']');
return sb.ToString();
}
}
+40 -23
View File
@@ -5,6 +5,9 @@ using Serilog;
namespace adas_core.Domain.Utils;
/// <summary>
/// Provides helper methods for relaying operations, data, or commands between components.
/// </summary>
public class RelayHelper
{
public static bool GetRelayStatusFromApiRest(Relay relay)
@@ -56,31 +59,40 @@ public class RelayHelper
//TODO son provisionales mientras se añade como nuget Smacs.Divers
/// <summary>
/// Powers on the specified relay by constructing a request to the local relay control endpoint at https://localhost:7186, where the relay is identified by its <see cref="Relay.RelayNumber"/> in the path.
/// </summary>
/// <param name="relay">The relay to power on.</param>
public static void PowerOnRelay(Relay relay)
{
UriBuilder builder = new()
{
Scheme = "https",
Host = "localhost",
Port = 7186,
Path = $"{relay.RelayNumber}/powerOn"
};
PowerRelay(relay, builder);
}
UriBuilder builder = new()
{
Scheme = "https",
Host = "localhost",
Port = 7186,
Path = $"{relay.RelayNumber}/powerOn"
};
PowerRelay(relay, builder);
}
/// <summary>
/// Sends a power off command to the specified relay by building a request to the local relay control endpoint
/// using the relay's number as the path identifier, then forwarding the call to the underlying relay handler.
/// </summary>
/// <param name="relay">The relay to power off, identified by its <see cref="Relay.RelayNumber"/> which is used to construct the request path.</param>
public static void PowerOffRelay(Relay relay)
{
UriBuilder builder = new()
{
Scheme = "https",
Host = "localhost",
Port = 7186,
Path = $"{relay.RelayNumber}/powerOff"
};
PowerRelay(relay, builder);
}
UriBuilder builder = new()
{
Scheme = "https",
Host = "localhost",
Port = 7186,
Path = $"{relay.RelayNumber}/powerOff"
};
PowerRelay(relay, builder);
}
private static void PowerRelay(Relay relay, UriBuilder builder)
{
@@ -111,8 +123,13 @@ public class RelayHelper
}
/// <summary>
/// Retrieves the current status of the specified relay. Currently always returns <c>false</c>, indicating the relay status is not available or is treated as inactive.
/// </summary>
/// <param name="relay">The relay whose status is being queried.</param>
/// <returns><c>true</c> if the relay is active; otherwise, <c>false</c>.</returns>
public static bool GetRelayStatus(Relay relay)
{
return false;
}
{
return false;
}
}
+48 -22
View File
@@ -1,34 +1,60 @@
namespace adas_core.Domain.Utils;
/// <summary>
/// Provides static extension methods for the <see cref="string"/> type.
/// </summary>
public static class StringEx
{
/// <summary>
/// Determines whether the specified string is <c>null</c> or an empty string.
/// </summary>
/// <param name="source">The string to evaluate.</param>
/// <returns><c>true</c> if <paramref name="source"/> is <c>null</c> or an empty string; otherwise, <c>false</c>.</returns>
public static bool IsEmpty(this string source)
{
return string.IsNullOrEmpty(source);
}
{
return string.IsNullOrEmpty(source);
}
/// <summary>
/// Determines whether the specified string is null, empty, or consists only of white-space characters.
/// </summary>
/// <param name="source">The string to evaluate.</param>
/// <returns>true if the string is null, empty, or contains only white space; otherwise, false.</returns>
public static bool IsEmptyOrWhiteSpace(this string source)
{
return string.IsNullOrWhiteSpace(source);
}
{
return string.IsNullOrWhiteSpace(source);
}
/// <summary>
/// Returns the portion of the source string that follows the first occurrence of the specified value, using ordinal (case-sensitive, culture-insensitive) comparison. If the source or value is null or empty, or the value is not found within the source, the original source is returned unchanged.
/// </summary>
/// <param name="source">The string to search within.</param>
/// <param name="value">The delimiter whose first occurrence marks the start of the returned substring.</param>
/// <returns>The substring after the first occurrence of <paramref name="value"/>; otherwise, the original <paramref name="source"/>.</returns>
public static string SubstringAfter(this string source, string value)
{
if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(value)) return source;
var index = source.IndexOf(value, StringComparison.Ordinal);
return index >= 0
? source.Substring(index + value.Length)
: source;
}
{
if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(value)) return source;
var index = source.IndexOf(value, StringComparison.Ordinal);
return index >= 0
? source.Substring(index + value.Length)
: source;
}
/// <summary>
/// Returns the portion of <paramref name="source"/> that precedes the first occurrence of <paramref name="value"/>, using ordinal comparison.
/// If either <paramref name="source"/> or <paramref name="value"/> is null or empty, or if <paramref name="value"/> is not found within <paramref name="source"/>, the original <paramref name="source"/> is returned unchanged.
/// </summary>
/// <param name="source">The string to extract the substring from.</param>
/// <param name="value">The delimiter whose first occurrence marks the end of the returned substring.</param>
/// <returns>The substring of <paramref name="source"/> before the first occurrence of <paramref name="value"/>, or the original <paramref name="source"/> when no match is found or when either input is null or empty.</returns>
public static string SubstringBefore(this string source, string value)
{
if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(value)) return source;
var index = source.IndexOf(value, StringComparison.Ordinal);
return index >= 0
? source.Substring(0, index)
: source;
}
{
if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(value)) return source;
var index = source.IndexOf(value, StringComparison.Ordinal);
return index >= 0
? source.Substring(0, index)
: source;
}
}