HL7 Programming using .NET and NHAPI - Creating ACK Messages
Introduction
This is part of my HL7 article series. 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". In this tutorial, we will explore how to create ACK (Acknowledgment) messages using .NET and the NHAPI framework. ACK messages are fundamental to HL7 communication as they confirm receipt and processing status of messages between healthcare systems.
When a HL7 message is sent from one system to another, the receiving system typically responds with an acknowledgment message to indicate whether the message was received successfully, encountered an error during processing, or was rejected entirely. Understanding how to properly create and handle these acknowledgment messages is crucial for building robust healthcare integration solutions.
Tools for Tutorial
- .NET 6.0 SDK or higher
- Visual Studio 2022, Visual Studio Code, or any other .NET IDE
- NHAPI NuGet package - install via:
dotnet add package NHapi - View NHAPI source code on their GitHub site here
- You can also find all the code demonstrated in this tutorial on GitHub here
“Good communication is the bridge between confusion and clarity.” ~ Nat Turner
Understanding HL7 Acknowledgment Codes
Before diving into the code, let us understand the different acknowledgment codes defined by the HL7 standard. These codes indicate the result of processing the original message:
- AA (Application Accept) - Message processed successfully
- AE (Application Error) - Error in message content (syntax or semantic error)
- AR (Application Reject) - Message rejected (application cannot process)
- CA (Commit Accept) - Message committed to safe storage
- CE (Commit Error) - Commit error occurred
- CR (Commit Reject) - Commit rejected
The ACK message structure consists of the following segments:
- MSH - Message Header (mirrors original with response details)
- MSA - Message Acknowledgment (acknowledgment code and reference to original message)
- ERR - Error segment (optional, for error details)
Step 1 of 3 - Define the Acknowledgment Codes
First, let us create an enumeration to represent the different acknowledgment codes we will be using:
/// <summary>
/// Enumeration of HL7 acknowledgment codes.
/// </summary>
public enum AcknowledgmentCode
{
/// <summary>Application Accept - Message processed successfully</summary>
AA,
/// <summary>Application Error - Error in message content</summary>
AE,
/// <summary>Application Reject - Message rejected</summary>
AR,
/// <summary>Commit Accept - Message committed to safe storage</summary>
CA,
/// <summary>Commit Error - Commit error</summary>
CE,
/// <summary>Commit Reject - Commit rejected</summary>
CR
}
Step 2 of 3 - Create the ACK Message Builder
Now let us create a builder class that handles the complex task of creating properly formatted ACK messages. This builder handles swapping sender/receiver information, referencing the original message control ID, setting appropriate acknowledgment codes, and optionally adding error information:
using System.Globalization;
using NHapi.Base.Model;
using NHapi.Model.V23.Message;
using NHapi.Model.V23.Segment;
/// <summary>
/// Builder class for creating ACK (Acknowledgment) messages.
/// </summary>
public static class AckMessageBuilder
{
/// <summary>
/// Creates an ACK message in response to the original message.
/// </summary>
/// <param name="originalMessage">The original message being acknowledged</param>
/// <param name="ackCode">The acknowledgment code (AA, AE, AR, etc.)</param>
/// <param name="textMessage">Optional text message describing the result</param>
/// <param name="errorSegmentId">Optional: Segment where error occurred</param>
/// <param name="errorFieldPosition">Optional: Field position of error</param>
/// <param name="errorMessage">Optional: Detailed error message</param>
/// <returns>A fully constructed ACK message</returns>
public static ACK CreateAck(
IMessage originalMessage,
AcknowledgmentCode ackCode,
string? textMessage = null,
string? errorSegmentId = null,
string? errorFieldPosition = null,
string? errorMessage = null)
{
var ack = new ACK();
// Build MSH segment (response header)
BuildMshSegment(ack.MSH, originalMessage);
// Build MSA segment (acknowledgment details)
BuildMsaSegment(ack.MSA, originalMessage, ackCode, textMessage);
// Build ERR segment if error details are provided
if (!string.IsNullOrEmpty(errorSegmentId) || !string.IsNullOrEmpty(errorMessage))
{
BuildErrSegment(ack.ERR, errorSegmentId, errorFieldPosition, errorMessage);
}
return ack;
}
private static void BuildMshSegment(MSH msh, IMessage originalMessage)
{
// Extract original MSH for reference
var originalMsh = (MSH)originalMessage.GetStructure("MSH");
// Standard encoding
msh.FieldSeparator.Value = "|";
msh.EncodingCharacters.Value = "^~\\&";
// Swap sending and receiving (response goes back to sender)
msh.SendingApplication.NamespaceID.Value = originalMsh.ReceivingApplication?.NamespaceID?.Value ?? "";
msh.SendingFacility.NamespaceID.Value = originalMsh.ReceivingFacility?.NamespaceID?.Value ?? "";
msh.ReceivingApplication.NamespaceID.Value = originalMsh.SendingApplication?.NamespaceID?.Value ?? "";
msh.ReceivingFacility.NamespaceID.Value = originalMsh.SendingFacility?.NamespaceID?.Value ?? "";
// Set response timestamp
msh.DateTimeOfMessage.TimeOfAnEvent.Value =
DateTime.Now.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture);
// Message type is ACK
msh.MessageType.MessageType.Value = "ACK";
msh.MessageType.TriggerEvent.Value = originalMsh.MessageType.TriggerEvent.Value;
// Generate new message control ID for the ACK
msh.MessageControlID.Value = $"ACK{DateTime.Now:yyyyMMddHHmmssfff}";
// Processing ID and version from original
msh.ProcessingID.ProcessingID.Value = originalMsh.ProcessingID.ProcessingID.Value;
msh.VersionID.Value = originalMsh.VersionID.Value;
}
private static void BuildMsaSegment(
MSA msa,
IMessage originalMessage,
AcknowledgmentCode ackCode,
string? textMessage)
{
var originalMsh = (MSH)originalMessage.GetStructure("MSH");
// Set acknowledgment code
msa.AcknowledgementCode.Value = ackCode.ToString();
// Reference the original message's control ID
msa.MessageControlID.Value = originalMsh.MessageControlID.Value;
// Optional text message
if (!string.IsNullOrEmpty(textMessage))
{
msa.TextMessage.Value = textMessage;
}
}
private static void BuildErrSegment(
ERR err,
string? segmentId,
string? fieldPosition,
string? errorMessage)
{
// ERR-1: Error Code and Location
if (!string.IsNullOrEmpty(segmentId))
{
err.GetErrorCodeAndLocation(0).SegmentID.Value = segmentId;
if (!string.IsNullOrEmpty(fieldPosition))
{
err.GetErrorCodeAndLocation(0).FieldPosition.Value = fieldPosition;
}
}
}
}
“To effectively communicate, we must realize that we are all different in the way we perceive the world.” ~ Tony Robbins
Step 3 of 3 - Demonstrate ACK Message Generation
Now let us create a demonstration program that shows how to generate different types of ACK messages in response to an incoming HL7 message:
using NHapi.Base.Model;
using NHapi.Base.Parser;
namespace Com.SaravananSubramanian.Nhapi.AckMessages;
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("=== NHAPI ACK Message Generation Example ===\n");
// Sample incoming ADT A01 message to acknowledge
const string incomingMessage =
"MSH|^~\\&|SENDING_APP|SENDING_FAC|RECEIVING_APP|RECEIVING_FAC|20240115120000||ADT^A01|MSG001|P|2.3|||AL|NE|\r" +
"EVN|A01|20240115120000|||\r" +
"PID|1||12345^^^HOSP^MR||DOE^JOHN^A||19800101|M|||||||||||\r" +
"PV1|1|I|ICU^101^A|||||||||||||||||||||||||||||||||||||||";
try
{
var pipeParser = new PipeParser();
// Parse the incoming message
Console.WriteLine("1. Incoming message to acknowledge:");
Console.WriteLine(FormatMessageForDisplay(incomingMessage));
Console.WriteLine();
var parsedMessage = pipeParser.Parse(incomingMessage);
// Demonstrate different ACK scenarios
DemonstrateSuccessfulAck(parsedMessage, pipeParser);
DemonstrateErrorAck(parsedMessage, pipeParser);
DemonstrateRejectAck(parsedMessage, pipeParser);
Console.WriteLine("\n=== ACK Generation Complete ===");
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
Console.Error.WriteLine(ex.StackTrace);
}
}
/// <summary>
/// Creates an AA (Application Accept) acknowledgment for successful processing.
/// </summary>
private static void DemonstrateSuccessfulAck(IMessage originalMessage, PipeParser parser)
{
Console.WriteLine("2. Creating AA (Application Accept) ACK:");
Console.WriteLine(" Use when message was processed successfully.\n");
var ack = AckMessageBuilder.CreateAck(
originalMessage,
AcknowledgmentCode.AA,
"Message processed successfully");
Console.WriteLine(" Generated ACK:");
Console.WriteLine(FormatMessageForDisplay(parser.Encode(ack)));
Console.WriteLine();
}
/// <summary>
/// Creates an AE (Application Error) acknowledgment for processing errors.
/// </summary>
private static void DemonstrateErrorAck(IMessage originalMessage, PipeParser parser)
{
Console.WriteLine("3. Creating AE (Application Error) ACK:");
Console.WriteLine(" Use when there's an error in message content.\n");
var ack = AckMessageBuilder.CreateAck(
originalMessage,
AcknowledgmentCode.AE,
"Patient ID 12345 not found in system");
Console.WriteLine(" Generated ACK:");
Console.WriteLine(FormatMessageForDisplay(parser.Encode(ack)));
Console.WriteLine();
}
/// <summary>
/// Creates an AR (Application Reject) acknowledgment for rejected messages.
/// </summary>
private static void DemonstrateRejectAck(IMessage originalMessage, PipeParser parser)
{
Console.WriteLine("4. Creating AR (Application Reject) ACK:");
Console.WriteLine(" Use when message is rejected (e.g., unsupported message type).\n");
var ack = AckMessageBuilder.CreateAck(
originalMessage,
AcknowledgmentCode.AR,
"ADT A01 messages are not accepted by this system",
"MSH",
"9",
"Message type not supported");
Console.WriteLine(" Generated ACK:");
Console.WriteLine(FormatMessageForDisplay(parser.Encode(ack)));
}
private static string FormatMessageForDisplay(string message)
{
var lines = message.Split('\r', StringSplitOptions.RemoveEmptyEntries);
return string.Join(Environment.NewLine, lines.Select(line => $" {line}"));
}
}
Sample Output
Running the code above will produce output similar to the following:
=== NHAPI ACK Message Generation Example ===
1. Incoming message to acknowledge:
MSH|^~\&|SENDING_APP|SENDING_FAC|RECEIVING_APP|RECEIVING_FAC|20240115120000||ADT^A01|MSG001|P|2.3|||AL|NE|
EVN|A01|20240115120000|||
PID|1||12345^^^HOSP^MR||DOE^JOHN^A||19800101|M|||||||||||
PV1|1|I|ICU^101^A|||||||||||||||||||||||||||||||||||||||
2. Creating AA (Application Accept) ACK:
Use when message was processed successfully.
Generated ACK:
MSH|^~\&|RECEIVING_APP|RECEIVING_FAC|SENDING_APP|SENDING_FAC|20250121143022||ACK^A01|ACK20250121143022123|P|2.3
MSA|AA|MSG001|Message processed successfully
3. Creating AE (Application Error) ACK:
Use when there's an error in message content.
Generated ACK:
MSH|^~\&|RECEIVING_APP|RECEIVING_FAC|SENDING_APP|SENDING_FAC|20250121143022||ACK^A01|ACK20250121143022456|P|2.3
MSA|AE|MSG001|Patient ID 12345 not found in system
4. Creating AR (Application Reject) ACK:
Use when message is rejected (e.g., unsupported message type).
Generated ACK:
MSH|^~\&|RECEIVING_APP|RECEIVING_FAC|SENDING_APP|SENDING_FAC|20250121143022||ACK^A01|ACK20250121143022789|P|2.3
MSA|AR|MSG001|ADT A01 messages are not accepted by this system
ERR|MSH^9
=== ACK Generation Complete ===
Best Practices for ACK Messages
When working with ACK messages in production systems, consider these best practices:
- Always respond - Every incoming message should receive an acknowledgment. Failure to respond can cause the sending system to timeout and potentially resend the message.
- Include meaningful error messages - When returning AE or AR codes, provide descriptive text in the MSA-3 field to help troubleshoot issues.
- Reference the original message - Always include the original message control ID in the MSA-2 field so the sender can correlate the response.
- Handle exceptions gracefully - If an error occurs during message processing, return an AE or AR acknowledgment rather than no response at all.
- Log all acknowledgments - Keep records of both positive and negative acknowledgments for auditing and troubleshooting purposes.
Conclusion
In this tutorial, we explored how to create HL7 ACK messages using the NHAPI framework in .NET. We covered the different acknowledgment codes, created a reusable ACK message builder, and demonstrated how to generate various types of acknowledgments. Understanding ACK messages is essential for building reliable healthcare integration systems that properly communicate processing results between applications. This concludes the HL7 .NET programming series. I hope you found these tutorials helpful in understanding how to build HL7 2.x applications using .NET and the NHAPI framework. Be sure to check out the other articles in my HL7 article series for more HL7 programming content including tutorials for Java using the HAPI framework.