Skip to content
Ewout Kramer edited this page May 19, 2022 · 5 revisions

The Firely SDK is currently published as a set of NuGet files, a large part of which is specific for a given version of FHIR. For example, we publish an STU3 and R4 version of the SDK, and there is still an (unmaintained) DSTU2 versions available as well. Although there are differences between the versions of the SDK, they also share a large body of code. The differences are mainly in the generated POCO's, as well as changes in snapshotting and validation logic.

Over the last few major versions of the SDK, we have gradually been moving growing amounts of shared code over to a separate git repo (https://github.com/FirelyTeam/firely-net-common), from which we produce a set of "common" NuGet packages that are shared by the STU3 and R4 versions of our SDK. Obviously, this reduces the amount of duplicated code, and saves us from having to fix bugs in multiple versions of the SDK. As well, for those of you who maintain software that supports multiple versions of FHIR, these "common" types (like ITypedElement, the parsers and the FhirPath engine) make it much easier to create a single codebase that works across versions.

There are limits to what we can move to this common repo: since essential conformance resources like StructureDefinition and ValueSet have changed from release to release, we were unable to re-use any code that depends on these POCO's. This is mainly the metadata-infrastructure used for parsing, snapshot generation and validation. With newer versions of FHIR on the horizon (R4B, R5, R6, R4C?), we are now faced with the difficulties of maintaining an ever growing set of versions of our SDK, for which the code from version to version is basically a verbatim copy.

There's light at the end of the tunnel however: as FHIR matures, the differences between the versions of HL7 FHIR are becoming smaller and smaller, and a set of these essential resources (and datatypes) have now been made "normative" in R4 - which in practice means that the newer versions of the POCO's should be able to work with data from older FHIR versions. This allows us to start thinking about the next steps in getting more code in the "common" section:

  • Identify the set of useful normative resource types that we can move over to shared code. Primary candidates are StructureDefinition, CodeSystem, ValueSet, Bundle, OperationDefinition, and CapabilityStatement. Binary and OperationOutcome have already been moved to common in previous versions.
  • For these resource types, generate a POCO according to the latest version available (currently: R5), and add this to "common".
  • Now, move the SDK code that is dependent on these classes over to common as well. This will include parsers, FhirClient, Validator, Snapshot generator, resource resolvers and most of the metadata classes.

After this move, most of the remaining code will be the version-specific (and mostly generated) code representing the POCO's. Some important initial points that we have discussed in the team so far:

  • We will retain the namespaces of the classes that we move to common, so that in principle it will only be required to change the NuGet/assembly references in the project file.
  • Shared code that needs information about the model to be used (e.g. the parsers will need to know what POCO's to create) will be configured by passing them a (list of) assemblies that contain the model classes. We already did this for the new System.Text.Json-based parsers in the 4.0 version of the SDK.
  • We might do the same for the important ModelInfo class - this is now a static singleton, one per version, but we can probably enable creating new instances of these passing it the correct assembly dynamic at runtime (for those who want to support multiple versions), or add an IModelInfo, so it will become easier to work with a single abstraction for dealing with multiple versions.

The advantages are clear: less maintenance for us (just basically a single codebase + a set of generated classes), less work for those of you supplying a PR (not necessary anymore to provide one for each version of the SDK), and just a single validator, snapshot generator and parser to work with within your own applications.

There are, of course, disadvantages too:

  • We probably cannot avoid changing (minor) parts of public interface of the SDK.
  • The STU3 version of the SDK only has non-normative (and in fact, incompatible) versions of the conformance resources, so it will not be able to participate in this sharing of code. This will only work from R4 and up.
  • Types will be moved across assemblies, forcing those of you using external alias to have to change your source code.

We are of the opinion however, that the advantages by far outweigh the disadvantages, especially moving forwards.

For STU3, which cannot participate in the move to the new common, we will create it's own common, basically freezing the current situation, like we have done for DSTU2 when we sunsetted DSTU2:

new-common

As you can see in the image, working with anything from R4 up would mean having one (set of) NuGets containing all the common classes plus one NuGet with the POCO's for each version of FHIR that are not yet normative.

Moving normative POCO's to a common library is done by generating a POCO from the latest version of FHIR, and using that for the previous versions as well. Since the resource types are normative, the newer POCO will be able to contain the data from older versions, but of course it also means that the parser will not complain when data from an older version of FHIR contains an element that is added in a later version. We are experimenting with specific annotations on the POCO's to indicate when a certain element was introduced in FHIR, so we might be able to flag warnings for these situations too. In any case, the profile validator will be able to catch such errors.

Additional ideas for making working cross-versions easier

Some components in common will now need to be configured, to know which version of FHIR to work with. Specifically, the parsers needs to be instructed which version of FHIR to expect. We have already started designing the new parsers in SDK 4.0 according to this principle:

 var jsonInput = "{.....}";
 var options = new JsonSerializerOptions().ForFhir(typeof(Patient).Assembly);
 var patient = JsonSerializer.Deserialize<Patient>(jsonInput, options);

Notice the typeof(Patient).Assembly, which tells the FHIR Json deserializer which assembly to use when looking for POCO's to create. Of course, this will automatically load all the POCO's from common as well.

We might do the same for ModelInfo. To not break too much code, we will retain the current (generated) ModelInfo concept, but additionally, it is useful to be able to work with multiple versions like this:

 IModelInfo r3ModelInfo = new ModelInfo(... POCO assembly loaded for R3....)
 IModelInfo r4ModelInfo = new ModelInfo(... POCO assembly loaded for R4....)

For those familiar with these classes, ModelInfo will probably be just a set of methods on top of the existing ModelInspector, which is currently used to load and cache FHIR metadata from the POCO classes.

We might even introduce a (static) class containing a pre-configured instance for each version, e.g.

  public class Framework
  {
      public IModelInfo ModelInfo;
      public ModelInspector ModelInspector;
      public JsonSerializerOptions SerializerOptions;
      public IStructureDefinitionSummaryProvider SummaryProvider;
  }

  // Each of these in a FHIR versions specific assembly
  public static Framework R4Framework = ....;
  public static Framework R4BFramework = ....;
  public static Framework R5Framework = ....;

Questions? Remarks?

Let us know! We are currently preparing and tinkering with these concepts, so any input is welcome at this point in time!

Clone this wiki locally