using System.Net.Http.Headers; using System.Text.Json; using ClinicalInsightsPro.API.DTOs; using ClinicalInsightsPro.API.Models; using ClinicalInsightsPro.API.Services.Interfaces; namespace ClinicalInsightsPro.API.Services; // Talks to a real Medplum FHIR R4 server over HTTP, using the OAuth2 client // credentials grant (Fhir:MedplumClientId / MedplumClientSecret, both loaded // from .env -- see Program.cs) against Fhir:MedplumTokenUrl, then // reading FHIR resources at Fhir:MedplumBaseUrl. Activated when // Fhir:UseLiveMedplum = true (MEDPLUM_USE_LIVE in .env); otherwise the app // falls back to MedplumFhirService, which reads the local PostgreSQL store // so the app still runs without live Medplum credentials. // // This service is READ-ONLY and never touches ApplicationDbContext -- the // entities built below (Condition, Observation, AllergyIntolerance, // MedicationRequest, Encounter, Patient) are transient objects used only to // (a) run them through IValidationService, the same way the local store's // rows are validated, and (b) shape the PatientChartDto response. So the // [Required]/FK attributes on those model classes (e.g. Condition.RecorderId) // are never enforced here -- nothing is ever SaveChanges'd. public class LiveMedplumFhirService : IMedplumFhirService { private readonly HttpClient _http; private readonly IConfiguration _config; private readonly IValidationService _validation; private readonly ILogger _logger; private readonly string _envFilePath; private string? _cachedToken; private DateTime _tokenExpiresAt = DateTime.MinValue; public LiveMedplumFhirService(HttpClient http, IConfiguration config, IValidationService validation, ILogger logger, IHostEnvironment hostEnvironment) { _http = http; _config = config; _validation = validation; _logger = logger; _envFilePath = Path.Combine(hostEnvironment.ContentRootPath, ".env"); // Reuse a still-valid token already written to .env (e.g. by a // previous startup within the token's lifetime) instead of hitting // Medplum again immediately. var (savedToken, savedExpiry) = EnvTokenStore.TryLoadMedplumToken(_envFilePath); if (!string.IsNullOrWhiteSpace(savedToken) && savedExpiry.HasValue && savedExpiry.Value > DateTime.UtcNow) { _cachedToken = savedToken; _tokenExpiresAt = savedExpiry.Value; } } // Called once at application startup (see Program.cs) so a fresh Medplum // access token is already generated and saved to .env by the time // Swagger/the API is reachable -- no manual "call the token endpoint // first" step required. public async Task InitializeTokenAsync() => await EnsureAuthorizedAsync(); private async Task EnsureAuthorizedAsync() { if (_cachedToken != null && DateTime.UtcNow < _tokenExpiresAt) { _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _cachedToken); return; } var tokenUrl = _config["Fhir:MedplumTokenUrl"]; var clientId = _config["Fhir:MedplumClientId"]; var clientSecret = _config["Fhir:MedplumClientSecret"]; if (string.IsNullOrWhiteSpace(tokenUrl) || string.IsNullOrWhiteSpace(clientId)) { throw new InvalidOperationException( "Live Medplum mode is enabled (MEDPLUM_USE_LIVE=true in .env) but MEDPLUM_TOKEN_URL / " + "MEDPLUM_CLIENT_ID are not set. Set these in .env to connect to Medplum."); } var form = new Dictionary { ["grant_type"] = "client_credentials", ["client_id"] = clientId, ["client_secret"] = clientSecret ?? string.Empty }; using var response = await _http.PostAsync(tokenUrl, new FormUrlEncodedContent(form)); response.EnsureSuccessStatusCode(); using var stream = await response.Content.ReadAsStreamAsync(); var payload = await JsonSerializer.DeserializeAsync(stream); _cachedToken = payload.GetProperty("access_token").GetString(); var expiresIn = payload.TryGetProperty("expires_in", out var exp) ? exp.GetInt32() : 300; _tokenExpiresAt = DateTime.UtcNow.AddSeconds(Math.Max(30, expiresIn - 30)); _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _cachedToken); // Persist so the token survives across scoped-service instances // within this run and is visible in the .env file, per project setup. EnvTokenStore.SaveMedplumToken(_envFilePath, _cachedToken!, _tokenExpiresAt); _logger.LogInformation("Obtained Medplum client-credentials access token (expires in {ExpiresIn}s) and saved it to {EnvFile}.", expiresIn, _envFilePath); } public async Task> GetPatientListAsync() { await EnsureAuthorizedAsync(); var baseUrl = _config["Fhir:MedplumBaseUrl"]!.TrimEnd('/'); var bundle = await GetBundleAsync($"{baseUrl}/Patient?_count=50"); var results = new List(); foreach (var entry in bundle.EnumerateEntries()) { var patient = MapPatient(entry); results.Add(new PatientSummaryDto { Id = patient.Id, FullName = $"{patient.GivenName} {patient.FamilyName}", Age = CalculateAge(patient.BirthDate), Gender = patient.Gender, MedicalRecordNumber = patient.MedicalRecordNumber }); } return results; } public async Task GetPatientChartAsync(string patientId) { await EnsureAuthorizedAsync(); var baseUrl = _config["Fhir:MedplumBaseUrl"]!.TrimEnd('/'); var patientResponse = await _http.GetAsync($"{baseUrl}/Patient/{patientId}"); if (patientResponse.StatusCode == System.Net.HttpStatusCode.NotFound) { throw new KeyNotFoundException($"Patient '{patientId}' was not found in Medplum."); } patientResponse.EnsureSuccessStatusCode(); using var patientStream = await patientResponse.Content.ReadAsStreamAsync(); var patientJson = await JsonSerializer.DeserializeAsync(patientStream); var patient = MapPatient(patientJson); var conditions = await FetchResourceListAsync($"{baseUrl}/Condition?patient={patientId}", MapCondition, patientId); var observations = await FetchResourceListAsync($"{baseUrl}/Observation?patient={patientId}", MapObservation, patientId); var allergies = await FetchResourceListAsync($"{baseUrl}/AllergyIntolerance?patient={patientId}", MapAllergy, patientId); var medications = await FetchResourceListAsync($"{baseUrl}/MedicationRequest?patient={patientId}", MapMedication, patientId); var encounters = await FetchResourceListAsync($"{baseUrl}/Encounter?patient={patientId}", MapEncounter, patientId); var notes = new List { "FHIR bundle retrieved live from Medplum and validated against R4 structure definitions." }; foreach (var c in conditions) notes.AddRange(_validation.ValidateAndNormalizeCondition(c)); foreach (var o in observations) notes.AddRange(_validation.ValidateAndNormalizeObservation(o)); foreach (var a in allergies) notes.AddRange(_validation.ValidateAndNormalizeAllergy(a)); foreach (var m in medications) notes.AddRange(_validation.ValidateAndNormalizeMedication(m)); foreach (var e in encounters) notes.AddRange(_validation.ValidateAndNormalizeEncounter(e)); if (!allergies.Any()) { notes.Add("No AllergyIntolerance resources found -- treated as 'no known allergies' (NKA)."); } return new PatientChartDto { PatientId = patient.Id, FullName = $"{patient.GivenName} {patient.FamilyName}", Age = CalculateAge(patient.BirthDate), MedicalRecordNumber = patient.MedicalRecordNumber, IdentifierSystem = patient.IdentifierSystem, IdentifierValue = patient.IdentifierValue, NameUse = patient.NameUse, Prefix = patient.Prefix, GivenName = patient.GivenName, FamilyName = patient.FamilyName, TelecomSystem = patient.TelecomSystem, TelecomUse = patient.TelecomUse, TelecomValue = patient.TelecomValue, Gender = patient.Gender, BloodGroup = patient.BloodGroup, Email = patient.Email, BirthDate = patient.BirthDate, AddressUse = patient.AddressUse, AddressType = patient.AddressType, AddressLine = patient.AddressLine, City = patient.City, State = patient.State, PostalCode = patient.PostalCode, MaritalStatusSystem = patient.MaritalStatusSystem, MaritalStatusCode = patient.MaritalStatusCode, MaritalStatusDisplay = patient.MaritalStatusDisplay, ContactNameUse = patient.ContactNameUse, ContactPrefix = patient.ContactPrefix, ContactTelecomSystem = patient.ContactTelecomSystem, ContactTelecomUse = patient.ContactTelecomUse, ContactTelecomValue = patient.ContactTelecomValue, ContactAddressUse = patient.ContactAddressUse, ContactAddressType = patient.ContactAddressType, ContactAddressLine = patient.ContactAddressLine, ContactCity = patient.ContactCity, ContactState = patient.ContactState, ContactPostalCode = patient.ContactPostalCode, ContactGender = patient.ContactGender, ContactOrganizationId = patient.ContactOrganizationId, ContactPeriodStart = patient.ContactPeriodStart, ContactPeriodEnd = patient.ContactPeriodEnd, ManagingOrganizationId = patient.ManagingOrganizationId, PhotoContentType = patient.PhotoContentType, PhotoUrl = patient.PhotoUrl, PhotoTitle = patient.PhotoTitle, GeneralPractitionerId = patient.GeneralPractitionerId, Conditions = conditions.Select(c => new ChartConditionDto { Id = c.Id, Code = c.ConditionCode, Display = c.ConditionDisplay, ClinicalStatus = c.ClinicalStatusDisplay, VerificationStatus = c.VerificationStatusDisplay, Severity = c.SeverityDisplay, OnsetDate = c.OnsetDateTime, AbatementDate = c.AbatementDateTime, RecordedDate = c.RecordedDate }).ToList(), Allergies = allergies.Select(a => new ChartAllergyDto { Id = a.Id, Type = a.Type, ClinicalStatus = a.ClinicalStatus, VerificationStatus = a.VerificationStatus, Criticality = a.Criticality, RecorderPractitionerId = a.RecorderPractitionerId }).ToList(), Medications = medications.Select(m => new ChartMedicationDto { Id = m.Id, MedicationCode = m.MedicationCode, MedicationDisplay = m.MedicationDisplay, Status = m.Status, Intent = m.Intent, Priority = m.Priority }).ToList(), Observations = observations.Select(o => new ChartObservationDto { Id = o.Id, Code = o.ObservationCode, Display = o.ObservationDisplay, Status = o.Status, CategoryDisplay = o.CategoryDisplay, InterpretationCode = o.InterpretationCode, InterpretationDisplay = o.InterpretationDisplay, EffectiveDate = o.EffectiveDateTime, NoteText = o.NoteText }).ToList(), Encounters = encounters.Select(e => new ChartEncounterDto { Id = e.Id, Status = e.Status, ClassCode = e.ClassCode, ClassDisplay = e.ClassDisplay, TypeDisplay = e.TypeDisplay, ServiceTypeDisplay = e.ServiceTypeDisplay, PeriodStart = e.PeriodStart, PeriodEnd = e.PeriodEnd }).ToList(), ValidationNotes = notes }; } private async Task> FetchResourceListAsync(string url, Func mapper, string patientId) { var bundle = await GetBundleAsync(url); return bundle.EnumerateEntries().Select(e => mapper(e, patientId)).ToList(); } private async Task GetBundleAsync(string url) { var response = await _http.GetAsync(url); response.EnsureSuccessStatusCode(); using var stream = await response.Content.ReadAsStreamAsync(); var json = await JsonSerializer.DeserializeAsync(stream); return new FhirBundle(json); } // ---- FHIR JSON -> domain model mapping ---- // Every mapper below targets the CURRENT Models/*.cs shape (the one // ApplicationDbContext/MedplumFhirService use), not the old // FirstName/LastName/Code/Display-style entities. These objects are only // ever read from, never saved -- see the class-level comment. private static Patient MapPatient(JsonElement res) { var name = FirstOrDefault(res, "name"); var given = name.ValueKind == JsonValueKind.Object ? GetFirstArrayString(name, "given") : string.Empty; var family = name.ValueKind == JsonValueKind.Object ? GetString(name, "family") : string.Empty; var nameUse = name.ValueKind == JsonValueKind.Object ? GetString(name, "use") : null; var prefix = name.ValueKind == JsonValueKind.Object ? GetFirstArrayString(name, "prefix") : null; var identifier = FirstOrDefault(res, "identifier"); var identifierSystem = identifier.ValueKind == JsonValueKind.Object ? GetString(identifier, "system") : null; var identifierValue = identifier.ValueKind == JsonValueKind.Object ? GetString(identifier, "value") : null; var telecom = FirstOfSystem(res, "telecom", "phone"); var email = FirstOfSystem(res, "telecom", "email"); var address = FirstOrDefault(res, "address"); var addressLine = address.ValueKind == JsonValueKind.Object ? GetFirstArrayString(address, "line") : null; var maritalStatus = res.TryGetProperty("maritalStatus", out var ms) ? ms : default; var managingOrgId = GetReferenceId(res, "managingOrganization"); var generalPractitionerRef = FirstOrDefault(res, "generalPractitioner"); var generalPractitionerId = generalPractitionerRef.ValueKind == JsonValueKind.Object ? ExtractReferenceId(GetString(generalPractitionerRef, "reference")) : null; var photo = FirstOrDefault(res, "photo"); return new Patient { Id = GetString(res, "id") ?? string.Empty, PatientId = GetString(res, "id") ?? string.Empty, ResourceType = "Patient", MedicalRecordNumber = identifierValue ?? string.Empty, IdentifierSystem = identifierSystem ?? string.Empty, IdentifierValue = identifierValue ?? string.Empty, NameUse = nameUse ?? "official", Prefix = prefix, GivenName = given ?? string.Empty, FamilyName = family ?? string.Empty, TelecomSystem = telecom.system ?? "phone", TelecomUse = telecom.use ?? "home", TelecomValue = telecom.value ?? string.Empty, Gender = GetString(res, "gender") ?? "unknown", Email = email.value, BirthDate = GetString(res, "birthDate") is string bd && DateTime.TryParse(bd, out var d) ? d : DateTime.UtcNow, AddressUse = address.ValueKind == JsonValueKind.Object ? GetString(address, "use") ?? "home" : "home", AddressType = address.ValueKind == JsonValueKind.Object ? GetString(address, "type") ?? "both" : "both", AddressLine = addressLine, City = address.ValueKind == JsonValueKind.Object ? GetString(address, "city") ?? string.Empty : string.Empty, State = address.ValueKind == JsonValueKind.Object ? GetString(address, "state") ?? string.Empty : string.Empty, PostalCode = address.ValueKind == JsonValueKind.Object ? GetString(address, "postalCode") ?? string.Empty : string.Empty, MaritalStatusSystem = maritalStatus.ValueKind == JsonValueKind.Object ? GetFirstCodingField(maritalStatus, "system") : null, MaritalStatusCode = maritalStatus.ValueKind == JsonValueKind.Object ? GetFirstCodingField(maritalStatus, "code") : null, MaritalStatusDisplay = maritalStatus.ValueKind == JsonValueKind.Object ? GetFirstCodingField(maritalStatus, "display") : null, ManagingOrganizationId = managingOrgId, GeneralPractitionerId = generalPractitionerId, PhotoContentType = photo.ValueKind == JsonValueKind.Object ? GetString(photo, "contentType") : null, PhotoUrl = photo.ValueKind == JsonValueKind.Object ? GetString(photo, "url") : null, PhotoTitle = photo.ValueKind == JsonValueKind.Object ? GetString(photo, "title") : null }; } private static Condition MapCondition(JsonElement res, string patientId) { var clinicalStatus = res.TryGetProperty("clinicalStatus", out var cs) ? cs : default; var verificationStatus = res.TryGetProperty("verificationStatus", out var vs) ? vs : default; var category = FirstOrDefault(res, "category"); var severity = res.TryGetProperty("severity", out var sev) ? sev : default; return new Condition { Id = GetString(res, "id") ?? Guid.NewGuid().ToString(), ConditionId = GetString(res, "id") ?? Guid.NewGuid().ToString(), ResourceType = "Condition", ClinicalStatusCode = (clinicalStatus.ValueKind == JsonValueKind.Object ? GetFirstCodingField(clinicalStatus, "code") : null) ?? string.Empty, ClinicalStatusDisplay = (clinicalStatus.ValueKind == JsonValueKind.Object ? GetFirstCodingField(clinicalStatus, "display") : null) ?? string.Empty, VerificationStatusCode = (verificationStatus.ValueKind == JsonValueKind.Object ? GetFirstCodingField(verificationStatus, "code") : null) ?? string.Empty, VerificationStatusDisplay = (verificationStatus.ValueKind == JsonValueKind.Object ? GetFirstCodingField(verificationStatus, "display") : null) ?? string.Empty, CategorySystem = category.ValueKind == JsonValueKind.Object ? GetFirstCodingField(category, "system") : null, CategoryCode = category.ValueKind == JsonValueKind.Object ? GetFirstCodingField(category, "code") : null, CategoryDisplay = category.ValueKind == JsonValueKind.Object ? GetFirstCodingField(category, "display") : null, SeverityCode = (severity.ValueKind == JsonValueKind.Object ? GetFirstCodingField(severity, "code") : null) ?? string.Empty, SeverityDisplay = (severity.ValueKind == JsonValueKind.Object ? GetFirstCodingField(severity, "display") : null) ?? string.Empty, ConditionCode = GetCodeableConceptCode(res, "code"), ConditionDisplay = GetCodeableConceptDisplay(res, "code"), PatientId = patientId, OnsetDateTime = GetString(res, "onsetDateTime") is string od && DateTime.TryParse(od, out var onset) ? onset : DateTime.UtcNow, AbatementDateTime = GetString(res, "abatementDateTime") is string ad && DateTime.TryParse(ad, out var abate) ? abate : (DateTime?)null, RecordedDate = GetString(res, "recordedDate") is string rd && DateTime.TryParse(rd, out var recorded) ? recorded : DateTime.UtcNow, RecorderId = ExtractReferenceId(GetReferenceString(res, "recorder")) ?? string.Empty, AsserterId = ExtractReferenceId(GetReferenceString(res, "asserter")) ?? string.Empty }; } private static Observation MapObservation(JsonElement res, string patientId) { var category = FirstOrDefault(res, "category"); var interpretation = FirstOrDefault(res, "interpretation"); var note = FirstOrDefault(res, "note"); return new Observation { Id = GetString(res, "id") ?? Guid.NewGuid().ToString(), Status = GetString(res, "status") ?? "final", CategoryCode = category.ValueKind == JsonValueKind.Object ? GetFirstCodingField(category, "code") : null, CategoryDisplay = category.ValueKind == JsonValueKind.Object ? GetFirstCodingField(category, "display") : null, ObservationCode = GetCodeableConceptCode(res, "code"), ObservationDisplay = GetCodeableConceptDisplay(res, "code"), PatientId = patientId, PerformerPractitionerId = ExtractReferenceId(GetFirstReferenceString(res, "performer")), EffectiveDateTime = GetString(res, "effectiveDateTime") is string ed && DateTimeOffset.TryParse(ed, out var eff) ? eff : (DateTimeOffset?)null, Issued = GetString(res, "issued") is string iss && DateTimeOffset.TryParse(iss, out var issued) ? issued : (DateTimeOffset?)null, NoteText = note.ValueKind == JsonValueKind.Object ? GetString(note, "text") : null, NoteAuthorPractitionerId = note.ValueKind == JsonValueKind.Object ? ExtractReferenceId(GetString(note, "authorReference")) : null, NoteTime = note.ValueKind == JsonValueKind.Object && GetString(note, "time") is string nt && DateTimeOffset.TryParse(nt, out var noteTime) ? noteTime : (DateTimeOffset?)null, InterpretationCode = interpretation.ValueKind == JsonValueKind.Object ? GetFirstCodingField(interpretation, "code") : null, InterpretationDisplay = interpretation.ValueKind == JsonValueKind.Object ? GetFirstCodingField(interpretation, "display") : null }; } private static AllergyIntolerance MapAllergy(JsonElement res, string patientId) { var clinicalStatus = res.TryGetProperty("clinicalStatus", out var cs) ? cs : default; var verificationStatus = res.TryGetProperty("verificationStatus", out var vs) ? vs : default; return new AllergyIntolerance { Id = GetString(res, "id") ?? Guid.NewGuid().ToString(), ClinicalStatus = (clinicalStatus.ValueKind == JsonValueKind.Object ? GetFirstCodingField(clinicalStatus, "code") : null) ?? "active", VerificationStatus = (verificationStatus.ValueKind == JsonValueKind.Object ? GetFirstCodingField(verificationStatus, "code") : null) ?? "unconfirmed", Type = GetString(res, "type") ?? "allergy", Criticality = GetString(res, "criticality"), PatientId = patientId, AsserterPatientId = ExtractReferenceId(GetReferenceString(res, "asserter")), RecorderPractitionerId = ExtractReferenceId(GetReferenceString(res, "recorder")) }; } private static MedicationRequest MapMedication(JsonElement res, string patientId) => new() { Id = GetString(res, "id") ?? Guid.NewGuid().ToString(), Status = GetString(res, "status") ?? "active", Intent = GetString(res, "intent") ?? "order", Priority = GetString(res, "priority"), PatientId = patientId, MedicationCode = GetCodeableConceptCode(res, "medicationCodeableConcept"), MedicationDisplay = GetCodeableConceptDisplay(res, "medicationCodeableConcept") }; private static Encounter MapEncounter(JsonElement res, string patientId) { var classField = res.TryGetProperty("class", out var cls) ? cls : default; var type = FirstOrDefault(res, "type"); var serviceType = res.TryGetProperty("serviceType", out var st) ? st : default; var priority = res.TryGetProperty("priority", out var pr) ? pr : default; var period = res.TryGetProperty("period", out var p) ? p : default; return new Encounter { Id = GetString(res, "id") ?? Guid.NewGuid().ToString(), EncounterId = GetString(res, "id") ?? Guid.NewGuid().ToString(), ResourceType = "Encounter", Status = GetString(res, "status") ?? "finished", ClassSystem = (classField.ValueKind == JsonValueKind.Object ? GetString(classField, "system") : null) ?? string.Empty, ClassCode = (classField.ValueKind == JsonValueKind.Object ? GetString(classField, "code") : null) ?? string.Empty, ClassDisplay = (classField.ValueKind == JsonValueKind.Object ? GetString(classField, "display") : null) ?? string.Empty, TypeSystem = type.ValueKind == JsonValueKind.Object ? GetFirstCodingField(type, "system") : null, TypeCode = type.ValueKind == JsonValueKind.Object ? GetFirstCodingField(type, "code") : null, TypeDisplay = type.ValueKind == JsonValueKind.Object ? GetFirstCodingField(type, "display") : null, ServiceTypeSystem = serviceType.ValueKind == JsonValueKind.Object ? GetFirstCodingField(serviceType, "system") : null, ServiceTypeCode = serviceType.ValueKind == JsonValueKind.Object ? GetFirstCodingField(serviceType, "code") : null, ServiceTypeDisplay = serviceType.ValueKind == JsonValueKind.Object ? GetFirstCodingField(serviceType, "display") : null, PrioritySystem = priority.ValueKind == JsonValueKind.Object ? GetFirstCodingField(priority, "system") : null, PriorityCode = priority.ValueKind == JsonValueKind.Object ? GetFirstCodingField(priority, "code") : null, PriorityDisplay = priority.ValueKind == JsonValueKind.Object ? GetFirstCodingField(priority, "display") : null, PatientId = patientId, PeriodStart = period.ValueKind == JsonValueKind.Object && GetString(period, "start") is string ps && DateTime.TryParse(ps, out var start) ? start : DateTime.UtcNow, PeriodEnd = period.ValueKind == JsonValueKind.Object && GetString(period, "end") is string pe && DateTime.TryParse(pe, out var end) ? end : DateTime.UtcNow }; } // ---- small JSON helpers ---- private static string? GetString(JsonElement el, string propertyName) => el.ValueKind == JsonValueKind.Object && el.TryGetProperty(propertyName, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null; private static JsonElement FirstOrDefault(JsonElement el, string arrayPropertyName) => el.ValueKind == JsonValueKind.Object && el.TryGetProperty(arrayPropertyName, out var arr) && arr.ValueKind == JsonValueKind.Array && arr.GetArrayLength() > 0 ? arr[0] : default; private static string? GetFirstArrayString(JsonElement el, string arrayPropertyName) { if (el.ValueKind != JsonValueKind.Object || !el.TryGetProperty(arrayPropertyName, out var arr) || arr.ValueKind != JsonValueKind.Array || arr.GetArrayLength() == 0) { return null; } return arr[0].ValueKind == JsonValueKind.String ? arr[0].GetString() : null; } // Finds the first entry of a telecom-style array whose "system" matches, // e.g. FirstOfSystem(patient, "telecom", "phone"). private static (string? system, string? use, string? value) FirstOfSystem(JsonElement el, string arrayPropertyName, string system) { if (el.ValueKind != JsonValueKind.Object || !el.TryGetProperty(arrayPropertyName, out var arr) || arr.ValueKind != JsonValueKind.Array) { return (null, null, null); } foreach (var item in arr.EnumerateArray()) { if (GetString(item, "system") == system) { return (GetString(item, "system"), GetString(item, "use"), GetString(item, "value")); } } return (null, null, null); } // Reads system/code/display off the first entry of a CodeableConcept's // "coding" array (or a "system"/"code"/"display" set directly on the // element itself, for the maritalStatus / clinicalStatus shape). private static string? GetFirstCodingField(JsonElement codeableConcept, string field) { if (codeableConcept.ValueKind != JsonValueKind.Object) return null; if (codeableConcept.TryGetProperty("coding", out var coding) && coding.ValueKind == JsonValueKind.Array && coding.GetArrayLength() > 0) { return GetString(coding[0], field); } return GetString(codeableConcept, field); } private static string GetCodeableConceptCode(JsonElement res, string propertyName) { if (!res.TryGetProperty(propertyName, out var cc)) return string.Empty; if (cc.ValueKind == JsonValueKind.String) return cc.GetString() ?? string.Empty; if (cc.TryGetProperty("coding", out var coding) && coding.GetArrayLength() > 0 && coding[0].TryGetProperty("code", out var code)) return code.GetString() ?? string.Empty; return string.Empty; } private static string GetCodeableConceptDisplay(JsonElement res, string propertyName) { if (!res.TryGetProperty(propertyName, out var cc)) return string.Empty; return GetCodeableConceptDisplay(cc); } private static string GetCodeableConceptDisplay(JsonElement cc) { if (cc.TryGetProperty("text", out var text)) return text.GetString() ?? string.Empty; if (cc.TryGetProperty("coding", out var coding) && coding.GetArrayLength() > 0 && coding[0].TryGetProperty("display", out var display)) return display.GetString() ?? string.Empty; return string.Empty; } // reference-style: { "reference": "Practitioner/123" } private static string? GetReferenceString(JsonElement res, string propertyName) => res.ValueKind == JsonValueKind.Object && res.TryGetProperty(propertyName, out var refEl) && refEl.ValueKind == JsonValueKind.Object ? GetString(refEl, "reference") : null; // array-of-reference-style: "performer": [ { "reference": "Practitioner/123" } ] private static string? GetFirstReferenceString(JsonElement res, string arrayPropertyName) { var first = FirstOrDefault(res, arrayPropertyName); return first.ValueKind == JsonValueKind.Object ? GetString(first, "reference") : null; } // "Practitioner/123" -> "123" private static string? ExtractReferenceId(string? reference) { if (string.IsNullOrWhiteSpace(reference)) return null; var slashIndex = reference.LastIndexOf('/'); return slashIndex >= 0 && slashIndex < reference.Length - 1 ? reference[(slashIndex + 1)..] : reference; } private static string? GetReferenceId(JsonElement res, string propertyName) => ExtractReferenceId(GetReferenceString(res, propertyName)); private static int CalculateAge(DateTime dob) { var today = DateTime.UtcNow; var age = today.Year - dob.Year; if (dob.Date > today.AddYears(-age)) age--; return age; } // Small helper over a FHIR searchset Bundle to enumerate entry.resource elements. private readonly struct FhirBundle { private readonly JsonElement _json; public FhirBundle(JsonElement json) { _json = json; } public IEnumerable EnumerateEntries() { if (!_json.TryGetProperty("entry", out var entries) || entries.ValueKind != JsonValueKind.Array) yield break; foreach (var entry in entries.EnumerateArray()) { if (entry.TryGetProperty("resource", out var resource)) yield return resource; } } } }