FHIR Programming using .NET - Searching FHIR Resources

Introduction

Welcome back to our ongoing series on FHIR Programming using .NET. In the previous articles, we've covered how to update and delete FHIR resources, which are crucial for managing healthcare data. Now, we will explore how to search for FHIR resources. Searching for resources is a fundamental operation that allows you to retrieve specific data based on various criteria, making it a powerful tool in healthcare applications.

In this tutorial, we'll guide you through the process of searching for FHIR resources using the FHIR .NET SDK. We'll cover basic searches, advanced search techniques, and how to handle search results, equipping you with the skills to efficiently access and manage healthcare data.

FHIR search is one of the most powerful features of the standard. Before diving into code, let's understand the comprehensive search capabilities FHIR provides.

Search Parameter Types

FHIR defines several types of search parameters, each designed for specific data types:

  • string - For text searches (names, addresses). By default, performs case-insensitive, accent-insensitive matching from the start of the string.
    • Example: Patient?name=john matches "John", "Johnny", "Johnston"
  • token - For coded values and identifiers. Matches exact code values, optionally scoped by system.
    • Example: Patient?identifier=http://hospital.org|12345
    • Example: Patient?gender=male
  • reference - For references to other resources. Can search by resource type and ID.
    • Example: Observation?subject=Patient/123
  • date - For date/time values. Supports precision levels and ranges.
    • Example: Patient?birthdate=1980-01-01
  • quantity - For numerical quantities with units. Matches value, system, and code.
    • Example: Observation?value-quantity=5.4|http://unitsofmeasure.org|mg
  • number - For numerical values without units.
    • Example: RiskAssessment?probability=0.5
  • uri - For URI values. Matches the full URI exactly.
    • Example: ValueSet?url=http://hl7.org/fhir/ValueSet/example
  • composite - Combines multiple parameters into a single search. Useful for searching multi-component values.
    • Example: Observation?component-code-value-quantity=http://loinc.org|8480-6$gt100

Search Modifiers

Modifiers change the behavior of search parameters:

  • :exact - For string parameters: matches the entire string exactly (case-sensitive)
    • Example: Patient?name:exact=John matches only "John", not "john" or "Johnny"
  • :contains - For string parameters: matches if the value appears anywhere in the target
    • Example: Patient?name:contains=onn matches "John", "Donna", "Connor"
  • :missing - Tests whether the element is present or absent
    • Example: Patient?birthdate:missing=true finds patients without birth dates
  • :not - For token parameters: negates the search (finds resources that don't match)
    • Example: Patient?gender:not=male finds non-male patients
  • :text - For token parameters: searches the display text rather than the code
    • Example: Condition?code:text=headache
  • :above/:below - For token parameters on hierarchical code systems: matches codes in the hierarchy
    • Example: Condition?code:below=http://snomed.info/sct|73211009 (diabetes and all subtypes)

Search Prefixes for Comparisons

For date, number, and quantity parameters, prefixes specify comparison operators:

  • eq - Equal (default if no prefix specified)
  • ne - Not equal
  • gt - Greater than
  • lt - Less than
  • ge - Greater than or equal
  • le - Less than or equal
  • sa - Starts after (for periods)
  • eb - Ends before (for periods)
  • ap - Approximately equal (the acceptable range is determined by the server implementation)
// Find patients born after 1980
var result = await fhirClient.SearchAsync<Patient>(new SearchParams()
    .Where("birthdate=ge1980-01-01"));

// Find observations with values greater than 100
var obsResult = await fhirClient.SearchAsync<Observation>(new SearchParams()
    .Where("value-quantity=gt100"));

Search Result Parameters

These parameters control what's included in search results:

  • _include - Include referenced resources in the result bundle
    • Example: MedicationRequest?_include=MedicationRequest:patient
    • Returns MedicationRequests AND their referenced Patients
  • _revinclude - Include resources that reference the matched resources
    • Example: Patient?_revinclude=Observation:subject
    • Returns Patients AND Observations that reference those Patients
  • _summary - Return only a subset of elements (true, false, text, count, data)
    • _summary=true returns only elements marked as "summary"
    • _summary=count returns only the total count, no actual resources
  • _elements - Specify exactly which elements to return
    • Example: Patient?_elements=name,birthDate,gender
  • _sort - Sort results by a search parameter
    • Example: Patient?_sort=birthdate (ascending)
    • Example: Patient?_sort=-birthdate (descending)
  • _count - Limit the number of results per page
  • _total - Control whether total count is returned (none, estimate, accurate)
// Search with includes and sorting
var searchParams = new SearchParams()
    .Where("family=Smith")
    .Include("Patient:organization")
    .OrderBy("birthdate", SortOrder.Descending)
    .LimitTo(20);

var result = await fhirClient.SearchAsync<Patient>(searchParams);

Compartment Searches

Compartments define logical groupings of resources related to a specific resource. The most common is the Patient compartment, which groups all resources related to a patient:

// Search all observations in a patient's compartment
// This finds all Observations where subject=Patient/123
var result = await fhirClient.SearchAsync<Observation>(new SearchParams(), "Patient", "123");

Common compartments include:

  • Patient - All resources related to a patient
  • Practitioner - All resources related to a practitioner
  • Encounter - All resources related to an encounter
  • RelatedPerson - All resources related to a related person
  • Device - All resources related to a device

Compartment searches are efficient ways to retrieve all data related to a specific entity, which is common in clinical workflows.

Chained and Reverse Chained Searches

Chained searches allow you to filter based on properties of referenced resources:

// Find observations where the patient's name is "Smith"
var chainedResult = await fhirClient.SearchAsync<Observation>(new SearchParams()
    .Where("subject:Patient.family=Smith"));

Reverse chained searches (using _has) find resources that are referenced by other resources matching criteria:

// Find patients who have observations with a specific code
var reverseChainedResult = await fhirClient.SearchAsync<Patient>(new SearchParams()
    .Where("_has:Observation:subject:code=http://loinc.org|8867-4"));

Prerequisites

Before proceeding, ensure you have the following set up:

  • An operational FHIR server with resources available for searching.
  • 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

“Data is the new oil, and healthcare data is no exception. Efficiently searching and accessing this data is key to unlocking its value.” ~ Clive Humby

Step 1 of 3: Basic Search by Criteria

To search for FHIR resources, you'll typically start with basic criteria, such as searching for patients by their family name. The following example demonstrates how to perform a basic search using the FHIR .NET SDK:

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";

        // Configure the FHIR client with proper settings
        var settings = new FhirClientSettings
        {
            PreferredFormat = ResourceFormat.Json,
            ReturnPreference = ReturnPreference.Representation
        };

        var fhirClient = new FhirClient(fhirServerUrl, settings);

        // Search for patients by family name using async/await
        var searchResult = await fhirClient.SearchAsync<Patient>(new SearchParams()
            .Where("family=Doe"));

        // Null-safe iteration through the search results
        foreach (var entry in searchResult?.Entry ?? new List<Bundle.EntryComponent>())
        {
            // Safe type check with pattern matching
            if (entry.Resource is Patient patient)
            {
                if (patient.Name?.Count > 0)
                {
                    var name = patient.Name[0];
                    Console.WriteLine($"Found Patient: {name.Family}, {string.Join(" ", name.Given ?? new List<string>())} (ID: {patient.Id})");
                }
            }
        }
    }
}

