Skip to content

Instantly share code, notes, and snippets.

@GinoCanessa
Created March 16, 2022 19:36
Show Gist options
  • Save GinoCanessa/3e593bc56b43dd290ecedbbb431e0447 to your computer and use it in GitHub Desktop.
Save GinoCanessa/3e593bc56b43dd290ecedbbb431e0447 to your computer and use it in GitHub Desktop.
FHIR Interface Hierarchy Exploration
Starting processing of Interfaces in the inheritance tree...
JSON of Interfaces included in inheritance tree:
------------
{
"Problematic": "Scalar value",
"Author": [
"This should be invalid"
],
"Url": "http://example.org/interfaceInInheritance",
"Id": "inherited-interface"
}
------------
Starting processing of Interfaces as an external concept...
Non-inheritance caught: This element is not valid on this Resource.
Non-inheritance caught: Questionnaire is restricted to a single Problematic value
JSON of Interfaces as a separate concept:
------------
{
"Url": "http://example.org/keepStrictInheritance",
"Problematic": "Scalar value",
"Id": "external-interface"
}
------------
Starting processing of Interfaces as an external concept (v2)...
Non-inheritance caught: This element is not valid on this Resource.
Non-inheritance caught: Questionnaire is restricted to a single Problematic value
JSON of Interfaces as a separate concept v2:
------------
{
"Url": "http://example.org/keepStrictInheritance",
"Problematic": [
"Scalar value"
],
"Id": "external-interface-v2"
}
------------
using System.Text.Json;
using System.Text.Json.Serialization;
/// <summary>A partial metadata resource.</summary>
public class PartialMetadataResource : PartialResource
{
/// <summary>Gets or sets URL of the document.</summary>
public Uri Url { get; set; }
/// <summary>Gets or sets the author.</summary>
public string[] Author { get; set; }
/// <summary>Gets or sets the especially problematic.</summary>
public string[] Problematic { get; set; }
}
/// <summary>Interface for partial metadata.</summary>
public interface IPartialMetadata
{
/// <summary>Gets or sets URL of the document.</summary>
Uri Url { get; set; }
/// <summary>Gets or sets the author.</summary>
string[] Author { get; set; }
/// <summary>Gets or sets the especially problematic.</summary>
string[] Problematic { get; set; }
}
/// <summary>A partial questionnaire inherit.</summary>
public class PartialQuestionnaireInherit : PartialMetadataResource
{
/// <summary>Gets or sets the especially problematic.</summary>
new public string Problematic { get; set; }
/// <summary>
/// Gets or sets the author.
/// Note that we need to explicitly flag this property NOT to be serialized, because
/// otherwise the serializer will get this exception and stop processing.
/// </summary>
/// <exception cref="Exception">Thrown when an exception error condition occurs.</exception>
[Obsolete("Metadata.Author is invalid", true)]
[JsonIgnore]
new public string[] Author
{
get => throw new Exception("This element is not valid on this Resource.");
set => throw new Exception("This element is not valid on this Resource.");
}
}
/// <summary>A partial questionnaire strict.</summary>
public class PartialQuestionnaireStrict : PartialResource, IPartialMetadata
{
/// <summary>Gets or sets URL of the document.</summary>
public Uri Url { get; set; }
/// <summary>Gets or sets the especially problematic.</summary>
public string Problematic { get; set; }
/// <summary>
/// Gets or sets the author.
/// Note that we need to explicitly flag this property NOT to be serialized, because
/// otherwise the serializer will get this exception and stop processing.
/// </summary>
[Obsolete("Metadata.Author is invalid")]
[JsonIgnore]
string[] IPartialMetadata.Author
{
get => throw new Exception("This element is not valid on this Resource.");
set => throw new Exception("This element is not valid on this Resource.");
}
/// <summary>Gets or sets URL of the document.</summary>
Uri IPartialMetadata.Url { get => this.Url; set => this.Url = value; }
/// <summary>Gets or sets the especially problematic.</summary>
string[] IPartialMetadata.Problematic
{
get
{
if (string.IsNullOrEmpty(this.Problematic))
{
return new string[0];
}
return new string[1] { this.Problematic };
}
set
{
if ((value == null) ||
(value.Length == 0))
{
this.Problematic = null;
return;
}
if (value.Length > 1)
{
throw new Exception("Questionnaire is restricted to a single Problematic value");
}
this.Problematic = value[0];
}
}
}
public class PartialQuestionnaireStrict2 : PartialResource, IPartialMetadata
{
/// <summary>Gets or sets URL of the document.</summary>
public Uri Url { get; set; }
/// <summary>
/// Gets or sets the especially problematic.
/// Here we define Problematic as an array, which is a better
/// compatibility story, but we only know that from the pain
/// of trying otherwise
/// </summary>
public string[] Problematic { get; set; }
/// <summary>
/// Gets or sets the author.
/// Note that we need to explicitly flag this property NOT to be serialized, because
/// otherwise the serializer will get this exception and stop processing.
/// </summary>
[Obsolete("Metadata.Author is invalid")]
[JsonIgnore]
string[] IPartialMetadata.Author
{
get => throw new Exception("This element is not valid on this Resource.");
set => throw new Exception("This element is not valid on this Resource.");
}
/// <summary>Gets or sets URL of the document.</summary>
Uri IPartialMetadata.Url { get => this.Url; set => this.Url = value; }
/// <summary>Gets or sets the especially problematic.</summary>
string[] IPartialMetadata.Problematic
{
get
{
if ((this.Problematic == null) ||
(this.Problematic.Length == 0))
{
return new string[0];
}
return this.Problematic;
}
set
{
if ((value == null) ||
(value.Length == 0))
{
this.Problematic = null;
return;
}
if (value.Length > 1)
{
throw new Exception("Questionnaire is restricted to a single Problematic value");
}
this.Problematic = value;
}
}
}
/// <summary>A program.</summary>
public static class Program
{
/// <summary>Main entry-point for this application.</summary>
/// <param name="args">An array of command-line argument strings.</param>
/// <returns>Exit-code for the process - 0 for success, else an error code.</returns>
public static int Main(string[] args)
{
// *******************************************************
// Start interfaces are included in inheritance tree
// *******************************************************
Console.WriteLine("Starting processing of Interfaces in the inheritance tree...");
// Author element is present, best developer experience I have
// discovered is marking it Obsolete, so it cannot be used directly
// this works with POCOs, but does not help with tree-based
// or dynamic navigation styles.
// *
// Problematic appears as scalar.
PartialQuestionnaireInherit inherit = new()
{
Id = "inherited-interface",
Url = new Uri("http://example.org/interfaceInInheritance"),
//Author = new string[1] { "Invalid Element Needs To Throw" },
Problematic = "Scalar value",
};
// create a meta-view based on the inherited class of metadata
PartialMetadataResource metaOfInherited = (PartialMetadataResource)inherit;
// despite the element being flagged in Questionnaire, it is present and can
// receive values. I have not found a way to bypass this.
metaOfInherited.Author = new string[1] { "This should be invalid" };
// in this view, Problematic is an array now
metaOfInherited.Problematic = new string[2] { "Scalar value", "An INVALID second value" };
// serialize to JSON
string jsonInherited = JsonSerializer.Serialize(inherit, new JsonSerializerOptions() { WriteIndented = true });
Console.WriteLine("JSON of Interfaces included in inheritance tree:");
Console.WriteLine("------------");
Console.WriteLine(jsonInherited);
Console.WriteLine("------------");
// *******************************************************
// Start interfaces are external from inheritance tree
// *******************************************************
Console.WriteLine("\nStarting processing of Interfaces as an external concept...");
PartialQuestionnaireStrict strict;
try
{
// Author element is not present, since it is not defined.
// *
// Problematic appears as scalar, but throws when set.
strict = new()
{
Id = "external-interface",
Url = new Uri("http://example.org/keepStrictInheritance"),
Problematic = "Scalar value",
};
}
catch (Exception ex)
{
Console.WriteLine($"Non-inheritance caught: {ex.Message}");
// This time, create without the problematic element
strict = new()
{
Id = "external-interface",
Url = new Uri("http://example.org/keepStrictInheritance"),
};
}
// create a meta-view based on the interface of metadata
IPartialMetadata iStrict = (IPartialMetadata)strict;
// despite the element being flagged in Questionnaire, it is present and will still fail at runtime
// but, at least it throws an exception here
try
{
iStrict.Author = new string[1] { "This should be invalid" };
}
catch (Exception ex)
{
Console.WriteLine($"Non-inheritance caught: {ex.Message}");
}
// in this view, Problematic is an array now, and it throws if we try to add a second value
try
{
iStrict.Problematic = new string[2] { "Scalar value", "An INVALID second value" };
}
catch (Exception ex)
{
Console.WriteLine($"Non-inheritance caught: {ex.Message}");
}
// serialize to JSON
string jsonStrict = JsonSerializer.Serialize(strict, new JsonSerializerOptions() { WriteIndented = true });
Console.WriteLine("JSON of Interfaces as a separate concept:");
Console.WriteLine("------------");
Console.WriteLine(jsonStrict);
Console.WriteLine("------------");
// *******************************************************
// Start interfaces are external from inheritance tree v2
// *******************************************************
Console.WriteLine("\nStarting processing of Interfaces as an external concept (v2)...");
// Author element is not present, since it is not defined.
// *
// Problematic appears as array, will throw if multiple values are passed.
PartialQuestionnaireStrict2 strict2 = new()
{
Id = "external-interface-v2",
Url = new Uri("http://example.org/keepStrictInheritance"),
Problematic = new string[1] { "Scalar value" },
};
// create a meta-view based on the interface of metadata
IPartialMetadata iStrict2 = (IPartialMetadata)strict2;
// despite the element being flagged as obsolete in Questionnaire, it is present and will still
// fail at runtime. But, at least it throws an exception here.
try
{
iStrict.Author = new string[1] { "This should be invalid" };
}
catch (Exception ex)
{
Console.WriteLine($"Non-inheritance caught: {ex.Message}");
}
// in this view, Problematic is an array now, and it throws if we try to add a second value
try
{
iStrict.Problematic = new string[2] { "Scalar value", "An INVALID second value" };
}
catch (Exception ex)
{
Console.WriteLine($"Non-inheritance caught: {ex.Message}");
}
// serialize to JSON
string jsonStrict2 = JsonSerializer.Serialize(strict2, new JsonSerializerOptions() { WriteIndented = true });
Console.WriteLine("JSON of Interfaces as a separate concept v2:");
Console.WriteLine("------------");
Console.WriteLine(jsonStrict2);
Console.WriteLine("------------");
return 0;
}
}
/// <summary>A partial FHIR resource.</summary>
public class PartialResource
{
/// <summary>Gets or sets the identifier.</summary>
public string Id { get; set; }
}
@GinoCanessa
Copy link
Author

