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:

  1. C-GET SCU sends a C-GET request to the C-GET SCP
  2. The server switches roles and sends data back on the same association
  3. The C-GET SCU receives data as a C-STORE SCP on the same connection
  4. 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

AspectC-GETC-MOVE
AssociationsSingleTwo separate
Incoming port requiredNoYes
AE registration on serverNot requiredRequired
Send to third partyNoYes
Historical supportLess commonUniversal
Firewall complexityLowHigh
Best forInternet/mobileLAN/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.