FHIR Programming using .NET - Chaining FHIR Operations
Introduction
Welcome back to our FHIR programming series using .NET. In the previous article, we explored how to validate FHIR resources to ensure compliance with FHIR standards. Today, we'll take a step further by learning how to chain FHIR operations. Chaining operations allow you to perform complex queries that span across multiple related resources, providing a powerful tool for retrieving comprehensive healthcare data.
In this article, we'll guide you through the process of chaining operations using the FHIR .NET SDK. You'll learn how to construct chained queries, how to interpret the results, and best practices for using these advanced operations in your healthcare applications.
Understanding FHIR Chaining and References
Before diving into code, let's understand the theoretical foundations of FHIR references and how chaining leverages them for powerful queries.
Resource Reference Mechanics
FHIR resources link to each other through References. Understanding the types of references is crucial for effective chaining:
Literal References - Point to a resource by its URL:
- Absolute URL:
http://server.com/fhir/Patient/123- Full URL including server - Relative URL:
Patient/123- Resource type and ID only (most common) - Version-specific:
Patient/123/_history/2- Points to a specific version
Logical References - Point to a resource by business identifier (not a URL):
- Uses
Identifierelement instead ofReference - Example:
"identifier": {"system": "http://hospital.org/mrn", "value": "12345"} - Useful when the actual resource URL isn't known or stable
- Requires server resolution to find the actual resource
Contained References - Point to a resource embedded within the parent:
- Uses
#prefix:#local-id - The referenced resource is included in the parent's
Containedelement - Used when the referenced resource has no independent existence
Forward Chaining
Forward chaining allows you to search for resources based on properties of resources they reference. The syntax uses a dot (.) to "chain" through the reference:
- Pattern:
[search-parameter]:[resource-type].[chained-parameter]=[value] - Example:
Observation?subject:Patient.name=Smith - Meaning: Find Observations where the subject (a Patient) has name matching "Smith"
Chains can go multiple levels deep:
Observation?subject:Patient.organization:Organization.name=General Hospital- Finds Observations for Patients whose managing Organization has a specific name
Reverse Chaining (_has)
Reverse chaining (using _has) works in the opposite direction - finding resources that are referenced BY other resources:
- Pattern:
[ResourceType]?_has:[RefResource]:[ref-param]:[search-param]=[value] - Example:
Patient?_has:Observation:subject:code=8867-4 - Meaning: Find Patients who are the subject of an Observation with code 8867-4
This is powerful for questions like:
- "Which patients have lab results above a certain threshold?"
- "Which practitioners have prescribed a specific medication?"
- "Which organizations have patients with a certain condition?"
Performance Implications of Chained Searches
While chaining is powerful, it has performance implications:
- Database Joins - Each chain level typically requires an additional database join, which can be expensive
- No Indexing - Some servers may not have indexes optimized for chained parameters
- Timeout Risks - Complex multi-level chains can timeout on large datasets
Best Practices for Performance:
- Limit chain depth when possible (1-2 levels is usually optimal)
- Add additional non-chained criteria to narrow results early
- Use
_countto limit result sets - Consider multiple simple queries instead of one complex chain
- Check your server's CapabilityStatement to see which chains are supported
// Good: Combine chaining with other criteria to narrow results
var results = await fhirClient.SearchAsync<Observation>(new SearchParams()
.Where("code=8867-4") // Narrow by code first
.Where("subject:Patient.family=Smith") // Then chain
.LimitTo(50)); // Limit results
_include and _revinclude: Alternatives to Chaining
Instead of chaining, you can use _include and _revinclude to retrieve related resources in a single query:
- _include - Include resources that matched resources reference
- _revinclude - Include resources that reference the matched resources
// Get Observations AND their referenced Patients in one query
var results = await fhirClient.SearchAsync<Observation>(new SearchParams()
.Where("code=8867-4")
.Include("Observation:subject")); // Include the Patient
// Results contain both Observations and Patients
foreach (var entry in results?.Entry ?? new List<Bundle.EntryComponent>())
{
if (entry.Resource is Observation obs)
Console.WriteLine($"Observation: {obs.Id}");
else if (entry.Resource is Patient pat)
Console.WriteLine($"Patient: {pat.Id}");
}
This approach can be more efficient than chaining when you need to retrieve the related resources anyway.
GraphQL: An Alternative for Complex Queries
For very complex queries, FHIR supports GraphQL as an alternative to chaining:
- Allows precise specification of exactly which fields to return
- Can traverse multiple resource relationships in a single query
- More efficient when you only need specific fields (reduces payload size)
- Better suited for complex, hierarchical data retrieval
Example GraphQL query structure:
{
PatientList(name: "Smith") {
id
name { family given }
Observations: ObservationList(_reference: subject) {
code { coding { code display } }
valueQuantity { value unit }
}
}
}
GraphQL support varies by server. Check your server's CapabilityStatement for $graphql operation support.
Prerequisites
Before you begin, ensure you have the following:
- An operational FHIR server with resources available for querying.
- The .NET SDK installed, available from the official .NET website.
- Visual Studio installed, which you can download from the Visual Studio website.
- You can find all the code demonstrated in this tutorial on GitHub here
“The present moment is filled with joy and happiness. If you are attentive, you will see it.” ~ Thich Nhat Hanh
Step 1 of 3: Understanding Chained Queries
Chained queries in FHIR allow you to search for resources based on parameters of related resources. For example, you can search for all `Observations` linked to a specific `Patient` by chaining the `patient` parameter:
using Hl7.Fhir.Model;
using Hl7.Fhir.Rest;
class Program
{
static async Task Main(string[] args)
{
// Replace with your FHIR server base URL
string fhirServerUrl = "http://hapi.fhir.org/baseR4";
var settings = new FhirClientSettings
{
PreferredFormat = ResourceFormat.Json,
ReturnPreference = ReturnPreference.Representation
};
var fhirClient = new FhirClient(fhirServerUrl, settings);
// First, search for a patient to get a valid ID
var patientSearch = await fhirClient.SearchAsync<Patient>(
new SearchParams().LimitTo(1));
if (patientSearch?.Entry?.Count == 0)
{
Console.WriteLine("No patients found on server.");
return;
}
var foundPatient = patientSearch.Entry[0].Resource as Patient;
string patientId = foundPatient?.Id;
Console.WriteLine($"Found patient with ID: {patientId}");
// Search for Observations related to this Patient
var observations = await fhirClient.SearchAsync<Observation>(
new SearchParams().Where($"patient={patientId}"));
// Print observation details with null-safe access and pattern matching
foreach (var entry in observations?.Entry ?? new List<Bundle.EntryComponent>())
{
if (entry.Resource is Observation obs)
{
Console.WriteLine($"Observation ID: {obs.Id}, Status: {obs.Status}");
}
}
}
}
In this example, we perform a chained query to retrieve all `Observations` associated with a specific `Patient`. Chained queries like these are invaluable for scenarios where you need to gather related data points across multiple FHIR resources.
Step 2 of 3: Advanced Chaining with Multiple Levels
Advanced chaining involves queries that span multiple levels of resource relationships. FHIR provides forward chaining (using `.` notation) and reverse chaining (using `_has`). For example, to find all `Observation` resources for a patient with a specific family name, you use forward chaining:
// Forward chaining: Find Observations for patients named "Smith"
var observations = await fhirClient.SearchAsync<Observation>(
new SearchParams().Where("subject:Patient.family=Smith"));
foreach (var entry in observations?.Entry ?? new List<Bundle.EntryComponent>())
{
if (entry.Resource is Observation obs)
{
Console.WriteLine($"Observation ID: {obs.Id}, Code: {obs.Code?.Text}");
}
}
// Reverse chaining: Find Patients who have an Observation with a specific code
// Use _has to search for patients who are referenced BY another resource
var patients = await fhirClient.SearchAsync<Patient>(
new SearchParams().Where("_has:Observation:subject:code=8867-4"));
foreach (var entry in patients?.Entry ?? new List<Bundle.EntryComponent>())
{
if (entry.Resource is Patient patient)
{
Console.WriteLine($"Patient ID: {patient.Id}");
}
}
Forward chaining (using `subject:Patient.family=Smith`) searches through a reference to filter by the referenced resource's properties. Reverse chaining (using `_has:Observation:subject:code`) finds resources that are referenced by other resources matching certain criteria. These techniques are powerful for clinical scenarios where relationships between different types of resources are crucial.
Step 3 of 3: Handling Chained Query Results
Handling the results of chained queries requires careful consideration, especially when dealing with large datasets. The FHIR .NET SDK provides methods to efficiently manage and paginate through query results:
var resultSet = await fhirClient.SearchAsync<Observation>(new string[] { $"patient={patientId}" });
while (resultSet != null)
{
foreach (var observation in resultSet.Entry)
{
Console.WriteLine($"Observation ID: {observation.Resource.Id}, Status: {((Observation)observation.Resource).Status}");
}
// Fetch the next page of results using the client's Continue method
resultSet = await fhirClient.ContinueAsync(resultSet);
}
In this example, we handle the results of a chained query by iterating through each page of results. The `Continue` method on the FhirClient takes the current Bundle and fetches the next page of data using the bundle's "next" link, ensuring that you can process large result sets efficiently.
Conclusion
In this article, we explored how to chain FHIR operations using .NET and the Azure FHIR Server. You learned how to construct basic and advanced chained queries, as well as how to handle the resulting data. Chaining operations is a powerful technique for retrieving related healthcare data across multiple FHIR resources, enabling more complex and insightful queries.
With these skills, you can harness the full potential of your FHIR server for comprehensive data retrieval. Stay tuned for the next tutorial in this series, where we'll delve deeper into other advanced aspects of FHIR programming.