using Microsoft.EntityFrameworkCore; using StackExchange.Redis; public class QsofaService : IQsofaService { private readonly AppDbContext _db; private readonly IConnectionMultiplexer _redis; public QsofaService(AppDbContext db, IConnectionMultiplexer redis) { _db = db; _redis = redis; } public async Task GetCurrentAsync(Guid encounterId) { await EnsureEncounterExistsAsync(encounterId); var values = await ReadCriterionValuesAsync(encounterId); return BuildResponse(values); } public async Task GetActiveCriteriaCountAsync(Guid encounterId) { var values = await ReadCriterionValuesAsync(encounterId); return QsofaCalculator.CountActiveCriteria(values); } public async Task> GetHistoryAsync( Guid encounterId, int limit, string? cursorToken) { limit = Math.Clamp(limit, 1, 100); var cursor = QsofaEvaluationCursor.Decode(cursorToken); var query = _db.QsofaEvaluations .AsNoTracking() .Where(e => e.EncounterId == encounterId); if (cursor is not null) { query = query.Where(e => e.EvaluatedAt < cursor.EvaluatedAt || (e.EvaluatedAt == cursor.EvaluatedAt && e.Id.CompareTo(cursor.Id) < 0)); } var items = await query .OrderByDescending(e => e.EvaluatedAt) .ThenByDescending(e => e.Id) .Take(limit + 1) .ToListAsync(); var hasMore = items.Count > limit; if (hasMore) items.RemoveAt(limit); var nextCursor = hasMore ? new QsofaEvaluationCursor(items[^1].EvaluatedAt, items[^1].Id).Encode() : null; return new CursorPage(items, nextCursor, hasMore); } private async Task EnsureEncounterExistsAsync(Guid encounterId) { var exists = await _db.Encounters.AnyAsync(e => e.Id == encounterId); if (!exists) throw new NotFoundException("Encounter not found.", "ENCOUNTER_NOT_FOUND"); } private async Task ReadCriterionValuesAsync(Guid encounterId) { var cache = _redis.GetDatabase(); return await cache.StringGetAsync(QsofaCalculator.AllCriterionKeys(encounterId)); } private static QsofaCurrentResponse BuildResponse(RedisValue[] values) { return new QsofaCurrentResponse( QsofaCalculator.CountActiveCriteria(values), QsofaCalculator.ParseCriteriaState(values)); } }