FHIR Programming using .NET - Validating FHIR Resources

Introduction

Welcome back to our FHIR programming series using .NET. In our previous article, we explored how to search for FHIR resources, a fundamental operation for retrieving specific healthcare data. In this article, we'll delve into the validation of FHIR resources. Validation is a critical step to ensure that the data being stored or exchanged conforms to the FHIR standard, maintaining the integrity and reliability of healthcare information.

We'll guide you through the process of validating FHIR resources using the FHIR .NET SDK. You'll learn how to check for compliance with FHIR specifications, how to handle validation results, and best practices for integrating validation into your healthcare applications.

Understanding FHIR Validation

Before diving into validation code, it's essential to understand the theoretical foundation of FHIR validation and the different layers involved.

StructureDefinitions: The Foundation of Validation

A StructureDefinition is a FHIR resource that defines the structure and constraints for other resources. It's the foundation of all FHIR validation:

  • Base Definitions - FHIR defines a StructureDefinition for each resource type (Patient, Observation, etc.). These define the "base" rules all instances must follow.
  • Profiles - StructureDefinitions that constrain or extend base resources for specific use cases. For example, US Core Patient adds requirements specific to US healthcare.
  • Extensions - StructureDefinitions that define new elements that can be added to resources to capture data not in the base specification.

StructureDefinitions specify:

  • Which elements are required, optional, or prohibited
  • Cardinality constraints (min/max occurrences)
  • Data type restrictions
  • Fixed values or patterns
  • Terminology bindings (which code systems/value sets to use)
  • Invariants (business rules expressed as FHIRPath expressions)

Validation Levels

FHIR validation occurs at multiple levels, each catching different types of issues:

1. Schema/Structural Validation

  • Verifies the resource is well-formed JSON or XML
  • Checks that element names are valid for the resource type
  • Validates data types (strings, booleans, dates are properly formatted)
  • Ensures required elements are present

2. Cardinality Validation

  • Checks minimum and maximum occurrences of elements
  • Example: Patient.Name has cardinality 0..* (optional, unlimited), while Observation.Status has 1..1 (required, exactly one)

3. Invariant/Business Rule Validation

  • Evaluates FHIRPath expressions that define constraints
  • Example: "If Observation.DataAbsentReason is present, Observation.Value should not be present"
  • Cross-element validation that can't be expressed by simple cardinality

4. Profile Conformance Validation

  • Validates against implementation guide profiles
  • Checks additional constraints beyond the base specification
  • Verifies required extensions are present

5. Terminology Binding Validation

  • Verifies coded values come from the correct code systems
  • Checks that codes are valid members of required value sets
  • Validates binding strength (required, extensible, preferred, example)

Binding Strengths

Terminology bindings have different strengths that affect validation:

  • Required - Must use a code from the specified value set. Validation fails if not.
  • Extensible - Must use a code from the value set if an appropriate code exists; otherwise, can use other codes.
  • Preferred - Recommended to use codes from the value set, but not enforced.
  • Example - Just examples; any code from the code system is acceptable.

OperationOutcome Resource

Validation results are returned as an OperationOutcome resource, which contains detailed information about issues found:

// OperationOutcome structure
var outcome = await fhirClient.ValidateAsync(patient);

foreach (var issue in outcome.Issue ?? new List<OperationOutcome.IssueComponent>())
{
    // Severity: fatal, error, warning, information
    Console.WriteLine($"Severity: {issue.Severity}");

    // Code: categorizes the type of issue
    Console.WriteLine($"Code: {issue.Code}");

    // Location: FHIRPath to the element with the issue
    if (issue.Location?.Any() == true)
        Console.WriteLine($"Location: {issue.Location.First()}");

    // Diagnostics: human-readable description
    Console.WriteLine($"Details: {issue.Diagnostics}");
}

Severity Levels:

  • Fatal - The resource is unusable; processing cannot continue
  • Error - A violation of the specification; the resource is not conformant
  • Warning - A potential issue that doesn't make the resource invalid
  • Information - Informational messages; no action required

Slicing and Discriminators

Slicing is a powerful profiling technique that allows you to define different constraints for different occurrences of a repeating element:

  • Slicing - Divides a repeating element into "slices" with different rules
  • Discriminator - Tells validators how to identify which slice an instance belongs to

Common discriminator types:

  • value - Match based on the value of a child element
  • pattern - Match based on a pattern in a child element
  • type - Match based on the data type used
  • profile - Match based on which profile a resource conforms to