In this snippet, we search for `Patient` resources that have the family name "Doe". The search results are returned in a `Bundle`, and we iterate through each entry to print the patient's details. This basic search operation is foundational for retrieving specific data sets.

Step 2 of 3: Advanced Search Techniques

In addition to basic searches, FHIR supports more advanced search techniques, such as searching by multiple criteria or chaining searches. Here's an example of how to search for `Patient` resources by both family name and birth date:

// Advanced search using async/await with multiple criteria
var advancedSearchResult = await fhirClient.SearchAsync<Patient>(new SearchParams()
    .Where("family=Doe")
    .Where("birthdate=1990-01-01"));

// Null-safe iteration with pattern matching
foreach (var entry in advancedSearchResult?.Entry ?? new List<Bundle.EntryComponent>())
{
    if (entry.Resource is Patient patient)
    {
        if (patient.Name?.Count > 0)
        {
            Console.WriteLine($"Advanced Search Result: {patient.Name[0].Family} (ID: {patient.Id})");
        }
    }
}

This example shows how to perform a more refined search by combining family name and birth date criteria. Advanced searches allow you to filter data more precisely, which is essential for handling complex healthcare scenarios.

Step 3 of 3: Handling Search Results

When performing searches, it's important to handle the results effectively, especially if the search returns a large number of resources. The FHIR .NET SDK provides mechanisms to paginate results or process them in batches. Below is an example of how to handle large search results:

// Paginated search using async/await
var paginatedSearchResult = await fhirClient.SearchAsync<Patient>(new SearchParams()
    .Where("family=Doe")
    .LimitTo(10)); // Limit results to 10 per page

// Null-safe iteration
foreach (var entry in paginatedSearchResult?.Entry ?? new List<Bundle.EntryComponent>())
{
    if (entry.Resource is Patient patient && patient.Name?.Count > 0)
    {
        Console.WriteLine($"Patient: {patient.Name[0].Family} (ID: {patient.Id})");
    }
}

// Check if there are more pages of results
if (paginatedSearchResult?.NextLink != null)
{
    var nextPage = await fhirClient.ContinueAsync(paginatedSearchResult);
    // Process the next page of results
    foreach (var entry in nextPage?.Entry ?? new List<Bundle.EntryComponent>())
    {
        if (entry.Resource is Patient patient)
        {
            // Process additional pages...
        }
    }
}

In this example, we limit the search results to 10 resources per page and then check if there are additional pages of results to process. Handling search results in this way ensures your application can manage large datasets efficiently.

Conclusion

In this article, we've explored the process of searching for FHIR resources using .NET and the Azure FHIR Server. We covered how to perform basic searches, advanced searches with multiple criteria, and how to handle large sets of search results. These search capabilities are crucial for accessing and managing healthcare data within your applications.

With the knowledge gained from this tutorial, you can now efficiently retrieve the data you need from your FHIR server. Stay tuned for the next tutorial in this series, where we will dive into even more advanced FHIR programming techniques.