Code structure notes:

  • Common
    • PartialResource : base class to root inheritance for all models
  • Interfaces in inheritance
    • PartialMetadataResource : class with the above noted properties for experimentation, child of PartialResource
    • PartialQuestionnaireInherit : child of PartialMetadataResource to show a 'final' class that handles interface properties
  • Interfaces NOT in inheritance (using language interface constructs)
    • IPartialMetadata : Interface exposing same properties for experimentation, for brevity it does not include what would be Resource elements, etc.
    • PartialQuestionnaireStrict : child of PartialResource, implements interface IPartialMetadata and exposes Problematic property as a single string and serializes it as one.
    • PartialQuestionnaireStrict2 : child of PartialResource, implements interface IPartialMetadata and exposes Problematic property as an array of strings.

General notes
Disclaimers/Assumptions/Context:

  • I attempted to give each model 'best effort', but know that I am biased on the topic. I posted all the source so that someone else can jump in with a better approach.
  • Coding each 'style' (in or out of inheritance tree) I used what would be the natural language constructs for doing so. I made sure to keep to single inheritance in the class hierarchy, and used language interfaces for external interface definitions.
  • I am using C#, with a focus on strongly-typed languages.
  • I am using classes that differentiate scalar value types from array value types.
  • I defined very simplistic models to illustrate points. I used three 'elements' from a root 'metadata':
    • Url is 0..1 in metadata and questionnaire
    • Author is 0..* in metadata, prohibited in questionnaire
    • Problematic is 0..* in metadata, 0..1 in questionnaire (this is defined valid, so I wanted to include it)
  • I went through some basic operations of creating objects and getting or setting elements. In order to show the differences in where they throw exceptions, I only put try blocks around things that throw in the example.
  • Thoughts are based the experience of tinkering with this, etc.

