using adas_core.Application.Exceptions; using adas_core.Application.Repositories.Interfaces; using adas_core.Domain.Enums; using adas_core.Domain.Models; using adas_core.Domain.Models.AppSettings; using adas_core.Domain.Models.Filter; using adas_core.Domain.Models.GroupedObservations; using adas_core.Domain.Models.MongoModels; using adas_core.Domain.Utils; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MongoDB.Bson; using MongoDB.Driver; using System.Diagnostics; using System.Text.RegularExpressions; using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils; namespace adas_core.Infrastructure.Repositories; public class ObservationRepository : MongoRepository, IObservationRepository { private readonly ApiSettings _apiSettings; private readonly ILogger _logger; public ObservationRepository( IOptions? apiSettings, IMongoDatabase database, ILogger logger) : base(database) { if (apiSettings != null) { _logger = logger; _apiSettings = apiSettings.Value; } else { throw new ArgumentNullException(nameof(apiSettings)); } } //For testing public override string GetCollectionName() { return _apiSettings.PatientsObservations ?? "patients_observations"; } public async Task> FindLastObservations(ObjectId patientId, string codingSystem, string code, int num = 2) { var filter = Builders.Filter.And( Builders.Filter.Eq(ob => ob.PatientId, patientId), Builders.Filter.Eq(ob => ob.CodingSystem, codingSystem), Builders.Filter.Eq(ob => ob.Code, code) ); var result = await Collection.FindAsync( filter, new FindOptions { Sort = Builders.Sort.Descending("time"), Limit = num } ); return result.ToEnumerable(); } public async Task> FindLastObservationsByCodingSystem(ObjectId patientId, string codingSystem, int num = 10) { var filter = Builders.Filter.And( Builders.Filter.Eq(ob => ob.PatientId, patientId), Builders.Filter.Eq(ob => ob.CodingSystem, codingSystem) ); var result = await Collection.FindAsync( filter, new FindOptions { Sort = Builders.Sort.Descending("time"), Limit = num } ); return await result.ToListAsync(); } public async Task> AggregatedPatientLastObservations(ObjectId patientId, int num, List? filterObservations = null) { filterObservations = await AggregatePatientObservations(patientId, filterObservations); var results = new List(); foreach (var obs in filterObservations) { var builder = Builders.Filter; var filter = builder.And( builder.Eq(o => o.PatientId, patientId), builder.Eq(o => o.Name, obs) ); var cursor = await Collection.FindAsync(filter, new FindOptions { Sort = Builders.Sort.Descending("time").Descending("id"), Limit = num }); results.AddRange(cursor.ToEnumerable()); } return results; } public async Task> AggregatedPatientLastObservationsByLastDate(ObjectId patientId, int num, DateTime lastDate, List? filterObservations = null) { filterObservations = await AggregatePatientObservations(patientId, filterObservations); var results = new List(); foreach (var obs in filterObservations) { var filter = Builders.Filter.And( Builders.Filter.Eq(o => o.PatientId, patientId), Builders.Filter.Eq(o => o.Name, obs), Builders.Filter.Lte(o => o.Time, lastDate) ); var cursor = await Collection.FindAsync( filter, new FindOptions { Sort = Builders.Sort.Descending("time").Descending("_id"), Limit = num } ); results.AddRange(await cursor.ToListAsync()); } return results; } public async Task> AggregatedPatientLastObservationsByField(ObjectId patientId, List? filterObservations) { try { var results = new List(); IAsyncCursor? cursor; var builder = Builders.Filter; if (filterObservations != null) { foreach (var obs in filterObservations) { FilterDefinition filter; if (obs is { OnlyExpired: true, Name: not null }) filter = builder.And( builder.Eq(o => o.PatientId, patientId), builder.Eq(o => o.Name, obs.Name), builder.Eq(o => o.Expired, obs.OnlyExpired) ); else filter = builder.And( builder.Eq(o => o.PatientId, patientId), builder.Eq(o => o.Name, obs.Name) ); cursor = await Collection.FindAsync( filter, new FindOptions { Sort = Builders.Sort.Descending("time").Descending("id"), Limit = obs.Last }); results.AddRange(cursor.ToEnumerable()); } } else { var filter = builder.Eq(o => o.PatientId, patientId); cursor = await Collection.FindAsync( filter, new FindOptions { Sort = Builders.Sort.Descending("time").Descending("id") }); results.AddRange(cursor.ToEnumerable()); } return results; } catch (Exception ex) { _logger.LogError("Error aggregated patient last observations by field {exMessage}", ex.Message); return []; } } public async Task> AggregatedPatientGroupedObservations(ObjectId patientId, GroupedField groupedField) { try { DateTime? lastObsTime = null; if (groupedField.Since == GroupedObservationEnum.Since.Last && !string.IsNullOrEmpty(groupedField.Name)) { var lastObsList = await AggregatedPatientLastObservations(patientId, 1, [groupedField.Name]); var lastObs = lastObsList.FirstOrDefault(); if (lastObs != null) { lastObsTime = lastObs.Time; lastObsTime = DateTime.SpecifyKind(lastObsTime.Value, DateTimeKind.Utc); } } var fields = new BsonArray(); var match = new BsonDocument { { "patientid", patientId } }; if (groupedField.GetNames().Count > 1) { groupedField.GetNames().ForEach(name => { fields.Add(new BsonDocument { { "name", name } }); }); match.Add("$or", fields); } else { match.Add("name", groupedField.Name); } var projectDate = new BsonDocument { { "y", new BsonDocument { { "$year", "$time" } } }, { "M", new BsonDocument { { "$month", "$time" } } }, { "d", new BsonDocument { { "$dayOfMonth", "$time" } } } }; var projectTimeFromParts = new BsonDocument { { "year", "$_id.year" }, { "month", "$_id.month" }, { "day", "$_id.day" } }; var idDocument = new BsonDocument { { "year", "$y" }, { "month", "$M" }, { "day", "$d" } }; var fromDate = DateTime.MinValue; if (groupedField.Regularity == GroupedObservationEnum.Regularity.Day) { if (groupedField.Since == GroupedObservationEnum.Since.Last && lastObsTime.HasValue) fromDate = lastObsTime.Value.AddDays(-1 * groupedField.Max); else fromDate = DateTime.UtcNow.AddDays(-1 * groupedField.Max); } if (groupedField.Regularity is GroupedObservationEnum.Regularity.Hour or GroupedObservationEnum.Regularity.Minute or GroupedObservationEnum.Regularity.Second or GroupedObservationEnum.Regularity.Times or GroupedObservationEnum.Regularity.Shift) { projectDate.Add("h", new BsonDocument { { "$hour", "$time" } }); idDocument.Add("hour", "$h"); projectTimeFromParts.Add("hour", "$_id.hour"); //fromDate = DateTime.UtcNow.AddHours(-1 * groupedField.max); if (groupedField.Since == GroupedObservationEnum.Since.Last && lastObsTime.HasValue) fromDate = new DateTime(lastObsTime.Value.Year, lastObsTime.Value.Month, lastObsTime.Value.Day, lastObsTime.Value.AddHours(1).Hour, 00, 00).AddHours(-1 * groupedField.Max); else fromDate = new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, DateTime.UtcNow.Day, DateTime.UtcNow.Hour, 00, 00).AddHours(-1 * groupedField.Max); } if (groupedField.Regularity == GroupedObservationEnum.Regularity.Minute || groupedField.Regularity == GroupedObservationEnum.Regularity.Second || groupedField.Regularity == GroupedObservationEnum.Regularity.Times //No queremos los minutos cuando pedimos por turno, en principio. Revisar si en algún caso necesitamos los minutos, quitado de momento por problemas // a la hora de devolver el last. //|| groupedField.regularity == Regularity.Shift ) { projectDate.Add("m", new BsonDocument { { "$minute", "$time" } }); idDocument.Add("minute", "$m"); projectTimeFromParts.Add("minute", "$_id.minute"); if (groupedField.Regularity != GroupedObservationEnum.Regularity.Shift) { if (groupedField.Since == GroupedObservationEnum.Since.Last && lastObsTime.HasValue) fromDate = lastObsTime.Value.AddMinutes(-1 * groupedField.Max); else fromDate = DateTime.UtcNow.AddMinutes(-1 * groupedField.Max); } } if (groupedField.Regularity == GroupedObservationEnum.Regularity.Second || groupedField.Regularity == GroupedObservationEnum.Regularity.Times) { projectDate.Add("s", new BsonDocument { { "$second", "$time" } }); idDocument.Add("second", "$s"); projectTimeFromParts.Add("second", "$_id.second"); if (groupedField.Since == GroupedObservationEnum.Since.Last && lastObsTime.HasValue) fromDate = lastObsTime.Value.AddSeconds(-1 * groupedField.Max); else fromDate = DateTime.UtcNow.AddSeconds(-1 * groupedField.Max); } if (groupedField.Regularity == GroupedObservationEnum.Regularity.Second || groupedField.Regularity == GroupedObservationEnum.Regularity.Times) { projectDate.Add("ms", new BsonDocument { { "$millisecond", "$time" } }); idDocument.Add("millisecond ", "$ms"); projectTimeFromParts.Add("millisecond", "$_id.millisecond"); } projectDate.Add("name", "$name"); projectDate.Add("min", "$min"); projectDate.Add("max", "$max"); projectDate.Add("time", "$time"); projectDate.Add("value", "$value"); idDocument.Add("name", "$name"); //No queremos meter el mínimo y el maximo en la agrupación. //idDocument.Add("min", "$min"); //idDocument.Add("max", "$max"); fromDate = DateTime.SpecifyKind(fromDate, DateTimeKind.Utc); if (groupedField.Max > 0) { //Add filter lte for ICCA future observations like hour balance. if (groupedField.Since == GroupedObservationEnum.Since.Last && lastObsTime.HasValue) match.Add("time", new BsonDocument("$gte", fromDate).Add("$lte", lastObsTime.Value)); else match.Add("time", new BsonDocument("$gte", fromDate).Add("$lte", DateTime.UtcNow)); } var group = new BsonDocument { { "_id", idDocument } }; if (groupedField.Result.Count == 0 || groupedField.Result.Contains(GroupedObservationEnum.Result.First)) group.Add("first", new BsonDocument { { "$first", new BsonDocument { { "time", "$time" }, { "value", "$value" }, { "min", "$min" }, { "max", "$max" } } } }); if (groupedField.Result.Contains(GroupedObservationEnum.Result.Last)) group.Add("last", new BsonDocument { { "$last", new BsonDocument { { "time", "$time" }, { "value", "$value" }, { "min", "$min" }, { "max", "$max" } } } }); if (groupedField.Result.Contains(GroupedObservationEnum.Result.LastFilled)) group.Add("lastfilled", new BsonDocument { { "$last", new BsonDocument { { "time", "$time" }, { "value", "$value" }, { "min", "$min" }, { "max", "$max" } } } }); if (groupedField.Result.Contains(GroupedObservationEnum.Result.Min)) group.Add("min", new BsonDocument { { "$min", new BsonDocument { { "value", "$value" }, { "time", "$time" }, { "min", "$min" }, { "max", "$max" } } } }); if (groupedField.Result.Contains(GroupedObservationEnum.Result.Max)) group.Add("max", new BsonDocument { { "$max", new BsonDocument { { "value", "$value" }, { "time", "$time" }, { "min", "$min" }, { "max", "$max" } } } }); if (groupedField.Result.Contains(GroupedObservationEnum.Result.Sum)) group.Add("sum", new BsonDocument { { "$sum", "$value" } }); if (groupedField.Result.Contains(GroupedObservationEnum.Result.Average)) group.Add("average", new BsonDocument { { "$avg", "$value" } }); if (groupedField.Result.Contains(GroupedObservationEnum.Result.Count)) group.Add("count", new BsonDocument { { "$sum", 1 } }); if (groupedField.Result.Contains(GroupedObservationEnum.Result.HalfHour)) group.Add("all", new BsonDocument { { "$push", new BsonDocument { { "value", "$value" }, { "time", "$time" }, { "min", "$min" }, { "max", "$max" } } } }); //group.Add("halfHour", new BsonDocument { { "$unset", new BsonDocument { { "time", "$time" }, { "value", "$value" }, { "min", "$min" }, { "max", "$max" } } } }); var pipeline = new List { new() { { "$match", match } }, new() { { "$sort", new BsonDocument { { "time", 1 } } } }, new() { { "$project", projectDate } }, new() { { "$group", group } }, new() { { "$addFields", new BsonDocument { { "time", new BsonDocument { { "$dateFromParts", projectTimeFromParts } } }, { "isFilled", false } } } }, new() { { "$sort", new BsonDocument { { "time", 1 } } } } }; if (groupedField is { Regularity: GroupedObservationEnum.Regularity.Times, Max: > 0 }) pipeline.Add(new BsonDocument("$limit", groupedField.Max)); //System.Diagnostics.Debug.WriteLine("AggregatedPatientGroupedObservations query: \n" + pipeline.ToJson()); _logger.LogDebug("Executing AggregatedPatientGroupedObservations query: {pipeline}:", pipeline.ToJson()); var resultsCursor = await Collection.AggregateAsync(pipeline, new AggregateOptions { AllowDiskUse = true }); var results = resultsCursor.ToList(); return results; } catch (Exception ex) { _logger.LogError("Error agregated patient grouped observations {exMessage}", ex.Message); return []; } } public new async Task InsertOneAsync(PatientObservation patientObservation) { const int maxRetries = 2; // Número máximo de reintentos var retryCount = 0; while (retryCount < maxRetries) try { await Collection.InsertOneAsync(patientObservation); return; } catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) { retryCount++; _logger.LogWarning( "Duplicate key error encountered. Retrying with new ObjectId. Attempt {attempt} of {maxRetries}", retryCount, maxRetries); if (retryCount < maxRetries) continue; _logger.LogError( "Maximum retry attempts reached. Could not insert document due to duplicate key error."); throw; // Relanzar la excepción después de alcanzar el número máximo de reintentos } catch (Exception ex) { _logger.LogError("Error inserting patient observation: {exMessage}", ex.Message); throw; } } public async Task DeleteByPatientId(ObjectId id) { var filter = Builders.Filter.Eq(po => po.PatientId, id); await Collection.DeleteManyAsync(filter); } public async Task> FindByPatientIdAsync(ObjectId patientId) { var filter = Builders.Filter.Eq(ob => ob.PatientId, patientId); return await Collection.FindAsync(filter, new FindOptions { BatchSize = 100 }); } public async Task> FindByPatientIdAndCodingSystemAsync(ObjectId patientId, string codingSystem, string name) { var filter = Builders.Filter.And( Builders.Filter.Eq(ob => ob.PatientId, patientId), Builders.Filter.Eq(ob => ob.CodingSystem, codingSystem), Builders.Filter.Eq(ob => ob.Name, name) ); return await Collection.FindAsync(filter, new FindOptions { BatchSize = 100 }); } public new async Task DeleteAsync(ObjectId id) { var filter = Builders.Filter.Eq(obs => obs.Id, id); await Collection.DeleteOneAsync(filter); } public async Task> DeleteOlderDaysAsync(string name, int retentionPolicyValue) { var dateLimit = DateTime.UtcNow.AddDays(-1 * retentionPolicyValue); var filter = Builders.Filter.And( Builders.Filter.Eq(obs => obs.Name, name), Builders.Filter.Lt(obs => obs.Time, dateLimit) ); var documentsToDelete = await Collection.Find(filter).ToListAsync(); await Collection.DeleteManyAsync(filter); return documentsToDelete; } public async Task> DeleteOlderSecondsAsync(string name, int retentionPolicyValue) { var dateLimit = DateTime.UtcNow.AddSeconds(-1 * retentionPolicyValue); var filter = Builders.Filter.And( Builders.Filter.Eq(obs => obs.Name, name), Builders.Filter.Lt(obs => obs.Time, dateLimit) ); var documentsToDelete = await Collection.Find(filter).ToListAsync(); await Collection.DeleteManyAsync(filter); return documentsToDelete; } public async Task> DeleteOlderNumberAsync(string name, int retentionPolicyValue) { var builder = Builders.Filter; var idsToDelete = await Collection.Find(obs => obs.Name == name) .Project(obs => obs.Id) .Sort("{time: -1}") .Skip(retentionPolicyValue) .ToListAsync(); List documentsToDelete = []; if (idsToDelete == null || !idsToDelete.Any()) return documentsToDelete; var idFilter = builder.In("_id", idsToDelete); documentsToDelete = await Collection.Find(idFilter).ToListAsync(); await Collection.DeleteManyAsync(idFilter); return documentsToDelete; } public async Task ExistBySystemId(ObjectId patientid, string systemId) { return await Collection.Find(Builders.Filter.And( Builders.Filter.Eq(obs => obs.PatientId, patientid), Builders.Filter.Eq(obs => obs.SystemId, systemId) )) .Project("{ _id: true }") .AnyAsync(); } public async Task FindLastObservationBeforeDate(ObjectId patientId, string? name, DateTime date) { var builder = Builders.Filter; var filter = builder.And( builder.Eq(obs => obs.PatientId, patientId), builder.Eq(obs => obs.Name, name), builder.Lt(obs => obs.Time, date) ); var result = await Collection.FindAsync(filter, new FindOptions { Sort = Builders.Sort.Descending("time").Descending("_id") }); return await result.FirstOrDefaultAsync(); } public async Task?> FindAnyWithSameDate(ObjectId patientId, string? name, DateTime date) { var builder = Builders.Filter; var filter = builder.Eq(obs => obs.PatientId, patientId) & builder.Eq(obs => obs.Name, name) & builder.Eq(obs => obs.Time, date); var result = await Collection.FindAsync(filter); return result.ToList(); } public async Task> FindAnyBeforeDate(ObjectId patientId, DateTime date) { var filterBuilder = Builders.Filter; var patientIdFilter = filterBuilder.Eq(obs => obs.PatientId, patientId); var dateFilter = filterBuilder.Lt(obs => obs.Time, date); var filter = filterBuilder.And(patientIdFilter, dateFilter); var result = await Collection.FindAsync(filter); return result.ToList(); } public async Task> FindLatestUniqueValuesByName(ObjectId patientId, string name, int? expires) { // Crear el filtro base var matchFilter = new BsonDocument { { "patientid", patientId }, { "name", name } }; // Agregar filtro de expiración si expires no es null if (expires.HasValue) { var expirationDate = DateTime.UtcNow.AddSeconds(-expires.Value); matchFilter.Add("time", new BsonDocument("$gte", expirationDate)); } var pipeline = new[] { // Filtrar por PatientId, name y (opcionalmente) tiempo no expirado new BsonDocument("$match", matchFilter), // Ordenar por tiempo en orden descendente new BsonDocument("$sort", new BsonDocument("time", -1)), // Agrupar por el campo `value` (valores únicos) new BsonDocument("$group", new BsonDocument { { "_id", "$value" }, { "LatestObservation", new BsonDocument("$first", "$$ROOT") } }), // Proyectar solo los campos originales new BsonDocument("$replaceRoot", new BsonDocument("newRoot", "$LatestObservation")) }; var result = await Collection.AggregateAsync(pipeline); return result.ToList(); } public async Task> AggregatedPatientActiveIntravenousLinesObservations(ObjectId patientId) { var results = new List(); var cursor = await Collection.FindAsync( o => o.PatientId == patientId && o.Name == "IntravenousLinesObs", new FindOptions { Sort = Builders.Sort.Descending("time").Descending("id") }); results.AddRange(cursor.ToEnumerable()); //Group by tpye and location, cant be in same location two of same type. return results .GroupBy(obs => new { ((PatientIntravenousLinesValue)obs.Value).Location, ((PatientIntravenousLinesValue)obs.Value).Type }) .Select(g => g.OrderBy(t => t.Time).ThenBy(t => t.Id).LastOrDefault()) .ToList(); } public async Task> FindAllLastPatientObservationTime() { var group = new BsonDocument { { "_id", "$patientid" }, { "time", new BsonDocument { { "$last", "$time" } } } }; var pipeline = new List { new() { { "$sort", new BsonDocument { { "time", 1 } } } }, new() { { "$group", group } } }; var cursor = await Collection.AggregateAsync(pipeline, new AggregateOptions { AllowDiskUse = true, BatchSize = 10 }); var result = new Dictionary(); while (await cursor.MoveNextAsync()) cursor.Current .Where(obs => obs.GetValue("_id").BsonType != BsonType.Null) .ToList() .ForEach(obs => { if (obs.Get("_id") != null) result.Add(obs.Get("_id")?.AsObjectId ?? new ObjectId(), obs.Get("time")?.ToUniversalTime() ?? DateTime.MinValue); } ); return result; } public async Task FindById(ObjectId id) { var filter = Builders.Filter.Eq(o => o.Id, id); var result = await Collection.FindAsync(filter); return await result.FirstOrDefaultAsync(); } public async Task?> FindByPatientId(ObjectId id) { var filter = Builders.Filter.Eq(o => o.PatientId, id); var result = await Collection.FindAsync(filter); return await result.ToListAsync(); } public async Task Update(PatientObservation observation) { await UpdateOneAsync(observation.Id, observation); } public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId) { await UpdateManyObjectIdAsync(nameId, id, oldId); } public async Task UpdateExpiredObservations(List expiredObservations) { var filter = Builders.Filter .In(x => x.Id, expiredObservations.Select(c => c.Id)); //var filter = Builders.Filter.In("Id", expiredObservations); var update = Builders.Update.Set("Expired", true); _ = await Collection.UpdateManyAsync(filter, update); } public async Task UpdateMany(IEnumerable patientObservations, UpdateDefinition update) { var filter = Builders.Filter.In(x => x.Id, patientObservations.Select(x => x.Id)); await Collection.UpdateManyAsync(filter, update); } public async Task> FindAll() { var result = await Collection.FindAsync(_ => true); return result.ToEnumerable(); } public async Task ExpireExpiredObservations( List configObservationsToExpire) { try { var builder = Builders.Filter; var filterObs = builder.Empty; configObservationsToExpire.ForEach(ob => { var minsToAdd = ob.Expires.HasValue ? ob.Expires * -1 : 0; if (filterObs == builder.Empty) filterObs = builder.Where(o => o.Name == ob.Name && o.Time < DateTime.Now.AddMinutes(minsToAdd.Value)); else filterObs |= builder.Where(o => o.Name == ob.Name && o.Time < DateTime.Now.AddMinutes(minsToAdd.Value)); }); var filterNotExpired = Builders.Filter.Where(o => !o.Expired); var combineFilter = filterObs & filterNotExpired; /*var documentSerializer = BsonSerializer.SerializerRegistry.GetSerializer(); var renderedFilter = combineFilter.Render(documentSerializer, BsonSerializer.SerializerRegistry); Debug.WriteLine($"Result : {renderedFilter}");*/ var updateDefinition = Builders.Update.Set(o => o.Expired, true); await Collection.UpdateManyAsync(combineFilter, updateDefinition); } catch (Exception ex) { _logger.LogError("exception expiring observations expired: {exMessage} ", ex); } } public async Task> FindNotExpired(List? filterObservations) { var filterBuilder = Builders.Filter; var filters = new List>(); if (filterObservations != null) { filters.Add(filterBuilder.Not(filterBuilder.Eq(o => o.Name, null))); filters.Add(filterBuilder.In(o => o.Name, filterObservations)); } filters.Add(filterBuilder.Ne(o => o.Expired, true)); var filter = filterBuilder.And(filters); var result = await Collection.FindAsync(filter); return await result.ToListAsync(); } public async Task> FindByName(string name, DateTime? fromDate = null) { if (string.IsNullOrWhiteSpace(name)) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); if (name.Length > 100) throw new BadRequestException("Name too long"); var safeName = Regex.Escape(name); var filterBuilder = Builders.Filter; var nameFilter = filterBuilder.And( filterBuilder.Ne(o => o.Name, null), filterBuilder.Regex(o => o.Name, new BsonRegularExpression(safeName, "i")) ); var dateFilter = fromDate.HasValue ? filterBuilder.Gt(o => o.Time, fromDate.Value) : filterBuilder.Empty; var filter = filterBuilder.And(nameFilter, dateFilter); var result = await Collection.FindAsync(filter); return await result.ToListAsync(); } public IFindFluent GetPaginatedObservations(PaginationFilter filter) { // Crear variable con la clase que construye los filtros var filterBuilder = Builders.Filter; // Crear una lista de filtros var filters = new List>(); // Ordenar los resultados por "time" en orden descendente var sort = Builders.Sort.Descending("time"); if (filter.FilteredRequest != null) { var requestFilter = filter.FilteredRequest; if (!requestFilter.Observations.IsNullOrEmpty()) filters.Add(filterBuilder.In(o => o.Name, requestFilter.Observations)); if (requestFilter.PatientId != null) { var parsed = ObjectId.TryParse(requestFilter.PatientId, out var patientObjectId); if (parsed) filters.Add(filterBuilder.Eq(o => o.PatientId, patientObjectId)); } } // Añadir los filtros por defecto en caso de llegar la lista de filtros vacia, si no la consulta a base de datos falla filters.Add(filterBuilder.Gt(p => p.Time, filter.FilteredRequest?.StartDate ?? DateTime.MinValue)); filters.Add(filterBuilder.Lt(p => p.Time, filter.FilteredRequest?.EndDate ?? DateTime.MaxValue)); // Combinar los filtros en una consulta compuesta con operador "$and" var combinedFilter = Builders.Filter.And(filters); return Collection .Find(combinedFilter) .Sort(sort); } public async Task> FindLastNotExpiredObservatonsByPatient(ObjectId patientId, string name, int? endAfter = null, int? num = null) { var sort = Builders.Sort.Descending("time"); var filterBuilder = Builders.Filter; if (endAfter.HasValue) { var now = DateTime.UtcNow; var thresholdTime = now.AddSeconds(-endAfter.Value); // Calcular la fecha límite var dateFilter = filterBuilder.Gte(o => o.Time, thresholdTime); // con fecha mayor que la fecha límite calculada filterBuilder.And(dateFilter); } var patientFilter = filterBuilder.Eq(o => o.PatientId, patientId); var nameFilter = filterBuilder.Eq(o => o.Name, name); var filters = filterBuilder.And(patientFilter, nameFilter); var result = await Collection.FindAsync( filters, new FindOptions { Sort = sort, Limit = num } ); return result.ToEnumerable(); } public override async Task CreateIndexes() { try { var options = new CreateIndexOptions { Background = true, Unique = false }; var indexes = new List> { new("{ patientid: 1 }", options), new("{ patientid: 1, name: 1, codingSystem: 1 }", options), new("{ patientid: 1, name: 1 }", options), new("{ name: 1, time: -1 }", options), new("{ patientid: 1, systemId: 1 }", options), new("{ name: 1 }", options), new("{ patientid: 1, name: 1 , time: -1, id: -1}", options), new("{ patientid: 1, name: 1 , time: 1}", options) }; await MongoUtils.EnsureIndexes(Collection, indexes); } catch (Exception e) { _logger.LogError( "error creating indexes for observation collection {eMessage} TRACE: {eStackTrace}", e.Message, e.StackTrace); throw; } } private async Task> AggregatePatientObservations(ObjectId patientId, List? filterObservations = null) { var matchPatient = new BsonDocument { { "patientid", patientId }, { "name", new BsonDocument { { "$ne", BsonNull.Value } } } }; if (filterObservations == null || !filterObservations.Any()) { // GET DISTINCT OBSERVATIONS var distinctObs = new BsonDocument { { "$group", new BsonDocument { { "_id", "1" }, { "obs", new BsonDocument { { "$addToSet", "$name" } } } } } }; var distinctPipeline = new[] { new() { { "$match", matchPatient } }, distinctObs }; Debug.WriteLine("AggregatedPatientLastObservations distinct obs: \n" + distinctPipeline.ToJson()); var resultList = await Collection.AggregateAsync(distinctPipeline, new AggregateOptions { AllowDiskUse = true }); var result = resultList.ToList().FirstOrDefault(); if (result != null) filterObservations = result.GetValue("obs").AsBsonArray.Select(it => it.AsString) .ToList(); } return filterObservations ?? []; } }