Working with ITypedElement

The main difference between ISourceNode (see Parsing with ISourceNode) and ITypedElement is the presence of type information: metadata coming from the FHIR specification about which elements exist for a given version of FHIR, whether they repeat, what the type of each element is, etcetera. Type information is necessary for many operations on data, most notably serialization, running FhirPath statements and doing validation. As such, it is more common to work with ITypedElement than it is to work with ISourceNode. However, as one could imagine, in cases where type information is not necessary, ISourceNode is more performant and has a smaller memory footprint.

This is what ITypedElement looks like:

public interface ITypedElement
{
    string Name { get; }
    object Value { get; }
    string Location { get; }

    string InstanceType { get; }
    IElementDefinitionSummary Definition { get; }

    IEnumerable<ITypedElement> Children(string name=null);
}

Just like ISourceNode, the interface represents a single node in the tree, has a name and a location and you can enumerate its children using Children().

Unlike ISourceNode however, type information is available by looking at the InstanceType and Definition properties. As a consequence, the Name property now returns the actually defined name of the element in the specification, so for choice types the element would not include the type suffix. For example, Observation.valueQuantity in an ISourceNode would turn into Observation.value in ITypedElement.

Similarly, the primitive value of the node (if any) is now of type object, and returns the value of the primitive, represented as a native .NET value. The following table lists the mapping between the encountered FHIR primitive and the .NET type used to represent the value:

Mapping between FHIR type and .NET type

FHIR Type

.NET type

instant

Hl7.Fhir.ElementModel.Types.DateTime

time

Hl7.Fhir.ElementModel.Types.Time

date

Hl7.Fhir.ElementModel.Types.Date

dateTime

Hl7.Fhir.ElementModel.Types.DateTime

decimal

decimal

boolean

bool

integer

long

unsignedInt

long

positiveInt

long

string

string

code

string

id

string

uri, oid, uuid, canonical, url

string

markdown

string

base64Binary

string (uuencoded)

xhtml

string

Note that Location is exactly the same on both interfaces - every part of the path still has an array index, whether the element repeats or not. If you would like to have a shortened path, where non-repeating elements have their array index removed, you can check whether the underlying implementation of ITypedElement implements IShortPathGenerator (which the implementations from the SDK do), and get its ShortPath property.

Important

The IElementDefinitionSummary interface returned by the Definition property is very likely still to change. You are welcome to experiment with it and provide feedback, but the next release of the SDK will most likely add (incompatible) capabilities.

The SDK offers a set of extension methods on top of ITypedElement (like Visit() and Descendants()) to make it easier to select subtrees and process the data in the tree.

Obtaining an ITypedElement

The SDK enables you to turn data in POCO or ISourceNode form into an ITypedElement by calling the ToTypedElement() extension method.

In the first case, the POCO has all additional type information available (being based on a strongly-typed object model), and simply surface this through the ITypedElement interface. In the second case, the SDK needs an external source of type information to associate type information to the untyped nodes in the ISourceNode tree. The ToTypedElement method on ISourceNode looks like this:

public static ITypedElement ToTypedElement(this ISourceNode node,
    IStructureDefinitionSummaryProvider provider, string type = null,
    TypedElementSettings settings = null);

Notice that the provider parameter is used to pass in type information structured by the IStructureDefinitionSummaryProvider interface. Currently, the SDK supplies two implementations of this interface:

  • The PocoStructureDefinitionSummaryProvider, which obtains type information from pre-compiled POCO classes. This is very similar to calling ToTypedElement() on a POCO, but this method does not require the caller to have data present in POCOs.

  • The StructureDefinitionSummaryProvider, which obtains type information from StructureDefinitions provided with the core specification and additional Implementation Guides and packages. The constructor for this provider needs a reference to an IResourceResolver, which is the subsystem used to get access to FHIR’s metadata resources (like StructureDefinition).

This is a complete example showing how to turn the patientNode from the last section into a ITypedElement by using external metadata providers:

ISourceNode patientNode = ...
IResourceResolver zipSource = ZipSource.CreateValidationSource();
ITypedElement patientRootElement = patientNode.ToTypedElement(zipSource);
ITypedElement activeElement = patientRootElement.Children("active").First();
Assert.AreEqual("boolean", activeElement.Type);

Compatibility with IElementNavigator

Previous versions of the SDK defined and used the precursor to ITypedElement, called IElementNavigator. Though functionally the same, ITypedElement is stateless, whereas IElementNavigator was not. To aid in parallellization, we have chosen to obsolete the stateful IElementNavigator in favor of ITypedElement. At this moment, not all parts of the SDK have been rewritten (yet) to use the new ITypedElement and we expect the same is true for current users of the SDK. To aid in migration from one concept to the other, the SDK provides a set of adapters to turn IElementNavigators into ITypedElements and vice versa. These can be constructed by simply calling ToElementNavigator() on a ITypedElement or ToTypedElement() on an IElementNavigator. The compiler will emit messages about this interface being obsolete to stimulate migration to the new paradigm.

Handling structural type errors

While traversing the ITypedElement tree, the implementations will try to associate type information from the specification with the data encountered. If this fails, errors are by default thrown as exceptions, but the all underlying implementations of ITypedElement implement IExceptionSource to alter this behaviour. See Handling errors for more information.

Detecting type errors is done lazily, so in order to detect all errors, one would have to do a complete visit of the tree, including forcing a read of the primitive data by getting the Value property. There is a convenience method VisitAll() that does exactly this. Additionally, there is a metehod VisitAndCatch() that will traverse the whole tree, returning a list of errors and warnings.