Implementation Guide

Understanding Firely SDK Modeling Patterns and Attributes

Essential guide to the internal modeling patterns, attributes, and conventions used by Firely.NET SDK that every developer should understand before building FHIR applications.

FirestarterPro Team
Feb 15, 2025
Updated Dec 25, 2025
intermediate
30 min read

Understanding Firely SDK Modeling Patterns and Attributes

Why This Matters

If you’ve started working with the Firely.NET SDK, you may have noticed some unusual patterns in the code. Properties have both Status and StatusElement versions. Collections might be null when you don’t expect them to be. Classes are decorated with mysterious attributes like [FhirElement("status", Order=90)].

These aren’t arbitrary choices—they’re carefully designed patterns that reflect FHIR’s unique requirements and the SDK’s commitment to being both developer-friendly and spec-compliant.

The SDK’s Design Philosophy

The Firely SDK faces a challenging design problem: FHIR resources need to be easy to work with in C#, but they also need to preserve all the metadata, extensions, and structural information that FHIR requires. The SDK solves this through a set of conventions and patterns that, once understood, make perfect sense.

Need a quick refresher? See FHIR for a concise definition, or What is FHIR for the canonical overview.

What You’ll Learn

This guide explains the “why” and “how” behind the SDK’s modeling patterns:

  • Why there are two properties for primitive values (Status vs StatusElement)
  • What all those C# attributes mean and how they affect your code
  • Why collections are null by default and how to work with them safely
  • How choice types (like value[x]) are represented in C#
  • The patterns that will save you hours of debugging

When to Reference This Guide

Read this guide:

  • Before you start implementing FHIR resources in your application
  • When you encounter confusing behavior with properties or serialization
  • Before adding extensions to your resources
  • When debugging why a property isn’t serializing as expected
  • When migrating between SDK versions

The Element Property Pattern

This is the single most important pattern to understand in the Firely SDK.

Understanding FHIR Primitive Elements

Why FHIR Primitives Are Special

In most data formats, a string is just a string, a date is just a date. But in FHIR, even primitive values are actually complex structures. A FHIR primitive can have:

  • An id attribute (for referencing within a resource)
  • Extensions (additional data attached to the value itself)
  • Internal elements (for XML representation)

This is defined in the FHIR specification’s Element type, which every FHIR data element inherits from.

Elements vs Simple Values

Consider this FHIR JSON:

{
  "resourceType": "Patient",
  "active": true,
  "_active": {
    "id": "active-1",
    "extension": [{
      "url": "http://example.org/confidence",
      "valueString": "confirmed"
    }]
  }
}

The active field has a simple boolean value (true), but the _active field contains metadata about that boolean—an ID and an extension. This is why FHIR primitives are special: they’re not just values, they’re elements that can carry additional information.

Extensions on Primitive Types

Why would you need extensions on a primitive? Real-world examples:

  • A patient’s birth date with an extension indicating “approximate” or “exact”
  • A status code with an extension explaining the certainty or source of the status
  • A string with an extension containing the original language before translation

Extensions in practice: For terminology and examples, see Extension and the canonical FHIR Extensions guide.

The Dual Property Pattern

The SDK represents this dual nature with two properties:

Convenience Property (e.g., Status)

public ClaimStatus? Status
{
    get { return StatusElement?.Value; }
    set
    {
        if (value == null)
            StatusElement = null;
        else
            StatusElement = new Code<ClaimStatus>(value);
    }
}

This is the property you’ll use 95% of the time. It works like a normal C# property:

claim.Status = ClaimStatus.Active;
if (claim.Status == ClaimStatus.Draft) { /* ... */ }

Behind the scenes, this property is actually getting and setting the StatusElement property’s Value.

Element Property (e.g., StatusElement)

[FhirElement("status", InSummary=true, Order=90)]
public Code<ClaimStatus> StatusElement { get; set; }

This is the actual property that gets serialized. It’s a full Element object that can have extensions, IDs, and all the FHIR metadata.

Use this when you need to:

  • Add extensions to the primitive value
  • Set an element ID
  • Access existing extensions
  • Preserve all metadata when cloning

When to Use Which

Use the convenience property (Status) when:

  • You’re just setting or reading the value
  • You don’t care about extensions or IDs
  • You want clean, simple code
claim.Status = ClaimStatus.Active;
var isDraft = claim.Status == ClaimStatus.Draft;

Use the element property (StatusElement) when:

  • You need to add or read extensions on the primitive
  • You need to set or read the element ID
  • You’re working with complex metadata
claim.StatusElement = new Code<ClaimStatus>(ClaimStatus.Active);
claim.StatusElement.Extension.Add(new Extension
{
    Url = "http://example.org/certainty",
    Value = new FhirString("confirmed")
});

Practical Examples

Example 1: Simple Assignment

Most of the time, you’ll use the simple property:

var patient = new Patient
{
    Active = true,
    Gender = AdministrativeGender.Female,
    BirthDate = "1980-05-15"
};

This is clean and straightforward. The SDK handles the Element properties behind the scenes.

Example 2: Adding Extension to Primitive

When you need extensions, use the Element property:

var patient = new Patient();

// Set the birth date with an extension indicating it's approximate
patient.BirthDateElement = new Date("1980-05-15");
patient.BirthDateElement.Extension.Add(new Extension
{
    Url = "http://hl7.org/fhir/StructureDefinition/patient-birthTime",
    Value = new FhirDateTime("1980-05-15T14:30:00Z")
});

Example 3: Setting Element ID

Element IDs are used for internal references within a resource:

var observation = new Observation();

observation.StatusElement = new Code<ObservationStatus>(ObservationStatus.Final);
observation.StatusElement.ElementId = "obs-status-1";

// Later, something else in the resource can reference #obs-status-1

Common Patterns

Reading Values Safely

Always check for null when reading Element properties:

// Unsafe - will throw if StatusElement is null
var status = claim.StatusElement.Value;

// Safe - using convenience property (already null-safe)
var status = claim.Status;

// Safe - explicit null check
var status = claim.StatusElement?.Value;

Modifying Existing Elements

When modifying, preserve existing extensions:

// WRONG - this loses any existing extensions
patient.BirthDate = "1985-01-01";

// RIGHT - preserve extensions
if (patient.BirthDateElement == null)
{
    patient.BirthDateElement = new Date("1985-01-01");
}
else
{
    patient.BirthDateElement.Value = "1985-01-01";
}

Preserving Extensions

When cloning or copying:

// WRONG - loses extensions
newClaim.Status = oldClaim.Status;

// RIGHT - preserves all metadata
newClaim.StatusElement = oldClaim.StatusElement?.DeepCopy() as Code<ClaimStatus>;

Pitfalls to Avoid

Pitfall 1: Losing Extensions on Assignment

// Patient has a birthDate with an extension
patient.BirthDateElement.Extension.Add(myExtension);

// This WIPES OUT the extension!
patient.BirthDate = "1980-05-15";

// The extension is gone because the setter creates a new Date object

Solution: Modify the Value property of the Element:

patient.BirthDateElement.Value = "1980-05-15"; // Preserves extensions

Pitfall 2: Null Reference on Element Property

// This will throw NullReferenceException if StatusElement is null
claim.StatusElement.Extension.Add(myExtension);

// Safe approach
if (claim.StatusElement == null)
    claim.StatusElement = new Code<ClaimStatus>(ClaimStatus.Active);

claim.StatusElement.Extension.Add(myExtension);

SDK Attributes Deep Dive

The SDK uses C# attributes extensively to control serialization, validation, and runtime behavior. Understanding these attributes helps you debug issues and understand what the SDK is doing.

[FhirElement] Attribute

This is the most important attribute—it marks a property as a FHIR element and controls how it’s serialized.

Purpose

The [FhirElement] attribute:

  • Maps the C# property name to the FHIR element name
  • Controls serialization order
  • Indicates if the element appears in summary mode
  • Specifies which FHIR version introduced the element
  • Marks choice type properties

Properties

Name (XML/JSON element name)
[FhirElement("birthDate")]
public Date BirthDateElement { get; set; }

The Name parameter specifies the exact name used in JSON and XML. C# property names follow PascalCase conventions, but FHIR uses camelCase, so this mapping is essential.

Choice (for choice types)
[FhirElement("value[x]", Choice = ChoiceType.DatatypeChoice)]
public DataType Value { get; set; }

Indicates this property represents a FHIR choice type (the [x] suffix in FHIR).

Order (serialization order)
[FhirElement("status", Order = 90)]
public Code<ClaimStatus> StatusElement { get; set; }

[FhirElement("identifier", Order = 100)]
public List<Identifier> Identifier { get; set; }

Controls the order elements appear in serialized output. Lower numbers serialize first. This ensures consistency with FHIR specification ordering.

Since (version introduced)
[FhirElement("statusReason", Since = FhirVersion.R5)]
public CodeableConcept StatusReason { get; set; }

Indicates when an element was added to FHIR. Useful for multi-version support.

How It Affects Serialization

The serializer reads these attributes to determine:

  • What to name the JSON/XML element
  • Whether to include it in _summary mode searches
  • What order to write elements
  • How to handle choice types

Example Usage

Looking at a real SDK property:

[FhirElement("status", InSummary=true, Order=90)]
[DeclaredType(Type = typeof(Code))]
public Code<ClaimStatus> StatusElement
{
    get { return _statusElement; }
    set { _statusElement = value; OnPropertyChanged("StatusElement"); }
}

This tells us:

  • JSON/XML element name is "status"
  • It appears in summary searches (InSummary=true)
  • It serializes at position 90
  • It’s declared as a Code type
  • It has property change notification

[FhirType] Attribute

Marks a class as a FHIR type and provides metadata about it.

Purpose

The [FhirType] attribute:

  • Identifies the FHIR type name for a C# class
  • Distinguishes resources from data types
  • Enables runtime type discovery
  • Helps the serializer identify polymorphic types

Resource Type Identification

[FhirType("Patient", IsResource = true)]
public class Patient : DomainResource
{
    public override string TypeName => "Patient";
    // ...
}

This links the C# class Patient to the FHIR resource type "Patient".

Example Usage

For resources:

[FhirType("Claim", IsResource = true)]
public class Claim : DomainResource

For data types:

[FhirType("HumanName")]
public class HumanName : DataType

For backbone elements:

[FhirType("Claim.Diagnosis")]
public class DiagnosisComponent : BackboneElement

Runtime Type Discovery

The SDK uses this for:

// Deserializing JSON to the correct C# type
var resource = parser.Parse<Resource>(json);
// The parser reads "resourceType": "Patient" and creates a Patient instance

// Type checking
if (resource.TypeName == "Patient")
{
    var patient = resource as Patient;
}

[NotMapped] Attribute

Indicates that a property should NOT be serialized—it’s a helper property for C# convenience only.

Purpose

Some properties exist purely for developer convenience and shouldn’t appear in FHIR JSON/XML:

  • Computed properties derived from other values
  • Helper methods that return formatted data
  • C#-specific functionality

Computed Properties

[NotMapped]
public string FullAddress
{
    get
    {
        if (Address == null || !Address.Any()) return null;
        var addr = Address.First();
        return $"{string.Join(" ", addr.Line)}, {addr.City}, {addr.State} {addr.PostalCode}";
    }
}

This property is useful in C# code but doesn’t correspond to any FHIR element.

Helper Properties

[NotMapped]
public bool HasActiveDiagnoses => Diagnosis?.Any(d => d.Active == true) ?? false;

Example Usage

Real-world example from the SDK:

public class HumanName : DataType
{
    // These are FHIR elements - they serialize
    [FhirElement("family")]
    public FhirString FamilyElement { get; set; }

    [FhirElement("given")]
    public List<FhirString> GivenElement { get; set; }

    // This is a helper - it doesn't serialize
    [NotMapped]
    public string FullName => $"{Family}, {string.Join(" ", Given ?? new string[0])}";
}

[DeclaredType] Attribute

Specifies the declared type for a property, used when the property type is more general than the actual FHIR type.

Purpose

FHIR elements sometimes use base types (like Element or DataType), but we need to know the specific type for validation and serialization.

Polymorphic Properties

[FhirElement("value[x]", Choice = ChoiceType.DatatypeChoice)]
[AllowedTypes(typeof(Quantity), typeof(CodeableConcept), typeof(FhirString))]
[DeclaredType(Type = typeof(DataType))]
public DataType Value { get; set; }

The property is declared as DataType (base class), but can actually hold Quantity, CodeableConcept, or FhirString.

Example Usage

// The actual type in JSON might be "valueQuantity"
observation.Value = new Quantity { Value = 120, Unit = "mmHg" };

// Or "valueCodeableConcept"
observation.Value = new CodeableConcept { Text = "Normal" };

[AllowedTypes] Attribute

Constrains which types are allowed for choice type properties or polymorphic references.

Purpose

FHIR choice types don’t allow any type—they’re restricted to a specific set. This attribute enforces those restrictions.

Constraining Choice Types

[FhirElement("value[x]", Choice = ChoiceType.DatatypeChoice)]
[AllowedTypes(typeof(Quantity), typeof(Range), typeof(Ratio))]
public DataType Value { get; set; }

This observation can have a value that’s a Quantity, Range, or Ratio, but not a FhirString or CodeableConcept.

Example Usage

The validator uses this to check:

// Valid
observation.Value = new Quantity { Value = 5.5 };

// Would be invalid - FhirString not in AllowedTypes
observation.Value = new FhirString("test"); // Validation error!

[Cardinality] Attribute

Specifies the minimum and maximum occurrences of an element (corresponds to FHIR cardinality like 0..1, 1..1, 0.., 1..).

Purpose

Helps validators understand:

  • Is this element required? (Min = 1)
  • Can it repeat? (Max > 1 or Max = -1 for unlimited)

Min and Max Constraints

// Required, single value (1..1)
[Cardinality(Min = 1, Max = 1)]
public Code<ClaimStatus> StatusElement { get; set; }

// Optional, single value (0..1)
[Cardinality(Min = 0, Max = 1)]
public FhirString Text { get; set; }

// Optional, multiple values (0..*)
[Cardinality(Min = 0, Max = -1)]
public List<Identifier> Identifier { get; set; }

// Required, at least one (1..*)
[Cardinality(Min = 1, Max = -1)]
public List<HumanName> Name { get; set; }

Example Usage

// This would fail validation - Status is required (Min=1)
var claim = new Claim();
// Missing: claim.Status = ClaimStatus.Active;
validator.Validate(claim); // Error: Status is required

[Choice] Attribute

Marks a property as a FHIR choice type.

Understanding Choice Types (value[x])

In FHIR, elements ending with [x] are “choice types”—they can be one of several types. For example, Observation.value[x] can be valueQuantity, valueCodeableConcept, valueString, etc.

How Choice Properties Work

The SDK represents all variants with a single property:

[FhirElement("value[x]", Choice = ChoiceType.DatatypeChoice)]
[AllowedTypes(typeof(Quantity), typeof(CodeableConcept), typeof(FhirString), /* ... */)]
public DataType Value { get; set; }

At runtime, this property holds one of the allowed types.

Example Usage

// Set as Quantity
observation.Value = new Quantity
{
    Value = 120,
    Unit = "mmHg",
    System = "http://unitsofmeasure.org",
    Code = "mm[Hg]"
};
// Serializes as: "valueQuantity": { "value": 120, "unit": "mmHg", ... }

// Or set as CodeableConcept
observation.Value = new CodeableConcept
{
    Text = "Positive"
};
// Serializes as: "valueCodeableConcept": { "text": "Positive" }

[References] Attribute

Constrains which resource types a Reference can point to.

Purpose

Not all FHIR references can point to any resource. For example, Patient.managingOrganization can only reference an Organization, not a Claim or Observation.

Constraining Reference Targets

[References("Patient", "Group")]
public ResourceReference Subject { get; set; }

This reference can only point to Patient or Group resources.

Example Usage

// Valid - Patient is an allowed reference type
observation.Subject = new ResourceReference("Patient/123");

// Would be invalid - Claim is not in the allowed list
observation.Subject = new ResourceReference("Claim/456"); // Validation error!

The validator checks this during validation to ensure reference integrity.

Reference rules: For a deeper dive, see References and the Resource glossary entry.

Property Change Notification

OnPropertyChanged Pattern

Many SDK resource properties call OnPropertyChanged when set:

private Code<ClaimStatus> _statusElement;

[FhirElement("status", InSummary=true, Order=90)]
public Code<ClaimStatus> StatusElement
{
    get { return _statusElement; }
    set
    {
        _statusElement = value;
        OnPropertyChanged("StatusElement");
    }
}

Purpose

This pattern implements the INotifyPropertyChanged interface, which:

  • Enables data binding in UI frameworks (WPF, WinForms, Blazor)
  • Allows change tracking
  • Supports audit logging
  • Enables undo/redo functionality

Data Binding Support

In a UI application:

// WPF binding
<TextBox Text="{Binding Patient.BirthDate}" />

// When BirthDate changes, OnPropertyChanged fires
// The UI automatically updates to show the new value

Tracking Changes

You can subscribe to changes:

patient.PropertyChanged += (sender, e) =>
{
    Console.WriteLine($"Property {e.PropertyName} changed");
    // Log the change, mark as dirty, etc.
};

patient.BirthDate = "1985-01-01"; // Triggers the event

When It Matters

UI Binding Scenarios

If you’re building a UI application with data binding, this is critical:

// View model bound to UI
public class PatientViewModel
{
    public Patient Patient { get; set; }

    // UI controls automatically update when Patient properties change
}

Change Tracking

For audit systems:

public class AuditedResource<T> where T : Resource
{
    public T Resource { get; }
    private List<string> _changedProperties = new();

    public AuditedResource(T resource)
    {
        Resource = resource;
        resource.PropertyChanged += (s, e) =>
        {
            _changedProperties.Add(e.PropertyName);
            LogChange(e.PropertyName);
        };
    }
}

Audit Requirements

HIPAA and other regulations often require tracking what changed in a resource. Property change events make this straightforward.

Custom Change Handlers

You can override OnPropertyChanged for custom behavior:

public class AuditedPatient : Patient
{
    protected override void OnPropertyChanged(string property)
    {
        base.OnPropertyChanged(property);

        // Custom audit logging
        AuditLog.RecordChange(this.Id, property, DateTime.Now);
    }
}

