HL7 Programming using .NET and NHAPI - Parsing HL7 Messages


Introduction

This is part of my HL7 article series in which we will look at important aspect of HL7 messaging processing which is message parsing. 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 an example of how a HL7 message can be easily created using the library in my tutorial titled "HL7 Programming using NHAPI and .NET - Creating HL7 Messages". We also looked at how to send HL7 messages in a subsequent tutorial titled "HL7 Programming using NHAPI and .NET - Sending HL7 Messages". In this tutorial, we will explore the many ways in which you can extract HL7 message structures such as segment, fields, components, sub-components, etc using the NHAPI framework.

Tools and Resources Needed

“Wonder is the beginning of wisdom.” ~ Greek Proverb

NHAPI Parsers - An Overview

NHAPI provides two mechanisms for accessing message information, and these are Parsers and Tersers. They both work quite differently from each other, and in this tutorial, we will look at the parser classes and their capabilities, and what you can use them for in your HL7-enabled applications. In a subsequent tutorial in this series, we will look at tersers as well.

In the field of computer science, the word "parser" often refers to the act of breaking down a piece of text such as a sentence, a string of words or any other linguistic construct into its smaller constituent parts to aid in either the structural or the semantic understanding of that material. This deconstructed information is often displayed in the form of a "parse tree" which refers to the act of displaying this information in a tree like structure. This enables us to see the big picture as to how the various parts are related to one another. The parser classes provided by HAPI contain many useful features including being able to convert from a message string to a message object (this is referred to as "parsing"), and also convert a HL7 message object back to a string format (referred to as "encoding" a message). With these parsers, you can translate the HL7 2.x information from as well as to various formats such as ER7 (sometimes also referred to as "normal encoding") and XML when serialization to a file, a database or transmission across the network is needed.

When you are dealing with message parsing, something you will begin to really appreciate about the NHAPI library is the many strongly typed classes that it provides for dealing with nearly every HL7 message definition laid out in the various HL7 2.x standards. Since there are many versions of the HL7 2.x standard each of which consist of hundreds of message types and trigger events, hand coding parser classes can become extremely time consuming. NHAPI authors overcame this problem through a rather ingenious way by auto-generating C# class definitions based on HL7 standard's official HL7 Microsoft Access Tables containing the HL7 2.x message definitions*. The .NET classes generated by this approach help provide convenient binding interfaces that enable application programmers using the HAPI library to access as well as update message data pretty much along the same HL7 abstract message syntax model defined by the HL7 group. So, if you are looking to write code that follows the "HL7 2.x Information Model" more closely, then NHAPI parsers can assist you with that. Let us look at some examples of using NHAPI parsers now.

Basic Parser Operations

The code below demonstrates a few different operations you can perform using the parser classes. It shows how we can take a HL7 message string (in our case an ACK message response) and parse it into a HL7 message object using the PipeParser. It also shows you how when we have the information in a strongly typed HL7 message object, we can then easily access as well as update any data on the message such as a segment, field, etc. It then shows you how to use the DefaultXmlParser to encode the message object into XML format for display purposes.

    using System;
    using System.Diagnostics;
    using NHapi.Base.Parser;
    using NHapi.Model.V23.Message;

    namespace HapiParserBasicOperations
    {
        public class Program
        {
            public static void Main(string[] args)
            {
                const string messageString = "MSH|^~\\&|SENDING_APPLICATION|SENDING_FACILITY|RECEIVING_APPLICATION|RECEIVING_FACILITY|20110614075841||ACK|1407511|P|2.3||||||\r\n" +
                                            "MSA|AA|1407511|Success||";

                // instantiate a PipeParser, which handles the "traditional or default encoding"
                var ourPipeParser = new PipeParser();

                try
                {
                    // parse the string format message into a Java message object
                    var hl7Message = ourPipeParser.Parse(messageString);

                    //cast to ACK message to get access to ACK message data
                    var ackResponseMessage = hl7Message as ACK;
                    if (ackResponseMessage != null) {
                        //access message data and display it
                        //note that I am using encode method at the end to convert it back to string for display
                        var mshSegmentMessageData = ackResponseMessage.MSH;
                        LogToDebugConsole("Message Type is " + mshSegmentMessageData.MessageType.MessageType);
                        LogToDebugConsole("Message Control Id is " + mshSegmentMessageData.MessageControlID);
                        LogToDebugConsole("Message Timestamp is " + mshSegmentMessageData.DateTimeOfMessage.TimeOfAnEvent.GetAsDate());
                        LogToDebugConsole("Sending Facility is " + mshSegmentMessageData.SendingFacility.NamespaceID.Value);

                        //update message data in MSA segment
                        ackResponseMessage.MSA.AcknowledgementCode.Value = "AR";
                    }

                    // Display the updated HL7 message using Pipe delimited format
                    LogToDebugConsole("HL7 Pipe Delimited Message Output:");
                    LogToDebugConsole(ourPipeParser.Encode(hl7Message));

                    // instantiate an XML parser that NHAPI provides
                    var ourXmlParser = new DefaultXMLParser();

                    // convert from default encoded message into XML format, and send it to standard out for display
                    LogToDebugConsole("HL7 XML Formatted Message Output:");
                    LogToDebugConsole(ourXmlParser.Encode(hl7Message));

                }
                catch (Exception e)
                {
                    //in real-life, do something about this exception
                    LogToDebugConsole($"Error occured -> {e.StackTrace}");
                }
            }

            private static void LogToDebugConsole(string informationToLog)
            {
                Debug.WriteLine(informationToLog);
            }
        }
    }

