FHIR Programming using .NET - Updating FHIR Resources

Introduction

Welcome back to our ongoing series on FHIR Programming using .NET. In our last article, we discussed how to read FHIR resources. Now that you can retrieve data from your FHIR server, it's time to learn how to update those resources. Updating FHIR resources is essential for maintaining accurate and up-to-date healthcare data, whether it's updating patient information, modifying observation results, or managing other critical healthcare resources.

In this tutorial, we'll guide you through the process of updating a FHIR resource using the FHIR .NET SDK. We'll focus on modifying a `Patient` resource, showing you how to use the FHIR API to perform updates. Understanding this process will empower you to build robust healthcare applications that can efficiently manage data modifications.

Understanding FHIR Update Semantics

Before diving into the code, it's important to understand the different ways FHIR handles resource modifications and the mechanisms in place to prevent data conflicts.

Full Replacement (PUT) vs Partial Update (PATCH)

FHIR supports two distinct approaches to updating resources:

Full Replacement (PUT) - The standard FHIR update operation:

  • Replaces the entire resource with the new version you provide
  • You must send the complete resource, not just the changed fields
  • Any elements not included in your update will be removed from the resource
  • Best practice: Always read the current version first, modify it, then update

Partial Update (PATCH) - Allows updating specific fields without sending the entire resource:

  • Uses either JSON Patch (RFC 6902) or FHIRPath Patch format
  • Only the fields you specify are modified; other fields remain unchanged
  • More efficient for small changes to large resources
  • Reduces the risk of accidentally overwriting concurrent changes
// FHIRPath Patch example - add a phone number
var patchParameters = new Parameters();
patchParameters.Add("operation", new Parameters.ParameterComponent
{
    Name = "operation",
    Part = new List<Parameters.ParameterComponent>
    {
        new Parameters.ParameterComponent { Name = "type", Value = new Code("add") },
        new Parameters.ParameterComponent { Name = "path", Value = new FhirString("Patient.telecom") },
        new Parameters.ParameterComponent { Name = "value", Value = new ContactPoint
        {
            System = ContactPoint.ContactPointSystem.Phone,
            Value = "555-1234"
        }}
    }
});

// Execute patch operation
var result = await fhirClient.PatchAsync<Patient>("Patient/123", patchParameters);

ETags and Optimistic Locking

FHIR uses HTTP ETags (Entity Tags) for optimistic concurrency control, preventing the "lost update" problem where two users might overwrite each other's changes:

  • ETag Header - When you read a resource, the server returns an ETag (usually the versionId) in the response header
  • If-Match Header - When updating, you can send the ETag in the If-Match header to ensure you're updating the version you think you're updating
  • 409 Conflict - If someone else modified the resource since you read it, the server returns a conflict error
// Read the patient - the SDK tracks the version automatically
var patient = await fhirClient.ReadAsync<Patient>("Patient/123");

// The version is stored in Meta.VersionId
Console.WriteLine($"Current version: {patient.Meta?.VersionId}");

// Modify the patient...
if (patient.Name?.Count > 0)
{
    patient.Name[0].Family = "NewName";
}

// Update - the SDK includes If-Match header automatically when VersionId is present
var updated = await fhirClient.UpdateAsync(patient);

// If successful, the server accepted our update
// If 409 Conflict, someone else modified it first

This optimistic locking approach allows multiple users to read and work with resources simultaneously while preventing silent data loss from concurrent updates.

Conditional Update Operations

Conditional updates let you update a resource based on search criteria rather than a known ID. This is useful when:

  • You know business identifiers but not the server's logical ID
  • You want to create-or-update in a single operation (upsert)
  • You're synchronizing data from external systems

The conditional update uses search parameters in the URL:

  • No matches - Creates a new resource (if the server supports it)
  • One match - Updates that resource
  • Multiple matches - Returns an error (412 Precondition Failed)

Common conditional update patterns:

// Update by business identifier
var searchParams = new SearchParams()
    .Where("identifier=http://hospital.org/mrn|12345");

var result = await fhirClient.UpdateAsync(patient, searchParams);

// Update by multiple criteria
var multiCriteria = new SearchParams()
    .Where("identifier=http://hospital.org/mrn|12345")
    .Where("birthdate=1980-01-01");

var result2 = await fhirClient.UpdateAsync(patient, multiCriteria);

Version-Aware Updates and Conflict Resolution

When conflicts occur, your application needs a strategy to handle them:

  1. Read-Modify-Write with Retry - Re-read the current version, apply your changes, and try again
  2. Merge Changes - Compare the conflicting versions and intelligently merge the differences
  3. User Resolution - Present both versions to the user and let them decide
  4. Last Writer Wins - Simply overwrite (use cautiously, may lose data)
// Example: Read-Modify-Write with retry pattern
int maxRetries = 3;
for (int attempt = 0; attempt < maxRetries; attempt++)
{
    try
    {
        var patient = await fhirClient.ReadAsync<Patient>("Patient/123");

        // Apply your modifications
        patient.Active = true;

        // Attempt update
        await fhirClient.UpdateAsync(patient);

        Console.WriteLine("Update successful");
        break;
    }
    catch (FhirOperationException ex) when (ex.Status == System.Net.HttpStatusCode.Conflict)
    {
        Console.WriteLine($"Conflict detected, retrying... (attempt {attempt + 1})");
        if (attempt == maxRetries - 1)
        {
            throw new Exception($"Failed to update after {maxRetries} attempts");
        }
    }
}

