DICOM Basics using .NET and C# - Understanding Association/ Negotiations

This is part of my series of articles on the DICOM standard that I am currently working on (a number of them have already been completed). If you are totally new to DICOM, please have a quick look at my earlier article titled “Introduction to the DICOM Standard” for a quick introduction to the standard. It might be useful to also look at my other tutorials that have been completed so far to get up to speed on a number of topics including DICOM Encoding, SOPs and IODs. My introductory tutorial titled "DICOM Basics using .NET and C# - Understanding DICOM Verification" will also be very useful to understanding the material that I will cover in this tutorial. This tutorial also assumes that you know the basics of C# or any equivalent object-oriented language such as Java or C++. A basic understanding of networking will also be useful to have but is not mandatory.

Presentation Context, Application Context, Abstract and Transfer Syntaxes in DICOM

DICOM networking looks very cryptic at first. With its unique jargon with words such as Abstract Syntax, Protocol Data Units, Application Context, Presentation Context, etc, any new comer to the standard can be totally put off by it at first. But once you start to understand what they really mean, and with some patience, things really start to make sense and the entire area of DICOM communications begins to look interesting and even fun. I will cover some fundamental terminology that you need to know before you dive into writing software applications that use or provide DICOM services from/to other DICOM-capable software applications. I will use plenty of examples in this tutorial to help you relate to these terms much better. Let us proceed.

If you have read my previous articles in this series, you will remember me describing that the DICOM standard helps devices that could be running on completely different operating systems exchange DICOM objects such as images, waveforms (such as ECG) and diagnostic reports with each other. We also saw that before any actual exchange of data occurs the two devices must agree on the “dialect” of DICOM they speak, or more formally the “transfer syntax” as the standard calls it. This transfer syntax specifies the byte ordering used on that operating system (Big Endian or Little Endian), the type of compression if any being used as well as the type of VR encoding being used (either explicit or implicit). You will also recall the concept of Service Class Users (or SCUs) as well as Service Class Providers (or SCPs), and how the same device can take on different roles when exchanging information with other devices. For instance, device A can play the role of a C-Find SCU when wanting to query for a set of results from other another device B (which will play the C-Find SCP role), but device A can also play a C-Store SCP role when any results that it is interested in is being pushed to it (device B playing the C-Store SCU role in this case).

“Of all the great national heroes and statesmen of history Lincoln is the only real giant. Alexander, Frederick the Great, Caesar, Napoleon, Gladstone and even Washington stand in greatness of character, in depth of feeling and in a certain moral power far behind Lincoln. Lincoln was a man of whom a nation has a right to be proud; he was a Christ in miniature, a saint of humanity, whose name will live thousands of years in the leg­ends of future generations. We are still too near to his greatness, and so can hardly appreciate his divine power; but after a few centuries more our posterity will find him considerably bigger than we do. His genius is still too strong and too powerful for the common understanding, just as the sun is too hot when its light beams directly on us” ~ Leo Tolstoy about Abraham Lincoln

DICOM refers to the actual network connection between two DICOM devices during which the initial negotiation on the dialect to use as well as the actual transmission of data that occurs after as an Association. During an association, a number of operations can occur. Each of these operations could be in fact be completely independent of one another, and help DICOM applications exchange different types of DICOM objects with one another. When one device (the SCU) attempts to open an association with another device (the SCP), some validation of the input information is checked before a connection is opened. This includes checks such as to whether the SCU (or the Calling AET) is configured at the SCP already. This is a security feature that is implemented in many DICOM applications to ensure that confidential information is not handed out to “rogue” applications requesting data. The SCP (or the Called AET) also checks to see whether it can handle the type of service (identified by SOP Class UIDs) that is being requested (such as CT Image Store, Query/Retrieval or Print) and can handle this service operation using a transfer syntax that the SCU says it can also handle. The SCP may also do additional checks to ensure that it has adequate capacity to handle this workload as there may be high traffic at the time when the association is being requested and so the association request may be rejected for that reason as well even if all else is good. If the initial validation checks are successful, then successful transmission of actual data corresponding to the SOP class otherwise known as Information Object Definition or IODs as they are called in DICOM) occurs. After all data pertaining to the operations are transferred, the association may terminated by either the initiating party (the SCU) or sometimes by the SCP as well. That’s it. This is the gist of what occurs during an association. But, before we dig deeper into the details I want to cover some jargon specific to the association process. This will help you understand the code examples that will follow.