Running the code above should result in the output similar to what is shown below. We are able to successfully parse the message string into a message object and able to access the message data easily. Here, I am displaying the message type, the message control id, the timestamp of when the message was origination as well as the sending facility for the message. We are also able to encode the message object into XML format for display.


Message Type is ACK
Message Control Id is 1407511
Message Timestamp is 2011-06-14 7:58:41 AM
Sending Facility is SENDING_FACILITY
HL7 Pipe Delimited Message Output:
MSH|^~\&|SENDING_APPLICATION|SENDING_FACILITY|RECEIVING_APPLICATION|RECEIVING_FACILITY|20110614075841||ACK|1407511|P|2.3
MSA|AR|1407511|Success

HL7 XML Formatted Message Output:
<ACK><MSH><MSH.1>|</MSH.1><MSH.2>^~\&amp;</MSH.2><MSH.3><HD.1>SENDING_APPLICATION</HD.1></MSH.3><MSH.4><HD.1>SENDING_FACILITY</HD.1></MSH.4><MSH.5><HD.1>RECEIVING_APPLICATION</HD.1></MSH.5><MSH.6><HD.1>RECEIVING_FACILITY</HD.1></MSH.6><MSH.7><TS.1>20110614075841</TS.1></MSH.7><MSH.9><CM_MSG.1>ACK</CM_MSG.1></MSH.9><MSH.10>1407511</MSH.10><MSH.11><PT.1>P</PT.1></MSH.11><MSH.12>2.3</MSH.12></MSH><MSA><MSA.1>AR</MSA.1><MSA.2>1407511</MSA.2><MSA.3>Success</MSA.3></MSA></ACK>

Parsing of Custom Message Models

An interesting and very useful feature that parsers in NHAPI (and tools built on top of NHAPI such as "NHAPI Tools") provide is in the area of HL7 message customization. Something you will run into occasionally when dealing with HL7 2.x messaging systems is what is known as a "Z-segment". Occasionally, a requirement emerges where communication of some special type of information is required which is not easily supported by the standard message definitions that are specified in the HL7 standard. In those situations, we can create a custom segment to help transmit this custom data. The standard convention is that all these custom segments begin with the letter Z. For instance, a "ZPV" segment may be used when customized patient visit information needs to be transmitted because there is some specialization information that needs to be recorded as part of the patient visit information. Because custom segments almost always start with the letter Z, they are also referred to as Z-segments. This feature helps provide quite a bit of flexibility to HL7 message communications. However, you need to be able to handle the extraction of the data from Z-segment when this is the case. Writing this parsing logic can be quite tricky as you need to handle the parsing of the entire message and the other message structures contained in it as well. I will show two different approaches for extracting Z-segment data from HL7 messages. The first approach will be by using the extensibility offered by the NHAPI library and the second approach will be by using another third-party library built on top of NHAPI called NHAPI Tools).

Approach 1 - Parsing of Custom Message Models using NHAPI


Step 1 of 3 - Create a Z-segment Class

