One of the major areas of work for the 2.0 version of the .NET API was better for working with multiple versions of FHIR within a single application. Since 1.6, we have been splitting up the API in parts that are specific for a given FHIR version, and parts that can be reused across FHIR versions. Additionally, we have seen use of the FHIR type infrastructure to define non-FHIR models like CDA. For this to work smoothly and consistently, we had to refactor the API into parts that are reusable for non-FHIR models, parts that are specific to FHIR and parts that are specific to a certain version of FHIR.
Unfortunately, some of this redesign required a breaking change in the API surface, even for parts that are commonly used. This document details those changes.
In the 1.x version of the API, ModelInfo exposes a wide variety of utility methods:
- Mapping between .NET types and Fhir types
- Meta-data oriented queries like "What are all conformance resources".
- FHIR inheritance hierarchy
- Version information
In 2.x, we are introducing UniModelInfo, a ModelInfo stripped down to contain information about the model itself, as well as listing the available datatypes. Instead of a single static instance, there will now (usually) be one instance for each model the application needs to deal with. For example, in a mapping application, this might be FHIR R3, FHIR R4 and CDA.
public abstract class UniModelInfo : IAnnotated
{
/// <summary>
/// An name given to this model, e.g. "FHIR". Is also used as a namespace
/// prefix to make type names unique, e.g. E.g. "CDA" in the fully
/// qualified type name "CDA.ST".
/// </summary>
public string Name { get; }
/// <summary>
/// The version of the model this class represents, e.g. "3.0.1"
/// </summary>
public string Version { get; }
/// <summary>
/// All the types defined in this model
/// </summary>
public abstract IReadOnlyCollection<IStructureDefinitionSummary> Types { get; }
/// <summary>
/// Source for model specific information.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public abstract IEnumerable<object> Annotations(Type type);
}Information about a model may be retrieved from several sources. In the 1.x version, ModelInfo (and much of the metadata) was fed by the information read from .NET Attributes on the POCO model. However, model information could also be retrieved from a set of FHIR StructureDefinitions, a FHIR core package or a CQL ELM file.
Concrete implementations for this abstract class currently are PocoModelInfo, which retrieves information from the POCOs (much like the ModelInfo in version 1.x of the library), StructureDefinitionModelInfo, which can read model information from a FHIR Bundle (like profiles-types.xml), a set of StructureDefinitions or a FHIR Package.
Implementer Note: There is also a
SystemModelInfoclass that contains the type information for the primitive "system" types on which all models are built, any concrete implementation ofUniModelInfois supposed to expose these types as well.
To guarantee backwards compatibility, the 2.0 .NET API will still include a ModelInfo in the assembly containing the generated POCOs for each FHIR version.
Design note: this
ModelInfoclass is actually a subclass ofPocoModelInfoand pre-loads the information from POCO's in the same assembly. A set of extension methods provide the same methods as the current 1.xModelInfo.
The UniModelInfo captures only information that is common and required for all types of information models (e.g. FHIR, CDA, ELM), however it will frequently be necessary to get information and characteristics that are specific for a model. Examples are the list of "retrievable classes" for CQL, or the list of "conformance classes" for FHIR. Concrete subclasses will implement support for these specific properties, but there are a few cross-cutting aspects that we have formalized, e.g. details about XML serialization are common to both FHIR, CDA and ELM:
interface IXmlSerializationModelInfo
{
XNamespace DefaultNamespace { get; }
Uri SchemaLocation { get; }
}Design note: other cross-cutting aspects may be implemented using extension methods, e.g. there could be a set of non-POCO but FHIR specific extension methods to get to information currently available on
UniModelInfo:public static bool IsReference(this UniInfoModel model, TypeInfo type) { ... }
Originally, ModelInfo was a static class, so each application had exactly one model instance. To be more precise: each assembly compiled for a specific version of FHIR contained its own ModelInfo. To support having multiple models, one can now load and use multiple instances using one of the concrete implementations as discussed above.
Since most applications will still use a single model, the user can set this single application-wide model using the static UniModelInfo.Default property. Note that this property is null by default, so if you choose to not use the "1.x"-style ModelInfo (for example to be able to use non-FHIR models), but still want the convenience of a single model, you have to initialize and set this global property at the start up of your application.
Better, however, is to explicitly instantiate and pass UniModelInfo instances to those components of the .NET API that need it. In 2.0, these components (like the parsers, serializers and validator) stick to the following design:
- They implement
IModelInfoAware(which is an interface with a single read-only propertyModelof typeUniModelInfo). - They have an additional constructor with a parameter of type
UniModelInfo(or specific subclass). - They may provide methods taking a
UniModelInfoparameter.
These components will determine the current UniModelInfo to use by first looking at the instance passed in the constructor. If no such instance was passed, they will default to look at the UniModelInfo.Default global default instance as described above.
In fact, there is a third option, which is a hybrid between setting a global model (which is not recommended) and passing dependencies (possibly deeply into nested parts of your application): the ModelInfoContext. Using this context sets a "temporary" default model to use by IModelInfoAware components:
var sourceData = FhirXmlParser.Parse(...);
var fhirR3ModelInfo = StructureDefinitionModelInfo.FromPackage(....);
var fhirR4ModelInfo = StructureDefinitionModelInfo.FromPackage(....);
var myPocoModelInfo = PocoModelInfo.LoadFromAssembly(...);
// We can first set the global default (if necessary).
UniModelInfo.Default = myPocoModelInfo;
// we can override this by creating a disposable `ModelInfoContext`
using(new ModelInfoContext(fhirR3ModelInfo))
{
// here, the R3 model info applies, not the one loaded from the POCOs
var x = sourceData.ToTypedElement();
// of course, you can now override this by passing in an explicit
// model info
var y = sourceData.ToTypedElement(fhirR4ModelInfo);
}
// here, the default applies again.One of the major functions of UniModelInfo is supplying information about the types that form the content of the model, using UniModelInfo.Types. This is a collection of TypeInfo:
public abstract class TypeInfo
{
TypeInfo Base { get; }
UniModelInfo DeclaringModel { get; }
bool IsAbstract { get; }
bool IsOrdered { get; }
bool IsBindable { get; }
bool IsPrimitive { get; }
/// <summary>
/// The unique name for the type (within this model).
/// </summary>
string Name { get; }
/// <summary>
/// A globally unique identifier, in FHIR this would be the canonical.
/// </summary>
string Identifier { get; }
IReadOnlyCollection<TypeElementInfo> Elements { get; }
}
public interface IFhirTypeInfo
{
bool IsResource { get; }
}
public inteface ICDATypeInfo
{
// This is an extension in StructureDefinition
string Namespace { get; }
}
public class PocoTypeInfo : TypeInfo, IFhirTypeInfo, IStructureDefinitionSummary
{
}
public class StructureDefinitionTypeInfo : TypeInfo, IFhirTypeInfo, ICDATypeInfo, IStructureDefinitionSummary
{
}Note that, just like with the UniModelInfo, TypeInfo is an abstract type for which different sources for the models will create concrete subclasses, potentially implementing interfaces containing information only applicable to that model (like IsResource here, for FHIR).
Each TypeInfo refers to the UniModelInfo that defines the type, since each type (and type name) is only unique within the context of that model, especially since the model contains a version label.
For performance reasons, applications should make sure they maintain a single instance of a model per application, so there will also be a single TypeInfo per type. This makes it easy (and more performant) to quickly reference type information from instance data encoded in ITypedElements and compare those types against the known types in the model
Design note: we should implement
Equals()in these classes to do aReferenceEqualsfirst, then -failing that- compare them by content.
Each TypeInfo lists its properties, here called TypeElementInfo to stick to the familiar FHIR terminology of 'element' for a property:
public abstract class TypeElementInfo // ElementDefinition
{
string Name { get; }
bool IsCollection { get; }
TypeInfo[] Type { get; }
}
interface IFhirTypeElementInfo
{
bool InSummary { get; } // only MaskingNode uses this
bool IsChoiceElement { get; }
bool IsRequired { get; } // only MaskingNode uses this
}
interface ICDATypeElementInfo
{
string DefaultTypeName { get; }
}
interface IXmlSerializationTypeElementInfo
{
int Order { get; }
string NonDefaultNamespace { get; }
XmlRepresentation Representation { get; }
}
public class PocoTypeElementInfo : IElementDefinitionSummary, IFhirTypeElementInfo, ICDATypeElementInfo, IXmlSerializationTypeElementInfo
{
}The biggest difference in structural design from the comparable IElementDefinitionSummary is that TypeElementInfo does not directly support the nested/backbone types common in FHIR anymore. Each backbone element is now referred to by its actual name and type, and an element cannot define unnamed nested structures. To enable this, the UniModelInfo will contain types for each of the nested structures defined in FHIR. This design aligns much better with the type systems used in common languages like C++, C# or Java. As well, this ensures that the type structure is no longer (possibly infinitely deeply) nested, which makes it easier to implement type information providers.
Design note: FHIR already has an
http://hl7.org/fhir/StructureDefinition/structuredefinition-explicit-type-nameextension that names these "anonymous" substructures. The full name for the type will be of the form "parent type" +#+ "explicit type name", e.g.Patient#Contact.
Less visible, but equally important, is the fact that the Elements property of TypeInfo will also include the primitive value element in FHIR primitive types. This aligns better with how other primitive elements (like Element.id or XHtml.div) are exposed. Since the introduction of the FhirPath System types, we have a clear way of identifying the types of such primitive elements in a StructureDefinition, and there is no longer a need to treat the value element differently. This will also make it possible for non-FHIR models (like CDA) to introduce primitive elements other than value.
In 1.x, we already supported working with data across versions and models using the ITypedElement interface. In 2.0, this interface will be changed for it to be able to use the new TypeInfo classes:
public interface ITypedElement
{
IEnumerable<ITypedElement> Children(string name=null);
string Name { get; }
TypeInfo InstanceType { get; }
object Value { get; }
string Location { get; }
}There are two important changes from the 1.x version of this interface:
InstanceTypenow refers directly to the corresponding type from theUniModelInfo. This makes it very straightforward to get to the corresponding type information, without the need for lookups via external providers (as is the case for theIStructureDefinitionSummaryProvidersnow). Since each type refers to its model, it is also now much easier to get a reference to theUniModelInfoand get metadata information about all the other types. Additionally, in the 1.x version, the named type inInstanceTypewas usually meant to be a FHIR type - in 2.0 this reference unambiguously refers to a specific type from a specific (possibly non-FHIR) model.- Since type information is easier to get to, we no longer need an explicit
ElementDefinitionproperty, this can be recovered by getting the member information for the declaring type for that property. This is a different access path than in the 1.x version, where the property itself owned its metadata, but is more aligned with existing reflection systems in Java and .NET and also makes implementingITypedElementmuch easier.
We can now also redesign
ElementNodeto be much more performant, and not require direct access to anIResourceResolveranymore. The user could (if necessary) supply instance data to an ElementNode, and only in the case of choice types would this be really necessary. In which case a single reference to a type in the currentUniModelInfowould suffice.
- (UniModelInfo + ModelInfo) or (ModelInfo + ModelInfoR3/R4/R5)?
- Have a single concrete ModelInfo and multiple ModelInfoProviders (which put model-specific stuff in annotations), or an abstract ModelInfo with multiple implementations (as documented here).
- Same is true for
TypeInfo. We could have the specific ModelInfo providers/implementations (see previous bullet) create either a specific subclass or create a single TypeInfo class. Again, model-specific stuff on Annotations. - Use Annotations (like the FhirParsers) for specific information, or just allow type sniffing (
if(myModel is IPocoMappingInfo)or evenif(myModel is PocoModelInfo))? - Must these concrete implementations be public at all?
- Shall we rebrand
IStructureDefinitionSummaryasITypeInfo? How much will that break? We can make the actual implementer ofITypeInfoalso implementIStructureDefinitionSummaryfor backwards compat I guess. Or aTypeInfobase class that implementsIStructureDefinitionSummary? - CQL defines specific subclasses of TypeInfo for classes, lists, intervals, tuples and simple types (primitives). In particular, should we have the separation between
PrimitiveTypeInfoandClassTypeInfo? For now, I have decided to stay close to the .NET (and Java) reflection setup (no subclasses of System.Type). We could have aIsPrimitiveboolean if needed. - Should we -for backw compat reasons- still include the old InstanceType property and add a new one to represent the
TypeInfo?