223 lines
8.3 KiB
C#
223 lines
8.3 KiB
C#
using Hl7.Fhir.Model;
|
|
using Hl7.Fhir.Serialization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
/// <summary>
|
|
/// FHIR R4 read and search interactions for Patient and Encounter resources.
|
|
/// </summary>
|
|
[ApiController]
|
|
[Route("fhir/R4")]
|
|
[AuthorizePermission(ClinicalPermissions.FhirRead)]
|
|
[ServiceFilter(typeof(FhirExceptionFilter))]
|
|
public class FhirReadController : ControllerBase
|
|
{
|
|
private static readonly FhirJsonSerializer Serializer = new();
|
|
|
|
private readonly AppDbContext _db;
|
|
private readonly IExternalIdentifierService _identifiers;
|
|
private readonly PatientFhirMapper _patientMapper;
|
|
private readonly EncounterFhirMapper _encounterMapper;
|
|
private readonly FhirOptions _options;
|
|
private readonly ClinicalMetrics _metrics;
|
|
|
|
public FhirReadController(
|
|
AppDbContext db,
|
|
IExternalIdentifierService identifiers,
|
|
PatientFhirMapper patientMapper,
|
|
EncounterFhirMapper encounterMapper,
|
|
IOptions<FhirOptions> options,
|
|
ClinicalMetrics metrics)
|
|
{
|
|
_db = db;
|
|
_identifiers = identifiers;
|
|
_patientMapper = patientMapper;
|
|
_encounterMapper = encounterMapper;
|
|
_options = options.Value;
|
|
_metrics = metrics;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads a Patient resource by internal ID.
|
|
/// </summary>
|
|
[HttpGet("Patient/{id:guid}")]
|
|
[Produces("application/fhir+json")]
|
|
[ProducesResponseType(typeof(Hl7.Fhir.Model.Patient), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status404NotFound)]
|
|
public async Task<IActionResult> ReadPatient(Guid id)
|
|
{
|
|
var patient = await _db.Patients.AsNoTracking().FirstOrDefaultAsync(p => p.Id == id);
|
|
if (patient is null)
|
|
throw new NotFoundException("Patient not found.", "FHIR_RESOURCE_NOT_FOUND");
|
|
|
|
var hospitalId = await _identifiers.FindPrimaryIdentifierAsync(
|
|
ExternalResourceType.Patient, patient.Id, _options.PatientIdentifierSystems);
|
|
|
|
var resource = _patientMapper.ToFhirResponse(patient, hospitalId);
|
|
_metrics.FhirReadTotal.WithLabels("Patient", "read", "success").Inc();
|
|
return Serialize(resource);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Searches for Patient resources by identifier (system|value).
|
|
/// </summary>
|
|
[HttpGet("Patient")]
|
|
[Produces("application/fhir+json")]
|
|
[ProducesResponseType(typeof(Bundle), StatusCodes.Status200OK)]
|
|
public async Task<IActionResult> SearchPatient(
|
|
[FromQuery] string? identifier,
|
|
[FromQuery] int _count = 20)
|
|
{
|
|
_count = Math.Clamp(_count, 1, 100);
|
|
|
|
var bundle = new Bundle
|
|
{
|
|
Type = Bundle.BundleType.Searchset,
|
|
Timestamp = DateTimeOffset.UtcNow
|
|
};
|
|
|
|
if (identifier is not null)
|
|
{
|
|
var parts = identifier.Split('|', 2);
|
|
if (parts.Length == 2 && !string.IsNullOrWhiteSpace(parts[0]) && !string.IsNullOrWhiteSpace(parts[1]))
|
|
{
|
|
var internalId = await _identifiers.ResolveInternalIdAsync(
|
|
ExternalResourceType.Patient, parts[0], parts[1]);
|
|
|
|
if (internalId.HasValue)
|
|
{
|
|
var patient = await _db.Patients.AsNoTracking().FirstOrDefaultAsync(p => p.Id == internalId.Value);
|
|
if (patient is not null)
|
|
{
|
|
var hospitalId = await _identifiers.FindPrimaryIdentifierAsync(
|
|
ExternalResourceType.Patient, patient.Id, _options.PatientIdentifierSystems);
|
|
var resource = _patientMapper.ToFhirResponse(patient, hospitalId);
|
|
bundle.Entry.Add(new Bundle.EntryComponent
|
|
{
|
|
FullUrl = $"{Request.Scheme}://{Request.Host}/fhir/R4/Patient/{patient.Id}",
|
|
Resource = resource
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var patients = await _db.Patients
|
|
.AsNoTracking()
|
|
.OrderBy(p => p.LastName).ThenBy(p => p.FirstName)
|
|
.Take(_count)
|
|
.ToListAsync();
|
|
|
|
foreach (var patient in patients)
|
|
{
|
|
var hospitalId = await _identifiers.FindPrimaryIdentifierAsync(
|
|
ExternalResourceType.Patient, patient.Id, _options.PatientIdentifierSystems);
|
|
var resource = _patientMapper.ToFhirResponse(patient, hospitalId);
|
|
bundle.Entry.Add(new Bundle.EntryComponent
|
|
{
|
|
FullUrl = $"{Request.Scheme}://{Request.Host}/fhir/R4/Patient/{patient.Id}",
|
|
Resource = resource
|
|
});
|
|
}
|
|
}
|
|
|
|
bundle.Total = bundle.Entry.Count;
|
|
_metrics.FhirReadTotal.WithLabels("Patient", "search", "success").Inc();
|
|
return Serialize(bundle);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads an Encounter resource by internal ID.
|
|
/// </summary>
|
|
[HttpGet("Encounter/{id:guid}")]
|
|
[Produces("application/fhir+json")]
|
|
[ProducesResponseType(typeof(Hl7.Fhir.Model.Encounter), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(typeof(OperationOutcome), StatusCodes.Status404NotFound)]
|
|
public async Task<IActionResult> ReadEncounter(Guid id)
|
|
{
|
|
var encounter = await _db.Encounters
|
|
.AsNoTracking()
|
|
.Include(e => e.Patient)
|
|
.FirstOrDefaultAsync(e => e.Id == id);
|
|
|
|
if (encounter is null)
|
|
throw new NotFoundException("Encounter not found.", "FHIR_RESOURCE_NOT_FOUND");
|
|
|
|
var hospitalId = await _identifiers.FindPrimaryIdentifierAsync(
|
|
ExternalResourceType.Encounter, encounter.Id, _options.EncounterIdentifierSystems);
|
|
|
|
var resource = _encounterMapper.ToFhirResponse(encounter, hospitalId);
|
|
_metrics.FhirReadTotal.WithLabels("Encounter", "read", "success").Inc();
|
|
return Serialize(resource);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Searches for Encounter resources by patient reference.
|
|
/// </summary>
|
|
[HttpGet("Encounter")]
|
|
[Produces("application/fhir+json")]
|
|
[ProducesResponseType(typeof(Bundle), StatusCodes.Status200OK)]
|
|
public async Task<IActionResult> SearchEncounter(
|
|
[FromQuery] string? patient,
|
|
[FromQuery] string? status,
|
|
[FromQuery] int _count = 20)
|
|
{
|
|
_count = Math.Clamp(_count, 1, 100);
|
|
|
|
var query = _db.Encounters.AsNoTracking().Include(e => e.Patient).AsQueryable();
|
|
|
|
if (patient is not null && Guid.TryParse(patient, out var patientId))
|
|
query = query.Where(e => e.PatientId == patientId);
|
|
|
|
if (status is not null)
|
|
{
|
|
var fhirStatus = status.ToLowerInvariant() switch
|
|
{
|
|
"in-progress" => EncounterStatus.Active,
|
|
"finished" => EncounterStatus.Discharged,
|
|
"cancelled" => EncounterStatus.Cancelled,
|
|
_ => (EncounterStatus?)null
|
|
};
|
|
if (fhirStatus.HasValue)
|
|
query = query.Where(e => e.Status == fhirStatus.Value);
|
|
}
|
|
|
|
var encounters = await query
|
|
.OrderByDescending(e => e.AdmittedAt)
|
|
.Take(_count)
|
|
.ToListAsync();
|
|
|
|
var bundle = new Bundle
|
|
{
|
|
Type = Bundle.BundleType.Searchset,
|
|
Total = encounters.Count,
|
|
Timestamp = DateTimeOffset.UtcNow
|
|
};
|
|
|
|
foreach (var encounter in encounters)
|
|
{
|
|
var hospitalId = await _identifiers.FindPrimaryIdentifierAsync(
|
|
ExternalResourceType.Encounter, encounter.Id, _options.EncounterIdentifierSystems);
|
|
var resource = _encounterMapper.ToFhirResponse(encounter, hospitalId);
|
|
bundle.Entry.Add(new Bundle.EntryComponent
|
|
{
|
|
FullUrl = $"{Request.Scheme}://{Request.Host}/fhir/R4/Encounter/{encounter.Id}",
|
|
Resource = resource
|
|
});
|
|
}
|
|
|
|
_metrics.FhirReadTotal.WithLabels("Encounter", "search", "success").Inc();
|
|
return Serialize(bundle);
|
|
}
|
|
|
|
private ContentResult Serialize(Resource resource, int statusCode = StatusCodes.Status200OK) =>
|
|
new()
|
|
{
|
|
Content = Serializer.SerializeToString(resource),
|
|
ContentType = "application/fhir+json",
|
|
StatusCode = statusCode
|
|
};
|
|
}
|