First, we need to create an new HL7 segment definition that we will store our custom fields. We do this by extending the Abstract Segment class that comes with NHAPI that provides a lot of pre-built functionality to manage segment data. This abstract class assists in providing useful methods to help initialize the various fields contained in the messages. We utilize the add method that is already implemented in the abstract this class to define as well as initialize the fields in this segment. In the example below, I am defining two custom fields named "custom notes" and "custom description" that are both of ST data type, but you can implement fields using whatever data type that is supported in the HL7 standard. For the two custom fields, I am also specifying other characteristics such as whether the field is mandatory, the number of repetitions of this field as well as the field length.


    using System;
    using NHapi.Base;
    using NHapi.Base.Log;
    using NHapi.Base.Model;
    using NHapi.Base.Parser;
    using NHapi.Model.V22.Datatype;

    namespace NHapi.Model.CustomZSegments.Segment
    {
        [Serializable]
        public sealed class ZPV : AbstractSegment
        {
            public ZPV(IGroup parent,IModelClassFactory factory) : base(parent, factory)
            {
                var message = Message;
                try
                {
                    add(typeof(ST), true, 1, 13, new object[]{message}, "Custom Notes");
                    add(typeof(ST), true, 1, 13, new object[] { message }, "Custom Description");
                }
                catch (HL7Exception he)
                {
                    //in real-life, do something about this exception
                    HapiLogFactory.GetHapiLog(GetType()).Error("Unable to instantiate segment:" + GetType().Name, he);
                }
            }

            public ST CustomNotes
            {
                get
                {
                    ST customNotes;
                    try
                    {
                        var fieldData = GetField(1, 0);
                        customNotes = (ST) fieldData;
                    }
                    catch (Exception ex)
                    {
                        const string errorMessage = "Unexpected error occured while obtaining Custom Notes field value.";
                        HapiLogFactory.GetHapiLog(GetType()).Error(errorMessage, ex);
                        throw new Exception(errorMessage, ex);
                    }
                    return customNotes;
                }
            }

            public ST CustomDescription
            {
                get
                {
                    ST customDescription;
                    try
                    {
                        var fieldData = GetField(2, 0);
                        customDescription = (ST)fieldData;
                    }
                    catch (Exception ex)
                    {
                        //in real-life, do something about this exception
                        var errorMessage = "Unexpected error occured while obtaining Custom Description field value.";
                        HapiLogFactory.GetHapiLog(GetType()).Error(errorMessage, ex);
                        throw new Exception(errorMessage, ex);
                    }
                    return customDescription;
                }
            }

        }
    }

“The clearest way into the Universe is through a forest wilderness.” ~ John Muir

Step 2 of 3 - Create a Specialized Class by Extending the ADT A01 Message Class

We need to define a new message type to carry the z-segment we just defined earlier as we need to inform the receiving system that this message is not a normal ADT A01 message. Here we are simply extending the ADT A01 message class provided by the NHAPI library and are adding additional behavior to support the new message segment. This approach enables us to build on the behavior already provided by the ADT A01 message class and the other classes and interfaces that it extends or implements already. As you can see in the code below, we are specifying some additional behavior in the constructor of this new message type class by invoking the Init method and are asking it to include new z-segment to its normal payload using the new z-segment class that we previously created and are marking it is a mandatory.

    using NHapi.Base;
    using NHapi.Base.Log;
    using NHapi.Base.Parser;
    using NHapi.Model.CustomZSegments.Segment;

    namespace NHapi.Model.CustomZSegments.Message
    {
        public class ADT_A01 : NHapi.Model.V23.Message.ADT_A01
        {
            public ADT_A01(IModelClassFactory factory) : base(factory){
            Init(factory);
            }

            public ADT_A01() : base(new DefaultModelClassFactory()) {
            Init(new DefaultModelClassFactory());
            }

            private void Init(IModelClassFactory factory) {
            try
            {
                add(typeof(ZPV),true,false); //mark this segment as required
            }
            catch(HL7Exception e)
            {
                //in real-life, do something about this exception
                HapiLogFactory.GetHapiLog(GetType()).Error("Error creating ADT_A01", e);
            }
            }

            public virtual ZPV ZPV
            {
                get
                {
                    ZPV segmentData = null;
                    try
                    {
                        segmentData = (ZPV) this.GetStructure("ZPV");
                    }
                    catch (HL7Exception e)
                    {
                        //in real-life, do something about this exception
                        const string errorMessage = "Unexpected error accessing ZPV segment data";
                        HapiLogFactory.GetHapiLog(this.GetType()).Error(errorMessage, e);
                        throw new System.Exception(errorMessage, e);
                    }
                    return segmentData;
                }
            }

        }
    }
