private static void UseParseOnlyToConfirmWeCanProcessTheMessage(string msgText) { Console.WriteLine("Parse Only"); Console.WriteLine("----------"); Console.WriteLine("Used when you don't want to proces the whole message, just check fields"); Console.WriteLine("Eg: Determine if the message type is supported by the application"); Console.WriteLine("OR For logging purposes where the system is simply storing the whole message"); string messageType = HL7Message.ParseOnly(msgText, "MSH.9"); if (!messageType.Substring(0, 3).Equals("ORU", StringComparison.CurrentCultureIgnoreCase)) { Console.WriteLine(); throw new FormatException($"Only processing ORU MessageType. Found message type {messageType}"); } }
static void Main(string[] args) { var msgText = @"MSH|^~\&|CERNER||PriorityHealth||20191020050600+1000||ORU^R01|Q479004375T431430612|P|2.3|" + "\r" + @"PID|||001677980~212323112^^^AUTH^MR^TESTING||SMITH^CURTIS||19680219|M||||||||||929645156318|123456789|" + "\r" + @"PD1||||1234567890^LAST^FIRST^M^^^^^NPI|" + "\r" + @"OBR|1|341856649^HNAM_ORDERID|000002006326002362|648088^Basic Metabolic Panel|||20061122151600|||||||||1620^Hooker^Robert^L||||||20061122154733|||F|||||||||||20061122140000|" + "\r" + @"OBX|1|NM|GLU^Glucose Lvl||59|mg/dL|65-99^65^99|L|||F|||20061122154733|" + "\r" + @"OBX|2|NM|ALT^Alanine Aminotransferase||23|umol/L|2-20^65^1000|H|||F|||20061122154733|" + "\r" + @"OBX|3|NM|8893-0^Pulse rate^LN^78564009^^SCT|1.9.2.1|70|bpm^^ISO+||N|||F|||201706071449+1000" + "\r"; try { Console.WriteLine("How to use the HL7 Enumerator for Decoding HL7 Messages"); Console.WriteLine("-------------------------------------------------------\r\n"); // Dont want to process the whole message? Use the Parse Only Method to check fields before processing // Here is an example: UseParseOnlyToConfirmWeCanProcessTheMessage(msgText); Console.WriteLine("\r\nProcessing ORU Message " + HL7Message.ParseOnly(msgText, "MSH.10")); // Process the HL7 message by Creating a HL7 Message Object // NOTE: this will always succeed - an exception should never be thrown, so there is no need // to test for it. HL7Message mesg = msgText; // yes, it is that simple, but the method below implements some other syntax versions AlternateMethodsForCreatingAHL7MessageObject(msgText); // Extract a Single field - say for logging purposes // Use the Element property to extract ANY specific field/subfield OR Group of fields/subFields DisplayExtractingAnElementMessage(); Console.WriteLine($"Message received from Sending system {mesg.Element("MSH.3")}"); // Extract a Whole Segment Console.Write("\r\nOR a whole PD1 Segment :\r\n "); Console.WriteLine(mesg.Element("PD1")); // Extract a Repeating Field Console.WriteLine("\r\nOR a SET of repeating Fields: eg PID.3[]"); var c = 0; mesg.Element("PID.3[]").ForEach(id => Console.WriteLine($"Repitition {++c}: {id}")); // Use the ALLSegments property with LINQ to extract and manipulate any part/subpart of a message // as a Low Level HL7Element DisplayUsingLinq(); Console.WriteLine("Eg Test Names: "); var OBXTestNames = mesg.AllSegments("OBX").Select(o => o.Element("*.3.2")); // Note the use of the "*" wildcard - avoids needing to specify the Segment level. // Use Implicit String Casting to Output any Element as a string. Console.WriteLine($"\r\nThe message contains {OBXTestNames.Count()} Test Results:"); foreach (string testName in OBXTestNames) { Console.WriteLine($" {testName}"); } // Use LINQ on ANY element at any level to iterate over sub elements Console.Write("\r\nUse LINQ at the Element (field/component) Level to get information\r\nEg: "); var NonEmptyComponentsInPD1 = mesg.Element("PD1.4").Where(f => !string.IsNullOrWhiteSpace(f)); Console.WriteLine($"PID.3 has {(mesg.Element("PD1.4").Count)} components, and {NonEmptyComponentsInPD1.Count()} have data"); foreach (var component in NonEmptyComponentsInPD1) { Console.WriteLine($" {component}"); } // Use self defined Constants for field definitions to make your code cleaner. DisplayConstantsMessage(); const string PatientFamilyName = "PID.5.1"; const string PatientGivenName = "PID.5.2"; Console.WriteLine($"Patients Name is : {mesg.Element(PatientFamilyName)}, {mesg.Element(PatientGivenName)}."); DisplayDataTypesMessage(); // Use the "HL7Types" Namespace Helpers to safely get Numbered Field Values from components // Remember that Element FIELDS are ZERO based but in Segments the Segment Name is 0 so the FIELD Numbers // still correspond to the HL7 field numbers. const int fldSequence = 1; const int fldDataType = 2; const int fldTestName = 3; const int fldValue = 5; const int fldUnits = 6; const int fldRefRange = 7; const int fldAbFlag = 8; // BUT Components are numbered 0 to n (not 1 to n). const int cvTestName = 1; const int cvTestCode = 0; // Now use our defintions to extract using Data Type primitives ElementValue and IndexedElement Console.WriteLine("\r\nUse Safe Element Options to Output only the Numeric test Results:\r\n"); mesg.AllSegments("OBX") // Test Segments .Where(o => o.ElementValue(fldDataType).Equals("NM")) // numeric fields .Select(o => $"{o.ElementValue(fldSequence),2}. " + $"{o.IndexedElement(fldTestName).ElementValue(cvTestName),-30} " + // test name $"({o.IndexedElement(fldTestName).ElementValue(cvTestCode),-6}) " + // test code $"{o.IndexedElement(fldValue),6} " + // test value $"{o.IndexedElement(fldUnits).ElementValue(0),-6} " + // units $"({o.IndexedElement(fldRefRange).ElementValue(0),-6}) " + // Range first field $"{(o.ElementValue(fldAbFlag).Equals("N") ? "" : o.ElementValue(fldAbFlag))}" // Abflag ).ToList() .ForEach(testResult => Console.WriteLine($" {testResult}")); // Use the HL7 Data Type DisplayCommonDataTypes(); // Use the DataType Extensions for Strict Typing of known Data Types. Console.WriteLine("\r\nUse A Common Data type to Extract Type safe properties"); Console.WriteLine("Eg: PID.5 (Patient Name) is an XPN DataType (often repeating)"); var ptName = mesg.Element("PID.5").AsXPN(); // note, in this case it is ok AsXPN() because we know there is only 1 replicate. // for repeating fields, use AsXPNs() to return an IEnumerable of XPN Console.WriteLine($"Patients Name is : {ptName.FamilyName}, {ptName.GivenName}."); // Using a CX Data Type for repeating fields.. var ids = mesg.Element("PID.3[]"); var patientIds = ids.AsCXs(); // we know from the spec, that PID.3 is a replicating field of CX 's. The included AsCXs method is // a simple extension method returning a CX_CompositeId type (supporting the IHL7Type interface). // If the include CX datatype is not suitable for your purpose, it is very easy to make your own. // Just Implement the HL7Type interface using the low level methods. // // NOTE: if you ask for a simple CX for a Replicating field, you will get a DEBUG exception indicating // that you should use an Enumerable type for this field Console.WriteLine($"Found {patientIds?.Count()} Ids using DataType Assigment"); foreach (var cx in patientIds) { // remember that certain elements in a CX MAY be null. Console.WriteLine($"ID: {cx?.ID}"); Console.WriteLine($" IDType: {cx?.IdentifierTypeCode}"); Console.WriteLine($" Authority:{cx?.AssigningAuthority?.NamespaceId?.BestValue}"); Console.WriteLine($" Facility: {cx?.AssigningFacility?.NamespaceId?.BestValue}"); } /// One Alternative in the case where no type has been defined, /// you can use the low level elements OR Self Element references to extract the data Console.WriteLine($" Found {ids.Count} Ids Using Elements (low level)"); foreach (var id in ids) { if (id.Count > 0) { Console.WriteLine($"ID: {id[0]}"); } else { Console.WriteLine($"ID: {id}"); // implict string casting } if (id.Count > 4) { Console.WriteLine($" IDType:{id[4]}"); } if (id.Count > 3) { Console.WriteLine($" Issuer:{id[3]}"); } } /// HOWEVER - it is better to define your own Custom Type. /// See the Custom Data Types example in this repository. DisplayCustomDataTypesMessage(); // Data Type Support. DisplayHL7TablesMessage(); // Coded Values have 3 propertes. Value, CodedValue and Best Value. // Use the Property 'BestValue' in most cases for display as it returns either Value // or Coded Value depending on whether a table is attached and the value is known in the table. // When no table is defined for the Coded Value will always be empty var pt1IdTypeCode = patientIds.Skip(1).First().IdentifierTypeCode; Console.WriteLine("\r\nIdentifier Properties: with NO table attached."); Console.WriteLine($" Value (Unvalidated) : {pt1IdTypeCode.Value}"); Console.WriteLine($" CodedValue (Validated) : {pt1IdTypeCode.CodedValue}"); Console.WriteLine($" BestValue (Coded Or Value): {pt1IdTypeCode.BestValue}"); Console.WriteLine($" Description : {pt1IdTypeCode.Description}"); Console.WriteLine($" Notes : {pt1IdTypeCode.Notes?.First()}..."); // Attach manually created Table to CX we just read. // Manually Create a Data Table into In-Memory Provider Console.WriteLine("\r\nManually Create Tables to validate the properties"); var tables = ManuallyCreateTablesIntoAInMemoryProvider(); pt1IdTypeCode.Table = tables.GetCodeTable("0203"); Console.WriteLine("\r\nIdentifier Properties: with table 0203 attached."); Console.WriteLine($" Value (Unvalidated) : {pt1IdTypeCode.Value}"); Console.WriteLine($" CodedValue (Validated) : {pt1IdTypeCode.CodedValue}"); Console.WriteLine($" BestValue (Coded Or Value): {pt1IdTypeCode.BestValue}"); Console.WriteLine($" Description : {pt1IdTypeCode.Description}"); Console.WriteLine($" Notes : {pt1IdTypeCode.Notes.First()}..."); // Automatically Attach suitable tables to the Data type as we read Assign it. // Assuming we hav the required tables, each datatype consists of a set of // Identifiers where data might be coded. DisplayAutomaticDataTableAssignment(); // First Use a Table Provider as a source of tables. // The File Provider can access any table in a folder by TableID is very useful for this purpose var HL7TablesFolder = Path.Combine( Path.GetDirectoryName(Assembly.GetExecutingAssembly().GetName().CodeBase).Substring(6), "ExampleTables"); Console.WriteLine($"The process is: First, Get an instance of a table provider"); Console.WriteLine("Eg: The 'FolderDataTableProvider'"); var tablesFromFolder = new FolderDataTableProvider(HL7TablesFolder); Console.WriteLine($"Using tables in folder:{tablesFromFolder.Folder}"); Console.WriteLine("Next, Look at the DataType (eg CX) to determine the number of Coded Elements in total"); Console.WriteLine($"A CX data has {CX_CompositeId.TotalCodedFieldCount} Coded elements. I need a List of corresponding table names"); // You will need 1 table reference for each Coded Item, in exact order that they will be constructed. Console.WriteLine("Add Required Table names to a String List IN Constructor Order\r\n"); var tableIds = new List <string>(); tableIds.Add("0061"); // CX_CompositeId.CheckDigitScheme needs table 0061 (we dont have that, but include it anyway) tableIds.Add("0363"); // CX_CompositeId.AssigningAuthority.NamespaceId needs table 0363 tableIds.Add("0301"); // CX_CompositeId.AssigningAuthority.UniversalIDType needs table 0301 tableIds.Add("0203"); // CX_CompositeId.IdentifierTypeCode needs table 0203 tableIds.Add(""); // CX_CompositeId.AssigningFacility.NamespaceId needs some unknown table - leave it blank. tableIds.Add("0301"); // CX_CompositeId.AssigningFacility.UniversalIDType needs 0301 again (include it again) Console.WriteLine("Table Ids required for PID.5 (CX)"); tableIds.ForEach(t => Console.WriteLine($" {t}")); var validatedPatientIds = ids.AsCXs(tableIds, tablesFromFolder); c = 0; validatedPatientIds.ToList().ForEach(id => { Console.WriteLine($"\r\nID {++c} {id.ID}"); var bestValue = id.CheckDigitScheme?.BestValue; var isValid = (id.CheckDigitScheme == null) ? "" : (id.CheckDigitScheme.IsValid.Value) ? "Yes" : "No"; var description = id.CheckDigitScheme.Description; Console.WriteLine($" CheckDigitScheme : {bestValue,7} Valid: {isValid} {description}"); bestValue = id.AssigningAuthority?.NamespaceId.BestValue; isValid = (id.AssigningAuthority?.NamespaceId == null) ? "" : (id.AssigningAuthority.NamespaceId.IsValid.Value) ? "Yes" : "No"; description = id.AssigningAuthority?.NamespaceId.Description; Console.WriteLine($" AssigningAuthority.NamespaceId : {bestValue,7} Valid: {isValid} {description}"); bestValue = id.AssigningAuthority?.UniversalIdType.BestValue; isValid = (id.AssigningAuthority?.UniversalIdType == null) ? "" : (id.AssigningAuthority.UniversalIdType.IsValid.Value) ? "Yes" : "No"; description = id.AssigningAuthority?.UniversalIdType.Description; Console.WriteLine($" AssigningAuthority.UniversalIdType: {bestValue,7} Valid: {isValid} {description}"); bestValue = id.IdentifierTypeCode?.BestValue; isValid = (id.IdentifierTypeCode == null) ? "" : (id.IdentifierTypeCode.IsValid.Value) ? "Yes" : "No"; description = id.IdentifierTypeCode?.Description; Console.WriteLine($" IdentifierTypeCode : {bestValue,7} Valid: {isValid} {description}"); bestValue = id.AssigningFacility?.NamespaceId.BestValue; isValid = (id.AssigningFacility?.NamespaceId == null) ? "" : (id.AssigningFacility.NamespaceId.IsValid.Value) ? "Yes" : "No"; description = id.AssigningFacility?.NamespaceId.Description; Console.WriteLine($" AssigningFacility.NamespaceId : {bestValue,7} Valid: {isValid} {description}"); bestValue = id.AssigningFacility?.UniversalIdType?.BestValue; isValid = (id.AssigningFacility?.UniversalIdType == null) ? "" : (id.AssigningFacility.UniversalIdType.IsValid.Value) ? "Yes" : "No"; description = id.AssigningFacility?.UniversalIdType?.Description; Console.WriteLine($" AssigningFacility.UniversalIdType : {bestValue,7} Valid: {isValid} {description}"); } ); DisplayTimeAndNumericConversion(); var ptDOB = "PID.7"; var mesgTime = "MSH.7"; var dob = mesg.Element(ptDOB); var mesgTs = mesg.Element(mesgTime); DisplayAsDateTimeInformation(); Console.WriteLine($"DOB.AsDateTime() {mesg.Element(ptDOB)} does not have Timezone, so assume Local"); Console.WriteLine($" {dob.AsDateTime()} = {dob.AsDateTime().Value.ToString("o")}"); Console.WriteLine($"msgTs.AsDateTime() {mesg.Element(mesgTime)} has Time Timezone. Stored as UTC."); Console.WriteLine($" {mesgTs.AsDateTime()} = {mesgTs.AsDateTime().Value.ToString("o")}"); DisplayAsLocalDateTimeInformation(); Console.WriteLine($"DOB {mesg.Element(ptDOB)} No timezone, so display Current LOCAL"); Console.WriteLine($" {dob.AsLocalTime()} = {dob.AsLocalTime().Value.ToString("o")}"); Console.WriteLine($"msgTime {mesg.Element(mesgTime)} Has TimeZone Shows in Current local"); Console.WriteLine($" {mesgTs.AsLocalTime()} = {mesgTs.AsLocalTime().Value.ToString("o")}"); DisplayAsUTCDateTimeInformation(); Console.WriteLine($"DOB {mesg.Element(ptDOB)} No Timezone, So safe to assume local to you"); Console.WriteLine($" {dob.AsUTCTime()} = {dob.AsUTCTime().Value.ToString("o")}"); Console.WriteLine($"DOB {mesg.Element(ptDOB)} No Timezone, but can be forced to UTC."); Console.WriteLine($" {dob.AsDateTime(DateTimeStyles.AssumeUniversal).Value.ToString("o")}"); Console.WriteLine($"msgTime {mesg.Element(mesgTime)} is converted to UTC Time."); Console.WriteLine($" {mesgTs.AsUTCTime()} = {mesgTs.AsUTCTime().Value.ToString("o")}"); // NM_Numeric data Type supports implicity, explicit and has As<Type> Methods. DisplayNumericConversion(); const string ResultValue = "OBX[1].5"; // Examples: var result1 = mesg.Element(ResultValue).AsNM(); int nmAsInt = result1.AsInteger(); var nmAsFloat = result1.AsFloat(); double nmAsdouble = result1; var nmAsCurrency = (decimal)result1; Console.WriteLine($"Result 1 ToString() {result1}"); Console.WriteLine($"Result 1 {nmAsInt.GetType().Name} value {nmAsInt}"); Console.WriteLine($"Result 1 {nmAsFloat.GetType().Name} value {nmAsFloat:0.0}"); Console.WriteLine($"Result 1 {nmAsdouble.GetType().Name} value {nmAsdouble:0.00000000}"); Console.WriteLine($"Result 1 {nmAsCurrency.GetType().Name} value {nmAsCurrency:0.00}"); DisplayComposingMessages(); // Compose a Segment Console.WriteLine("\r\n Composing a Segment using DataTypes"); // Composed PID: PID||1234567^4^M11^ADT01^MR^University Hospital|1234567^4^M11^ADT01^MR^University Hospital~8003608833357361^^^AUSHIC^NI^ var patientIdentifiers = new List <CX_CompositeId>() { new CX_CompositeId() { ID = "1234567", CheckDigit = "4", CheckDigitScheme = "M11", AssigningAuthority = new HD_HierarchicDesignator() { NamespaceId = "ADT01" }, IdentifierTypeCode = "MR", AssigningFacility = new HD_HierarchicDesignator() { NamespaceId = "University Hospital" } }, new CX_CompositeId() { ID = "8003608833357361", AssigningAuthority = new HD_HierarchicDesignator() { NamespaceId = "AUSHIC" }, IdentifierTypeCode = "NI" } }; // Compose a PID var pid = new HL7Segment("PID"); pid.SetField(2, patientIdentifiers.First()); pid.SetField(3, patientIdentifiers.AsElement()); Console.WriteLine("\r\nComposed PID: " + pid); } catch (Exception e) { Console.WriteLine("Unexpected exception : {0}", e.Message); } Console.ReadLine(); }