DICOM Association How It Works

If you see my my illustration above, it should provide a you a good idea of how DICOM association works between two devices at a very high level. The whole process begins by the initiating party (often the SCU aka the Calling AE - here it is Device A) establishing a socket connection to the other party (often the SCP aka the Called AE - here it is Device B). This is done by providing the IP address as well as a port number during the socket connection establishment. Some security checks are done to ensure that the calling party is registered at the Called AE database already, and if not, the connection is not permitted here. If this is good, the socket connection is established.

Association Negotiations in DICOM

Next something called the Association Negotiation occurs during which the Calling AE sends some objects called the Presentation Contexts to the other party. Each presentation context object itself consists of two objects. One called the Abstract Syntax and the other object called a Transfer Syntax List. The Abstract Syntax specifies the type of SOP class (specified through a SOP UID we saw earlier) as well as the role it is wanting to play - SCU or SCP). The Called AE must support this Abstract Syntax otherwise, it rejects the association request outright. For example, the Calling AE may specify that it wants the C-Find service from the Called AE. If this service is provided, the Called AE then looks at the Transfer Syntax list that was sent to it. This specifies the dialect of DICOM that the Calling AE wishes to speak. For instance, the Calling AE may wish to use the Explicit VR Little-endian indicated through an UID of 1.2.840.10008.1.2.1. The Called AE may not support this transfer syntax, and may look at other transfer syntaxes in the list to see if there is anything in the list that it understands. If none of the transfer syntaxes in the list are supported, the association request is rejected. I do want to mention here that there should at least one transfer syntax that all DICOM applications must support. This transfer syntax is the Implicit VR Little-endian indicated by an UID of 1.2.840.10008.1.2. This is the only mandatory DICOM transfer syntax that all DICOM applications must support. The problem with this syntax is that as the name specifies the VR encoding is implicit and hence requires the called application to have an up to date DICOM dictionary to make any sense of the incoming data. However, the recommendation is to always use some kind of explicit VR encoding transfer syntax wherever possible as the VR type can be understood from the passed in data itself.

At the point, the association is either accepted or rejected. The Calling AE is notified on which presentation contexts are acceptable in the response message. Please note that there can be more than one presentation context accepted as multiple SOP classes or “Abstract Syntaxes” may supported by the device. Along with this information, the specific transfer syntax that is supported for that presentation context is also indicated. At this point, the Calling AE knows what services to expect from the Called AE, and this completes what is called Association Establishment in DICOM networking. At this point, the Calling AE can start sending DICOM commands along with any associated data to the Called AE.

In addition to the presentation contexts that are transmitted, other information such as Application Context and User Information objects are also transmitted when attempting to start an association. These objects provide ways to perform fine-grain control over the communications between the devices although most of this is optional or left to their default values in my opinion. The application context information enables us to essentially identify the calling application name and manufacturer information. Many vendors use this to recognize that the calling application is their own and can then switch to a more optimized non-DICOM protocol for further communication. When not specified, most implementation default to the NEMA specified UID (1.2.840.10008.3.1.1.1). You can however request a special UID from this organization as well which is a recommended practice which enables you to uniquely identify the application. User information is not what you think. This essentially enables us to control (if supported by the called application) things such as maximum size of data sent in chunks, the version number of the application, whether the operations to be carried can synchronous or asynchronous in nature, what specific role each device will play (SCU or SCP) as well as the type of queries supported (hierarchical, or extended which means support for relational type queries). I have rarely mucked with these and usually leave them at their default settings and things have still worked fine. However, I encourage you to refer to the DICOM standard for more information.

How do DICOM operations work within an association?

DICOM SOP class (SOP stands for Service Object Pair) is a combination of DICOM Message Service Elements (called DIMSEs) which are essentially commands along with object data defined by Information Object Definitions (IODs). For instance, to perform a CT image store operation, the calling application needs to send both the command (C-STORE) as well as the actual CT image (CT IOD). Both the command and the data objects are represented by well defined structure using the same DICOM element groupings that we saw earlier. Now, what I didn't mention or go into earlier (since that would have totally confused you at that point) is that these DICOM commands fall into two categories namely Composite and Normalized. The gist of the differences between the two is this: composite services are heavily optimized by design for image interchange where only new/unmodifiable objects are exchanged (think permanent here), and no further “updates” of existing data is required as this is generally forbidden by DICOM design. This is because any altered data should generally be considered a brand new instance in the DICOM world. Normalized services on the other-hand were designed for use with data on which update or delete operations can occur (management functions). One such example is the N-SET command which is used to update the status of a modality procedure performed step in a DICOM workflow - we will cover these later). Such operations cannot be performed by composite services which are restricted to search and retrieve-type operations only. Because normalized commands operate on normalized entities of data, several of them have to be sequenced and built together for an entire operation to work as each entity of data will point to another entity of data which then has to retrieved through a separate command. Because of this composite commands are generally thought to perform well because they are “chunkier” and don’t require multiple calls but take up more bandwidth during data transfer as data tends to be repeated in the composite objects such as images when a study is transferred between two devices for instance. Normalized operations are “leaner” but require more coordination between the devices. Anyways, I created a quick illustration (see below) that shows the various categories of DICOM commands for you to get a high level idea of these commands quickly.

DICOM Command Types

One way to remember the differences is by using this simple rule which should hold true for 95% of the situations which is that composite operations deal with permanent objects such as an image or a structured report that is being archived or retrieved whereas normalized operations deal with update or delete operations and also when you are dealing with data that is temporary or transient such as an image that is sent for printing which is deleted from the print queue after the operation is completed (done using N-Delete). Most of the operations that we have seen so far (such as C-Echo) as well as we will see (C-Find, C-Move, C-Get, C-Store) are composite in nature, but I will also cover normalized operations in my upcoming tutorials that will deal with modality work-list, printing as well as storage commitment services. There is also a third group of DICOM service elements in addition to the composite and normalized types. This is the Storage Media-related operations which help handle both composite and normalized data as serialized files. Examples of commands include M-Read, M-Write, M-Inquire-File,etc. These work a little bit differently as device capabilities cannot be negotiated between the participating actors in real-time and other means (Application Profiles) of negotiating is required. I will cover all these in more detail in my subsequent tutorials in this series.

Digging Deeper into DIMSEs

When association is successfully established, the two DICOM entities transmit a series of DIMSEs (DICOM Message Service Elements) to one another. DIMSEs are to operations what IODs are to data. You need both for successful DICOM networking and data communication to happen. The DIMSE commands consist of the same modular grouping of DICOM elements like DICOM IODs specifying information such as an unique id for the message, the type of command or operation, a command priority (very rarely used), the DICOM AE requesting this operation, as well as indicator specifying whether there is data accompanying this command. If the data flag is set to true on the command, then one and only one IOD data object is transmitted immediately following the command. The Called AE or SCP then responds to this command (and any data that accompanied it) with a response message indicating the results of the operation. For instance, when a CT scanner decides to transmit a series of images to the PACS storage server, it establishes an association first, and then send a C-Store DIMSE command followed by a CT image IOD instance that needs to be stored. The C-Store SCP responds back with a success or a failure response indicating the result of the operation. The C-Store SCU may continue to transmit a series of more commands for every image that it requires to be store, and the process continues back and forth between the two devices along the same pattern.

DICOM Request and Response Message Structures

I have included an illustration above showing the C-Store request and response message structures to help you make sense of everything I have described regarding DIMSEs. The tables shown in the illustration above are screen captures from the DICOM standard Part 7 document that covers message exchange fundamentals. Enough talk. Let us look at some code to make sense of everything I have covered so far.

Before We Get Started…

Much like my previous programming examples, I will use the most bare minimum code and approach to help illustrate the concepts that I cover in this tutorial. This means that the code I write here is best suited to simply show the concept that I am trying to explain and is not necessarily the most efficient code to deploy in real life and in your production application.

To get started, you will need to configure a few things on your machine including a .NET development environment as well as the Fellow Oak (fo-dicom) DICOM library before you can run the example if you want to try this out yourself.

  1. Download a .NET IDE or Editor such as Visual Studio IDE or Visual Studio Code (even a text editor should suffice)

  2. Download the Fellow Oak DICOM library either through NuGet Package Manager or download the source code directly from here

  3. You can also find the source code used in this tutorial on GitHub

  4. You must set up a DICOM remote peer (a “Verification SCP”) to try out this example. For the code below, I am using a public DICOM server provided by Dr.Dave Harvey. Other options including downloading free DICOM software such as Orthanc Server (available for Windows and Macs) as well as ClearCanvas Open Source Community Edition (for Windows only). See my article on using Orthanc DICOM Server if you wanted to try it out.

In the code shown below, I am registering three additional callback methods with the DICOM client that help track the various events relating to association establishment and termination. If an association could not be establisheed for any reason, then we can get the reason using the AssociationRejectedEventArgs parameter that is passed into the association rejection callback. On the otherhand, if the association was established, then we can get additional details about the established association using the AssociationAcceptedEventArgs parameter that is passed into the association accepted callback. Details of the established association such as the abstract syntax as well as the transfer syntaxes that was negotiated are logged to the program console.

    using System;
    using System.Diagnostics;
    using Dicom.Network;

    namespace UnderstandingDicomAssociationNegotiations
    {
        class Program
        {
            static void Main(string[] args)
            {
                try
                {
                    //replace these with your settings
                    //Here, I am using Dr.Dave Harvey's public server 
                    //please be careful not to send any confidential info as all traffic is logged
                    var dicomRemoteHost = "www.dicomserver.co.uk";
                    var dicomRemoteHostPort = 11112;
                    var useTls = false;
                    var ourDotNetTestClientDicomAeTitle = "Our Dot Net Test Client";
                    var remoteDicomHostAeTitle = "Dr.Dave Harvey's Server";

                    //create DICOM echo verification client with handlers
                    var client = CreateDicomVerificationClient();

                    //send the verification request to the remote DICOM server
                    client.Send(dicomRemoteHost, dicomRemoteHostPort, useTls, ourDotNetTestClientDicomAeTitle, remoteDicomHostAeTitle);
                }
                catch (Exception e)
                {
                    LogToDebugConsole($"Error occured during DICOM association request -> {e.StackTrace}");
                }
            }

            private static DicomClient CreateDicomVerificationClient()
            {
                var client = new DicomClient();

                //register that we want to do a DICOM ping here
                var dicomCEchoRequest = new DicomCEchoRequest();
                
                //attach an event handler when remote peer responds to echo request 
                dicomCEchoRequest.OnResponseReceived += OnEchoResponseReceivedFromRemoteHost;
                client.AddRequest(dicomCEchoRequest);

                //add event handlers for overall association connectivity information
                client.AssociationAccepted += ClientOnAssociationAccepted;
                client.AssociationRejected += ClientOnAssociationRejected;
                client.AssociationReleased += ClientOnAssociationReleased;

                return client;
            }

            private static void OnEchoResponseReceivedFromRemoteHost(DicomCEchoRequest request, DicomCEchoResponse response)
            {
                LogToDebugConsole($"DICOM Echo Verification request was received by remote host");
                LogToDebugConsole($"Response was received from remote host...");
                LogToDebugConsole($"Verification response status returned was:{response.Status.ToString()}");
            }

            private static void ClientOnAssociationReleased(object sender, EventArgs e)
            {
                LogToDebugConsole("Association was released");
            }

            private static void ClientOnAssociationRejected(object sender, AssociationRejectedEventArgs e)
            {
                LogToDebugConsole($"Association was rejected. Rejected Reason:{e.Reason}");
            }

            private static void ClientOnAssociationAccepted(object sender, AssociationAcceptedEventArgs e)
            {
                var association = e.Association;
                LogToDebugConsole($"Association was accepted by remote host: {association.RemoteHost} running on port: {association.RemotePort}");

                foreach (var presentationContext in association.PresentationContexts)
                {
                    if (presentationContext.Result == DicomPresentationContextResult.Accept)
                    {
                        LogToDebugConsole($"\t {presentationContext.AbstractSyntax} was accepted");
                        LogToDebugConsole($"\t Negotiation result was: {presentationContext.GetResultDescription()}");
                        LogToDebugConsole($"\t Abstract syntax accepted: {presentationContext.AbstractSyntax}");
                        LogToDebugConsole($"\t Transfer syntax accepted: {presentationContext.AcceptedTransferSyntax.ToString()}");
                    }
                    else 
                    {
                        LogToDebugConsole($"\t Presentation context with proposed abstract syntax of '{presentationContext.AbstractSyntax}' was not accepted");
                        LogToDebugConsole($"\t Reject reason was {presentationContext.GetResultDescription()}");
                    }
                }
            }

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

Output shown in program console when running the code sample above is shown below. You can see from the response that an association is first established between the two DICOM devices followed by the response from the remote host that it does supports the DICOM Verification service (it acts as the "Verification SCP" here). It then releases the association as no other operation was requested by the client.


Association was accepted by remote host: www.dicomserver.co.uk running on port: 11112
	 Abstract syntax accepted: Verification SOP Class [1.2.840.10008.1.1]
	 Transfer syntax accepted: Implicit VR Little Endian: Default Transfer Syntax for DICOM
DICOM Echo Verification request was received by remote host
Response was received from remote host...
Verification response status returned was:Success
Association was released

“There are perhaps no days of our childhood we lived so fully as those we spent with a favorite book.” ~ Marcel Proust

In the code illustration below, I have tried to simulate a condition which causes a association rejection by passing a bad application context.

    using System;
    using System.Diagnostics;
    using Dicom;
    using Dicom.Network;

    namespace UnderstandingDicomVerification
    {
        class Program
        {
            static void Main(string[] args)
            {
                try
                {
                    //replace these with your settings
                    //Here, I am using Dr.Dave Harvey's public server 
                    //please be careful not to send any confidential info as all traffic is logged
                    var dicomRemoteHost = "www.dicomserver.co.uk";
                    var dicomRemoteHostPort = 11112;
                    var useTls = false;
                    var ourDotNetTestClientDicomAeTitle = "Our Dot Net Test Client";
                    var remoteDicomHostAeTitle = "Dr.Dave Harvey's Server";

                    //create DICOM client
                    var client = new DicomClient();

                    //Create a meaningless DICOM request. This association should be rejected by the server
                    var badAbstractSyntax = new DicomUID("000", string.Empty, DicomUidType.Unknown);
                    client.AdditionalPresentationContexts.Add(new DicomPresentationContext(0, badAbstractSyntax));

                    //add event handler to track the association rejection event
                    client.AssociationRejected += ClientOnAssociationRejected;

                    //send a bad DICOM request to the remote DICOM server
                    client.Send(dicomRemoteHost, dicomRemoteHostPort, useTls, ourDotNetTestClientDicomAeTitle, remoteDicomHostAeTitle);
                }
                catch (Exception e)
                {
                    LogToDebugConsole("Error was thrown here during DICOM association request");
                    LogToDebugConsole($"Error was: {e.Message}");
                }
            }

            private static void ClientOnAssociationRejected(object sender, AssociationRejectedEventArgs e)
            {
                LogToDebugConsole($"Association was rejected. Rejected Reason:{e.Reason}");
            }

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

Output shown in program console when running the code sample above is shown below. The association is rejected by the remote DICOM peer because it does not understand what we are asking it to do.


Association was rejected. Rejected Reason:ApplicationContextNotSupported
Error should be thrown here during DICOM association request
Error was: Association rejected [result: Permanent; source: ServiceUser; reason: ApplicationContextNotSupported]

This concludes my short introductory tutorial on DICOM association establishment/negotiation. This is an extremely important aspect of DICOM networking and I have tried to convey the very essence of this complex aspect of DICOM communications hopefully in a way that made sense to you in this short tutorial. I will be covering the DIMSEs themselves through separate tutorials of their own in this series. If you have any questions or comments regarding this tutorial, please feel free to send me an email. Please note that I may not get back to you right away due to work and other commitments. In the next tutorial in this series, I will cover query/retrieve-related operations in DICOM. See you then.