Saturday 4 June 2022

Making use of extension methods to extract data from FHIR bundles

This article shows some extension methods to extract data from FHIR bundles, where FHIR stands for Fast Healthcare Interoperability Resources. The standard is used as a global or country specific standard with its own
variants. It is also a standard that allows for extensibility and its goal is to define interoperability and an information model definining resources which are then comprised of smaller elements which can define different kinds of information.
FHIR also defines API standards and is defined in different formats such as XML and json. We will look into some example extension methods for retrieving data deep inside a FHIR Bundle. A bundle is a top level data which got a lot of components in a hierarchical structure as we define the data in XML or JSON for example, i.e. a tree structure. Let us say that we want to retrieve data like medications a pasient is taking as his or her DDD (Defined Daily Dosage). We want to be able to find a medication statement inside our bundle and then retrieve the medication dosage quantity. We know the unit is measured in micrograms (ug) and the medication (drug) is called Fentanyl. Here is a property getter with logic to retrieve this value.
 
   public int? Fentanyl
   {
            get
            {
                var dosageQuantity = _bundle.SearchMedicationStatements("http://someacme.no/fhir/MedicationStatement/")
                    ?.GetMedicationDosageQuantity("Fentanyl", "ug");
                //value is already a decimal? data type and must be parsed 
                if (int.TryParse(dosageQuantity?.Value?.ToString(), out var dosageQuantityParsed))
                {
                    return dosageQuantityParsed;
                }
                return null;
            }
    }
 

We have these two extension methods to help us with retrieving the data :
 
      
        public static List<MedicationStatement>? SearchMedicationStatements(this Bundle bundle, string resourcePath)
        {
            var medicationStatementsMatching = bundle?.Entry?.Where(e => e.FullUrl.StartsWith(resourcePath))?.Select(m => m.Resource)?.OfType<MedicationStatement>()?.ToList();
            return medicationStatementsMatching;
        }

        public static Dosage? GetMedicationDosageDosage(this List<MedicationStatement> medicationStatements, string displayText)
        {
            //find dosage with given display text 

            foreach (var medicationStatement in medicationStatements)
            {
                var medicationStatementMedication = medicationStatement?.Medication as CodeableConcept; 
                if (medicationStatementMedication == null)
                {
                    continue; 
                }
                var medicationCoding = medicationStatementMedication?.Coding?.FirstOrDefault(med => med.Display?.Equals(displayText, StringComparison.InvariantCultureIgnoreCase) == true);  
                if (medicationCoding != null)
                {
                    var quantity = medicationStatement?.Dosage?.FirstOrDefault();
                    return quantity; 
                }           
            }

            return null;
        }
 
 
