Compare commits

...

13 Commits

Author SHA1 Message Date
0324d207cc Upload files to "Demo" 2026-08-31 13:30:52 +00:00
888eaa3dac nisha capstone project NEW 2026-08-31 10:02:53 +00:00
9925126701 Upload files to ".Net Capstone Project" 2026-08-31 09:40:43 +00:00
da525bc8d3 Upload files to ".Net Capstone Project" 2026-08-31 09:27:05 +00:00
d549583de0 Fixed 2026-08-31 08:52:57 +00:00
2806df61f1 database 2026-08-31 08:33:08 +00:00
c928f1493f New project 2026-08-31 08:32:33 +00:00
8fc19f1868 Upload files to ".Net Capstone Project" 2026-08-31 06:47:41 +00:00
502918bbe2 database 2026-08-31 06:27:56 +00:00
b352665fda nisha capstone project 31-08-2026 2026-08-31 06:25:47 +00:00
457b801fb1 New upload
New update
2026-08-29 20:38:01 +00:00
e3ed1134ca Delete
Delete
2026-08-29 20:37:32 +00:00
7ab8bff37b new code
new code
2026-08-29 20:11:40 +00:00
11 changed files with 665 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,665 @@
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<LiveMedplumFhirService> _logger;
private readonly string _envFilePath;
private string? _cachedToken;
private DateTime _tokenExpiresAt = DateTime.MinValue;
public LiveMedplumFhirService(HttpClient http, IConfiguration config, IValidationService validation,
ILogger<LiveMedplumFhirService> 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<string, string>
{
["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<JsonElement>(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<List<PatientSummaryDto>> GetPatientListAsync()
{
await EnsureAuthorizedAsync();
var baseUrl = _config["Fhir:MedplumBaseUrl"]!.TrimEnd('/');
var bundle = await GetBundleAsync($"{baseUrl}/Patient?_count=50");
var results = new List<PatientSummaryDto>();
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<PatientChartDto> 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<JsonElement>(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<string> { "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<List<T>> FetchResourceListAsync<T>(string url, Func<JsonElement, string, T> mapper, string patientId)
{
var bundle = await GetBundleAsync(url);
return bundle.EnumerateEntries().Select(e => mapper(e, patientId)).ToList();
}
private async Task<FhirBundle> GetBundleAsync(string url)
{
var response = await _http.GetAsync(url);
response.EnsureSuccessStatusCode();
using var stream = await response.Content.ReadAsStreamAsync();
var json = await JsonSerializer.DeserializeAsync<JsonElement>(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<JsonElement> 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;
}
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.