HL7 Programming using NHAPI and .NET - Using Tersers
Introduction
This is part of my HL7 article series in which we will look at an alternate approach to extracting and updating HL7 message data using the NHAPI HL7 library for .NET. Before we get started on this tutorial, have a quick look at my earlier article titled “A Very Short Introduction to the HL7 2.x Standard”. One of my earlier tutorials in this series titled "HL7 Programming using .NET - A Short Tutorial" gave you a foundational understanding of HL7 2.x message transmission, reception and message acknowledgement. I then introduced you to a .NET library called "NHAPI" (a port of the original Java-based HAPI HL7 framework which I covered in my previous tutorials) and showed you how HL7 2.x messages can be easily created and transmitted using this library in conjunction with other tools through a couple of tutorials. We then looked at a special type of classes in the NHAPI framework known as "parsers" in my previous tutorial titled "HL7 Programming using NHAPI and .NET - Parsing HL7 Messages". These classes enable you to extract as well as update HL7 message data. In this tutorial, we will look at another class known as a "terser" which offers an alternate way to access HL7 message data. Let us get started.
Tools and Resources Needed
- .NET Framework 4.5 or higher
- A .NET IDE or Editor such as Visual Studio IDE or Visual Studio Code (even a text editor should suffice)
- NHAPI GitHub page is located here
- Install NHAPI Nuget Package using NuGet Package Manager
- (Optional) Download HAPI Test Panel from here
- (Optional) JDK 1.4 SDK or higher (this may be required to run HAPI Test Panel)
- You can also find all the code demonstrated in this tutorial on GitHub here
“A kind word is like a Spring day.” ~ Russian Proverb
NHAPI Tersers - An Overview
If you read my previous tutorial in this series titled “HL7 Programming using HAPI - Parsing Operations”, I explained in it that there were primarily two approaches for accessing as well as updating HL7 message data, namely via "Parsers" and "Tersers". We then reviewed what parsers were at a high level, and later explored some basic operations provided by the various HAPI parser classes in that tutorial. We will now look at a special class called Terser and the features it provides.
Being "terse" means being concise in the English language, and I suppose the authors of this library named these classes "Tersers" because they enable a concise way to access HL7 message data. In fact, tersers can be way more concise that parsers when retrieving as well as updating information and they use a Xpath-like syntax to refer to locations in the HL7 message. They eliminate the need for using the binding classes and the sometimes deeply nested methods to dig into the data hierarchy of a HL7 message. Tersers come in extremely handy when you are interested in retrieving only specific portions of a HL7 message, and you are not particularly interested in rest of the message data during your message processing workflow. Using the code below, I will demonstrate how we can access as well as update message data using tersers.
Some Basic Operations using Tersers
Before I demostrate the terser functionality, I will create a small helper class to wrap the getter and setter behaviors around a terser instance so it is a bit easier to understand the operations in our code examples that follow underneath. You don't need to use a wrapper class like this in your application, but if you find yourself using terser expressions all over your code, streamlining atleast the core data access methods into a single location should allow to add additional behavior such as logging and debugging a bit easier.
using System;
using NHapi.Base.Util;
namespace HapiTerserBasicOperations
{
public class OurTerserHelper
{
private readonly Terser _terser;
public OurTerserHelper(Terser terser)
{
if (terser == null)
throw new ArgumentNullException(nameof(terser),
"Terser object must be passed in for data retrieval operation");
_terser = terser;
}
public string GetData(string terserExpression)
{
if (string.IsNullOrEmpty(terserExpression))
throw new ArgumentNullException(nameof(terserExpression),
"Terser expression must be supplied for data retrieval operation");
return _terser.Get(terserExpression);
}
public void SetData(string terserExpression, string value)
{
if (string.IsNullOrEmpty(terserExpression))
throw new ArgumentNullException(nameof(terserExpression),
"Terser expression must be supplied for set operation");
if (value == null) //we will let an empty string still go through
throw new ArgumentNullException(nameof(value), "Value for set operation must be supplied");
_terser.Set(terserExpression, value);
}
}
}
Now that we have our helper class, let us look at some basic operations that use the Terser class. Notice in the code below that I instantiate a terser by wrapping it around a message object. The operations shown below demonstrate various operations such as retrieving field, component and subcomponent-level data from a HL7 message using various terser expressions. Also, please keep the test HL7 file FileWithObservationResultMessage.txt handy as you read through the code below to make sense of what is going on. I have shared this file in my GitHub repository.
using System;
using System.Diagnostics;
using System.IO;
using NHapi.Base.Parser;
using NHapi.Base.Util;
namespace HapiTerserBasicOperations
{
class HapiTerserBasicOperations
{
static void Main(string[] args)
{
try
{
// see my GitHub page for this file
var messageString = ReadHl7MessageFromFileAsString(
"C:\\HL7TestInputFiles\\FileWithObservationResultMessage.txt");
// instantiate a PipeParser, which handles the normal HL7 encoding
var ourPipeParser = new PipeParser();
// parse the message string into a Java message object
var orderResultsHl7Message = ourPipeParser.Parse(messageString);
// create a terser object instance by wrapping it around the message object
var terser = new Terser(orderResultsHl7Message);
// now, let us do various operations on the message
var terserHelper = new OurTerserHelper(terser);
var terserExpression = "MSH-6";
var dataRetrieved = terserHelper.GetData(terserExpression);
LogToDebugConsole($"Field 6 of MSH segment using expression '{terserExpression}' was '{dataRetrieved}'");
terserExpression = "/.PID-5-2"; // notice the /. to indicate relative position to root node
dataRetrieved = terserHelper.GetData(terserExpression);
LogToDebugConsole($"Field 5 and Component 2 of the PID segment using expression '{terserExpression}' was {dataRetrieved}'");
terserExpression = "/.*ID-5-2";
dataRetrieved = terserHelper.GetData(terserExpression);
LogToDebugConsole($"Field 5 and Component 2 of the PID segment using wildcard-based expression '{terserExpression}' was '{dataRetrieved}'");
terserExpression = "/.P?D-5-2";
dataRetrieved = terserHelper.GetData(terserExpression);
LogToDebugConsole($"Field 5 and Component 2 of the PID segment using another wildcard-based expression '{terserExpression}' was '{dataRetrieved}'");
terserExpression = "/.PV1-9(1)-1"; // note: field repetitions are zero-indexed
dataRetrieved = terserHelper.GetData(terserExpression);
LogToDebugConsole($"2nd repetition of Field 9 and Component 1 for it in the PV1 segment using expression '{terserExpression}' was '{dataRetrieved}'");
}
catch (Exception e)
{
//in real-life, do something about this exception
LogToDebugConsole($"Error occured while creating HL7 message {e.Message}");
}
}
public static string ReadHl7MessageFromFileAsString(string fileName)
{
return File.ReadAllText(fileName);
}
private static void LogToDebugConsole(string informationToLog)
{
Debug.WriteLine(informationToLog);
}
}
}
The console output from running our program is shown below. The terser expressions shown here should be easy to follow if you are familiar with other languages such X-Path which enable you to navigate through a object hierarchy. Let us look at some more advanced operations using tersers next.
Field 6 of MSH segment using expression 'MSH-6' was 'ReceivingFac'
Field 5 and Component 2 of the PID segment using expression '/.PID-5-2' was BEBE'
Field 5 and Component 2 of the PID segment using wildcard-based expression '/.*ID-5-2' was 'BEBE'
Field 5 and Component 2 of the PID segment using another wildcard-based expression '/.P?D-5-2' was 'BEBE'
2nd repetition of Field 9 and Component 1 for it in the PV1 segment using expression '/.PV1-9(1)-1' was '02807'
“You are the sun and the rain, the water and the plants, the birds and the animals. There is no such thing as ‘nature,’ apart from you and me. You are nature, I am nature, just as you are me and I am you.” ~ John Lundin
More Advanced Operations using Tersers
In my opinion, tersers really shine when dealing with deeply nested HL7 messages such as orders and lab results. They are especially useful when needing to deal with HL7 segment groups both for get and set operations on the data in these messages. In HL7, a segment group is a collection of segments that always appear together in sequence. Not all message types contain segment groups. Even when they do, the segment groups can be optional, conditional and/or repeating. Some message types may even have multiple repeating groups of the same segment nested under different segments. See diagram below for an example of a HL7 vaccine update message where I have pointed out repeating OBX segment groups. This makes the process of parsing message data very complex especially if you are writing a custom parser from scratch. However, tersers in HAPI come to our rescue in these situations. They use expression syntaxes that follow the same object model names that define the segment groups in the message definition such as "PATIENT", "ORDER", "OBSERVATION", etc enabling you to traverse the segment hierarchy relatively easily. Tersers also allow us to set data easily.
Let us now look at a code example highlighting some of these advanced capabilities. Again, keep the test file FileWithObservationResultMessage.txt that I have uploaded in my GitHub repository handy when you are going through the code below. I would also recommend that you experiment with any other long and complex HL7 message and explore the expression syntaxes even further. The investment will pay off especially if you are going to be dealing with complex HL7 message data.
using System;
using System.Diagnostics;
using System.IO;
using HapiTerserBasicOperations;
using NHapi.Base.Parser;
using NHapi.Base.Util;
namespace HapiTerserAdvancedOperations
{
public class Program
{
public static void Main(string[] args)
{
try
{
//see my GitHub page for this file
var messageString = ReadHl7MessageFromFileAsString("C:\\HL7TestInputFiles\\FileWithObservationResultMessage.txt");
// instantiate a PipeParser, which handles the "traditional or default encoding"
var ourPipeParser = new PipeParser();
// parse the message string into a Java message object
var orderResultsHl7Message = ourPipeParser.Parse(messageString);
//create a terser object instance by wrapping it around the message object
var terser = new Terser(orderResultsHl7Message);
//now, let us do various operations on the message
var terserDemonstrator = new OurTerserHelper(terser);
//use a HL7 test utility such as HAPI Test Panel Utility as reference
//for a visual breakdown of these structures if you need to understand these terser expressions
var terserExpression = "/RESPONSE/PATIENT/PID-5-1";
var dataRetrieved = terserDemonstrator.GetData(terserExpression);
LogToDebugConsole($"Terser expression '{terserExpression}' yielded '{dataRetrieved}'");
terserExpression = "/RESPONSE/PATIENT/VISIT/PV1-9-3";
dataRetrieved = terserDemonstrator.GetData(terserExpression);
LogToDebugConsole($"Terser expression '{terserExpression}' yielded '{dataRetrieved}'");
terserExpression = "/RESPONSE/ORDER_OBSERVATION(0)/OBSERVATION(1)/OBX-3";
dataRetrieved = terserDemonstrator.GetData(terserExpression);
LogToDebugConsole($"Terser expression '{terserExpression}' yielded '{dataRetrieved}'");
terserExpression = "/.ORDER_OBSERVATION(0)/ORC-12-3";
dataRetrieved = terserDemonstrator.GetData(terserExpression);
LogToDebugConsole($"Terser expression '{terserExpression}' yielded '{dataRetrieved}'");
//let us now try a set operation using the terser
terserExpression = "/.OBSERVATION(0)/NTE-3";
terserDemonstrator.SetData(terserExpression, "This is our override value using the setter");
LogToDebugConsole("Set the data for second repetition of the NTE segment and its Third field..");
LogToDebugConsole("\nWill display our modified message below \n");
LogToDebugConsole(ourPipeParser.Encode(orderResultsHl7Message));
}
catch (Exception e)
{
//in real-life, do something about this exception
LogToDebugConsole($"Error occured while creating HL7 message {e.Message}");
}
}
public static string ReadHl7MessageFromFileAsString(string fileName)
{
return File.ReadAllText(fileName);
}
private static void LogToDebugConsole(string informationToLog)
{
Debug.WriteLine(informationToLog);
}
}
}
The program console output from running our program is shown below. Notice that we can access any data in a message no matter how deeply it is nested. Tersers expressions are very powerful indeed, and can save us a lot of time and effort when needing to access message data in our HL7 applications.
Terser expression '/RESPONSE/PATIENT/PID-5-1' yielded 'GUNN'
Terser expression '/RESPONSE/PATIENT/VISIT/PV1-9-3' yielded 'RUTH'
Terser expression '/RESPONSE/ORDER_OBSERVATION(0)/OBSERVATION(1)/OBX-3' yielded 'PT (INR)'
Terser expression '/.ORDER_OBSERVATION(0)/ORC-12-3' yielded 'DAVID'
Set the data for second repetition of the NTE segment and its Third field..
Will display our modified message below
MSH|^~\&|SendingApp|SendingFac|ReceivingApp|ReceivingFac|20120226102502||ORU^R01|Q161522306T164850327|P|2.3
PID|1||000168674|000168674|GUNN^BEBE||19821201|F||||||||M|||890-12-3456|||N||||||||N
PV1|1|I||EL|||00976^PHYSICIAN^DAVID^G|976^PHYSICIAN^DAVID^G|01055^PHYSICIAN^RUTH^K~02807^PHYSICIAN^ERIC^LEE~07019^GI^ASSOCIATES~01255^PHYSICIAN^ADAM^I~02084^PHYSICIAN^SAYED~01116^PHYSICIAN^NURUDEEN^A~01434^PHYSICIAN^DONNA^K~02991^PHYSICIAN^NICOLE|MED||||7|||00976^PHYSICIAN^DAVID^G||^^^Chart ID^Vis|||||||||||||||||||||||||20120127204900
ORC|RE|||||||||||00976^PHYSICIAN^DAVID^G
OBR|1|88855701^STDOM|88855701|4083023^PT|||20120226095400|||||||20120226101300|Blood|01255||||000002012057000145||20120226102500||LA|F||1^^^20120226040000^^R~^^^^^R|||||||||20120226040000
NTE|||This is our override value using the setter
OBX|1|NM|PT Patient^PT||22.5|second(s)|11.7-14.9|H|||F|||20120226102500||1^SYSTEM^SYSTEM
OBX|2|NM|PT (INR)^INR||1.94||||||F|||20120226102500||1^SYSTEM^SYSTEM
NTE|1||The optimal INR therapeutic range for stable patients on oral anticoagulants is 2.0 - 3.0. With mechanical heart valves,
NTE|2||the range is 2.5 - 3.5.
NTE|3
NTE|4||Studies published in NEJM show that patients treated long-term with low intensity warfarin therapy for prevention of recurrent
NTE|5||venous thromboembolism (with a target INR of 1.5 - 2.0) had a superior outcome. These results were seen in patients after a median
NTE|6||6 months of full dose anti-coagulation.
Screen capture of the HAPI Test Panel seen below illustrates the breakdown of the test HL7 message data hierarchy. This information should help in making sense of many of the terser expressions used in my examples above. I would urge you to spend some time experimenting with the various terser expressions against any HL7 message to get familiar with the range of possibilities for message extraction as well as for updating message data using the Terser class. The HAPI Test Panel will prove to be extremely useful during this process, and I highly recommend that you consider using such a tool for HL7-related development as well as for troubleshooting purposes.
Conclusion
That concludes our tutorial on using tersers provided by the NHAPI HL7 library. There are a lot of other expressions that are available for you to try out, but hopefully I covered all the important ones that you will frequently use for accessing HL7 message data in your HAPI-enabled HL7 applications. You can review the NHAPI source code for additional information. Something I would caution you here is to avoid the temptation to over use these in your application logic especially from a code readability and maintenance perspective. Troubleshooting terser expressions can sometimes be a problem if you are not careful about what you actually intended to do for a specific operation. Unit tests can be extremely useful in these situations since they enable you to see whether the expression works exactly as you intended for a message being considered for your test. In the next tutorial in my HL7 article series, we will look at how to transmit as well as receive binary data (such as PDFs, Waveforms,etc)during HL7 message workflows after which we will go back to the parser classes and see how they help us towards message validation. See you then!