And our unit test will then be very simply with some static Fhir bundle data like this :
 
 
    [TestFixture]
    public class SomeAcmeManagerTests
    {
        private Bundle? _bundle = new();
        private SomeAcmeColoscopyDomainModel? _domainModel;

        [SetUp]
        public void TestInitialize()
        {
            _bundle = new FhirJsonParser().Parse<Bundle>(File.ReadAllText(@"TestData/JSON_someacme.json"));
            _domainModel = new SomeAcmeColoscopyDomainModel(_bundle, _metadataVersionId);
        }
 
        [Test]
        public void Map_Fhir_Bundle_To_Property_Fentanyl()
        {
            _domainModel?.Fentanyl.Should().Be(2);
        }
 
Now, we have used some classes here as you have seen called FhirJsonParser and MedicationStatements. These classes and functionality is available in some selected nuget packages :
 
  <PackageReference Include="Hl7.Fhir.R4" Version="4.0.0" />
  <PackageReference Include="Hl7.Fhir.Serialization" Version="4.0.0" />
  <PackageReference Include="Hl7.Fhir.Support" Version="4.0.0" />
  <PackageReference Include="Hl7.Fhir.Support.Poco" Version="4.0.0" />
 
This packages are licensed under BSD-3 license and are 'free' as long as you include the copyright notice. See this url for more info - it is the HL7 FHIR SDK for Microsoft .net platform. As you see we have additional nuget packages for (de)serialization and Poco objects. We also have common interfaces and classes in the HL7.Fhir.Support and HL7.Fhir.Poco nuget packages. This makes it way easier to work with a large bundle of Fhir data. Now, about sample data - this is often given in a .json file by the for example another organization, that you want to integrate with. FHIR is about interoperability and a common understanding of different health information systems via a common standard. The sample data starts with these data at the top - a bundle and a diagnosticreport. Of course, FHIR is a very large standard and which data you work against will vary a lot.
 
 {
  "resourceType": "Bundle",
  "meta": {
    "profile": [
      "http://someacmeregistries.no/fhir/StructureDefinition/colonoscopyreport-bundle-someacme"
    ]
  },
  "identifier": {
    "system": "http://someacmeotherorg.no/fhir/NamingSystem/colonoscopy-report-id",
    "value": "IdPlaceholder"
  },
  "type": "collection",
  "timestamp": "2022-05-10T10:26:40.6221425+02:00",
  "entry": [
    {
      "fullUrl": "http://somethirdacme.no/fhir/DiagnosticReport/c83b2f53-2d01-46e7-aed4-703396d5433f",
      "resource": {
        "resourceType": "DiagnosticReport",
        "meta": {
          "profile": [
            "http://someacmeregistries.no/fhir/StructureDefinition/colonoscopyreport-diagnosticreport-gastronet"
          ]
        },
        "status": "final",
        "code": {
          "coding": [
            {
              "system": "http://snomed.info/sct",
              "code": "73761001",
              "display": "Koloskopi"
            }
          ]
        },
        "subject": {
          "reference": "http://someacme.no/fhir/Patient/84c51e6c-020c-469b-b6c8-a6e9b2db6ff6"
        },
 
 
As you can see we also have something called Snomed SCT codes in our data. The meaning of these codes can be looked up online. International edition is here: https://browser.ihtsdotools.org/? Our medication drug statment for Fentanyl is defined further into the FHIR json bundle.


    {
      "fullUrl": "http://someacme.no/fhir/MedicationStatement/ae48d3bf-c289-4290-b7ed-78ef7bb6f1b5",
      "resource": {
        "resourceType": "MedicationStatement",
        "meta": {
          "profile": [
            "http://someame.no/fhir/StructureDefinition/colonoscopyreport-medicationstatement-gastronet"
          ]
        },
        "partOf": [
          {
            "reference": "http://someacme.no/fhir/Procedure/14f9f2e3-0d53-46c9-936f-3f0f85dd8cce"
          }
        ],
        "status": "active",
        "medicationCodeableConcept": {
          "coding": [
            {
              "system": "http://snomed.info/sct",
              "code": "373492002",
              "display": "Fentanyl"
            }
          ]
        },
        "subject": {
          "reference": "http://someacme.no/fhir/Patient/84c51e6c-020c-469b-b6c8-a6e9b2db6ff6"
        },
        "dosage": [
          {
            "doseAndRate": [
              {
                "doseQuantity": {
                  "value": 2,
                  "unit": "ug",
                  "system": "http://unitsofmeasure.org",
                  "code": "ug"
                }
              }
            ]
          }
        ]
      }
    },



As you can see, a FHIR json bundle will be quite lengthy and my sample file, which is a sample diagnostic report for coloscopy is right above 1000 lines and 33 kilobytes. To retrieve the medication dosage we need to go deep into the FHIR json structure sometimes and sometimes look at sibling nodes or further down. What helped me a lot while creating the extension methods I will mention next, was debugging and looking into the immediate window and inspect which kind of Entry it is. An Entry is a generic term which describes that our component is a general term in FHIR which can be many different types, such as a MedicationStatment and contain a Codable concept which ultimately will contain the dose and quantity of our medication (drug). What I did was the following to make working with the FHIR bundle in a more code friendly manner :

GENERIC APPROACH - Implement a mapping from a FHIR bundle into a DOMAIN MODEL which then can be used in your SYSTEM

  • Use the debugger and unit tests and explore in the immediate window which kind of Entry each component in the FHIR bundle is. Identity if we can cast an Entry into a correct subtype, such as a MedicationStatement. These POCO objects are available in the noted nuget packages above.
  • After finding a way to retrieve the data - generalize the retrival into extension methods, which can be chained and then make use of these extension methods into property getter logic.
  • Use a TDD approach to retrieve the data. Each property was found in the sample doc for me (about 100 properties) and I was trying to find generic ways to find these property values
  • Sometimes you need fine tuned logic too to find data. FHIR contains some extensions and different FHIR bundles, although it is a standard, may vary some.
Okay, here is all the extension methods I made for this case. I have masked the real organization names here. My approach can be used in many different scenarios for retrieving FHIR bundle data.
 
 
 
using Hl7.Fhir.Model;
using static Hl7.Fhir.Model.Observation;

namespace SomeAcme.FhirFacade.SomeProduct.HelperMethods
{

    /// <summary>
    /// Helper methods for Fhir bundle. Generic use helper methods.  
    /// </summary>
    public static class FhirHelperExtensions
    {


        public static string? SearchForPractitioner(this Bundle bundle, string procedurePath, string functionRole)
        {
            var performer = bundle.SearchForProcedure(procedurePath)?
                .Performer?.FirstOrDefault(p => p?.Function?.Coding?.FirstOrDefault()?.Display?.Equals(functionRole, StringComparison.InvariantCultureIgnoreCase) == true);
            return performer?.Function?.Coding?.FirstOrDefault()?.Code;
        }

        /// <summary>
        /// Looks up an identifier value (e.g. hpr number or similar) of practitioner
        /// </summary>
        /// <param name="practitioner"></param>
        /// <returns></returns>
        public static string? GetPractitionerIdentifierValue(this Practitioner practitioner)
        {
            return practitioner?.Identifier?.FirstOrDefault()?.Value; 
        }

        public static Organization? SearchForOrganization(this Bundle bundle, string resourcePath, bool startsWith = true)
        {
            if (startsWith)
            {
                var organizationMatching = bundle?.Entry.FirstOrDefault(e => e.FullUrl.StartsWith(resourcePath))?.Resource as Organization;
                return organizationMatching;
            }

            var organization = bundle?.FindEntry(resourcePath).FirstOrDefault()?.Resource as Organization;
            return organization; 
        }

        public static MedicationStatement? SearchMedicationStatement(this Bundle bundle, string resourcePath, bool startsWith = true)
        {
            if (startsWith)
            {
                var medicationStatementMatching = bundle?.Entry.FirstOrDefault(e => e.FullUrl.StartsWith(resourcePath))?.Resource as MedicationStatement;
                return medicationStatementMatching;
            }

            var medicationStatement = bundle?.FindEntry(resourcePath).FirstOrDefault()?.Resource as MedicationStatement;
            return medicationStatement;
        }

        public static List<MedicationStatement>? SearchMedicationStatements(this Bundle bundle, string resourcePath)
        {
            var medicationStatementsMatching = bundle?.Entry?.Where(e => e.FullUrl.StartsWith(resourcePath))?.Select(m => m.Resource)?.OfType<MedicationStatement>()?.ToList();
            return medicationStatementsMatching;
        }

        public static Dosage? GetMedicationDosageDosage(this List<MedicationStatement> medicationStatements, string displayText)
        {
            //find dosage with given display text 

            foreach (var medicationStatement in medicationStatements)
            {
                var medicationStatementMedication = medicationStatement?.Medication as CodeableConcept; 
                if (medicationStatementMedication == null)
                {
                    continue; 
                }
                var medicationCoding = medicationStatementMedication?.Coding?.FirstOrDefault(med => med.Display?.Equals(displayText, StringComparison.InvariantCultureIgnoreCase) == true);  
                if (medicationCoding != null)
                {
                    var quantity = medicationStatement?.Dosage?.FirstOrDefault();
                    return quantity; 
                }           
            }

            return null;
        }

        public static Quantity? GetMedicationDosageQuantity(this List<MedicationStatement> medicationStatements, string displayText, string? expectedUnitname = null)
        {
            //find quantity for dosage with given display text 

            foreach (var medicationStatement in medicationStatements)
            {
                var medicationStatementMedication = medicationStatement?.Medication as CodeableConcept;
                if (medicationStatementMedication == null)
                {
                    continue;
                }
                var medicationCoding = medicationStatementMedication?.Coding?.FirstOrDefault(med => med.Display?.Equals(displayText, StringComparison.InvariantCultureIgnoreCase) == true);
                if (medicationCoding != null)
                {
                    if (medicationStatement?.Dosage?.FirstOrDefault()?.DoseAndRate?.FirstOrDefault()?.Dose is Quantity quantity)
                    {
                        if (!string.IsNullOrWhiteSpace(expectedUnitname) && expectedUnitname.Equals(expectedUnitname, StringComparison.InvariantCultureIgnoreCase))
                        {
                            return quantity; 
                        }
                        return null; //found the right dosage - but the unit name does not agree 

                    }
                }
            }
            return null; 
        }


        public static string? GetOrganizationIdentifierValue(this Organization organization)
        {
            return organization?.Identifier?.FirstOrDefault()?.Value;   
        }

        public static Observation? SearchForObservation(this Bundle bundle, string resourcePath, bool startsWith = true)
        {
            if (startsWith)
            {
                var observationMatching = bundle?.Entry?.FirstOrDefault(e => e.FullUrl.StartsWith(resourcePath))?.Resource as Observation;
                return observationMatching;
            }

            var observation = bundle?.FindEntry(resourcePath).FirstOrDefault()?.Resource as Observation;
            return observation;
        }
     
        public static ComponentComponent? GetObservationComponent(this Observation observation, string observationComponentDisplayText)
        {
            //TODO : these observations is not the same as the observation in SomeProduct and must be additionally mapped (enums does not agree) 
            foreach (var observationComponent in observation.Component)
            {
                if (observationComponent?.Code?.Coding?.Any() != true)
                {
                    continue;
                }
                foreach (var observationEntry in observationComponent.Code.Coding)
                {
                    if (observationEntry?.Display.Contains(observationComponentDisplayText, StringComparison.InvariantCultureIgnoreCase) == true)
                    {
                        return observationComponent;
                    }
                }
            }
            return null;
        }

        public static string? GetObservationComponentCodeValue(this Observation observation, string observationComponentDisplayText)
        {
            //TODO : these observations is not the same as the observation in Gastronet and must be additionally mapped (enums does not agree) 
            foreach (var observationComponent in observation.Component)
            {
                if (observationComponent?.Code?.Coding?.Any() != true)
                {
                    continue; 
                }
                foreach (var observationEntry in observationComponent.Code.Coding)
                {
                    if (observationEntry?.Display.Contains(observationComponentDisplayText, StringComparison.InvariantCultureIgnoreCase) == true)
                    {
                        return observationEntry?.Code; 
                    }
                }                
            }

            return null; 
        }

        public static Patient? SearchForPatient(this Bundle bundle, string resourcePath, bool startsWith = true)
        {
            if (startsWith)
            {
                var patientMatching = bundle?.Entry?.FirstOrDefault(e => e.FullUrl.StartsWith(resourcePath))?.Resource as Patient;
                return patientMatching;
            }
            var patient = bundle?.FindEntry(resourcePath).FirstOrDefault()?.Resource as Patient;
            return patient;
        }

        public static CodeableConcept? SearchForProcedureReason(this Procedure procedure, string code)
        {
            return procedure?.ReasonCode?.FirstOrDefault(c => c?.Coding?.FirstOrDefault()?.Code?.Equals(code, StringComparison.InvariantCultureIgnoreCase) == true);
        }

        public static CodeableConcept? SearchForProcedureReasonViaDisplay(this Procedure procedure, string display)
        {
            return procedure?.ReasonCode?.FirstOrDefault(c => c?.Coding?.FirstOrDefault()?.Display?.Equals(display, StringComparison.InvariantCultureIgnoreCase) == true);
        }

        public static Procedure? SearchForProcedure(this Bundle bundle, string resourcePath, bool startsWithMatching = true)
        {
            if (startsWithMatching)
            {
                var procedureMatching = bundle?.Entry.FirstOrDefault(e => e.FullUrl.StartsWith(resourcePath))?.Resource as Procedure;
                return procedureMatching;
            }
            var procedure = bundle?.FindEntry(resourcePath).FirstOrDefault()?.Resource as Procedure;
            return procedure;
        }

        public static bool SearchForProcedureComplication(this Bundle bundle, string resourcePath, string displayText)
        {
            var procedure = SearchForProcedure(bundle, resourcePath); 
            if (procedure?.Complication?.Any() == true)
            {
                var complications = procedure.Complication.ToList();
                var complicationMatching = complications.FirstOrDefault(x => x.Coding?.FirstOrDefault()?.Display?.ToLower() == displayText);
                //TODO : consider complicaitonCode here or just mere precense ? Ask Kreftreg ? string complicationCode = complicationMatching?.Coding?.FirstOrDefault()?.Code;
                //Other developer confirmed checking mere precense is okay. going for this then.
                return complicationMatching != null;
            }
            return false; 
        }

        public static Quantity? GetObservationQuantity(this Observation observation)
        {
            var quantity = observation?.Value as Quantity;
            return quantity;             
        }

        public static DiagnosticReport? SearchForDiagnosis(this Bundle bundle, string resourcePath, bool startsWithMatching = true)
        {
            if (startsWithMatching)
            {
                var diagnosisMatching = bundle?.Entry.FirstOrDefault(e => e.FullUrl.StartsWith(resourcePath))?.Resource as DiagnosticReport;
                return diagnosisMatching;

            }
            var diagnosticReport = bundle?.FindEntry(resourcePath).FirstOrDefault()?.Resource as DiagnosticReport;
            return diagnosticReport;
        }

        public static string? GetDiagnosticProcedureCode(this DiagnosticReport diagnosticReport, int nthProcedureCode)
        {
            var codes = diagnosticReport?.Code?.Coding?.ToList();
            if (codes == null || !codes.Any() || codes.Count < nthProcedureCode+1)
            {
                return null;
            }
            var coding = codes.ElementAt(nthProcedureCode) as Coding;
            return coding.Display;
        }

        public static string? GetDiagnosisCode(this DiagnosticReport diagnostic, int position)
        {
            var diagnoses = diagnostic?.ConclusionCode?.ToList();
            if (diagnoses == null)
            {
                return null; 
            }
            if (diagnoses?.Count -1 >= position)
            {
                try
                {
                    return $"{diagnoses![position]?.Coding?.First().Code} {diagnoses[position]?.Coding.First().Display}";
                }
                catch { return null; }
            }
            return null;         
        }

        public static Extension? SearchForExtensionInsideProcedure(this Procedure procedure, string extensionUrl)
        {
            var extension = procedure?.Extension?.FirstOrDefault(e => e?.Url == extensionUrl) as Extension;
            return extension; 
        }

        public static List<Extension>? SearchForExtensionsInsideProcedure(this Procedure procedure, string extensionUrl)
        {
            var extensions = procedure?.Extension?.Where(e => e?.Url == extensionUrl)?.ToList();
            return extensions;
        }

        public static CodeableConcept? GetCodeableConceptInsideExtension(this Extension extension)
        {
            if (extension?.Value == null)
            {
                return null; 
            }
            var codeableConcept = extension?.Value as CodeableConcept;
            return codeableConcept;            
        }

        public static Extension? SearchForSubExtensionInsideExtension(this Extension extension, string extensionUrl)
        {
            var subExtension = extension?.Extension?.FirstOrDefault(e => e?.Url == extensionUrl);
            return subExtension;
        }

        public static Duration? GetExtensionDuration(this Extension extension)
        {
            var duration = extension?.Value as Duration;
            return duration; 
        }

        public static FhirBoolean? GetExtensionBoolean(this Extension extension)
        {
            return extension?.Value as FhirBoolean;
        }

        public static Coding? GetExtensionCodeValue(this Extension extension, string url, string system)
        {
            var subExtension = extension?.Extension?.FirstOrDefault(e => e?.Url == url) as Extension;
            var codeContainer = subExtension?.Value as CodeableConcept;
            return codeContainer?.Coding?.FirstOrDefault(c => c.System == system); 
        }      

        public static string? GetPatientIdentifier(this Patient patient)
        {
            return patient?.Identifier?.FirstOrDefault()?.Value;
        }


        public static string GetPatientName(this Patient patient)
        {

            var firstName = (patient?.Name?.FirstOrDefault())?.Given?.FirstOrDefault();
            var lastName = patient?.Name?.FirstOrDefault()?.Family;
            var middleName = (patient?.Name?.FirstOrDefault()?.Extension?.FirstOrDefault())?.Value?.ToString();
            return $"{firstName}{(!string.IsNullOrWhiteSpace(middleName) ? " " + middleName + " " : " ")}{lastName}";

        }

    }


}

 
 

I hope you found this article helpful in case you need to extract data from a FHIR bundle document. I am not specializing into working with FHIR, I just worked 1-2 weeks on such a FHIR bundle document and found my approach to maybe be of general interest and use. At least I found my approach
scalable for mapping each fields. Also note that I made a domain model as a model object where I put logic into the getters of a property and this corresponds to a field in the FHIR JSON bundle we want to retrieve. So I would then repeat the approach by extending the list of steps to successfully map a FHIR bundle into a domain model which THEN can be used as a better prepared model for INPUT to YOUR system. i.e. we go via a domain model that then can be input to your system where mapping will be trivial inside your system, e.g. save a domain model field into a database or similar if you want to input a FHIR json bundle and create a new POCO entity into your system and store it to a database.

GENERIC APPROACH - Implement a mapping from a FHIR bundle into a DOMAIN MODEL which then can be used in your SYSTEM

  • Use the debugger and unit tests and explore in the immediate window which kind of Entry each component in the FHIR bundle is. Identity if we can cast an Entry into a correct subtype, such as a MedicationStatement. These POCO objects are available in the noted nuget packages above.
  • After finding a way to retrieve the data - generalize the retrival into extension methods, which can be chained and then make use of these extension methods into property getter logic. These properties reside in the DOMAIN MODEL object. For example ColoscopyDomainModel .
  • Use a TDD approach to retrieve the data. Each property was found in the sample doc for me (about 100 properties) and I was trying to find generic ways to find these property values
  • Sometimes you need fine tuned logic too to find data. FHIR contains some extensions and different FHIR bundles, although it is a standard, may vary some.
  • + REMEMBER - then utilize the DOMAIN MODEL which is mapped into YOUR SYSTEM and then save the fields to a database or other storage for example. Or maybe you only want to use the domain model as is without it doing anything else than represent its data from the FHIR bundle.
Of course you will need some more infrastructure around handling FHIR documents, such as a REST API for example, but this article focused on FHIR bundle parsing. And finally, FHIR supports also the formats XML and RDF. The web site of FHIR can explain more, if you want to delve into details. I found it most helpful just to get started coding here with the GENERIC APPROACH mentioned above. HL 7 FHIR web site: http://hl7.org/fhir/

Saturday 28 May 2022

Using expression trees to build up loops - Gauss Summation

I tested out Expression trees today in more depth and played around with Gauss summation. The Gauss summation is a well-known theorem in Calculus. It states that for a plain arithmetic sequence of numbers with a distance of 1 (i.e. 1,2,3,4,5,6...) from 1..n the sum of these numbers are equal to the formula : Sum(n) = (n*(n+1)) / 2 Johan Karl Friendrich Gauss is renowned to have come up with this as a very young student when a school teacher asked the class to sum the numbers from 1 to 100 and give him the answer. Gauss almost instantly replied '5050', which was also the correct answer. This may or may not have been the case. The formula itself can anyways be theorized by summing the largest and
smallest number and then approaching the middle of the sequence. You can add 1 and 100 to get 101, 2 and 99 to get 101 and so on. The sum is always 101 (n+1) and there are a hundred such 'pairs' (n). But we want to only sum the numbers once, so we divide by 2 => we have the Gauss summation formula ! Let's look at how to do such a summation using Expression trees in C#. But I have only created a loop algorithm here, we calculate the same answer but we instead use expression trees. In demonstrates how
we can get started with expression trees in C# using loops (it is while loop which is created here) and parameter expressions and other components, such as 'labels' used in 'gotos'. This is actually needed in
expression trees to get the looping and breaking to work. The 'SumRange' method looks like this :

public static Expression SumRange(ParameterExpression value)
{
    LabelTarget label = Expression.Label(typeof(int));

    ParameterExpression result = Expression.Variable(typeof(int), "result");
    var initializeResult = Expression.Assign(result, Expression.Constant(0));

    var innerLogicBlock = Expression.Block(
        Expression.Assign(result,
            Expression.Add(result, value)),
        Expression.PostDecrementAssign(value)
    );

    BlockExpression body = Expression.Block(
       new[] { result },
       initializeResult,
       Expression.Loop(
           Expression.IfThenElse(
            Expression.GreaterThanOrEqual(value, Expression.Constant(1)),
            innerLogicBlock,
            Expression.Break(label, result)
            ),
            label
         )
    );
    return body;
}

We pass in a parameter expression. We then declare a 'label' which is used in a 'goto' execution flow when we want to break out of our loop, created by Expression.Loop. The initializeResult is listed here inside Expression block as we want to assign the result variable to the initial value (the expression constant '0'). We then have an 'outer logic' where we have a If-Then-Else condition where we check if value is greater than or equal to 1 and then
we perform the 'inner logicl block' assigned earlier, where we assign result to itself and the value variable passed in as a parameterexpression to this method. Note, we will do some type checking via Expression.Lambda which call this SumRange method explained further below. Note the use of 'PostDecrementAssign' expression which decrements the 'value' and ensures we can exit out of the loop. It can be of course hard to follow along such expression trees without some tooling. I use the ReadableExpressions:Visualizer plugin for VS 2022 here : https://marketplace.visualstudio.com/items?itemName=vs-publisher-1232914.ReadableExpressionsVisualizers You can use it to preview expressions as shown in the below screen shot :
And our unit test passes with expected result.
 

        [Fact]
        public void SumRange()
        {
            var value = Expression.Parameter(typeof(int));
            var result = ScriptingEngine.SumRange(value);
            var expr = Expression.Lambda<Func<int, int>>(result, value);
            var func = expr.Compile(); 
             Assert.Equal(5050, func(100)); 
        }

 
As you can see, even for a simple method, we need to type a lot of code to build up an expression tree. There are helper libraries such as AgileObjects.ReadableExpressions and System.Linq.Dynamic.Core which
can help out a lot when using expression trees. Add these to your package references (.csproj) for example :

  <ItemGroup>
    <PackageReference Include="AgileObjects.ReadableExpressions" Version="3.3.0" />
    <PackageReference Include="System.Linq.Dynamic.Core" Version="1.2.18" />
  </ItemGroup>

The first package of these got a handy method ToReadableString and the last one got a handy helper class called DynamicExpressionParser, which in tandem can create pretty complex expression trees. When will you use such logic ? Most often when wanting to build up custom logic query filters. You should not allow end users to arbitrarily build up all kinds of query filters, but may offer them a set of user controls to build and combine query filters so they can retrieve data via rather complex rules. The code libs mentioned here is supported in .NET Framework 3.5 or later (and .NET Standard 1.0), so most target frameworks are supported then.

Get properties of a given type in C#

This article shows how we can find all properties with a given property type. The provided code also can find private properties or look for nullable of the property type. E.g. find all DateTime properties and also include all properties which are Nullable of DateTime, Nullable. An extension method for this looks like the following (put the method into a static class as it is an extension method) :
  

  
          /// <summary>
        /// Retrieves a list of properties (property info) with given type <paramref name="propertyType"/> in a nested object
        /// </summary>
        /// <param name="rootObject"></param>
        /// <param name="propertyType"></param>
        /// <param name="includePrivateProperties">If set to true, includes private properties</param>
        /// <param name="includeNullableVariant">If set to true, return also propertie which are the nullable variant of the <paramref name="propertyType"/>.</param>
        /// <returns>A list of properties with given <paramref name="propertyType"/>, possibly including also non-nullable variant of the type and both public and private properties set with the parameters <paramref name="includePrivateProperties"/> and <paramref name="includeNullableVariant"/></returns>
        public static IEnumerable<PropertyInfo> GetPropertiesOfType(this object rootObject, Type propertyType,
            bool includePrivateProperties = false, bool includeNullableVariant = false)
        {
            if (rootObject == null)
            {
                yield return null;
            }
            var bindingFlagsFilter = !includePrivateProperties ? BindingFlags.Public | BindingFlags.Instance : BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
            var propertiesOfType = rootObject.GetType().GetProperties(bindingFlagsFilter)
                .Where(p => p.PropertyType == propertyType || (includeNullableVariant && propertyType == Nullable.GetUnderlyingType(p.PropertyType)))
                .ToList();
            foreach (var prop in propertiesOfType)
            {
                yield return prop;
            }
            var nestableProperties = rootObject.GetType().GetProperties(bindingFlagsFilter)
              .Where(p => p.PropertyType.IsClass && p.PropertyType != typeof(string))
              .ToList(); //ignoring properties of type strings as they are not nested, though a class
            foreach (var prop in nestableProperties)
            {
                if (prop.GetIndexParameters().Length > 0)
                {
                    continue; //skip indexer properties 
                }
                var rootObjectLevel = prop.GetValue(rootObject, null);
                if (rootObjectLevel == null)
                {
                    continue;
                }
                foreach (var propertyAtLevel in GetPropertiesOfType(rootObjectLevel, propertyType, includePrivateProperties, includeNullableVariant))
                {
                    yield return propertyAtLevel;
                }
            }
        }

We find the properties matching the property type and then recursively fetch such properties at nested levels too if the property is a class and therefore can contain sub properties. We end up with all the properties of a given type. As we see, we adjust the binding flags to include private properties too or not. And we use the Nullable.GetUnderlyingType method to match the underlying type in case we want to look for DateTime and DateTime? properties. This method is fairly fast, in the order of a few milliseconds (1-5 when I tested for an ordinary two level nested object.) But we are using reflection here and the method could be faster if we made use of some other techniques, perhaps with
IL 'magic'. I have not found a way to do this yet though.. Here is another utility method (extension method) for finding 'property paths'. This is handly if you want to craft an SQL select statement for example as we need the fully qualified path perhaps if our tooling creates fields in the database similar to POCO object
and the nested object is similar to table structure. Maybe your nested properties are mapped to SomeInnerTable1_SomeField1 and so on. Anyways, it is handly to have 'property paths' to the properties to get a fast overview of where the properties are located in the nested structure of your (possibly complex) object.



         /// <summary>
        /// This method looks for properties of given type in a nested object (e.g. a form data contract) 
        /// </summary>
        /// <param name="rootObject"></param>
        /// <param name="propertyType"></param>
        /// <returns></returns>
        private IEnumerable<string> GetPropertyPathsForType(object rootObject, Type propertyType, string prefixAtLevel = "")
        {
            if (rootObject == null)
            {
                yield return string.Empty;
            }
            var propertiesOfType = rootObject.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
                .Where(p => p.PropertyType == propertyType)
                .ToList();

            foreach (var prop in propertiesOfType)
            {
                if (string.IsNullOrWhiteSpace(prefixAtLevel))
                {
                    yield return prop.Name; //root properties have no prefix 
                }
                else
                {
                    yield return prefixAtLevel.TrimStart('.') + "." + prop.Name;
                }
            }

            var nestableProperties = rootObject.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
              .Where(p => p.PropertyType.IsClass && p.PropertyType != typeof(string))
              .ToList(); //ignoring properties of type strings as they are not nested, though a class

            foreach (var prop in nestableProperties)
            {
                if (prop.GetIndexParameters().Length > 0)
                {
                    continue; //skip indexer properties - this is identified as required 
                }
                var rootObjectLevel = prop.GetValue(rootObject, null);
                if (rootObjectLevel == null)
                {
                    continue;
                }
                foreach (var propertyAtLevel in GetPropertyPathsForType(rootObjectLevel, propertyType, prefixAtLevel + "." + prop.Name))
                {
                    yield return propertyAtLevel.TrimStart('.').TrimEnd('.');
                }
            }
        }


The code above could use the first method more to support including public and private properties, but I leave it out 'as an exercise to the reader' as text books often states. So this code is very handy if you for example at work need to find 'all the datetime properties in a domain object' and similar cases. Maybe you want to deny all datetimes have a future datetime in case it is a report for a patient treatment report being performed yesterday and so on, and for
that particular model, there will be no future date time values.