Step 3 of 3 - Stitch The Behavior Together

In this last step, we bring all these specialized behaviors together to help parse the new message type successfully. The NHAPI pipe parser helps load as well as parse the HL7 message string into the custom ADT_A01 message. We can then extract the custom ZPV message segment including the custom message data from this message as shown in the code illustration below. Please note that some additional configuration is needed for the parser class to load the necessary classes during the parse operation. Please have a look at the Readme.txt file included with the sample project in GitHub for additional instructions.

    using System.Diagnostics;
    using NHapi.Base.Parser;
    using NHapi.Model.CustomZSegments;

    namespace NHapiParserCustomMessageModelExample
    {
        public class Program
        {
            public static void Main(string[] args)
            {

                const string customSegmentBasedHl7Message = "MSH|^~\\&|SUNS1|OVI02|AZIS|CMD|200606221348||ADT^A01|1049691900|P|2.3\r"
                                                            + "EVN|A01|200803051509||||200803031508\r"
                                                            + "PID|||5520255^^^PK^PK~ZZZZZZ83M64Z148R^^^CF^CF~ZZZZZZ83M64Z148R^^^SSN^SSN^^20070103^99991231~^^^^TEAM||ZZZ^ZZZ||19830824|F||||||||||||||||||||||N\r"
                                                            + "ZPV|Some Custom Notes|Additional custom description of the visit goes here";

                var parser = new PipeParser();

                var parsedMessage = parser.Parse(customSegmentBasedHl7Message, Constants.Version);

                LogToDebugConsole("Type: " + parsedMessage.GetType());

                //cast this to the custom message that we have overridden
                var zdtA01 = parsedMessage as NHapi.Model.CustomZSegments.Message.ADT_A01;
                if (zdtA01 != null)
                {
                    LogToDebugConsole(zdtA01.ZPV.CustomNotes.Value);
                    LogToDebugConsole(zdtA01.ZPV.CustomDescription.Value);
                }
            }

            private static void LogToDebugConsole(string informationToLog)
            {
                Debug.WriteLine(informationToLog);
            }
        }
    }

The results of running the main program above are shown below. By using an approach such as this, we reduce the amount of code we otherwise need to write and support in our custom HL7 applications. If you are interested in exploring this area further, you should look up the official NHAPI source code and/or documentation for more information. While on the topic of Z-segments, I would ask you to be careful around the amount of customization you generally do as this tends to decrease the interoperability and supportability of the overall system especially if the remote systems are upgraded or when new systems come onboard. Besides, the standard already provides support for most scenarios anyway, and so you should always look at the HL7 official documentation before deciding to use Z-segments as there may be ways to do what you are looking for already.


Attempting to parse message string into HL7 message object...
Parsed message into generic ADT_A01 message successfully...
Type: NHapi.Model.CustomZSegments.Message.ADT_A01
Casting the parsed message object into our custom ADT_A01 message...
Custom Notes retrieved from ZPV segment was -> Some Custom Notes
Custom Description retrieved from ZPV segment was -> Additional custom description of the visit goes here
Entire parse operation completed successfully...

Approach 2 - Parsing of Custom Message Models using NHAPI Tools

Yet another way of extracting Z-segment data from customized HL7 messages is by using a facility in NHAPI Tools called the GenericMessageWrapper class. In this approach, we still define the custom Z-segment and the custom HL7 message class as we did in the previous approach (see steps 1 and 2). However, the third step of message extraction works slightly differently. Here, we simply parse the HL7 message data into a generic HL7 message structure using the GenericMessageWrapper class which works in conjunction with the EnhancedModelClassFactory class under the covers and provides a convenient method to get at all the segments that are contained in a generic message object using the Unwrap method. We then check to see if this generic HL7 message object contains the custom segment that we are interested in and extract it if it exists. Some additional configuration is required in the App.Config file to help the class loader of NHAPI parser to look for the custom HL7 message definition that we previously defined in a separate .NET project that is referenced by the main project. Please have a look at the Readme.txt file included with the sample project in GitHub for additional instructions.


    using System.Diagnostics;
    using NHapi.Base.Model;
    using NHapi.Base.Parser;
    using NHapiTools.Base.Model;
    using NHapiTools.Base.Parser;

    namespace NHapiToolsGenericMessageWrapperParsingApproach
    {
        public class Program
        {
            static void Main(string[] args)
            {
                const string customSegmentBasedHl7Message = "MSH|^~\\&|SUNS1|OVI02|AZIS|CMD|200606221348||ADT^A01|1049691900|P|2.3\r"
                                                            + "EVN|A01|200803051509||||200803031508\r"
                                                            + "PID|||5520255^^^PK^PK~ZZZZZZ83M64Z148R^^^CF^CF~ZZZZZZ83M64Z148R^^^SSN^SSN^^20070103^99991231~^^^^TEAM||ZZZ^ZZZ||19830824|F||||||||||||||||||||||N\r"
                                                            + "ZPV|Some Custom Notes|Additional custom description of the visit goes here";

                var enhancedModelClassFactory = new EnhancedModelClassFactory();
                var pipeParser = new PipeParser(enhancedModelClassFactory);
                enhancedModelClassFactory.ValidationContext = pipeParser.ValidationContext;

                LogToDebugConsole("Attempting to parse message string into HL7 message object...");

                var ourHl7Message = pipeParser.Parse(customSegmentBasedHl7Message);

                LogToDebugConsole("Parsed message into generic ADT_A01 message successfully...");

                LogToDebugConsole("Unwrapping payload from our custom ADT_A01 message using GenericMessageWrapper class...");

                var wrappedGenericMessage = ourHl7Message as GenericMessageWrapper;
                var originalMessage = wrappedGenericMessage?.Unwrap();

                if (wrappedGenericMessage?.GetSegment<ISegment>("ZPV") != null)
                {
                    LogToDebugConsole("Casting unwrapped payload into our custom ADT A01 class...");
                    //casting to our custom class and retrieving the custom segment within it
                    var zpvSegment = ((NHapi.Model.CustomZSegments.Message.ADT_A01)originalMessage).ZPV;
                    LogToDebugConsole($"Custom Notes retrieved from ZPV segment was -> {zpvSegment.CustomNotes.Value}");
                    LogToDebugConsole($"Custom Description retrieved from ZPV segment was -> {zpvSegment.CustomDescription.Value}");
                    LogToDebugConsole("Entire parse operation completed successfully...");
                }
            }

            private static void LogToDebugConsole(string informationToLog)
            {
                Debug.WriteLine(informationToLog);
            }
        }
    }

The results of running the main program above are shown below. As you can see, both approaches outlined here can get the job done for us. I will leave it to you to decide which approach you like and want to employ in your custom application.


Attempting to parse message string into HL7 message object...
Parsed message into generic ADT_A01 message successfully...
Unwrapping payload from our custom ADT_A01 message using GenericMessageWrapper class...
Casting unwrapped payload into our custom ADT A01 class...
Custom Notes retrieved from ZPV segment was -> Some Custom Notes
Custom Description retrieved from ZPV segment was -> Additional custom description of the visit goes here
Entire parse operation completed successfully...

Conclusion

That brings us to the end of yet another tutorial on using the NHAPI HL7 library. We looked at some basic message parsing capabilities provided in the NHAPI library. The various parser classes provided by this library help us to convert messages from and to various formats such as ER7, XML, etc. We also looked at how the parser classes in the NHAPI library enable us to process special Z-segments when site-specific customizations are in place. I showed you two different approaches that you can use during custom message parsing (one using the core NHAPI library and another approach using the NHAPI Tools library). By using the various classes provided by the NHAPI library and by the NHAPI Tools framework, application programmers can reduce the time required to create and/or extend HL7 2.x message-enabled applications especially when we need to process many different message types or trigger events. In the next tutorial in my HL7 article series, we will look at tersers quickly before moving on to review the more advanced features provided by the parser classes such as message validation and message reception. See you then!

* - Always consult the official HAPI and HL7 documentation for latest information as this may change or may have changed already.