FHIR Programming using .NET - Reading a FHIR Resource
Introduction
Welcome back to our ongoing series on FHIR Programming using .NET. In our last article, we explored the process of creating a FHIR resource. Now that you've successfully created your first FHIR resource, it's time to learn how to read and retrieve these resources from the FHIR server. Understanding how to read FHIR resources is fundamental for interacting with your healthcare application, as it allows you to access and display patient data, observations, and other critical information.
In this tutorial, we'll guide you through the process of reading a FHIR resource using the FHIR .NET SDK, focusing specifically on retrieving a `Patient` resource. You'll learn how to use the FHIR API to search for and read resources, allowing you to build robust applications that can query and display healthcare data in real-time.
Understanding FHIR Read Operations
Before diving into the code, let's understand the key concepts that govern how FHIR resources are identified, versioned, and retrieved.
Resource Identity: Logical ID vs Business Identifiers
FHIR resources have two distinct types of identifiers:
Logical ID (Resource.Id) - A server-assigned identifier that uniquely identifies a resource within a specific FHIR server:
- Assigned by the server when the resource is created
- Used in the resource URL:
[base]/Patient/123 - Only unique within that server - the same logical ID may exist on different servers
- Should not carry business meaning (it's just a technical identifier)
Business Identifiers (Resource.Identifier) - Organization-assigned identifiers that have meaning in the real world:
- Examples: Medical Record Number (MRN), Social Security Number, Driver's License
- Portable across systems - the same patient keeps their MRN regardless of which FHIR server stores their data
- Use System/Value pairs for global uniqueness
- A resource can have multiple business identifiers
When designing integrations, prefer searching by business identifiers rather than logical IDs, as business identifiers remain consistent across different FHIR servers.
Resource Versioning
Every FHIR resource maintains version information in its Meta element:
- VersionId - A server-assigned version number that increments with each update. Combined with the logical ID, it creates a unique reference to a specific version:
Patient/123/_history/2 - LastUpdated - Timestamp indicating when the resource was last modified on the server
You can retrieve specific versions or the complete history of a resource:
// Read the current version
var current = await fhirClient.ReadAsync<Patient>("Patient/123");
// Read a specific version
var version2 = await fhirClient.ReadAsync<Patient>("Patient/123/_history/2");
// Get complete history
var history = await fhirClient.HistoryAsync("Patient/123");
Bundle Types
When reading multiple resources or search results, FHIR returns them in a Bundle. Understanding bundle types is essential:
- searchset - Results from a search operation. Includes pagination links and total count.
- collection - A curated set of resources grouped together (no specific processing semantics).
- transaction - A set of operations to be performed atomically (all-or-nothing).
- transaction-response - The server's response to a transaction bundle.
- batch - Similar to transaction but operations are independent (some may fail while others succeed).
- batch-response - The server's response to a batch bundle.
- history - Version history of a resource or set of resources.
- document - A clinical document with a Composition as the first entry.
- message - A FHIR message with MessageHeader as the first entry.
Search results always return a searchset bundle with entries containing the matched resources:
var results = await fhirClient.SearchAsync<Patient>();
// Bundle type will be "Searchset"
Console.WriteLine($"Bundle type: {results.Type}");
// Total number of matches (may be more than entries returned due to pagination)
Console.WriteLine($"Total matches: {results.Total}");
// Check for pagination
var nextLink = results.Link.FirstOrDefault(l => l.Relation == "next");
if (nextLink != null)
{
Console.WriteLine("More results available");
}
HTTP Content Negotiation
FHIR supports both JSON and XML representations. Clients can request their preferred format using HTTP content negotiation:
- Accept header - Specifies the desired response format:
application/fhir+json- Request JSON formatapplication/fhir+xml- Request XML format
- _format parameter - Alternative way to specify format in the URL:
?_format=json - Content-Type header - Specifies the format of data being sent to the server
In the .NET SDK, you can configure the preferred format when creating the FhirClient:
// Set encoding preference in client settings
var settings = new FhirClientSettings
{
PreferredFormat = ResourceFormat.Json // or ResourceFormat.Xml
};
var fhirClient = new FhirClient(fhirServerUrl, settings);
// Serialize resources in a specific format
var jsonSerializer = new FhirJsonSerializer();
var xmlSerializer = new FhirXmlSerializer();
string jsonOutput = jsonSerializer.SerializeToString(patient);
string xmlOutput = xmlSerializer.SerializeToString(patient);
JSON is generally preferred for web applications due to smaller payload size and native JavaScript support, while XML may be required for certain legacy integrations.
Error Handling Patterns
Robust error handling is essential when reading FHIR resources. Common scenarios include:
- 404 Not Found - Resource doesn't exist (FhirOperationException with NotFound status)
- 410 Gone - Resource was deleted
- 403 Forbidden - Insufficient permissions to read the resource
- 400 Bad Request - Invalid search parameters or malformed request
FHIR servers return an OperationOutcome resource with error details, which can provide valuable diagnostic information. The .NET SDK includes this in the FhirOperationException.Outcome property.
Prerequisites
Before proceeding, ensure you have the following set up:
- A functioning FHIR server with a `Patient` resource already created (refer to the previous article if needed).
- 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 only limit to our realization of tomorrow is our doubts of today.” ~ Franklin D. Roosevelt
Step 1 of 3: Reading a FHIR Resource
Before we begin coding, it's important to understand what a FHIR resource is. A FHIR resource is the core building block of the FHIR standard. Each resource represents a specific piece of data related to healthcare, such as a patient, a medication, or a diagnosis. Resources are represented in either JSON or XML format, and each resource type has a predefined structure defined by the FHIR standard.
For this tutorial, we'll focus on reading a `Patient` resource. The `Patient` resource is one of the most commonly used resources in FHIR, representing an individual receiving care. It contains information such as the patient's name, gender, birth date, and contact information. Understanding the structure of the `Patient` resource will help you better interact with the FHIR API and effectively create and manage patient data in your healthcare application.
To read a FHIR resource, you'll use the `FhirClient.ReadAsync
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);
// First, search for a patient to get a valid ID (since we need an existing patient)
var searchResult = await fhirClient.SearchAsync<Patient>(
new SearchParams().LimitTo(1));
if (searchResult?.Entry?.Count == 0)
{
Console.WriteLine("No patients found on server.");
return;
}
// Get the ID from the first patient found
var firstPatient = searchResult.Entry[0].Resource as Patient;
var patientId = firstPatient?.Id;
Console.WriteLine($"Found patient with ID: {patientId}");
// Read the Patient resource by its ID using async/await
var patient = await fhirClient.ReadAsync<Patient>($"Patient/{patientId}");
// Null-safe check for the retrieved patient
if (patient == null)
{
Console.WriteLine("Error: Failed to retrieve patient from server.");
return;
}
// Output the patient details with null-safe access
Console.WriteLine("Patient ID: " + patient.Id);
if (patient.Name?.Count > 0)
{
var name = patient.Name[0];
Console.WriteLine($"Patient Name: {name.Family}, {string.Join(" ", name.Given ?? new List<string>())}");
}
Console.WriteLine("Patient Birthdate: " + patient.BirthDate);
}
}
Step 2 of 3: Searching for a FHIR Resource
Sometimes, you may not have the specific ID of the resource you need to read. In such cases, you can search for resources using the `FhirClient.Search
// Search using async/await pattern
var searchParams = new SearchParams()
.Where("family=Doe")
.LimitTo(10);
var searchResult = await fhirClient.SearchAsync<Patient>(searchParams);
// Null-safe iteration over results
foreach (var entry in searchResult?.Entry ?? new List<Bundle.EntryComponent>())
{
// Safe type check with pattern matching
if (entry.Resource is Patient patient)
{
Console.WriteLine("Found Patient ID: " + patient.Id);
if (patient.Name?.Count > 0)
{
var name = patient.Name[0];
Console.WriteLine($"Patient Name: {name.Family}, {string.Join(" ", name.Given ?? new List<string>())}");
}
}
}
In this example, the search query is performed using the family name "Doe". The `SearchParams` class is used to define the search parameters, and the result is a bundle containing all matching `Patient` resources. The retrieved resources are then iterated through and displayed.
Step 3 of 3: Handling Errors and Exceptions
When interacting with a FHIR server, it's important to handle potential errors and exceptions that may occur, such as a resource not being found or a connection issue. The FHIR .NET SDK provides various exceptions like `FhirOperationException` that you can catch and handle appropriately:
try
{
// Async read operation
var patient = await fhirClient.ReadAsync<Patient>("Patient/non-existent-id");
}
catch (FhirOperationException ex)
{
Console.WriteLine("Error: " + ex.Message);
if (ex.Status == System.Net.HttpStatusCode.NotFound)
{
Console.WriteLine("The specified patient could not be found (404 Not Found).");
}
// Optionally display operation outcome details
if (ex.Outcome != null)
{
foreach (var issue in ex.Outcome.Issue)
{
Console.WriteLine($" Issue: {issue.Diagnostics}");
}
}
}
In this snippet, an attempt is made to read a `Patient` resource that doesn't exist. The `FhirOperationException` is caught, and the error message is displayed. Handling errors like this ensures your application can gracefully respond to issues and provide meaningful feedback to users.
“In the end, we will remember not the words of our enemies, but the silence of our friends.” ~ Martin Luther King Jr.
Conclusion
By following this tutorial, you've learned how to read FHIR resources from an Azure FHIR Server using .NET. This knowledge is crucial for developing healthcare applications that can effectively retrieve and display patient data and other healthcare information. In the next tutorial in this series, we'll cover how to create FHIR resources, allowing you to add new healthcare data to your FHIR server.
Continue exploring the possibilities of FHIR programming with .NET, and stay tuned for more in-depth guides in our series.