Server-Side Update Behavior

Different FHIR servers may handle updates differently. Important considerations:

  • Version Increment - Most servers increment VersionId on every update, even if content didn't change
  • LastUpdated - The server typically sets this timestamp; client-provided values are usually ignored
  • Validation - Servers may validate the updated resource against profiles before accepting
  • Business Rules - Servers may enforce additional constraints (e.g., preventing certain status transitions)

Prerequisites

Before proceeding, ensure you have the following set up:

“Change is the law of life. And those who look only to the past or present are certain to miss the future.” ~ John F. Kennedy

Step 1 of 3: Updating a FHIR Resource

To update a FHIR resource, you'll first need to retrieve the existing resource, make the necessary changes, and then send the updated resource back to the FHIR server. The example below demonstrates how to update the `Patient` resource's name:

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 foundPatient = searchResult.Entry[0].Resource as Patient;
        var patientId = foundPatient?.Id;
        Console.WriteLine($"Found patient with ID: {patientId}");

        // Read the existing Patient resource by its ID using async/await
        var patient = await fhirClient.ReadAsync<Patient>($"Patient/{patientId}");

        // Null-safe check before modifying
        if (patient == null)
        {
            Console.WriteLine("Error: Patient not found.");
            return;
        }

        // Update the patient's family name with null-safe access
        if (patient.Name?.Count > 0)
        {
            patient.Name[0].Family = "UpdatedFamilyName";
        }
        else
        {
            patient.Name = new List<HumanName>
            {
                new HumanName { Family = "UpdatedFamilyName" }
            };
        }

        // Perform the update on the server using async/await
        var updatedPatient = await fhirClient.UpdateAsync(patient);

        // Null-safe output of the updated patient details
        if (updatedPatient != null)
        {
            Console.WriteLine("Updated Patient ID: " + updatedPatient.Id);
            if (updatedPatient.Name?.Count > 0)
            {
                Console.WriteLine($"Updated Patient Name: {updatedPatient.Name[0].Family}");
            }
        }
    }
}

In this snippet, the `Patient` resource is read from the FHIR server using its ID. The family name is then updated, and the modified resource is sent back to the server using the `FhirClient.Update` method. The updated resource is returned by the server, allowing you to verify the changes.

Step 2 of 3: Conditional Updates

There are situations where you may want to update a resource conditionally, based on specific criteria rather than a unique ID. This is particularly useful when dealing with resources that may not have a direct identifier. The following example demonstrates how to perform a conditional update on a `Patient` resource based on an identifier:

// Conditional update using async/await
var searchCriteria = new SearchParams()
    .Where("identifier=example-identifier");

var conditionalUpdateOutcome = await fhirClient.UpdateAsync(
    patient,
    searchCriteria
);

// Null-safe access for the outcome
if (conditionalUpdateOutcome != null)
{
    Console.WriteLine("Conditional update successful. Patient ID: " + conditionalUpdateOutcome.Id);
}

In this example, the update is applied conditionally to any `Patient` resource that matches the provided identifier. If a matching resource is found, it will be updated; otherwise, a new resource may be created depending on the server's configuration. Conditional updates are a powerful feature for maintaining data integrity in dynamic environments.

Step 3 of 3: Handling Update Conflicts and Exceptions

When updating resources, it's important to handle potential conflicts and exceptions that may occur, such as version conflicts or validation errors. The FHIR .NET SDK provides mechanisms to manage these situations effectively. Here's how you can handle update conflicts:

try
{
    // Search for a patient to update
    var searchResult = await fhirClient.SearchAsync<Patient>(
        new SearchParams().LimitTo(1));

    var foundPatient = searchResult?.Entry?.FirstOrDefault()?.Resource as Patient;
    if (foundPatient == null)
    {
        Console.WriteLine("No patient found to update.");
        return;
    }

    // Read using async/await
    var patient = await fhirClient.ReadAsync<Patient>($"Patient/{foundPatient.Id}");

    if (patient == null)
    {
        Console.WriteLine("Patient not found.");
        return;
    }

    // Null-safe update
    if (patient.Name?.Count > 0)
    {
        patient.Name[0].Family = "ConflictingName";
    }

    // Update using async/await
    var updatedPatient = await fhirClient.UpdateAsync(patient);
}
catch (FhirOperationException ex) when (ex.Status == System.Net.HttpStatusCode.Conflict)
{
    Console.WriteLine("Conflict detected: " + ex.Message);
    if (ex.Outcome != null)
    {
        foreach (var issue in ex.Outcome.Issue)
        {
            Console.WriteLine($"  Issue: {issue.Diagnostics}");
        }
    }
}

In this snippet, a potential conflict during the update operation is caught and handled. The `FhirOperationException` is used to detect if the server returned a conflict status, allowing your application to respond appropriately, such as retrying the update or alerting the user.

“In the end, it’s not the years in your life that count. It’s the life in your years.” ~ Abraham Lincoln

Conclusion

In this article, you've learned how to update FHIR resources on an Azure FHIR Server using .NET. This knowledge is crucial for managing and maintaining accurate healthcare data within your applications. From simple updates to handling conditional updates and conflicts, you now have the tools to modify FHIR resources effectively.

In the next tutorial in this series, we'll dive into how to delete FHIR resources, completing the full cycle of CRUD operations necessary for robust healthcare data management in FHIR-compliant systems.