Files
adas-core/adas-core.Infrastructure/Repositories/ObservationArchiveRepository.cs
T
2026-06-26 10:29:23 +02:00

210 lines
9.0 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>
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>
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>
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>
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>
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>
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>
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>
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>
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 ?? [];
}
}