DICOM Basics using .NET and C# - Query and Retrieve Operations (C-GET)
Introduction
This is part of my series of articles on the DICOM standard. In this tutorial, we'll explore the C-GET composite service, another method for retrieving DICOM data. Unlike C-MOVE, C-GET operates over a single association, making it simpler to configure and more firewall-friendly.
Before diving in, please review my earlier tutorials on C-FIND and C-MOVE as understanding those operations will help you appreciate the differences and when to use each approach.
Prerequisites
Before you begin, ensure you have the following:
- A .NET development environment (Visual Studio or Visual Studio Code)
- The Fellow Oak DICOM library (fo-dicom) installed via NuGet
- A PACS server that supports C-GET (Orthanc, DCM4CHEE, or similar)
- You can find all the code demonstrated in this tutorial on GitHub here
“The only true voyage of discovery would be not to visit strange lands but to possess other eyes.” ~ Marcel Proust
How C-GET Works
C-GET is a more modern and intuitive approach to retrieving DICOM data:
- C-GET SCU sends a C-GET request to the C-GET SCP
- The server switches roles and sends data back on the same association
- The C-GET SCU receives data as a C-STORE SCP on the same connection
- No separate incoming connection is required
Key advantages of C-GET:
- Single association - no need for separate C-STORE SCP port
- Firewall-friendly - no incoming connections required
- Simpler configuration - no AE title registration on server needed
- Works better for web/mobile applications behind NAT
C-GET vs C-MOVE Comparison
| Aspect | C-GET | C-MOVE |
|---|---|---|
| Associations | Single | Two separate |
| Incoming port required | No | Yes |
| AE registration on server | Not required | Required |
| Send to third party | No | Yes |
| Historical support | Less common | Universal |
| Firewall complexity | Low | High |
| Best for | Internet/mobile | LAN/Enterprise |
Step 1 of 3: Setting Up the C-GET SCU
Unlike C-MOVE, we don't need a separate C-STORE SCP server. The storage handling is integrated into the same client:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using FellowOakDicom;
using System.Threading;
using System.Threading.Tasks;
using FellowOakDicom.Network;
using System.Threading;
using System.Threading.Tasks;
using FellowOakDicom.Network.Client;
namespace DicomCGetExample
{
public class Program
{
// Configuration
private static readonly string PacsServerHost = "localhost";
private static readonly int PacsServerPort = 4242;
private static readonly string PacsAeTitle = "ORTHANC";
private static readonly string LocalAeTitle = "FODICOM_GET";
private static readonly string OutputDirectory =
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "RetrievedImages");
public static async Task Main(string[] args)
{
try
{
LogToDebugConsole("=== DICOM C-GET Tutorial ===");
LogToDebugConsole("");
LogToDebugConsole("C-GET retrieves data over a single association.");
LogToDebugConsole("No separate C-STORE SCP is required.");
LogToDebugConsole("");
// Ensure output directory exists
if (!Directory.Exists(OutputDirectory))
{
Directory.CreateDirectory(OutputDirectory);
}
// Step 1: Query for studies (C-FIND)
LogToDebugConsole("Step 1: Querying for studies...");
var studyInfo = await FindStudyAsync();
if (studyInfo != null)
{
// Step 2: Retrieve the study using C-GET
LogToDebugConsole($"Step 2: Retrieving study: {studyInfo.StudyInstanceUid}");
await GetStudyAsync(studyInfo);
}
LogToDebugConsole("");
LogToDebugConsole("C-GET operation completed.");
LogToDebugConsole($"Retrieved files saved to: {OutputDirectory}");
}
catch (Exception e)
{
LogToDebugConsole($"Error: {e.Message}");
LogToDebugConsole(e.StackTrace);
}
}
private static void LogToDebugConsole(string message)
{
Debug.WriteLine(message);
}
}
public class StudyInfo
{
public string StudyInstanceUid { get; set; }
public string PatientName { get; set; }
public HashSet<string> SopClassesInStudy { get; set; } = new HashSet<string>();
}
}
Step 2 of 3: Finding Studies and SOP Classes
For C-GET to work efficiently, we need to know which SOP Classes are in the study so we can propose appropriate presentation contexts:
private static async Task<StudyInfo> FindStudyAsync()
{
StudyInfo result = null;
var client = DicomClientFactory.Create(DicomServerHost, DicomServerPort, UseTls, LocalAeTitle, RemoteAeTitle);
// Query for studies - also request SOPClassesInStudy
var findRequest = DicomCFindRequest.CreateStudyQuery(patientName: "*");
findRequest.Dataset.AddOrUpdate(DicomTag.StudyDate, "");
findRequest.Dataset.AddOrUpdate(DicomTag.StudyDescription, "");
findRequest.Dataset.AddOrUpdate(DicomTag.SOPClassesInStudy, "");
findRequest.OnResponseReceived += (request, response) =>
{
if (response.Status == DicomStatus.Pending && response.Dataset != null)
{
var studyUid = response.Dataset.GetSingleValueOrDefault(
DicomTag.StudyInstanceUID, "");
var patientName = response.Dataset.GetSingleValueOrDefault(
DicomTag.PatientName, "");
LogToDebugConsole($" Found: {patientName}");
LogToDebugConsole($" Study UID: {studyUid}");
// Store first study found
if (result == null && !string.IsNullOrEmpty(studyUid))
{
result = new StudyInfo
{
StudyInstanceUid = studyUid,
PatientName = patientName
};
// Get SOP Classes in study (if available)
var sopClasses = response.Dataset.GetValues<string>(
DicomTag.SOPClassesInStudy);
if (sopClasses != null)
{
foreach (var sopClass in sopClasses)
{
result.SopClassesInStudy.Add(sopClass);
LogToDebugConsole($" SOP Class: {sopClass}");
}
}
}
}
};
await client.AddRequestAsync(findRequest);
await client.SendAsync(PacsServerHost, PacsServerPort, false,
LocalAeTitle, PacsAeTitle);
return result;
}
Step 3 of 3: Implementing the C-GET Request
The C-GET request includes a handler for receiving images on the same association:
private static async Task GetStudyAsync(StudyInfo studyInfo)
{
var client = DicomClientFactory.Create(DicomServerHost, DicomServerPort, UseTls, LocalAeTitle, RemoteAeTitle);
// Create C-GET request
var getRequest = new DicomCGetRequest(studyInfo.StudyInstanceUid);
int receivedCount = 0;
// Handle incoming images (C-STORE sub-operation on same association)
client.OnCStoreRequest += (DicomCStoreRequest storeRequest) =>
{
receivedCount++;
// Save the received file
var fileName = Path.Combine(OutputDirectory,
$"{storeRequest.SOPInstanceUID.UID}.dcm");
storeRequest.File.Save(fileName);
LogToDebugConsole($" Received [{receivedCount}]: {storeRequest.SOPInstanceUID.UID}");
// Return success status
return Task.FromResult(new DicomCStoreResponse(storeRequest, DicomStatus.Success));
};
// Track C-GET progress
getRequest.OnResponseReceived += (request, response) =>
{
if (response.Status == DicomStatus.Pending)
{
var completed = response.Dataset?.GetSingleValueOrDefault(
DicomTag.NumberOfCompletedSuboperations, 0) ?? 0;
var remaining = response.Dataset?.GetSingleValueOrDefault(
DicomTag.NumberOfRemainingSuboperations, 0) ?? 0;
LogToDebugConsole($" Progress: {completed} completed, {remaining} remaining");
}
else if (response.Status == DicomStatus.Success)
{
LogToDebugConsole($" C-GET completed successfully!");
LogToDebugConsole($" Total files received: {receivedCount}");
}
else
{
LogToDebugConsole($" C-GET status: {response.Status}");
}
};
await client.AddRequestAsync(getRequest);
// Add presentation contexts for expected SOP Classes
// This is important - we need to accept the storage classes
if (studyInfo.SopClassesInStudy.Count > 0)
{
foreach (var sopClass in studyInfo.SopClassesInStudy)
{
client.AdditionalPresentationContexts.Add(
new DicomPresentationContext(
0, // Will be assigned automatically
DicomUID.Parse(sopClass),
DicomTransferSyntax.ExplicitVRLittleEndian,
DicomTransferSyntax.ImplicitVRLittleEndian));
}
}
LogToDebugConsole($" Sending C-GET request...");
await client.SendAsync(PacsServerHost, PacsServerPort, false,
LocalAeTitle, PacsAeTitle);
}
Sample output:
=== DICOM C-GET Tutorial ===
C-GET retrieves data over a single association.
No separate C-STORE SCP is required.
Step 1: Querying for studies...
Found: Smith^John
Study UID: 1.2.840.113619.2.55.3.12345
SOP Class: 1.2.840.10008.5.1.4.1.1.2
Step 2: Retrieving study: 1.2.840.113619.2.55.3.12345
Sending C-GET request...
Received [1]: 1.2.840.113619.2.55.3.12345.1
Progress: 1 completed, 9 remaining
Received [2]: 1.2.840.113619.2.55.3.12345.2
Progress: 2 completed, 8 remaining
...
Received [10]: 1.2.840.113619.2.55.3.12345.10
Progress: 10 completed, 0 remaining
C-GET completed successfully!
Total files received: 10
C-GET operation completed.
Retrieved files saved to: C:\...\RetrievedImages
Handling Unknown SOP Classes
If you don't know the SOP Classes in advance, you can propose all common storage SOP Classes:
// Add all common storage SOP classes
var commonStorageClasses = new[]
{
DicomUID.CTImageStorage,
DicomUID.MRImageStorage,
DicomUID.UltrasoundImageStorage,
DicomUID.DigitalXRayImageStorageForPresentation,
DicomUID.SecondaryCaptureImageStorage,
// Add more as needed
};
foreach (var sopClass in commonStorageClasses)
{
client.AdditionalPresentationContexts.Add(
new DicomPresentationContext(
0,
sopClass,
DicomTransferSyntax.ExplicitVRLittleEndian,
DicomTransferSyntax.ImplicitVRLittleEndian));
}
Server Support for C-GET
Not all PACS servers support C-GET. To enable C-GET in Orthanc, ensure your configuration includes:
{
"DicomServerEnabled": true,
"DicomPort": 4242
}
Orthanc supports C-GET by default. For other servers, check their documentation.
When to Use C-GET vs C-MOVE
Use C-GET when:
- Your application is behind a firewall or NAT
- You're building a web or mobile application
- You want simpler configuration
- The server supports C-GET
Use C-MOVE when:
- You need to route images to a third-party destination
- Working with legacy systems that only support C-MOVE
- You're in a controlled enterprise environment
- Universal compatibility is required
Conclusion
C-GET provides a simpler, more firewall-friendly alternative to C-MOVE for retrieving DICOM data. By operating over a single association, it eliminates the need for a separate C-STORE SCP and incoming port configuration.
While C-MOVE remains more widely supported, C-GET is increasingly important for modern applications, especially those operating over the internet or behind restrictive firewalls. Understanding both operations allows you to choose the right approach for your specific use case.
Please check out the next tutorial in this series where we cover DICOM C-STORE push operations.