Files
adas-core/adas-core.Application/Services/ConfigObservationService.cs

642 lines
26 KiB
C#

using System.Collections.Concurrent;
using adas_core.Application.Exceptions;
using adas_core.Application.Repositories.Interfaces;
using adas_core.Application.Services.Interfaces;
using adas_core.Domain.Enums;
using adas_core.Domain.Models;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.DTO;
using adas_core.Domain.Models.Filter;
using adas_core.Domain.Models.GroupedObservations;
using adas_core.Domain.Models.MongoModels;
using adas_core.Domain.Models.Pumps;
using adas_core.Domain.Models.Responses;
using adas_core.Domain.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
namespace adas_core.Application.Services;
public class ConfigObservationService : IConfigObservationService
{
private static readonly ConcurrentDictionary<ObjectId, ConfigObservationCached> CachedConfigObservations = new();
private static readonly ConcurrentDictionary<string, ConfigObservationKeyCached>
CachedConfigObservationKeys = new();
private readonly IOptions<ApiSettings> _apiSettings;
private readonly ILocalAuditService _auditService;
private readonly IConfigObservationRepository _configObservationRepository;
private readonly RetentionPolicy _defaultRetentionPolicy;
private readonly int _defaultRetentionPolicyValue;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly bool _ignoreUnknownTreatment;
private readonly ILogger<ConfigObservationService> _logger;
private readonly int? _refreshTimeout;
private readonly IUnitService _unitService;
private readonly ICacheService _cacheService;
private readonly CacheSettings? _cacheSettings;
private bool IgnoreUnknownObservation =>
_apiSettings.Value.ConfigObservation?.IgnoreUnknownObservation ?? false;
public ConfigObservationService(
IConfigObservationRepository configObservationRepository,
IOptions<ApiSettings> apiSettings,
IOptions<CacheSettings> cacheSettings,
ILogger<ConfigObservationService> logger,
IUnitService unitService,
IHttpContextAccessor httpContextAccessor,
ILocalAuditService auditService,
ICacheService cacheService
)
{
_cacheService = cacheService;
_cacheSettings = cacheSettings.Value;
_configObservationRepository = configObservationRepository;
_apiSettings = apiSettings;
_logger = logger;
_unitService = unitService;
_refreshTimeout = _apiSettings.Value.ConfigObservation?.Refresh;
_ignoreUnknownTreatment = _apiSettings.Value.ConfigObservation?.IgnoreUnknownTreatment ?? false;
_defaultRetentionPolicyValue = _apiSettings.Value.RetentionPolicyValue;
_defaultRetentionPolicy = RetentionPolicy.NoDelete;
_httpContextAccessor = httpContextAccessor;
_auditService = auditService;
}
public async Task<ICollection<ConfigObservation>> GetAllConfigs(CancellationToken ct = default)
{
var (key, ttl) = CacheKeys.ConfigObservationsAllKeyWithTtl(_cacheSettings);
var result = await _cacheService.GetOrSetObjectAsync(
key,
async () => await _configObservationRepository.FindAll(),
ttl,
ct);
return result;
}
public async Task<ConfigObservationDto> GetAllCompact()
{
var count = await _configObservationRepository.Count();
return new ConfigObservationDto { ItemCount = count };
}
public async Task<PaginationResponse<ConfigObservation>> GetPaginatedItems(PaginationFilter filter)
{
var result = await _configObservationRepository.GetPaginatedItems(filter);
var count = await _configObservationRepository.Count();
return new PaginationResponse<ConfigObservation>(result.ToList(), filter.PageNumber, filter.PageSize,
count);
}
public async Task<ConfigObservation?> GetConfigById(ObjectId id)
{
return await _configObservationRepository.FindById(id) ?? null;
}
public async Task<List<string>> GetConfigNames(string id)
{
return await _configObservationRepository.GetConfigNames(id);
}
public async Task<List<string>> GetConfigNames()
{
return await _configObservationRepository.GetConfigNames();
}
public async Task<ObservatitonRetentionResult?> RetentionActions<T>(T obs) where T : BasePatientObservation
{
var conf = await Get(obs);
return conf is { RetentionPolicy: not null } ?
new ObservatitonRetentionResult(conf.RetentionPolicy.Value, conf.RetentionPolicyValue) :
new ObservatitonRetentionResult(RetentionPolicy.NoDelete, null);
}
public async Task<StatusEnum.Type> GroupedObservationStatus(GroupedField groupedField,
GroupedObservationEnum.Result result,
string name, object value, double? min, double? max)
{
var conf = await Get(name);
if (conf == null) return StatusEnum.Type.Ok;
PatientObservation? mapObs;
if (conf.Grouped != null && groupedField.Group != null &&
conf.Grouped.TryGetValue(groupedField.Group, out var grp))
{
mapObs = await MapConf(new PatientObservation { Name = name, Value = value, Min = min, Max = max }, grp);
if (mapObs != null) return mapObs.Status;
}
if (conf.Grouped != null && conf.Grouped.ContainsKey(result.ToString()))
{
mapObs = await MapConf(new PatientObservation { Name = name, Value = value, Min = min, Max = max },
conf.Grouped[result.ToString()]);
if (mapObs != null) return mapObs.Status;
}
mapObs = await MapConf(new PatientObservation { Name = name, Value = value, Min = min, Max = max }, conf);
if (mapObs != null) return mapObs.Status;
return StatusEnum.Type.Ok;
}
public async Task<T?> Map<T>(T obs, bool onlyByName = false) where T : BasePatientObservation
{
var conf = onlyByName ? await Get(obs, onlyByName) : await Get(obs);
if (conf == null)
{
_logger.LogDebug("Ignore Unknown Observation. {Name} {Code} {CodingSystem}", obs.Name, obs.Code,
obs.CodingSystem);
return IgnoreUnknownObservation ? null : obs;
}
if (string.IsNullOrEmpty(conf.Name))
{
_logger.LogError("Config Name is null or empty. {conf}", conf);
return null;
}
return await MapConf(obs, conf);
}
public async Task<ConfigObservation?> RemoveConfigItem(ObjectId id)
{
var item = await _configObservationRepository.FindById(id);
if (item == null) return null;
var deleted = await _configObservationRepository.Delete(id);
// Invalidar CACHE (colección completa)
await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.ConfigObservations));
return deleted;
}
public async Task<PatientTreatment?> Map(PatientTreatment treatment)
{
ConfigObservation? conf = null;
foreach (var requestGiveCode in treatment.RequestedGiveCodes)
conf = string.IsNullOrEmpty(requestGiveCode.CodingSystem) &&
string.IsNullOrEmpty(requestGiveCode.Identifier)
? await Get(requestGiveCode.Text)
: await GetByCodeSysAndCode(requestGiveCode.CodingSystem, requestGiveCode.Identifier);
if (conf == null) return _ignoreUnknownTreatment ? null : treatment;
return conf.Name == null ? null : treatment;
}
public async Task<ConfigObservation?> Get<T>(T obs, bool onlyByName = false)
where T : BasePatientObservation
{
var items = await GetAllConfigs();
if (items.Count == 0) return null;
if (onlyByName)
{
var configItem = items.FirstOrDefault(i => i.Name == obs.Name);
if (configItem != null) return await Process(configItem);
_logger.LogError("Error getting Config observation Item. Observation: {obs}", obs);
return null;
}
ConfigObservation? item = null;
if ((!string.IsNullOrEmpty(obs.Code) && !string.IsNullOrEmpty(obs.CodingSystem)) ||
obs.ParentData is { Code: not null, CodingSystem: not null })
foreach (var obsConfig in items)
{
if (obs.Code != null && obs.Code != obsConfig.Code)
continue;
if (obsConfig.CodingSystem != null && obs.CodingSystem != obsConfig.CodingSystem)
continue;
if (obsConfig.OriginalName != null && obs.Name != obsConfig.OriginalName)
continue;
if (obsConfig.ParentCode != null && obs.ParentData?.Code != obsConfig.ParentCode)
continue;
if (obsConfig.ParentCodingSystem != null &&
obs.ParentData?.CodingSystem != obsConfig.ParentCodingSystem)
continue;
if (obsConfig.ParentName != null && obs.ParentData?.Name != obsConfig.ParentName)
continue;
if (obsConfig.OriginalName == null && obsConfig.Name != null && obs.Name != null &&
obs.Name.Contains("Alarm") && obs.Name != obsConfig.Name)
continue;
if (obsConfig.Code == null && obsConfig.CodingSystem == null && obsConfig.ParentCode == null &&
obsConfig.ParentCodingSystem == null && obs.Name != null && !obs.Name.Contains("Alarm"))
continue;
if (obsConfig.Code == null &&
obsConfig is { CodingSystem: not null, ParentCode: null, ParentCodingSystem: null } &&
obs.Name != null && !obs.Name.Contains("Alarm"))
continue;
if (obsConfig.Code == null && obsConfig.CodingSystem == null && obsConfig.ParentCode == null &&
obsConfig.ParentCodingSystem != null && obs.Name != null && !obs.Name.Contains("Alarm"))
continue;
if (obsConfig.Code == null &&
obsConfig is { CodingSystem: not null, ParentCode: null, ParentCodingSystem: not null } &&
obs.Name != null && !obs.Name.Contains("Alarm"))
continue;
item = obsConfig;
break;
}
else item = items.FirstOrDefault(i => i.Name == obs.Name);
return item != null ? await Process(item) : null;
}
// public async Task<List<ConfigObservation>> GetAlarmWithRecordingConfig(ObjectId patientId)
// {
// var configObservationId = await GetConfigObservationKeyFromPatientId(patientId);
// var configObservation = await GetConfig(configObservationId);
// return configObservation?.Items
// .Where(i => i is { CodingSystem: "ADAS_ALARM", Alarm.Recording.Enabled: true })
// .ToList() ?? [];
// }
public async Task<ConfigObservation?> GetByCodeSysAndCode(string? codingSystem, string? code)
{
var filteredResult = await _configObservationRepository.GetByCodeSysAndCode(codingSystem, code);
if (filteredResult != null) return await Process(filteredResult);
_logger.LogWarning("Config observation item not found. CodingSystem: {codingSystem} code: {code} ",
codingSystem ?? "null", code ?? "null");
return null;
}
public async Task<ConfigObservation?> Get(string? name)
{
if (string.IsNullOrEmpty(name)) return null;
var result = await GetConfigByName(name);
if (result == null)
{
// _logger.LogWarning(
// "Config observation item not found. name: {name} configObservationId: {configObservationId} ",
// name, name);
return null;
}
return await Process(result);
}
public async Task<ConfigObservation?> UpdateConfig(ConfigObservation configObservationItem)
{
// if (!configObservationItem.Id.HasValue)
// return await _configObservationRepository.InsertOneAsyncAndReturn(configObservationItem);
var configObservation = await _configObservationRepository.FindById(configObservationItem.Id) ??
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
var updatedConfig = await _configObservationRepository.Update(configObservation);
// Invalidar CACHE (colección completa)
await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.ConfigObservations));
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, configObservation,
updatedConfig!);
return updatedConfig;
}
public async Task<IEnumerable<ConfigObservation>?> GetConfigObservationItem(string name)
{
var configObservationItems = await _configObservationRepository.FindAllByName(name);
if (configObservationItems.Count != 0)
return configObservationItems;
_logger.LogWarning("Cant get config item, config not found, name: {name} ", name);
throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
}
public async Task<ConfigObservation?> GetSingleConfigObservationItem(string? code, string? codingSystem,
string? name, string? originalName)
{
var matchingItem =
await _configObservationRepository.GetSingleConfigObservationItem(code, codingSystem, name, originalName);
if (matchingItem == null) throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundNoMatches);
return matchingItem;
}
public async Task<bool> DeleteSingleConfigObservationItem(ConfigObservation configObservationItem)
{
var configObservation = await _configObservationRepository.FindById(configObservationItem.Id) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed);
var auxConfigObservation = await _auditService.DeepCopyAsync(configObservation);
_ = await _configObservationRepository.DeleteAsync(configObservation.Id) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed);
// Invalidar CACHE (colección completa)
await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.ConfigObservations));
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, auxConfigObservation,
configObservation);
return true;
}
public async Task<IEnumerable<ConfigObservation>> GetConfigObservationItemsByName(string name)
{
return await _configObservationRepository.FindAllByName(name);
}
public async Task<ConfigObservation?> CreateConfig(ConfigObservation configObservation)
{
var existing = await _configObservationRepository.FindById(configObservation.Id);
if (existing != null)
throw new BadRequestException(HttpEnum.ErrorMessage.ConflictCreationFailed);
await _configObservationRepository.InsertOneAsyncAndReturn(configObservation);
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, configObservation);
return configObservation;
}
public async Task<ConfigObservation?> RemoveConfigItem(string itemName)
{
var configObservation = await GetConfigByName(itemName) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed);
var auxConfigObservation = await GetConfigByName(itemName) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed);
await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, auxConfigObservation,
configObservation);
var result = await _configObservationRepository.Delete(configObservation.Id!);
// Invalidar CACHE (colección completa)
await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.ConfigObservations));
return result;
}
public async Task<ConfigObservation?> GetConfig(ObjectId configObservationId)
{
RefreshCachedConfigObservations();
if (CachedConfigObservations.TryGetValue(configObservationId, out var cached)
&& DateTime.Now <= cached.NextRefresh)
return cached.ConfigObservation;
var config = await _configObservationRepository.FindById(configObservationId);
cached = new ConfigObservationCached
{
ConfigObservation = config ?? new ConfigObservation(),
NextRefresh = _refreshTimeout.HasValue ? DateTime.Now.AddSeconds(_refreshTimeout.Value) : DateTime.MinValue
};
CachedConfigObservations[configObservationId] = cached;
return cached.ConfigObservation;
}
public async Task<ConfigObservation?> GetConfigByName(string name)
{
if (string.IsNullOrWhiteSpace(name)) return null;
RefreshCachedConfigObservations();
var cachedItem = CachedConfigObservations.Values
.FirstOrDefault(cached =>
DateTime.Now <= cached.NextRefresh &&
cached.ConfigObservation is { Name: not null } &&
cached.ConfigObservation.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (cachedItem != null) return cachedItem.ConfigObservation;
var config = await _configObservationRepository.FindByName(name);
if (config == null) return null;
var newCachedItem = new ConfigObservationCached
{
ConfigObservation = config,
NextRefresh = _refreshTimeout.HasValue
? DateTime.Now.AddSeconds(_refreshTimeout.Value)
: DateTime.MinValue
};
CachedConfigObservations[config.Id!] = newCachedItem;
return config;
}
private void RefreshCachedConfigObservations()
{
if (!_refreshTimeout.HasValue) return;
// Use ConcurrentDictionary's thread-safe features to identify and remove expired keys
var keysToRemove = CachedConfigObservations
.Where(k => DateTime.Now > k.Value.NextRefresh)
.Select(k => k.Key)
.ToList();
foreach (var key in keysToRemove) CachedConfigObservations.TryRemove(key, out _);
}
private Task<ConfigObservation> Process(ConfigObservation confItem)
{
confItem.RetentionPolicy ??= _defaultRetentionPolicy;
if (confItem.RetentionPolicy != RetentionPolicy.NoDelete && !confItem.RetentionPolicyValue.HasValue)
{
confItem.RetentionPolicyValue = _defaultRetentionPolicyValue;
if (confItem.RetentionPolicyValue <= 0) confItem.RetentionPolicyValue = null;
}
else
{
confItem.RetentionPolicyValue = null;
}
return Task.FromResult(confItem);
}
private void RefreshCachedConfigObservationKeys()
{
try
{
if (!_refreshTimeout.HasValue) return;
// Use ConcurrentDictionary's thread-safe features to identify and remove expired keys
var keysToRemove = CachedConfigObservationKeys
.Where(k => DateTime.Now > k.Value.NextRefresh)
.Select(k => k.Key)
.ToList();
foreach (var key in keysToRemove) CachedConfigObservationKeys.TryRemove(key, out _);
}
catch (Exception ex)
{
_logger.LogError("Error refreshing cached config observation keys. Exception: {ex}", ex);
}
}
private async Task<T?> MapConf<T>(T obs, ConfigObservation conf) where T : BasePatientObservation
{
_logger.LogTrace("Mapping observation: {obs}", obs);
obs.Name = conf.Name;
if (obs is PatientObservation)
{
var noCalculateStatusWithCodingSystem = _apiSettings.Value.NoCalculateStatusWithCodingSystem;
if (noCalculateStatusWithCodingSystem != null &&
obs.CodingSystem == noCalculateStatusWithCodingSystem) return obs;
if (obs is not PatientObservation pobs)
return obs;
if (conf.MaxAlert != null && (conf.ForceAlert || !pobs.Max.HasValue)) pobs.Max = conf.MaxAlert;
if (conf.MinAlert.HasValue && (conf.ForceAlert || !pobs.Min.HasValue)) pobs.Min = conf.MinAlert;
if (conf.MaxWarn.HasValue && (conf.ForceWarn || !pobs.MaxWarn.HasValue)) pobs.MaxWarn = conf.MaxWarn;
if (conf.MinWarn.HasValue && (conf.ForceWarn || !pobs.MinWarn.HasValue)) pobs.MinWarn = conf.MinWarn;
if (conf.LevelCondition != null)
try
{
var evalCondition = await CSharpScript.EvaluateAsync(conf.LevelCondition, globals: pobs);
if (int.TryParse(evalCondition.ToString(), out var evalInt))
pobs.Level = evalInt;
else
_logger.LogError("Error evaluating condition: {evalCondition} for obs: {pobs}",
evalCondition, pobs);
}
catch (CompilationErrorException e)
{
_logger.LogError("Error evaluating expression for obs: {obs} error: {diagnostics}", obs,
e.Diagnostics);
}
catch (Exception e)
{
_logger.LogError("Error evaluating expression for obs: {obs} error: {e}", obs, e);
}
pobs.Status = StatusEnum.Type.Ok;
if (pobs.Value.IsNumber())
{
if (pobs.Min.HasValue && pobs.Min > pobs.Value.ToDouble() &&
conf.Alert is null or '<')
{
pobs.Status = StatusEnum.Type.Alert;
pobs.AlertColor = conf.AlertColor;
}
else if (pobs.Max.HasValue && pobs.Max < pobs.Value.ToDouble() &&
conf.Alert is null or '>')
{
pobs.Status = StatusEnum.Type.Alert;
pobs.AlertColor = conf.AlertColor;
}
else if (pobs.MinWarn.HasValue && pobs.MinWarn > pobs.Value.ToDouble() &&
conf.Alert is null or '<')
{
pobs.Status = StatusEnum.Type.Warning;
pobs.WarnColor = conf.WarnColor;
}
else if (pobs.MaxWarn.HasValue && pobs.MaxWarn < pobs.Value.ToDouble() &&
conf.Alert is null or '>')
{
pobs.Status = StatusEnum.Type.Warning;
pobs.WarnColor = conf.WarnColor;
}
}
else if (pobs.Value is string)
{
if (conf.AlertValues != null && conf.AlertValues.Any() && conf.AlertValues.Contains(pobs.Value))
pobs.Status = StatusEnum.Type.Alert;
else if (conf.WarningValues != null && conf.WarningValues.Any() &&
conf.WarningValues.Contains(pobs.Value)) pobs.Status = StatusEnum.Type.Warning;
}
if (conf.Expires is > 0)
{
pobs.Expires = conf.Expires;
//check if already is expired
var expireTime = obs.Time.ToLocalTime().AddSeconds(Convert.ToDouble(conf.Expires));
pobs.Expired = DateTime.Now.CompareTo(expireTime) > 0;
}
pobs.ShowOnExpired = conf.ShowOnExpired;
if (conf.ColorOnExpired != null) pobs.ColorOnExpired = conf.ColorOnExpired;
if (conf.Persist != null)
pobs.Persist = conf.Persist;
if (conf.UiConfiguration != null && conf.UiConfiguration.Any()) pobs.UiConfiguration = conf.UiConfiguration;
if (conf.Alarm != null) pobs.Alarm = conf.Alarm;
if (conf.TimeFromMessageTime)
if (pobs.MessageTime.CompareTo(DateTime.MinValue) != 0)
pobs.Time = pobs.MessageTime;
if (pobs.Units == null || conf.ForceUnits) pobs.Units = conf.Units;
if (conf.CreateObservation != null) pobs.CreateObservation = conf.CreateObservation;
pobs.CheckObservations = conf.CheckObservations;
}
if (obs is PatientObservationAlarm)
{
if (obs is not PatientObservationAlarm pobs)
return obs;
if (conf.Expires is > 0)
{
pobs.Expires = conf.Expires;
//check if already is expired
var expireTime = obs.Time.ToLocalTime().AddSeconds(Convert.ToDouble(conf.Expires));
pobs.Expired = DateTime.Now.CompareTo(expireTime) > 0;
}
if (conf.Persist != null)
pobs.Persist = conf.Persist;
if (conf.Alarm != null) pobs.AlarmConfig = conf.Alarm;
if (conf.AlertColor != null) pobs.AlertColor = conf.AlertColor;
if (conf.TimeFromMessageTime)
if (pobs.MessageTime.CompareTo(DateTime.MinValue) != 0)
pobs.Time = pobs.MessageTime;
if (pobs.Units == null || conf.ForceUnits) pobs.Units = conf.Units;
if (conf.CreateObservation != null) pobs.CreateObservation = conf.CreateObservation;
pobs.CheckObservations = conf.CheckObservations;
}
_logger.LogTrace("Observation mapped: {obs}", obs);
return obs;
}
private class ConfigObservationCached
{
public DateTime NextRefresh { get; set; }
public ConfigObservation? ConfigObservation { get; set; }
}
private class ConfigObservationKeyCached
{
public DateTime NextRefresh { get; set; }
public string? Key { get; set; }
}
}