So, my attempt at collecting thoughts...

-- 'Profiling' in resource definitions:

  • What is the JSON serialization for the Questionnaire.problematic element? It needs to either be an array or a value, and I have no idea which is 'more correct'
    • If it should be an array, we cannot discover that from the snapshot today - we need to either add more information or we need to parse the interface and apply the logic ourselves.
    • If it should be a scalar, developers will need to discern between scalar types and array types in an odd way.
    • Either way, if we keep this feature we lose some of the 'magic' of FHIR in how easy it is to 'just use'.
  • Note this can be seen in the ...Strict vs. the ...Strict2 models - I did not do it twice in the 'in tree' model since it ends up so similar and this one was less code.

-- Interfaces in the class hierarchy:

  • I could not figure out a way to have the language enforce not having the Author property on Questionnaire when casted as a parent Metadata object. The language does not have the ability to prevent setting a value on a parent casted as that type.
  • This is consistent with OO in general - a descendent class cannot influence ancestors.
  • This results in the ability to generate Questionnaire objects with Authors.
    • It can be caught by the parent class, but requires some serious and painful inversion.
    • It can be caught with a custom serializer, but adds a lot of overhead (edit: and relies on developers only using the provided serializers/parsers).

-- General thoughts

  • Both models cause: runtime errors, invalid data, or implicitly 'swallowed' data - this must be handled at the application layer regardless of which structure is used (though it is more or less obvious depending on the route taken).
  • This makes sense, since we are defining contracts that are not really contracts - the cost of modeling elements that may or may not exist is handling that complexity at runtime.
  • Having spent a fair bit of time on this, I am confident I can generate either form of code based on either modeling structure.
    Including interfaces in the modeling inheritance means I have to deal with the strangeness of implied elements, etc. in order to use any model that has one (with the implication that more of these will appear in the future). Leaving it out means that I only add that complexity if I want to use interfaces.
    • complexity here means both code complexity and runtime cycles

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment