DICOM Basics using .NET and C# - Push Operations (C-STORE)
How the C-STORE Composite Service Works
This article is part of my series of articles on the DICOM standard. If you are totally new to DICOM, please have a quick look at my earlier article titled “DICOM Explained: How Medical Images Are Stored, Moved and Shared in 2026” for a quick introduction to the standard. It may also be useful to look at the other tutorials in this series to get up to speed on a number of topics including DICOM Encoding, SOPs and IODs. My DICOM networking-related tutorials on DICOM Verification and on DICOM Associations should also provide some foundations to help understand the material that is covered here. 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.
Introduction
In this tutorial, we are going to look at a DICOM composite service called C-STORE that helps push data to a remote server for storage (see my tutorial on DICOM Associations if you want to understand what 'composite' and 'normalized' services are). We are going to explore how to build a C-STORE SCU client that can perform such an operation.
Fellow Oak (fo-dicom) DICOM Toolkit - Quick Overview
For the purposes of illustrating many aspects of DICOM that I plan to cover in this tutorial series, I will be using a freely available and powerful DICOM toolkit called fo-dicom DICOM Toolkit. This is a completely stand-alone DICOM toolkit that implements functionality such as DICOM file and directory processing as well DICOM networking-related operations. This toolkit is completely free for both commercial or non-profit use. The use of this toolkit in my tutorial does not in anyway imply my official endorsement of it for implementing a production application. Every situation is unique, and only you are ultimately in the best position to decide that. This article is also not meant to be a tutorial on the Fellow Oak DICOM Toolkit, and my focus here is simply to tie DICOM theory to what a practical (although simple) implementation might look like. So, if your goal is to learn how to use the Fellow Oak DICOM library, I would encourage you to visit its website itself or check out the fo-dicom issues pages for details.
“Make your interests gradually wider and more impersonal, until bit by bit the walls of the ego recede, and your life becomes increasingly merged in the universal life. An individual human existence should be like a river — small at first, narrowly contained within its banks, and rushing passionately past rocks and over waterfalls. Gradually the river grows wider, the banks recede, the waters flow more quietly, and in the end, without any visible break, they become merged in the sea, and painlessly lose their individual being.” ~ Bertrand Russell
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.
- Download a .NET IDE or Editor such as Visual Studio IDE or Visual Studio Code (even a text editor should suffice)
- Download the Fellow Oak DICOM library either through NuGet Package Manager or download the source code directly from here
- You can also find the source code and images used in this tutorial on GitHub
- You can download more DICOM images from this site if you want as well
- Also included with the source code is the Orthanc Server configuration file (orthanc.json) for your reference
PACS Server Requirement
In addition to the tools described above, you will also need a DICOM server to execute some of the operations described in this tutorial. If you don't have access to one, you can download one of the many open source PACS servers available on the Internet. Orthanc Server is one such tool and is relatively straightforward set up and get going. Please see my article on getting started with Orthanc Server for more information.
Pushing Data for Storage using C-STORE
If you recall my tutorial on DICOM associations, you will recall that before any two DICOM devices can exchange service requests and results between each other, an association first needs to be established. During the association establishment/negotiation, several activities happen. The two devices first check to see if they are accessible and can actually 'speak' DICOM (the association negotiation process via A-ASSOCIATE handles this). Note that while "DICOM ping" (more formally known as "C-Echo", which we saw in an earlier tutorial) is useful for verifying connectivity, it is not a prerequisite for other DICOM operations -- it is the association negotiation (A-ASSOCIATE) itself that establishes communication capability. They also check to see if they are allowed to communicate with one another from a DICOM security standpoint. This is done by checking whether the DICOM AE configurations have been set up on both sides. They then check to see if they support the DICOM operation that is desired often referred to as the "abstract syntax". Then, as a last step, they must check to see if they can agree on a "transfer syntax" for the information being exchanged (such as the VR encoding, compression and the byte ordering to be used). All these steps have to be completed before they proceed to performing the actual operations that are desired.
During the operational execution phase, the two DICOM peers exchange something called DICOM Message Service Elements (DIMSEs) with one another. These objects help indicate the actual operation that is required to be peformed, and they are accompanied by additional data referred to as Information Object Definitions (IODs) such as textual information or images that these operations are performed on. Together, these service elements and information objects they act on combine to form what are referred to in DICOM as Service Object Pairs (SOPs). The type of DIMSEs exchanged vary according to the type of operation being performed. 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-RQ' 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 (using a 'C-STORE-RSP' object) 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.
The structure of the C-Store message request and message response objects is shown below. The tables below are adapted from Section 9.3.1 of Part 7 (PS3.7) of the DICOM standard, which covers message exchange. The client may abort the association anytime while the store operation is in progress (using an 'A-ABORT' request), though this will terminate the entire association rather than just cancelling the individual store operation. Note that unlike C-FIND, C-GET, and C-MOVE, the DICOM standard does not define a C-CANCEL operation for C-STORE. Please see the official DICOM documentation for more details as the specification is huge and I can only provide an overview in this article.
C-STORE-RQ - request message fields
| Message field | Tag | VR | VM | Description |
|---|---|---|---|---|
| Command Group Length | (0000,0000) | UL | 1 | The even number of bytes from the end of this value field to the beginning of the next group |
| Affected SOP Class UID | (0000,0002) | UI | 1 | SOP Class UID associated with the operation |
| Command Field | (0000,0100) | US | 1 | Distinguishes the DIMSE-C operation conveyed by this message - 0001H for C-STORE-RQ |
| Message ID | (0000,0110) | US | 1 | Implementation-specific value that distinguishes this message from other messages |
| Priority | (0000,0700) | US | 1 | LOW = 0002H, MEDIUM = 0000H, HIGH = 0001H |
| Command Data Set Type | (0000,0800) | US | 1 | Any value other than 0101H - indicates that a data set is present in the message |
| Affected SOP Instance UID | (0000,1000) | UI | 1 | UID of the SOP Instance being stored |
| Move Originator Application Entity Title | (0000,1030) | AE | 1 | Only when this store is a C-MOVE sub-operation: AE title of the AE that invoked the C-MOVE |
| Move Originator Message ID | (0000,1031) | US | 1 | Only when this store is a C-MOVE sub-operation: Message ID of the originating C-MOVE-RQ |
| Data Set | (no tag) | - | - | The SOP Instance itself - the complete DICOM object being stored |
C-STORE-RSP - response message fields
| Message field | Tag | VR | VM | Description |
|---|---|---|---|---|
| Command Group Length | (0000,0000) | UL | 1 | The even number of bytes from the end of this value field to the beginning of the next group |
| Affected SOP Class UID | (0000,0002) | UI | 1 | SOP Class UID associated with the operation |
| Command Field | (0000,0100) | US | 1 | Distinguishes the DIMSE-C operation conveyed by this message - 8001H for C-STORE-RSP |
| Message ID Being Responded To | (0000,0120) | US | 1 | Set to the value of the Message ID (0000,0110) used in the associated request |
| Command Data Set Type | (0000,0800) | US | 1 | 0101H - indicates that no data set is present in the message |
| Status | (0000,0900) | US | 1 | Result of the operation - Success, Pending, Warning, Failure or Cancel (the status codes are defined in Annex C) |
| Affected SOP Instance UID | (0000,1000) | UI | 1 | UID of the SOP Instance that was stored |
Adapted from DICOM PS3.7, Section 9.3.1 (C-STORE Protocol). A data set that accompanies a command is not itself a command field, hence "no tag".
“To delight a child, to add a new joy to the crowded miracles of childhood, is no less worth doing than to leave a Sistine Chapel to astound a somewhat bored procession of tourists; or to have written a classic that sells by the thousands and is possessed unread by all save an infinitesimal percentage of its owners. It is, then, not an ignoble thing to do one’s very best to give our coming rulers – children – a taste of the Kingdom of Art..” ~ Gleeson White
Example of C-STORE Operation
Let us have quick look at some code now. Here, I create a factory method that provides a IDicomClient object (created via DicomClientFactory) with a C-Store request attached to it. I then attach some event handlers to the DICOM client to let us be notified of various events that occur during association negotiation, store operation responses and also association termination. I then initiate the store operation on the DicomClient object by invoking its SendAsync method passing in the path of the SOP instance that we want to transmit for storage to Orthanc Server. There are many convenient methods and attributes returned in the event handlers to give more insight into many aspects of the DICOM negotiation and store operation such as the abstract syntax, the transfer syntax and meta data for the SOP instance being transmitted for the store operation. I will let you explore those on your own.
using System.Threading;
using System.Threading.Tasks;
using FellowOakDicom.Network;
using System.Threading;
using System.Threading.Tasks;
using FellowOakDicom.Network.Client;
using System;
using System.Diagnostics;
using System.IO;
namespace DICOMEchoVerificationWithOrthancServer
{
public class Program
{
private static readonly string PathToDicomTestFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Test Files", "0002.dcm");
static async Task Main(string[] args)
{
try
{
var dicomRemoteHost = "localhost";
var dicomRemoteHostPort = 4242;
var useTls = false;
var ourDotNetTestClientDicomAeTitle = "FODICOM_SCU";
var remoteDicomHostAeTitle = "ORTHANC";
//create DICOM store SCU client with handlers
var client = CreateDicomStoreClient(PathToDicomTestFile);
//send the verification request to the remote DICOM server
await client.SendAsync(CancellationToken.None);
LogToDebugConsole("Our DICOM CStore operation was successfully completed");
}
catch (Exception e)
{
LogToDebugConsole($"Error occurred during DICOM verification request -> {e.StackTrace}");
}
}
private static DicomClient CreateDicomStoreClient(string fileToTransmit)
{
var client = DicomClientFactory.Create(DicomServerHost, DicomServerPort, UseTls, LocalAeTitle, RemoteAeTitle);
//request for DICOM store operation
var dicomCStoreRequest = new DicomCStoreRequest(fileToTransmit);
//attach an event handler when remote peer responds to store request
dicomCStoreRequest.OnResponseReceived += OnStoreResponseReceivedFromRemoteHost;
await client.AddRequestAsync(dicomCStoreRequest);
//Add a handler to be notified of any association rejections
client.AssociationRejected += OnAssociationRejected;
//Add a handler to be notified of any association information on successful connections
client.AssociationAccepted += OnAssociationAccepted;
//Add a handler to be notified when association is successfully released - this can be triggered by the remote peer as well
client.AssociationReleased += OnAssociationReleased;
return client;
}
private static void OnStoreResponseReceivedFromRemoteHost(DicomCStoreRequest request, DicomCStoreResponse response)
{
LogToDebugConsole("DICOM Store request was received by remote host for storage...");
LogToDebugConsole($"DICOM Store request was received by remote host for SOP instance transmitted for storage:{request.SOPInstanceUID}");
LogToDebugConsole($"Store operation response status returned was:{response.Status}");
}
private static void OnAssociationAccepted(object sender, AssociationAcceptedEventArgs e)
{
LogToDebugConsole($"Association was accepted by:{e.Association.RemoteHost}");
}
private static void OnAssociationRejected(object sender, AssociationRejectedEventArgs e)
{
LogToDebugConsole($"Association was rejected. Rejected Reason:{e.Reason}");
}
private static void OnAssociationReleased(object sender, EventArgs e)
{
LogToDebugConsole("Association was released. BYE BYE!");
}
private static void LogToDebugConsole(string informationToLog)
{
Debug.WriteLine(informationToLog);
}
}
}
“Dreaming, after all, is a form of planning.” ~ Gloria Steinem
Results of running the code example is shown below. As you can see, the association is first established and then the C-STORE-SCP (the Orthanc Server in our case) responds back with the status of the store operation. The association is then released and our entire operation is successfully completed. Please note that one cannot 'batch' files for transmission, and each file has to be transmitted one by one. However, it is common to see some convenient abstractions on top of the C-STORE operation to enable bulk file transmissions in DICOM software. Please note that if the file you are transmitting already exists on the C-STORE SCP, you may receive either a warning or an error, and this is perfectly okay and you should design your software to continue to transmit the rest of the images and not halt the operation entirely as a result of a single error. Please see DICOM documentation on more guidance and tips on this aspect.
Association was accepted by:localhost
DICOM Store request was received by remote host for storage...
DICOM Store request was received by remote host for SOP instance transmitted for storage:Unknown [1.3.12.2.1107.5.4.3.321890.19960124.162922.29]
Store operation response status returned was:Success
Association was released. BYE BYE!
Our DICOM CStore operation was successfully completed
Testing Tools for DICOM Store Operations
When you need to troubleshoot store operations in DICOM, it will helpful to use one of the many useful DICOM testing tools out there. The one that I have used in the past and have liked is DCMTK. The toolkit comes with many standalone testing utilities that help you test various aspects related to DICOM processing through a command line interface. The storescu command is the one that I will use here to push a local DICOM file to Dr. Dave Harvey's free online DICOM test server provided here. The remote server is listening to requests on port 104 in this example. The console output generated when running this command is shown below for reference.
C:\SaravananDicomTestingTools\DCMTK-3.6.5-Win64-Dynamic\bin>storescu.exe -aet OurStoreScu "www.dicomserver.co.uk" 104 -aec MEDCONNECTIONS C:\DICOMImages\1.2.826.0.1.3680043.11.106\2.25.101703809854595919801950834747690813074.dcm -v
I: checking input files ...
I: Requesting Association
I: Association Accepted (Max Send PDV: 65524)
I: Sending file: C:\Junk\DICOMImages\1.2.826.0.1.3680043.11.106\2.25.101703809854595919801950834747690813074.dcm
I: Converting transfer syntax: Little Endian Explicit -> Little Endian Explicit
I: Sending Store Request (MsgID 1, OPb)
XMIT: .........................................................................................................................................................................
I: Received Store Response (Unknown Status: 0x111)
I: Releasing Association
Conclusion
This concludes the article on how the C-STORE operation works in DICOM. This is one of the most important operations in DICOM, and is often used to transmit DICOM images from MRIs, CTs and other modalities to PACS serves and other remote destinations.
Please check out the next tutorial in this series where we cover DICOM Storage Commitment service.