Example: A profile might slice Patient.Identifier to require:

  • One identifier with System = "http://hospital.org/mrn" (MRN)
  • One identifier with System = "http://hl7.org/fhir/sid/us-ssn" (SSN)

Terminology Services for Validation

FHIR defines terminology operations that support validation:

  • $validate-code - Check if a code is valid in a code system or value set
  • $lookup - Get details about a code (display name, properties)
  • $expand - Get all codes in a value set
// Validate a code against a value set
var parameters = new Parameters();
parameters.Add("system", new FhirUri("http://loinc.org"));
parameters.Add("code", new Code("8867-4"));
parameters.Add("url", new FhirUri("http://hl7.org/fhir/ValueSet/observation-vitalsignresult"));

var result = await fhirClient.TypeOperationAsync<ValueSet>("validate-code", parameters);

var isValid = (result.Parameter.FirstOrDefault(p => p.Name == "result")?.Value as FhirBoolean)?.Value ?? false;

Prerequisites

Before you begin, ensure you have the following:

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

“Time changes everything except something within us which is always surprised by change.” ~ Thomas Hardy

Step 1 of 3: Basic Resource Validation

To validate a FHIR resource, you'll use the `Validate` operation provided by the FHIR .NET SDK. The following example demonstrates how to validate a `Patient` resource:

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

        // Create a sample patient resource
        var patient = new Patient()
        {
            Active = true,
            Name = new List<HumanName> { new HumanName { Family = "Doe", Given = new[] { "John" } } },
            Gender = AdministrativeGender.Male,
            BirthDate = "1980-01-01"
        };

        // Validate the patient resource using async/await
        var validationResult = await fhirClient.ValidateAsync(patient);

        // Print validation results with null-safe access
        if (validationResult?.Issue?.Count > 0)
        {
            Console.WriteLine($"Validation status: {validationResult.Issue[0].Severity}");
            foreach (var issue in validationResult.Issue)
            {
                Console.WriteLine($"Issue: {issue.Diagnostics}");
            }
        }
        else
        {
            Console.WriteLine("Validation passed with no issues.");
        }
    }
}

In this snippet, we create a sample `Patient` resource and validate it against the FHIR server. The validation result is then analyzed to determine if there are any issues, such as missing required elements or incorrect data types. Basic validation ensures that your resources adhere to FHIR standards before being stored or shared.

Step 2 of 3: Advanced Validation Scenarios

Advanced validation scenarios involve checking resources against profiles or performing validation in different contexts. For example, you might need to validate a resource against a specific FHIR profile required by a healthcare organization:

// Validate against a specific profile using async/await
var profileUri = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient";
var validationWithProfile = await fhirClient.ValidateAsync(patient, profile: profileUri);

// Null-safe iteration over issues
foreach (var issue in validationWithProfile?.Issue ?? new List<OperationOutcome.IssueComponent>())
{
    Console.WriteLine($"Profile Validation Issue: {issue.Diagnostics}");
}

In this example, we validate the `Patient` resource against the US Core Patient profile. This type of validation ensures that the resource not only meets general FHIR standards but also complies with specific guidelines for use in particular healthcare settings.

Step 3 of 3: Handling Validation Results

Handling validation results properly is crucial, especially in production environments where invalid data could disrupt healthcare operations. The FHIR .NET SDK provides mechanisms to interpret and respond to validation feedback:

if (validationResult.Issue.Any(issue => issue.Severity == OperationOutcome.IssueSeverity.Error))
{
    Console.WriteLine("Resource validation failed. Please correct the errors and try again.");
    foreach (var issue in validationResult.Issue)
    {
        if (issue.Severity == OperationOutcome.IssueSeverity.Error)
        {
            Console.WriteLine($"Error: {issue.Diagnostics}");
        }
    }
}
else
{
    Console.WriteLine("Resource validated successfully. Proceeding with the next steps.");
}

In this example, we check if any validation issues have a severity of `Error`. If errors are found, they are logged, and the resource is not processed further. This ensures that only valid and compliant resources are used in your application, reducing the risk of data issues down the line.

Conclusion

In this article, we covered the validation of FHIR resources using .NET and the Azure FHIR Server. You learned how to perform basic validation, advanced validation against specific profiles, and how to handle the results effectively. Validating your resources is a key step in maintaining the quality and integrity of healthcare data.

Armed with these validation techniques, you can ensure that your FHIR resources meet the necessary standards before they are stored or exchanged. Stay tuned for the next tutorial in this series, where we'll explore even more advanced aspects of FHIR programming.