Collection Initialization Patterns

One of the most common sources of NullReferenceException errors in SDK code is collection properties.

List Properties in FHIR Resources

FHIR resources have many collection properties:

public class Patient : DomainResource
{
    public List<Identifier> Identifier { get; set; }
    public List<HumanName> Name { get; set; }
    public List<ContactPoint> Telecom { get; set; }
    public List<Address> Address { get; set; }
    // ... many more
}

Lazy Initialization

Why Collections Are Null by Default

When you create a new Patient:

var patient = new Patient();
// patient.Identifier is NULL
// patient.Name is NULL
// patient.Address is NULL

Why? Performance and memory efficiency. If every resource pre-initialized every collection, you’d create hundreds of empty lists for properties you might never use.

Consider a Bundle with 100 Patients, each having 20+ collection properties. That’s 2000+ empty lists if we pre-initialize everything!

When Collections Get Initialized

Collections are initialized when:

  • You explicitly set them: patient.Name = new List<HumanName>()
  • You parse JSON/XML that contains elements for that collection
  • You use the convenience initializer: Identifier ??= new List<Identifier>()

Safe Collection Access

The wrong way (throws NullReferenceException):

var patient = new Patient();
patient.Identifier.Add(new Identifier { Value = "12345" });
// BOOM! Identifier is null

The right way - always check or initialize:

var patient = new Patient();

// Option 1: Initialize explicitly
patient.Identifier = new List<Identifier>();
patient.Identifier.Add(new Identifier { Value = "12345" });

// Option 2: Check for null
if (patient.Identifier == null)
    patient.Identifier = new List<Identifier>();
patient.Identifier.Add(new Identifier { Value = "12345" });

// Option 3: Null-coalescing (C# 8+)
patient.Identifier ??= new List<Identifier>();
patient.Identifier.Add(new Identifier { Value = "12345" });

// Option 4: Object initializer
var patient = new Patient
{
    Identifier = new List<Identifier>
    {
        new Identifier { Value = "12345" }
    }
};

Collection Helper Patterns

Create extension methods for safer collection access:

public static class ResourceExtensions
{
    public static void AddIdentifier(this Patient patient, Identifier identifier)
    {
        patient.Identifier ??= new List<Identifier>();
        patient.Identifier.Add(identifier);
    }

    public static void AddName(this Patient patient, HumanName name)
    {
        patient.Name ??= new List<HumanName>();
        patient.Name.Add(name);
    }
}

// Usage
patient.AddIdentifier(new Identifier { Value = "12345" });
patient.AddName(new HumanName { Family = "Smith" });

Null Coalescing for Collections

The modern C# approach:

// Initialize if null, then add
patient.Identifier ??= new List<Identifier>();
patient.Identifier.Add(identifier);

// Or in one line with collection expression (C# 12+)
patient.Identifier = [..patient.Identifier ?? [], identifier];

Best practice: Always initialize collections before adding to them. Use null-coalescing assignment (??=) for concise, safe code.

Choice Types Deep Dive

Choice types are one of FHIR’s most powerful—and confusing—features.

What Are Choice Types?

In FHIR, some elements can have different types. Instead of defining multiple elements, FHIR uses the [x] suffix to indicate “this can be one of several types.”

For example, Observation.value[x] can be:

  • valueQuantity - numerical measurement
  • valueCodeableConcept - coded value
  • valueString - text value
  • valueBoolean - true/false
  • valueInteger - whole number
  • valueRange - range of values
  • …and more

The value[x] Pattern

In FHIR JSON, you only include ONE of the possible types:

{
  "resourceType": "Observation",
  "valueQuantity": {
    "value": 120,
    "unit": "mmHg"
  }
}

OR:

{
  "resourceType": "Observation",
  "valueCodeableConcept": {
    "text": "Normal"
  }
}

But never both at the same time.

How SDK Handles Choices

The SDK represents all choice variants with a single property:

Property Naming Convention

// In FHIR spec: "value[x]"
// In SDK: "Value" property of type DataType

[FhirElement("value[x]", Choice = ChoiceType.DatatypeChoice)]
[AllowedTypes(typeof(Quantity), typeof(CodeableConcept), typeof(FhirString),
              typeof(FhirBoolean), typeof(Integer), typeof(Range),
              typeof(Ratio), typeof(SampledData), typeof(Time),
              typeof(FhirDateTime), typeof(Period))]
public DataType Value { get; set; }

The property is declared as the base type DataType, but at runtime it holds one of the allowed specific types.

Type Checking Choice Properties

Always check the actual type before using:

if (observation.Value is Quantity quantity)
{
    Console.WriteLine($"Measurement: {quantity.Value} {quantity.Unit}");
}
else if (observation.Value is CodeableConcept concept)
{
    Console.WriteLine($"Coded value: {concept.Text}");
}
else if (observation.Value is FhirString str)
{
    Console.WriteLine($"Text value: {str.Value}");
}
else
{
    Console.WriteLine($"Unexpected type: {observation.Value?.GetType().Name}");
}

