using Hl7.Fhir.Model; using Hl7.Fhir.Serialization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; [ApiController] [Route("fhir/R4")] [ServiceFilter(typeof(FhirExceptionFilter))] public class FhirIngestController : ControllerBase { private static readonly FhirJsonParser Parser = new(); private static readonly FhirJsonSerializer Serializer = new(); private readonly IPatientService _patients; private readonly IEncounterService _encounters; private readonly IObservationService _observations; private readonly IMedicationService _medications; private readonly IExternalIdentifierService _identifiers; private readonly PatientFhirMapper _patientMapper; private readonly EncounterFhirMapper _encounterMapper; private readonly ObservationFhirMapper _observationMapper; private readonly MedicationAdministrationFhirMapper _medMapper; private readonly FhirBundleProcessor _bundleProcessor; private readonly FhirOptions _options; private readonly ClinicalMetrics _metrics; public FhirIngestController( IPatientService patients, IEncounterService encounters, IObservationService observations, IMedicationService medications, IExternalIdentifierService identifiers, PatientFhirMapper patientMapper, EncounterFhirMapper encounterMapper, ObservationFhirMapper observationMapper, MedicationAdministrationFhirMapper medMapper, FhirBundleProcessor bundleProcessor, IOptions options, ClinicalMetrics metrics) { _patients = patients; _encounters = encounters; _observations = observations; _medications = medications; _identifiers = identifiers; _patientMapper = patientMapper; _encounterMapper = encounterMapper; _observationMapper = observationMapper; _medMapper = medMapper; _bundleProcessor = bundleProcessor; _options = options.Value; _metrics = metrics; } [HttpPost("Patient")] [Consumes("application/fhir+json")] [Produces("application/fhir+json")] public async Task CreatePatient() { var fhir = await ParseBodyAsync(); var req = _patientMapper.ToUpsertRequest(fhir); var patient = await _patients.RegisterOrUpdateByIdentifierAsync(req); var hospitalId = await _identifiers.FindPrimaryIdentifierAsync( ExternalResourceType.Patient, patient.Id, _options.PatientIdentifierSystems); var response = _patientMapper.ToFhirResponse(patient, hospitalId); _metrics.FhirIngestTotal.WithLabels("Patient", "success").Inc(); return Created($"{Request.Path}/{patient.Id}", Serialize(response)); } [HttpPost("Encounter")] [Consumes("application/fhir+json")] [Produces("application/fhir+json")] public async Task CreateEncounter() { var fhir = await ParseBodyAsync(); var req = await _encounterMapper.ToUpsertRequestAsync(fhir); var encounter = await _encounters.OpenOrUpdateByIdentifierAsync(req); var hospitalId = await _identifiers.FindPrimaryIdentifierAsync( ExternalResourceType.Encounter, encounter.Id, _options.EncounterIdentifierSystems); var response = _encounterMapper.ToFhirResponse(encounter, hospitalId); _metrics.FhirIngestTotal.WithLabels("Encounter", "success").Inc(); return Created($"{Request.Path}/{encounter.Id}", Serialize(response)); } [HttpPost("Observation")] [Consumes("application/fhir+json")] [Produces("application/fhir+json")] public async Task CreateObservation() { var fhir = await ParseBodyAsync(); var mapped = await _observationMapper.ToIngestRequestsAsync(fhir); Resource? lastResponse = null; foreach (var item in mapped) { var result = await _observations.IngestAsync(item.EncounterId, item.Request); lastResponse = new Hl7.Fhir.Model.Observation { Id = result.Observation.Id.ToString(), Status = ObservationStatus.Final, Code = new CodeableConcept("http://loinc.org", item.Request.ObservationCode), Value = new Quantity(item.Request.Value, item.Request.Unit) }; } _metrics.FhirIngestTotal.WithLabels("Observation", "success").Inc(); return Created(Request.Path.Value!, Serialize(lastResponse!)); } [HttpPost("MedicationAdministration")] [Consumes("application/fhir+json")] [Produces("application/fhir+json")] public async Task CreateMedicationAdministration() { var fhir = await ParseBodyAsync(); var (req, encounterId) = await _medMapper.ToCreateRequestAsync(fhir); var med = await _medications.CreateAsync(encounterId, req); var response = new Hl7.Fhir.Model.MedicationAdministration { Id = med.Id.ToString() }; _metrics.FhirIngestTotal.WithLabels("MedicationAdministration", "success").Inc(); return Created($"{Request.Path}/{med.Id}", Serialize(response)); } /// Accepts Bundle.type=transaction (ADT admit) or batch. [HttpPost] [Consumes("application/fhir+json")] [Produces("application/fhir+json")] public async Task ProcessBundle() { using var reader = new StreamReader(Request.Body); var json = await reader.ReadToEndAsync(); var bundle = Parser.Parse(json); if (bundle.Type != Bundle.BundleType.Transaction) throw new FhirMappingException("Only transaction Bundles are supported.", "not-supported"); var responseBundle = await _bundleProcessor.ProcessTransactionAsync(bundle); _metrics.FhirIngestTotal.WithLabels("Bundle", "success").Inc(); return Ok(Serialize(responseBundle)); } private async Task ParseBodyAsync() where T : Resource { using var reader = new StreamReader(Request.Body); var json = await reader.ReadToEndAsync(); return Parser.Parse(json); } private ContentResult Serialize(Resource resource) => Content(Serializer.SerializeToString(resource), "application/fhir+json"); }