222 lines
9.9 KiB
C#
222 lines
9.9 KiB
C#
using System.Diagnostics;
|
|
using adas_core.Application.Repositories.Interfaces;
|
|
using adas_core.Domain.Models;
|
|
using adas_core.Domain.Models.AppSettings;
|
|
using Microsoft.Extensions.Options;
|
|
using MongoDB.Bson;
|
|
using MongoDB.Driver;
|
|
using Serilog;
|
|
|
|
namespace adas_core.Infrastructure.Repositories;
|
|
|
|
/// <summary>
|
|
/// Repository implementation for managing PatientObservation archive entities in MongoDB.
|
|
/// Provides operations for storing and retrieving historical patient observations.
|
|
/// </summary>
|
|
/// <!-- aidoc:v1 sig=208c7de -->
|
|
public class ObservationArchiveRepository : MongoRepository<PatientObservation>, IObservationArchiveRepository
|
|
{
|
|
private readonly ApiSettings _apiSettings;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the ObservationArchiveRepository.
|
|
/// </summary>
|
|
/// <param name="apiSettings">API settings containing collection names configuration.</param>
|
|
/// <param name="database">The MongoDB database instance.</param>
|
|
/// <exception cref="ArgumentNullException">Thrown when apiSettings is null.</exception>
|
|
/// <!-- aidoc:v1 sig=fa9b83d body=d2b18a3 -->
|
|
public ObservationArchiveRepository(IOptions<ApiSettings> apiSettings, IMongoDatabase database) : base(database)
|
|
{
|
|
if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings));
|
|
_apiSettings = apiSettings.Value;
|
|
} //For testing
|
|
|
|
/// <summary>
|
|
/// Retrieves the aggregated last observations for a specific patient.
|
|
/// Filters observations by name and date, returning the most recent ones.
|
|
/// </summary>
|
|
/// <param name="patientId">The ObjectId of the patient.</param>
|
|
/// <param name="num">The maximum number of observations to return per observation type.</param>
|
|
/// <param name="lastDate">The cutoff date to filter observations (inclusive).</param>
|
|
/// <param name="filterObservations">Optional list of observation names to filter by. If null, retrieves all distinct observations.</param>
|
|
/// <returns>A list of PatientObservation entities sorted by time descending.</returns>
|
|
/// <!-- aidoc-review:v1 severity=low kind=wrong_returns
|
|
/// "The returned list is sorted by time descending only within each observation name group (each foreach iteration), not globally across all observation types, since results are appended via AddRange without re-sorting." -->
|
|
public async Task<List<PatientObservation>> AggregatedPatientLastObservations(ObjectId patientId, int num,
|
|
DateTime lastDate, List<string>? filterObservations = null)
|
|
{
|
|
filterObservations = await AggregatePatientObservations(patientId, filterObservations);
|
|
var results = new List<PatientObservation>();
|
|
|
|
foreach (var obs in filterObservations)
|
|
{
|
|
var filter = Builders<PatientObservation>.Filter.And(
|
|
Builders<PatientObservation>.Filter.Eq(o => o.PatientId, patientId),
|
|
Builders<PatientObservation>.Filter.Eq(o => o.Name, obs),
|
|
Builders<PatientObservation>.Filter.Lte(o => o.Time, lastDate)
|
|
);
|
|
|
|
var sort = Builders<PatientObservation>.Sort.Descending(o => o.Time);
|
|
|
|
results.AddRange(Collection.Find(filter).Sort(sort).Limit(num).ToEnumerable());
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the name of the collection for archived patient observations.
|
|
/// </summary>
|
|
/// <returns>The collection name from API settings, or default "archive_patients_observations".</returns>
|
|
/// <!-- aidoc:v1 sig=94e22ff body=81daff1 -->
|
|
public override string GetCollectionName()
|
|
{
|
|
return _apiSettings.ArchivePatientsObservations ?? "archive_patients_observations";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inserts a new patient observation into the archive with retry logic for duplicate key errors.
|
|
/// If a duplicate key error occurs, generates a new ObjectId and retries up to maxRetries times.
|
|
/// </summary>
|
|
/// <param name="patientObservation">The PatientObservation entity to insert.</param>
|
|
/// <exception cref="MongoWriteException">Throws when duplicate key error persists after max retries.</exception>
|
|
/// <exception cref="Exception">Throws when insertion fails for reasons other than duplicate key.</exception>
|
|
/// <!-- aidoc:v1 sig=ae8c780 body=b4e7347 -->
|
|
public new async Task InsertOneAsync(PatientObservation patientObservation)
|
|
{
|
|
const int maxRetries = 2; // Número máximo de reintentos
|
|
var retryCount = 0;
|
|
|
|
while (true)
|
|
try
|
|
{
|
|
await Collection.InsertOneAsync(patientObservation);
|
|
return;
|
|
}
|
|
catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
|
|
{
|
|
retryCount++;
|
|
|
|
Log.Warning(
|
|
"Duplicate key error encountered. Retrying with new ObjectId. Attempt {attempt} of {maxRetries}",
|
|
retryCount, maxRetries);
|
|
|
|
patientObservation.Id = new ObjectId();
|
|
Log.Information("Generated ObjectId: {objectId}", patientObservation.Id);
|
|
|
|
if (retryCount >= maxRetries)
|
|
{
|
|
Log.Error("Maximum retry attempts reached. Could not insert document due to duplicate key error.");
|
|
throw; // Re-lanzar la excepción después de alcanzar el número máximo de reintentos
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Error("Error inserting patient observation: {exMessage}", ex.Message);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes all patient observations before a specified date.
|
|
/// Useful for archival cleanup operations.
|
|
/// </summary>
|
|
/// <param name="date">The cutoff date. Observations older than this date will be deleted.</param>
|
|
/// <returns>The number of deleted documents.</returns>
|
|
/// <!-- aidoc-review:v1 severity=high kind=wrong_returns
|
|
/// "Method returns Task, not Task<int>; the number of deleted documents is not exposed to callers." -->
|
|
public async Task DeleteBeforeDate(DateTime date)
|
|
{
|
|
var filter = Builders<PatientObservation>.Filter.Lt(po => po.Time, date);
|
|
await Collection.DeleteManyAsync(filter);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inserts a batch of patient observations using bulk write operation.
|
|
/// </summary>
|
|
/// <param name="observations">An enumerable of PatientObservation entities to insert.</param>
|
|
/// <returns>The count of successfully inserted documents.</returns>
|
|
/// <!-- aidoc:v1 sig=f522e46 body=daebb76 -->
|
|
public async Task<long> InsertBatch(IEnumerable<PatientObservation> observations)
|
|
{
|
|
var writes = new List<WriteModel<PatientObservation>>();
|
|
writes.AddRange(observations.Select(d => new InsertOneModel<PatientObservation>(d)));
|
|
|
|
var bulkInsert = await Collection.BulkWriteAsync(writes);
|
|
|
|
return bulkInsert.InsertedCount;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves all archived observations for a specific patient.
|
|
/// </summary>
|
|
/// <param name="patientId">The ObjectId of the patient.</param>
|
|
/// <returns>A list of all PatientObservation entities for the patient.</returns>
|
|
/// <!-- aidoc-review:v1 severity=high kind=wrong_summary
|
|
/// "Summary states observations are 'archived', but the code only filters by PatientId with no archive-status filter, implying retrieval behavior the code does not perform." -->
|
|
public async Task<List<PatientObservation>> FindAllFromPatient(ObjectId patientId)
|
|
{
|
|
var filter = Builders<PatientObservation>.Filter.Eq(p => p.PatientId, patientId);
|
|
var result = await Collection.FindAsync(filter);
|
|
|
|
return await result.ToListAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Aggregates distinct observation names for a patient using MongoDB aggregation pipeline.
|
|
/// If filterObservations is provided, returns that list; otherwise, computes distinct observations.
|
|
/// </summary>
|
|
/// <param name="patientId">The ObjectId of the patient.</param>
|
|
/// <param name="filterObservations">Optional pre-filtered list of observation names. If null or empty, computes distinct values.</param>
|
|
/// <returns>A list of distinct observation name strings.</returns>
|
|
/// <!-- aidoc:v1 sig=b7f7b8f body=198b120 -->
|
|
private async Task<List<string>> AggregatePatientObservations(ObjectId patientId,
|
|
List<string>? 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("AggregatedArchivedPatientLastObservations distinct obs: \n" + distinctPipeline.ToJson());
|
|
var resultList =
|
|
await Collection.AggregateAsync<BsonDocument>(distinctPipeline,
|
|
new AggregateOptions { AllowDiskUse = true });
|
|
var result = resultList.ToList().FirstOrDefault();
|
|
|
|
if (result != null && result.Any())
|
|
filterObservations = result.GetValue("obs").AsBsonArray.Select(it => it.AsString).ToList();
|
|
}
|
|
|
|
return filterObservations ?? [];
|
|
}
|
|
} |