Pattern matching (C# 8+) makes this elegant:

var description = observation.Value switch
{
    Quantity q => $"{q.Value} {q.Unit}",
    CodeableConcept c => c.Text,
    FhirString s => s.Value,
    FhirBoolean b => b.Value.ToString(),
    _ => "Unknown value type"
};

Allowed Types Enforcement

The [AllowedTypes] attribute restricts what you can assign:

// Valid - Quantity is in the allowed types
observation.Value = new Quantity { Value = 120 };

// Valid - CodeableConcept is allowed
observation.Value = new CodeableConcept { Text = "Normal" };

// Compiles, but validation will fail - Address is not allowed for Observation.value[x]
observation.Value = new Address(); // Validation error!

Important: The C# compiler can’t enforce this (because the property is typed as DataType), but the FHIR validator will catch it.

Common Choice Type Patterns

Effective[x] (Date or Period)

Many resources use this pattern:

// Effective as a specific date/time
condition.Effective = new FhirDateTime("2024-01-15");

// OR effective as a period
condition.Effective = new Period
{
    Start = "2024-01-01",
    End = "2024-01-31"
};

Onset[x] (Age, Date, Period, Range, String)

Conditions can have various onset types:

// Patient was 45 years old when condition started
condition.Onset = new Age { Value = 45, Unit = "years" };

// OR specific date
condition.Onset = new FhirDateTime("2020-03-15");

// OR approximate period
condition.Onset = new Period
{
    Start = "2020-03-01",
    End = "2020-03-31"
};

// OR age range
condition.Onset = new Range
{
    Low = new Quantity { Value = 40, Unit = "years" },
    High = new Quantity { Value = 45, Unit = "years" }
};

Backbone Elements and Components

FHIR resources aren’t flat—they have nested structures called “backbone elements.”

What Are Backbone Elements?

Backbone elements are complex nested structures within a resource that have their own set of elements. They’re not independent resources or reusable data types—they’re specific to their containing resource.

For example, Claim.diagnosis isn’t just a list of codes—each diagnosis entry has:

  • sequence (number)
  • diagnosis (CodeableConcept or Reference)
  • type (category of diagnosis)
  • onAdmission (was this present on admission?)
  • packageCode (DRG or similar)

Nested Classes Pattern

The SDK represents backbone elements as nested classes:

public class Claim : DomainResource
{
    [FhirType("Claim.Diagnosis")]
    public class DiagnosisComponent : BackboneElement
    {
        [FhirElement("sequence", Order = 40)]
        public PositiveInt SequenceElement { get; set; }

        [FhirElement("diagnosis[x]", Choice = ChoiceType.DatatypeChoice, Order = 50)]
        [AllowedTypes(typeof(CodeableConcept), typeof(ResourceReference))]
        public DataType Diagnosis { get; set; }

        [FhirElement("type", Order = 60)]
        public List<CodeableConcept> Type { get; set; }

        // ... more elements
    }

    [FhirElement("diagnosis", Order = 350)]
    public List<DiagnosisComponent> Diagnosis { get; set; }
}

Why Nested Classes?

This design:

  • Prevents namespace pollution: DiagnosisComponent is clearly part of Claim
  • Matches FHIR structure: The FHIR spec shows Claim.Diagnosis as a nested element
  • Enables type safety: You can’t accidentally use Claim.DiagnosisComponent where Condition.StageComponent is expected
  • Improves IntelliSense: Typing Claim. shows you all the components specific to Claim

Working with Components

Creating backbone elements:

var claim = new Claim
{
    Diagnosis = new List<Claim.DiagnosisComponent>
    {
        new Claim.DiagnosisComponent
        {
            Sequence = 1,
            Diagnosis = new CodeableConcept
            {
                Coding = new List<Coding>
                {
                    new Coding("http://hl7.org/fhir/sid/icd-10", "E11.9")
                }
            },
            Type = new List<CodeableConcept>
            {
                new CodeableConcept
                {
                    Coding = new List<Coding>
                    {
                        new Coding("http://terminology.hl7.org/CodeSystem/ex-diagnosistype", "principal")
                    }
                }
            }
        }
    }
};

Accessing backbone elements:

// Initialize collection if needed
claim.Diagnosis ??= new List<Claim.DiagnosisComponent>();

// Add a diagnosis
claim.Diagnosis.Add(new Claim.DiagnosisComponent
{
    Sequence = claim.Diagnosis.Count + 1,
    Diagnosis = new CodeableConcept { Text = "Type 2 Diabetes" }
});

// Read diagnoses
foreach (var dx in claim.Diagnosis ?? Enumerable.Empty<Claim.DiagnosisComponent>())
{
    if (dx.Diagnosis is CodeableConcept cc)
    {
        Console.WriteLine($"Diagnosis {dx.Sequence}: {cc.Text}");
    }
}

Resource Metadata and Meta Property

Every FHIR resource has a Meta property that contains metadata about the resource itself.

The Meta Class

public class Meta : Element
{
    public Id VersionIdElement { get; set; }
    public Instant LastUpdatedElement { get; set; }
    public FhirUri SourceElement { get; set; }
    public List<Canonical> Profile { get; set; }
    public List<Coding> Security { get; set; }
    public List<Coding> Tag { get; set; }
}

VersionId

The version of this specific instance:

patient.Meta = new Meta { VersionId = "1" };
// After update: VersionId = "2"

Servers increment this on each update. Useful for:

  • Optimistic locking (update only if version matches)
  • Audit trails
  • Version history

LastUpdated

When the resource was last modified:

patient.Meta = new Meta
{
    LastUpdated = DateTimeOffset.UtcNow
};

Automatically set by servers. Useful for:

  • Sync operations (“get all patients updated since X”)
  • Cache invalidation
  • Audit logs

Profile

Which FHIR profiles this resource claims to conform to:

patient.Meta = new Meta
{
    Profile = new List<Canonical>
    {
        "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient"
    }
};

Tells validators: “Validate me against US Core Patient profile.”

Profiles explained: See Profile for the short definition and Profiling for the canonical deep dive.

Tag

Application-specific tags:

patient.Meta = new Meta
{
    Tag = new List<Coding>
    {
        new Coding
        {
            System = "http://example.org/tags",
            Code = "vip",
            Display = "VIP Patient"
        }
    }
};

Useful for:

  • Application-specific categorization
  • Workflow states
  • Custom labels

Security

Security labels (confidentiality, sensitivity):

patient.Meta = new Meta
{
    Security = new List<Coding>
    {
        new Coding
        {
            System = "http://terminology.hl7.org/CodeSystem/v3-Confidentiality",
            Code = "R",
            Display = "Restricted"
        }
    }
};

Used for access control decisions.

Accessing Metadata

Always initialize Meta before setting properties:

// Initialize if null
patient.Meta ??= new Meta();

// Set version
patient.Meta.VersionId = "1";

// Add profile
patient.Meta.Profile ??= new List<Canonical>();
patient.Meta.Profile.Add("http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient");

// Add tag
patient.Meta.Tag ??= new List<Coding>();
patient.Meta.Tag.Add(new Coding("http://example.org/tags", "test"));

Type System and Inheritance

Understanding the SDK’s type hierarchy helps you work with resources generically.

FHIR Type Hierarchy

Base
  ├─ Element
  │   ├─ BackboneElement
  │   └─ DataType
  │       ├─ Primitive types (FhirString, Integer, etc.)
  │       └─ Complex types (Identifier, CodeableConcept, etc.)
  └─ Resource
      ├─ DomainResource (has text, contained, extension)
      │   ├─ Patient
      │   ├─ Observation
      │   ├─ Claim
      │   └─ ... most resources
      └─ Bundle, Parameters (infrastructure resources)

Resource

Base class for all FHIR resources:

public abstract class Resource : Base
{
    public Id IdElement { get; set; }
    public Meta Meta { get; set; }
    public FhirUri ImplicitRulesElement { get; set; }
    public Code LanguageElement { get; set; }

    public abstract string TypeName { get; }
}

Every resource inherits these properties.

DomainResource

Most clinical resources inherit from DomainResource:

public abstract class DomainResource : Resource
{
    public Narrative Text { get; set; }
    public List<Resource> Contained { get; set; }
    public List<Extension> Extension { get; set; }
    public List<Extension> ModifierExtension { get; set; }
}

This adds human-readable narrative, contained resources, and extensions.

BackboneElement

Nested structures within resources:

public abstract class BackboneElement : Element
{
    public List<Extension> ModifierExtension { get; set; }
}

Similar to DomainResource but for nested elements.

Element

Base for all FHIR elements:

public abstract class Element : Base
{
    public string ElementId { get; set; }
    public List<Extension> Extension { get; set; }
}

Everything in FHIR can have an ID and extensions.

DataType

Base for all data types (primitives and complex types):

public abstract class DataType : Element
{
}

C# Type Mapping

FHIR types map to C# classes:

FHIR TypeSDK Class
stringFhirString
booleanFhirBoolean
integerInteger
decimalFhirDecimal
dateDate
dateTimeFhirDateTime
timeTime
instantInstant
uriFhirUri
codeCode or Code<T>
IdentifierIdentifier
HumanNameHumanName
AddressAddress

Primitive Type Wrappers

Why Wrap Primitives?

C# primitives (string, int, bool) can’t have:

  • Extensions
  • Element IDs
  • Metadata

So the SDK wraps them:

public class FhirString : PrimitiveType
{
    public string Value { get; set; }
    // Inherited from Element:
    // public string ElementId { get; set; }
    // public List<Extension> Extension { get; set; }
}

This lets you do:

var name = new FhirString("John");
name.Extension.Add(new Extension
{
    Url = "http://example.org/pronunciation",
    Value = new FhirString("JAWN")
});

FhirString, FhirBoolean, Integer, etc.

Common primitive wrappers:

// String
var str = new FhirString("Hello");
var value = str.Value; // "Hello"

// Boolean
var flag = new FhirBoolean(true);
var value = flag.Value; // true

// Integer
var num = new Integer(42);
var value = num.Value; // 42

// Date
var date = new Date("2024-01-15");
var value = date.Value; // "2024-01-15"

// DateTime
var dt = new FhirDateTime("2024-01-15T10:30:00Z");
var value = dt.Value; // "2024-01-15T10:30:00Z"

Complex Types

Complex types are structures with multiple elements:

// Identifier
var identifier = new Identifier
{
    System = "http://hospital.example.org/patients",
    Value = "12345",
    Use = Identifier.IdentifierUse.Official
};

// CodeableConcept
var concept = new CodeableConcept
{
    Coding = new List<Coding>
    {
        new Coding("http://loinc.org", "8867-4", "Heart rate")
    },
    Text = "Heart Rate"
};

// Quantity
var quantity = new Quantity
{
    Value = 120,
    Unit = "mmHg",
    System = "http://unitsofmeasure.org",
    Code = "mm[Hg]"
};

Type Casting and Checking

Working with base types:

// Generic resource handling
Resource resource = GetResource(); // Could be any resource type

if (resource is Patient patient)
{
    Console.WriteLine($"Patient: {patient.Name?.FirstOrDefault()?.Family}");
}
else if (resource is Observation observation)
{
    Console.WriteLine($"Observation: {observation.Code?.Text}");
}

// Type checking with TypeName
switch (resource.TypeName)
{
    case "Patient":
        var p = (Patient)resource;
        break;
    case "Observation":
        var o = (Observation)resource;
        break;
}

// Generic method
T GetResourceById<T>(string id) where T : Resource
{
    var resource = client.Read<T>($"{typeof(T).Name}/{id}");
    return resource;
}

var patient = GetResourceById<Patient>("123");

Versioning Considerations

FHIR evolves, and the SDK must support multiple versions.

Property Name Changes Between Versions

R4 vs R5 Differences

Some properties were renamed or restructured between versions:

Example: MedicationRequest

R4:

medicationRequest.MedicationCodeableConcept = new CodeableConcept { /* ... */ };

R5:

medicationRequest.Medication = new CodeableReference { /* ... */ };

The element changed from a choice type to a new CodeableReference type.

Migration Challenges

When upgrading SDK versions:

  • Properties may be renamed
  • Types may change
  • New required elements may be added
  • Deprecated elements may be removed

Always test thoroughly when upgrading between major FHIR versions.

Handling Version-Specific Properties

Use conditional compilation or runtime checks:

#if FHIR_R4
medicationRequest.MedicationCodeableConcept = medication;
#elif FHIR_R5
medicationRequest.Medication = new CodeableReference { Concept = medication };
#endif

Or use abstraction layers:

public interface IMedicationRequestBuilder
{
    void SetMedication(CodeableConcept medication);
}

public class R4MedicationRequestBuilder : IMedicationRequestBuilder
{
    public void SetMedication(CodeableConcept medication)
    {
        _request.MedicationCodeableConcept = medication;
    }
}

public class R5MedicationRequestBuilder : IMedicationRequestBuilder
{
    public void SetMedication(CodeableConcept medication)
    {
        _request.Medication = new CodeableReference { Concept = medication };
    }
}

Since Attribute

The Since attribute marks when an element was introduced:

[FhirElement("statusReason", Since = FhirVersion.R5)]
public CodeableConcept StatusReason { get; set; }

This helps the serializer:

  • Exclude R5-only elements when serializing R4 resources
  • Warn developers about version-specific features
  • Support multi-version applications

Deprecated Elements

Some elements are deprecated but maintained for backward compatibility:

[Obsolete("Use NewProperty instead")]
[FhirElement("oldProperty")]
public FhirString OldProperty { get; set; }

[FhirElement("newProperty", Since = FhirVersion.R5)]
public FhirString NewProperty { get; set; }

The compiler warns you when using deprecated elements, but they still work.

Custom Resources and Profiles

Sometimes you need to extend the SDK with custom resources.

When to Create Custom Resources

  • You’re implementing a FHIR profile with custom constraints
  • You need a custom resource type not in the base spec
  • You want to add helper methods or computed properties

Note: Most of the time, you should use Extension rather than custom resources.

Extending Base Classes

Create a custom resource:

[FhirType("CustomPatient", IsResource = true)]
public class CustomPatient : Patient
{
    // Add custom properties
    [NotMapped]
    public string InternalId { get; set; }

    // Add helper methods
    public HumanName GetPreferredName()
    {
        return Name?.FirstOrDefault(n => n.Use == HumanName.NameUse.Official)
            ?? Name?.FirstOrDefault();
    }

    // Add computed properties
    [NotMapped]
    public int? AgeInYears
    {
        get
        {
            if (BirthDate == null) return null;
            var birth = Date.Parse(BirthDate).ToDateTimeOffset();
            var age = DateTime.Now.Year - birth.Year;
            if (DateTime.Now < birth.AddYears(age)) age--;
            return age;
        }
    }
}

Custom Backbone Elements

Extend backbone elements:

public class CustomClaim : Claim
{
    [FhirType("CustomClaim.CustomDiagnosis")]
    public class CustomDiagnosisComponent : DiagnosisComponent
    {
        // Add custom properties
        [FhirElement("customCode")]
        public CodeableConcept CustomCode { get; set; }
    }
}

Registration and Discovery

For custom resources to serialize properly, register them:

// In your app startup
ModelInfo.ModelInspector.Register(typeof(CustomPatient));

This tells the SDK about your custom type so the serializer knows how to handle it.

Serialization of Custom Types

Custom types serialize like standard resources:

var customPatient = new CustomPatient
{
    Active = true,
    InternalId = "ABC123" // Won't serialize ([NotMapped])
};

var json = serializer.SerializeToString(customPatient);
// Produces standard Patient JSON (InternalId is excluded)

Custom FHIR elements (with [FhirElement]) will serialize:

public class ExtendedPatient : Patient
{
    [FhirElement("customField", Order = 1000)]
    public FhirString CustomFieldElement { get; set; }

    [NotMapped]
    public string CustomField
    {
        get => CustomFieldElement?.Value;
        set => CustomFieldElement = new FhirString(value);
    }
}

// This WILL serialize with "customField" in the JSON

Debugging and Introspection

Inspecting Attributes at Runtime

You can examine attributes via reflection:

var patientType = typeof(Patient);
var birthDateProp = patientType.GetProperty("BirthDateElement");

// Get FhirElement attribute
var fhirAttr = birthDateProp.GetCustomAttribute<FhirElementAttribute>();
Console.WriteLine($"Element name: {fhirAttr.Name}"); // "birthDate"
Console.WriteLine($"In summary: {fhirAttr.InSummary}"); // true
Console.WriteLine($"Order: {fhirAttr.Order}"); // 180

// Get Cardinality
var cardAttr = birthDateProp.GetCustomAttribute<CardinalityAttribute>();
Console.WriteLine($"Min: {cardAttr?.Min}, Max: {cardAttr?.Max}"); // Min: 0, Max: 1

// List all FHIR elements
foreach (var prop in patientType.GetProperties())
{
    var attr = prop.GetCustomAttribute<FhirElementAttribute>();
    if (attr != null)
    {
        Console.WriteLine($"{attr.Name} ({prop.Name})");
    }
}

Understanding Serialization Behavior

Debug why a property isn’t serializing:

var property = typeof(Patient).GetProperty("BirthDateElement");

// Check for [NotMapped]
var notMapped = property.GetCustomAttribute<NotMappedAttribute>() != null;
if (notMapped)
{
    Console.WriteLine("Property is [NotMapped] and won't serialize");
}

// Check for [FhirElement]
var fhirElement = property.GetCustomAttribute<FhirElementAttribute>();
if (fhirElement == null)
{
    Console.WriteLine("Property lacks [FhirElement] and won't serialize");
}

// Check the value
var patient = new Patient { BirthDate = "1980-05-15" };
var value = property.GetValue(patient);
if (value == null)
{
    Console.WriteLine("Property value is null and won't serialize");
}

Debugging Missing Properties

If a property isn’t appearing in JSON:

  1. Is it null? Null properties don’t serialize
  2. Is it [NotMapped]? Won’t serialize
  3. Does it have [FhirElement]? Required for serialization
  4. Is it a convenience property? Only Element properties serialize (e.g., BirthDateElement, not BirthDate)

Debugging Unexpected Serialization

If an unexpected property appears in JSON:

  1. Check for custom extensions on base classes
  2. Check if you’re using a custom resource with extra [FhirElement] attributes
  3. Verify the SDK version - newer versions may have new elements
  4. Check contained resources - they might be adding unexpected data

Serialization Internals

How Attributes Drive Serialization

The serializer works like this:

  1. Reflection: Get all properties on the resource
  2. Filter: Keep only properties with [FhirElement], exclude [NotMapped]
  3. Order: Sort by the Order property of [FhirElement]
  4. Name mapping: Use the Name from [FhirElement] for JSON/XML element name
  5. Serialize: Convert the C# value to JSON/XML

Example:

[FhirElement("birthDate", InSummary=true, Order=180)]
public Date BirthDateElement { get; set; }

Tells the serializer:

  • JSON element name: "birthDate"
  • Serialize at position 180 (after earlier properties)
  • Include in summary mode
  • Serialize the Date object’s value

InSummary Flag

When you request _summary=true in a FHIR search:

GET /Patient/123?_summary=true

The server returns only elements where InSummary=true:

[FhirElement("id", InSummary=true)]
[FhirElement("active", InSummary=true)]
[FhirElement("name", InSummary=true)]
[FhirElement("birthDate", InSummary=true)]
// ...but not elements with InSummary=false

This reduces payload size for scenarios where you don’t need all the data.

Order Property

Ensures consistent element ordering in output:

{
  "resourceType": "Patient",
  "id": "123",              // Order = 10
  "meta": { ... },          // Order = 20
  "language": "en",         // Order = 40
  "text": { ... },          // Order = 50
  "identifier": [ ... ],    // Order = 100
  "active": true,           // Order = 110
  "name": [ ... ],          // Order = 120
  "birthDate": "1980-05-15" // Order = 180
}

Consistent ordering makes:

  • Diffs easier to read
  • Testing more reliable
  • Debugging simpler

Optional vs Required

The serializer doesn’t enforce required elements—that’s the validator’s job.

But understanding cardinality helps:

[Cardinality(Min=1, Max=1)] // Required, single value
public Code<ClaimStatus> StatusElement { get; set; }

If StatusElement is null, the resource will serialize without a status element, but validation will fail.

Narrative Generation

The Text property (narrative):

public class DomainResource : Resource
{
    [FhirElement("text", Order=50)]
    public Narrative Text { get; set; }
}

Contains human-readable HTML:

patient.Text = new Narrative
{
    Status = Narrative.NarrativeStatus.Generated,
    Div = "<div xmlns=\"http://www.w3.org/1999/xhtml\">John Smith, born 1980-05-15</div>"
};

The SDK doesn’t auto-generate narrative—you must create it yourself or use a narrative generator library.

Performance Implications

Understanding SDK patterns helps you write performant code.

Lazy Loading Benefits

Collections are null by default (lazy):

Memory saved: A Patient with 20 collection properties, in a Bundle of 100 Patients = 2000 potential empty lists NOT created.

Initialization cost saved: No time spent allocating and constructing lists that won’t be used.

Downside: You must check for null before accessing.

Reflection Overhead

The serializer uses reflection to read attributes:

// This happens during serialization
var properties = typeof(Patient).GetProperties();
foreach (var prop in properties)
{
    var attr = prop.GetCustomAttribute<FhirElementAttribute>();
    // ... serialize based on attribute
}

Cost: Reflection is slower than direct property access.

Mitigation: The SDK caches reflection results internally, so repeated serialization of the same type is faster.

Serialization Performance

Serializing 1000 Patient resources:

  • JSON: ~100-200ms (fast, optimized)
  • XML: ~200-400ms (slower due to XML complexity)

Tips for better performance:

  • Serialize to Stream instead of String when possible (avoids extra string allocation)
  • Use _summary=true to reduce payload size
  • Batch serialization rather than serializing one-by-one

When to Pre-Initialize Collections

If you know you’ll use a collection, initialize it early:

// Good - if you're definitely adding names
var patient = new Patient
{
    Name = new List<HumanName>()
};

foreach (var nameData in importData)
{
    patient.Name.Add(new HumanName { Family = nameData.LastName });
}

But don’t pre-initialize collections you might not use:

// Bad - wastes memory if no photos are added
patient.Photo = new List<Attachment>();

Advanced Patterns

Pattern 1: Resource Cloning with Extensions Preserved

Simple approach (loses extensions):

var newPatient = new Patient
{
    Active = oldPatient.Active,
    BirthDate = oldPatient.BirthDate,
    // ...
};

Better approach (preserves everything):

var newPatient = (Patient)oldPatient.DeepCopy();

The DeepCopy() method:

  • Clones all properties
  • Preserves extensions
  • Preserves element IDs
  • Creates a completely independent copy

Selective copying:

var newPatient = new Patient
{
    // Preserve complete elements with extensions
    ActiveElement = oldPatient.ActiveElement?.DeepCopy() as FhirBoolean,
    BirthDateElement = oldPatient.BirthDateElement?.DeepCopy() as Date,

    // Clone collections
    Name = oldPatient.Name?.Select(n => (HumanName)n.DeepCopy()).ToList()
};

Pattern 2: Dynamic Resource Creation

Create resources by type name:

public Resource CreateResource(string resourceType)
{
    var type = Type.GetType($"Hl7.Fhir.Model.{resourceType}, Hl7.Fhir.R4.Core");
    if (type == null || !typeof(Resource).IsAssignableFrom(type))
    {
        throw new ArgumentException($"Unknown resource type: {resourceType}");
    }

    return (Resource)Activator.CreateInstance(type);
}

var patient = CreateResource("Patient"); // Returns a Patient instance
var observation = CreateResource("Observation"); // Returns an Observation instance

Use cases:

  • Generic FHIR servers
  • Dynamic form builders
  • Test data generators

Pattern 3: Attribute-Based Validation

Build custom validators using attributes:

public class ResourceValidator
{
    public List<string> Validate(Resource resource)
    {
        var errors = new List<string>();
        var type = resource.GetType();

        foreach (var prop in type.GetProperties())
        {
            var cardAttr = prop.GetCustomAttribute<CardinalityAttribute>();
            if (cardAttr != null && cardAttr.Min > 0)
            {
                var value = prop.GetValue(resource);
                if (value == null)
                {
                    var fhirAttr = prop.GetCustomAttribute<FhirElementAttribute>();
                    errors.Add($"Required element '{fhirAttr.Name}' is missing");
                }
            }
        }

        return errors;
    }
}

var errors = validator.Validate(patient);
if (errors.Any())
{
    Console.WriteLine("Validation errors:");
    errors.ForEach(Console.WriteLine);
}

Pattern 4: Custom Type Converters

Convert between SDK types and your domain models:

public class PatientConverter
{
    public Patient ToFhir(DomainPatient domainPatient)
    {
        return new Patient
        {
            Identifier = domainPatient.Identifiers
                .Select(id => new Identifier
                {
                    System = id.System,
                    Value = id.Value
                })
                .ToList(),

            Name = new List<HumanName>
            {
                new HumanName
                {
                    Family = domainPatient.LastName,
                    Given = new[] { domainPatient.FirstName }
                }
            },

            BirthDate = domainPatient.DateOfBirth.ToString("yyyy-MM-dd")
        };
    }

    public DomainPatient FromFhir(Patient fhirPatient)
    {
        return new DomainPatient
        {
            FirstName = fhirPatient.Name?.FirstOrDefault()?.Given?.FirstOrDefault(),
            LastName = fhirPatient.Name?.FirstOrDefault()?.Family,
            DateOfBirth = Date.Parse(fhirPatient.BirthDate).ToDateTimeOffset().Date
        };
    }
}

Practical Examples

Example 1: Complete Patient with Extensions

var patient = new Patient
{
    // Standard elements
    Active = true,

    // Identifier with standard use
    Identifier = new List<Identifier>
    {
        new Identifier
        {
            System = "http://hospital.example.org/patients",
            Value = "MRN-12345",
            Use = Identifier.IdentifierUse.Official
        }
    },

    // Name with extension
    Name = new List<HumanName>
    {
        new HumanName
        {
            Use = HumanName.NameUse.Official,
            Family = "Smith",
            Given = new[] { "John", "Robert" }
        }
    }
};

// Add extension to primitive element (birth date)
patient.BirthDateElement = new Date("1980-05-15");
patient.BirthDateElement.Extension.Add(new Extension
{
    Url = "http://hl7.org/fhir/StructureDefinition/patient-birthTime",
    Value = new FhirDateTime("1980-05-15T08:30:00Z")
});

// Add US Core race extension
patient.Extension ??= new List<Extension>();
patient.Extension.Add(new Extension
{
    Url = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race",
    Extension = new List<Extension>
    {
        new Extension
        {
            Url = "ombCategory",
            Value = new Coding
            {
                System = "urn:oid:2.16.840.1.113883.6.238",
                Code = "2106-3",
                Display = "White"
            }
        },
        new Extension
        {
            Url = "text",
            Value = new FhirString("White")
        }
    }
});

Example 2: Observation with Choice Type

var observation = new Observation
{
    Status = ObservationStatus.Final,

    Code = new CodeableConcept
    {
        Coding = new List<Coding>
        {
            new Coding
            {
                System = "http://loinc.org",
                Code = "8867-4",
                Display = "Heart rate"
            }
        }
    },

    Subject = new ResourceReference("Patient/123"),

    // Choice type - using Quantity
    Value = new Quantity
    {
        Value = 72,
        Unit = "beats/minute",
        System = "http://unitsofmeasure.org",
        Code = "/min"
    }
};

// Later, check what type it is
if (observation.Value is Quantity quantity)
{
    Console.WriteLine($"Heart rate: {quantity.Value} {quantity.Unit}");
}

// Or use a different type
observation.Value = new CodeableConcept
{
    Text = "Normal"
};

Example 3: Claim with Backbone Elements

var claim = new Claim
{
    Status = ClaimStatus.Active,
    Type = new CodeableConcept { Text = "Professional" },
    Use = Use.Claim,

    Patient = new ResourceReference("Patient/123"),

    Created = FhirDateTime.Now().Value,

    Provider = new ResourceReference("Organization/456"),

    // Backbone element - diagnosis
    Diagnosis = new List<Claim.DiagnosisComponent>
    {
        new Claim.DiagnosisComponent
        {
            Sequence = 1,
            Diagnosis = new CodeableConcept
            {
                Coding = new List<Coding>
                {
                    new Coding
                    {
                        System = "http://hl7.org/fhir/sid/icd-10-cm",
                        Code = "E11.9",
                        Display = "Type 2 diabetes mellitus without complications"
                    }
                }
            },
            Type = new List<CodeableConcept>
            {
                new CodeableConcept
                {
                    Coding = new List<Coding>
                    {
                        new Coding
                        {
                            System = "http://terminology.hl7.org/CodeSystem/ex-diagnosistype",
                            Code = "principal"
                        }
                    }
                }
            }
        }
    },

    // Backbone element - item (line items)
    Item = new List<Claim.ItemComponent>
    {
        new Claim.ItemComponent
        {
            Sequence = 1,
            ProductOrService = new CodeableConcept
            {
                Coding = new List<Coding>
                {
                    new Coding
                    {
                        System = "http://www.ama-assn.org/go/cpt",
                        Code = "99213",
                        Display = "Office visit"
                    }
                }
            },
            UnitPrice = new Money
            {
                Value = 150.00m,
                Currency = "USD"
            }
        }
    }
};

Example 4: Working with Collections Safely

public class PatientBuilder
{
    private readonly Patient _patient = new Patient();

    public PatientBuilder AddIdentifier(string system, string value)
    {
        // Safe collection initialization
        _patient.Identifier ??= new List<Identifier>();

        _patient.Identifier.Add(new Identifier
        {
            System = system,
            Value = value
        });

        return this;
    }

    public PatientBuilder AddName(string family, params string[] given)
    {
        _patient.Name ??= new List<HumanName>();

        _patient.Name.Add(new HumanName
        {
            Family = family,
            Given = given
        });

        return this;
    }

    public PatientBuilder AddTelecom(ContactPoint.ContactPointSystem system, string value)
    {
        _patient.Telecom ??= new List<ContactPoint>();

        _patient.Telecom.Add(new ContactPoint
        {
            System = system,
            Value = value
        });

        return this;
    }

    public Patient Build() => _patient;
}

// Usage
var patient = new PatientBuilder()
    .AddIdentifier("http://hospital.example.org/patients", "MRN-123")
    .AddName("Smith", "John", "Robert")
    .AddTelecom(ContactPoint.ContactPointSystem.Phone, "555-1234")
    .AddTelecom(ContactPoint.ContactPointSystem.Email, "john@example.com")
    .Build();

Example 5: Metadata Management

public class ResourceMetadataManager
{
    public void SetMetadata(Resource resource, string profile, string tag)
    {
        // Initialize Meta if needed
        resource.Meta ??= new Meta();

        // Set version
        resource.Meta.VersionId = "1";

        // Set last updated
        resource.Meta.LastUpdated = DateTimeOffset.UtcNow;

        // Add profile
        resource.Meta.Profile ??= new List<Canonical>();
        if (!resource.Meta.Profile.Contains(profile))
        {
            resource.Meta.Profile.Add(profile);
        }

        // Add tag
        resource.Meta.Tag ??= new List<Coding>();
        if (!resource.Meta.Tag.Any(t => t.Code == tag))
        {
            resource.Meta.Tag.Add(new Coding
            {
                System = "http://example.org/tags",
                Code = tag
            });
        }
    }

    public bool HasProfile(Resource resource, string profile)
    {
        return resource.Meta?.Profile?.Contains(profile) ?? false;
    }

    public bool HasTag(Resource resource, string tag)
    {
        return resource.Meta?.Tag?.Any(t => t.Code == tag) ?? false;
    }
}

// Usage
var manager = new ResourceMetadataManager();
manager.SetMetadata(patient,
    "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient",
    "verified");

if (manager.HasTag(patient, "verified"))
{
    Console.WriteLine("Patient has been verified");
}

Example 6: Custom Resource Type

[FhirType("EnhancedPatient", IsResource = true)]
public class EnhancedPatient : Patient
{
    // Add convenience properties (won't serialize)
    [NotMapped]
    public string FullName =>
        Name?.FirstOrDefault()?.Given?.FirstOrDefault() + " " +
        Name?.FirstOrDefault()?.Family;

    [NotMapped]
    public int? AgeInYears
    {
        get
        {
            if (string.IsNullOrEmpty(BirthDate)) return null;
            var birthDate = Date.Parse(BirthDate).ToDateTimeOffset();
            var today = DateTime.Today;
            var age = today.Year - birthDate.Year;
            if (birthDate.Date > today.AddYears(-age)) age--;
            return age;
        }
    }

    // Add custom FHIR extension as a first-class property
    [NotMapped]
    public string PreferredLanguage
    {
        get => Communication?
            .FirstOrDefault(c => c.Preferred == true)?
            .Language?.Text;
        set
        {
            Communication ??= new List<PatientCommunicationComponent>();
            var comm = Communication.FirstOrDefault(c => c.Preferred == true);
            if (comm == null)
            {
                comm = new PatientCommunicationComponent { Preferred = true };
                Communication.Add(comm);
            }
            comm.Language = new CodeableConcept { Text = value };
        }
    }

    // Add helper methods
    public Identifier GetMRN()
    {
        return Identifier?.FirstOrDefault(id =>
            id.Type?.Coding?.Any(c => c.Code == "MR") ?? false);
    }

    public void SetMRN(string system, string value)
    {
        Identifier ??= new List<Identifier>();

        var mrn = GetMRN();
        if (mrn != null)
        {
            mrn.System = system;
            mrn.Value = value;
        }
        else
        {
            Identifier.Add(new Identifier
            {
                System = system,
                Value = value,
                Type = new CodeableConcept
                {
                    Coding = new List<Coding>
                    {
                        new Coding
                        {
                            System = "http://terminology.hl7.org/CodeSystem/v2-0203",
                            Code = "MR"
                        }
                    }
                }
            });
        }
    }
}

// Usage
var patient = new EnhancedPatient
{
    Name = new List<HumanName>
    {
        new HumanName { Given = new[] { "John" }, Family = "Smith" }
    },
    BirthDate = "1980-05-15"
};

Console.WriteLine($"Full name: {patient.FullName}"); // "John Smith"
Console.WriteLine($"Age: {patient.AgeInYears}"); // Calculated age

patient.SetMRN("http://hospital.example.org/patients", "MRN-123");
var mrn = patient.GetMRN();
Console.WriteLine($"MRN: {mrn.Value}"); // "MRN-123"

patient.PreferredLanguage = "en";

Example 7: Debugging Serialization Issues

public class SerializationDebugger
{
    public void DebugResource(Resource resource)
    {
        var type = resource.GetType();
        Console.WriteLine($"Debugging resource: {type.Name}");
        Console.WriteLine();

        // Get all properties with FhirElement
        var fhirProps = type.GetProperties()
            .Where(p => p.GetCustomAttribute<FhirElementAttribute>() != null)
            .OrderBy(p => p.GetCustomAttribute<FhirElementAttribute>().Order);

        foreach (var prop in fhirProps)
        {
            var attr = prop.GetCustomAttribute<FhirElementAttribute>();
            var value = prop.GetValue(resource);

            Console.WriteLine($"Property: {prop.Name}");
            Console.WriteLine($"  FHIR name: {attr.Name}");
            Console.WriteLine($"  Order: {attr.Order}");
            Console.WriteLine($"  In summary: {attr.InSummary}");
            Console.WriteLine($"  Value: {value ?? "(null)"}");
            Console.WriteLine($"  Will serialize: {value != null}");

            // Check for NotMapped
            var notMapped = prop.GetCustomAttribute<NotMappedAttribute>();
            if (notMapped != null)
            {
                Console.WriteLine($"  WARNING: [NotMapped] - won't serialize!");
            }

            Console.WriteLine();
        }

        // Check for NotMapped properties
        var notMappedProps = type.GetProperties()
            .Where(p => p.GetCustomAttribute<NotMappedAttribute>() != null);

        if (notMappedProps.Any())
        {
            Console.WriteLine("NotMapped properties (won't serialize):");
            foreach (var prop in notMappedProps)
            {
                Console.WriteLine($"  - {prop.Name}");
            }
        }
    }
}

// Usage
var patient = new Patient
{
    Active = true,
    BirthDate = "1980-05-15"
};

var debugger = new SerializationDebugger();
debugger.DebugResource(patient);

/* Output:
Debugging resource: Patient

Property: IdElement
  FHIR name: id
  Order: 10
  In summary: true
  Value: (null)
  Will serialize: False

Property: ActiveElement
  FHIR name: active
  Order: 110
  In summary: true
  Value: True
  Will serialize: True

Property: BirthDateElement
  FHIR name: birthDate
  Order: 180
  In summary: true
  Value: 1980-05-15
  Will serialize: True

...
*/

Common Mistakes

Mistake 1: Direct Collection Assignment Without Check

// WRONG - throws NullReferenceException
var patient = new Patient();
patient.Identifier.Add(new Identifier { Value = "123" });

// RIGHT
patient.Identifier ??= new List<Identifier>();
patient.Identifier.Add(new Identifier { Value = "123" });

Mistake 2: Losing Extensions on Property Updates

var patient = new Patient();

// Add extension to birth date
patient.BirthDateElement = new Date("1980-05-15");
patient.BirthDateElement.Extension.Add(myExtension);

// WRONG - this creates a new Date object, losing the extension!
patient.BirthDate = "1985-01-01";

// RIGHT - modify the Value property
patient.BirthDateElement.Value = "1985-01-01";

// OR - preserve extensions explicitly
var extensions = patient.BirthDateElement.Extension;
patient.BirthDate = "1985-01-01";
patient.BirthDateElement.Extension = extensions;

Mistake 3: Incorrect Choice Type Assignment

var observation = new Observation();

// WRONG - type mismatch, will fail validation
// Observation.value[x] doesn't allow Address
observation.Value = new Address { City = "Boston" };

// RIGHT - use an allowed type
observation.Value = new Quantity { Value = 120, Unit = "mmHg" };

// Always check allowed types in documentation or [AllowedTypes] attribute

Mistake 4: Ignoring Element vs Convenience Property

var claim = new Claim();

// WRONG - trying to add extension to convenience property
// This won't compile - Status is ClaimStatus?, not an Element
claim.Status.Extension.Add(myExtension); // Compilation error!

// RIGHT - use the Element property
claim.StatusElement ??= new Code<ClaimStatus>(ClaimStatus.Active);
claim.StatusElement.Extension.Add(myExtension);

Mistake 5: Not Understanding InSummary

// You request _summary=true
var patient = client.Read<Patient>("Patient/123?_summary=true");

// WRONG - assuming all data is present
var line1 = patient.Address.First().Line.First(); // Might be null!

// RIGHT - check for null, summary mode excludes many elements
var line1 = patient.Address?.FirstOrDefault()?.Line?.FirstOrDefault();

// Or don't use _summary=true if you need all data
var patient = client.Read<Patient>("Patient/123");

Mistake 6: Modifying During Iteration

var patient = new Patient
{
    Identifier = new List<Identifier>
    {
        new Identifier { Value = "A" },
        new Identifier { Value = "B" },
        new Identifier { Value = "C" }
    }
};

// WRONG - modifying collection during iteration
foreach (var id in patient.Identifier)
{
    if (id.Value == "B")
        patient.Identifier.Remove(id); // Exception!
}

// RIGHT - iterate over a copy or use LINQ
patient.Identifier = patient.Identifier
    .Where(id => id.Value != "B")
    .ToList();

// OR - iterate backwards
for (int i = patient.Identifier.Count - 1; i >= 0; i--)
{
    if (patient.Identifier[i].Value == "B")
        patient.Identifier.RemoveAt(i);
}

Best Practices

Always Check Collection Nullability

// Safe patterns
patient.Identifier ??= new List<Identifier>();
patient.Identifier.Add(id);

// Or
if (patient.Name?.Any() == true)
{
    var firstName = patient.Name.First().Given?.FirstOrDefault();
}

// Or
foreach (var name in patient.Name ?? Enumerable.Empty<HumanName>())
{
    Console.WriteLine(name.Family);
}

Use Element Properties for Extensions

// When working with extensions, always use the Element property
patient.BirthDateElement ??= new Date(patient.BirthDate);
patient.BirthDateElement.Extension.Add(extension);

// Not the convenience property
// patient.BirthDate doesn't have Extension property

Understand Choice Type Constraints

// Before assigning to a choice type, verify it's allowed
// Check [AllowedTypes] attribute or FHIR documentation

// Good - defensive programming
if (observation.Value is Quantity || observation.Value is CodeableConcept)
{
    // OK to process
}
else
{
    throw new InvalidOperationException($"Unexpected value type: {observation.Value?.GetType().Name}");
}

Respect Cardinality Constraints

// Check cardinality before assuming presence
// [Cardinality(Min=1, Max=1)] means required, single value
if (claim.Status == null)
{
    throw new InvalidOperationException("Status is required");
}

// [Cardinality(Min=0, Max=1)] means optional, single value
var birthDate = patient.BirthDate; // Might be null

// [Cardinality(Min=0, Max=-1)] means optional, repeating
if (patient.Name?.Any() == true)
{
    // Has at least one name
}

Leverage Attributes for Validation

// Use reflection and attributes to build generic validators
public bool IsValid<T>(T resource) where T : Resource
{
    var errors = new List<string>();
    var properties = typeof(T).GetProperties();

    foreach (var prop in properties)
    {
        var card = prop.GetCustomAttribute<CardinalityAttribute>();
        if (card?.Min > 0)
        {
            var value = prop.GetValue(resource);
            if (value == null)
            {
                var fhirElement = prop.GetCustomAttribute<FhirElementAttribute>();
                errors.Add($"Required element {fhirElement.Name} is missing");
            }
        }
    }

    return errors.Count == 0;
}

Document Custom Patterns

/// <summary>
/// Enhanced Patient with convenience methods for common operations.
///
/// Custom patterns used:
/// - GetMRN(): Retrieves MRN identifier (type = MR)
/// - SetMRN(): Sets or updates MRN identifier
/// - FullName: Computed property for display name
/// - AgeInYears: Computed property from birth date
///
/// Note: Convenience properties are [NotMapped] and won't serialize.
/// </summary>
public class EnhancedPatient : Patient
{
    // ...
}

Testing SDK Patterns

Unit Testing Element Properties

[Test]
public void BirthDate_PreservesExtensions_WhenValueChanged()
{
    // Arrange
    var patient = new Patient();
    patient.BirthDateElement = new Date("1980-05-15");
    var extension = new Extension
    {
        Url = "http://example.org/test",
        Value = new FhirString("test")
    };
    patient.BirthDateElement.Extension.Add(extension);

    // Act - modify via Element property Value
    patient.BirthDateElement.Value = "1985-01-01";

    // Assert
    Assert.AreEqual("1985-01-01", patient.BirthDate);
    Assert.IsNotNull(patient.BirthDateElement.Extension);
    Assert.AreEqual(1, patient.BirthDateElement.Extension.Count);
    Assert.AreEqual("http://example.org/test", patient.BirthDateElement.Extension[0].Url);
}

[Test]
public void BirthDate_LosesExtensions_WhenConveniencePropertySet()
{
    // Arrange
    var patient = new Patient();
    patient.BirthDateElement = new Date("1980-05-15");
    patient.BirthDateElement.Extension.Add(new Extension
    {
        Url = "http://example.org/test"
    });

    // Act - modify via convenience property (creates new Date object)
    patient.BirthDate = "1985-01-01";

    // Assert
    Assert.AreEqual("1985-01-01", patient.BirthDate);
    Assert.AreEqual(0, patient.BirthDateElement.Extension.Count); // Extensions lost!
}

Testing Choice Types

[Test]
public void Observation_Value_AcceptsQuantity()
{
    // Arrange
    var observation = new Observation();
    var quantity = new Quantity { Value = 120, Unit = "mmHg" };

    // Act
    observation.Value = quantity;

    // Assert
    Assert.IsInstanceOf<Quantity>(observation.Value);
    Assert.AreEqual(120, ((Quantity)observation.Value).Value);
}

[Test]
public void Observation_Value_AcceptsCodeableConcept()
{
    // Arrange
    var observation = new Observation();
    var concept = new CodeableConcept { Text = "Normal" };

    // Act
    observation.Value = concept;

    // Assert
    Assert.IsInstanceOf<CodeableConcept>(observation.Value);
    Assert.AreEqual("Normal", ((CodeableConcept)observation.Value).Text);
}

[TestCase(typeof(Quantity))]
[TestCase(typeof(CodeableConcept))]
[TestCase(typeof(FhirString))]
public void Observation_Value_AllowedTypes(Type type)
{
    // Verify allowed types using reflection
    var prop = typeof(Observation).GetProperty("Value");
    var allowedTypes = prop.GetCustomAttribute<AllowedTypesAttribute>();

    Assert.IsTrue(allowedTypes.Types.Contains(type),
        $"{type.Name} should be an allowed type for Observation.value[x]");
}

Testing Collections

[Test]
public void Patient_Identifier_IsNullByDefault()
{
    // Arrange & Act
    var patient = new Patient();

    // Assert
    Assert.IsNull(patient.Identifier);
}

[Test]
public void Patient_AddIdentifier_InitializesCollection()
{
    // Arrange
    var patient = new Patient();

    // Act
    patient.Identifier ??= new List<Identifier>();
    patient.Identifier.Add(new Identifier { Value = "123" });

    // Assert
    Assert.IsNotNull(patient.Identifier);
    Assert.AreEqual(1, patient.Identifier.Count);
    Assert.AreEqual("123", patient.Identifier[0].Value);
}

[Test]
public void Patient_Identifier_ThrowsIfNotInitialized()
{
    // Arrange
    var patient = new Patient();

    // Act & Assert
    Assert.Throws<NullReferenceException>(() =>
    {
        patient.Identifier.Add(new Identifier { Value = "123" });
    });
}

Testing Metadata

[Test]
public void Resource_Meta_CanSetVersionId()
{
    // Arrange
    var patient = new Patient();

    // Act
    patient.Meta = new Meta { VersionId = "1" };

    // Assert
    Assert.AreEqual("1", patient.Meta.VersionId);
}

[Test]
public void Resource_Meta_CanAddProfile()
{
    // Arrange
    var patient = new Patient { Meta = new Meta() };
    var profileUrl = "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient";

    // Act
    patient.Meta.Profile ??= new List<Canonical>();
    patient.Meta.Profile.Add(profileUrl);

    // Assert
    Assert.Contains(profileUrl, patient.Meta.Profile);
}

Migration Between SDK Versions

Breaking Changes

Major version updates can include breaking changes:

Example R4 to R5 migration:

// R4
medicationRequest.MedicationCodeableConcept = new CodeableConcept
{
    Text = "Aspirin"
};

// R5 - property renamed and type changed
medicationRequest.Medication = new CodeableReference
{
    Concept = new CodeableConcept { Text = "Aspirin" }
};

Strategy: Use abstraction layers to isolate version-specific code:

public interface IMedicationHelper
{
    void SetMedication(MedicationRequest request, CodeableConcept medication);
}

public class R4MedicationHelper : IMedicationHelper
{
    public void SetMedication(MedicationRequest request, CodeableConcept medication)
    {
        request.MedicationCodeableConcept = medication;
    }
}

public class R5MedicationHelper : IMedicationHelper
{
    public void SetMedication(MedicationRequest request, CodeableConcept medication)
    {
        request.Medication = new CodeableReference { Concept = medication };
    }
}

// In your DI container
#if FHIR_R4
services.AddSingleton<IMedicationHelper, R4MedicationHelper>();
#elif FHIR_R5
services.AddSingleton<IMedicationHelper, R5MedicationHelper>();
#endif

Deprecation Warnings

The SDK marks deprecated elements:

[Obsolete("Use NewProperty instead. This property is deprecated in FHIR R5.")]
[FhirElement("oldProperty")]
public FhirString OldProperty { get; set; }

When you upgrade, you’ll see compiler warnings guiding you to the new approach.

Property Renames

Create extension methods to smooth transitions:

public static class MedicationRequestExtensions
{
    // Helper for R5 that mimics R4 API
    public static CodeableConcept GetMedicationCodeableConcept(this MedicationRequest request)
    {
        #if FHIR_R4
        return request.MedicationCodeableConcept;
        #elif FHIR_R5
        return (request.Medication as CodeableReference)?.Concept;
        #endif
    }

    public static void SetMedicationCodeableConcept(this MedicationRequest request, CodeableConcept concept)
    {
        #if FHIR_R4
        request.MedicationCodeableConcept = concept;
        #elif FHIR_R5
        request.Medication = new CodeableReference { Concept = concept };
        #endif
    }
}

// Code works in both R4 and R5
medicationRequest.SetMedicationCodeableConcept(new CodeableConcept { Text = "Aspirin" });

Strategy for Updates

  1. Read release notes carefully - understand what changed
  2. Update one major version at a time - don’t skip versions
  3. Run all tests after upgrading
  4. Address deprecation warnings before they become errors
  5. Consider abstraction layers for multi-version support
  6. Test serialization round-trips - ensure JSON/XML compatibility

Resources


You’ve reached the end of this guide! You now understand the core modeling patterns that make the Firely SDK work. These patterns—the Element property pattern, SDK attributes, collection initialization, choice types, and more—will serve you well as you build FHIR applications. Keep this guide handy as a reference when you encounter confusing behavior, and you’ll save hours of debugging time.

Topics Covered

FHIR Firely dotnet csharp SDK attributes modeling patterns intermediate