Query/Retrieve operations allow you to search for and retrieve DICOM objects from remote systems.
C-FIND Query Service
C-FIND searches for studies, series, or images based on matching criteria.
Query Levels
Implementing Study-Level Query
Study-level queries are the most common starting point for searching a PACS archive. By specifying search criteria like patient name, date range, or modality, you can retrieve a list of matching studies. Each result includes the Study Instance UID needed for subsequent series and image queries.
public class DicomQueryService
{
public List<DicomDataSet> QueryStudies(
string callingAE,
string calledAE,
string host,
int port,
string patientName = "",
string studyDate = "",
string modality = "")
{
List<DicomDataSet> results = new List<DicomDataSet>();
DicomAssociation association = new DicomAssociation
{
CallingAETitle = callingAE,
CalledAETitle = calledAE,
Host = host,
Port = port
};
// Add Study Root Query/Retrieve presentation context
association.AddPresentationContext(
SOPClasses.StudyRootQueryRetrieve,
DicomTransferSyntax.ExplicitVRLittleEndian
);
try
{
association.Open();
// Create query dataset
DicomDataSet query = CreateStudyLevelQuery(patientName, studyDate, modality);
// Create C-FIND request
DicomCommandSet request = new DicomCommandSet(DicomCommandType.C_FIND_RQ);
request.AffectedSOPClassUID = SOPClasses.StudyRootQueryRetrieve;
request.MessageID = 1;
request.Priority = 0; // MEDIUM priority
// Send query and collect results
DicomCommandSet response = null;
DicomDataSet responseData = null;
do
{
response = association.SendRequest(request, query, out responseData);
if (responseData != null && response.Status == 0xFF00) // Pending
{
results.Add(responseData);
DisplayStudyResult(responseData);
}
} while (response.Status == 0xFF00); // Continue while pending
if (response.Status == 0) // Success
{
Console.WriteLine($"\nQuery completed. Found {results.Count} studies.");
}
else
{
Console.WriteLine($"Query failed with status: 0x{response.Status:X4}");
}
}
catch (Exception ex)
{
Console.WriteLine($"Query error: {ex.Message}");
}
finally
{
if (association.IsOpen)
{
association.Close();
}
}
return results;
}
private DicomDataSet CreateStudyLevelQuery(
string patientName, string studyDate, string modality)
{
DicomDataSet query = new DicomDataSet();
// Query/Retrieve Level - MUST specify
query.Add(DicomTag.QueryRetrieveLevel, "STUDY");
// Matching Keys (values to search for)
query.Add(DicomTag.PatientName, patientName); // Empty = wildcard
query.Add(DicomTag.StudyDate, studyDate);
query.Add(DicomTag.ModalitiesInStudy, modality);
// Return Keys (empty values = return these fields)
query.Add(DicomTag.PatientID, "");
query.Add(DicomTag.PatientBirthDate, "");
query.Add(DicomTag.PatientSex, "");
query.Add(DicomTag.StudyInstanceUID, "");
query.Add(DicomTag.StudyTime, "");
query.Add(DicomTag.AccessionNumber, "");
query.Add(DicomTag.StudyDescription, "");
query.Add(DicomTag.StudyID, "");
query.Add(DicomTag.NumberOfStudyRelatedSeries, "");
query.Add(DicomTag.NumberOfStudyRelatedInstances, "");
return query;
}
private void DisplayStudyResult(DicomDataSet result)
{
Console.WriteLine("\n=== Study Found ===");
Console.WriteLine($"Patient Name: {result[DicomTag.PatientName]?.Value}");
Console.WriteLine($"Patient ID: {result[DicomTag.PatientID]?.Value}");
Console.WriteLine($"Study Date: {result[DicomTag.StudyDate]?.Value}");
Console.WriteLine($"Description: {result[DicomTag.StudyDescription]?.Value}");
Console.WriteLine($"Modality: {result[DicomTag.ModalitiesInStudy]?.Value}");
Console.WriteLine($"Study UID: {result[DicomTag.StudyInstanceUID]?.Value}");
}
}
Series-Level Query
After identifying a study of interest, you can drill down to query its series. Series-level queries require the Study Instance UID and optionally filter by modality. This helps identify specific imaging sequences within a multi-series study.
public List<DicomDataSet> QuerySeries(
string callingAE,
string calledAE,
string host,
int port,
string studyInstanceUID,
string modality = "")
{
List<DicomDataSet> results = new List<DicomDataSet>();
// ... association setup ...
// Create series level query
DicomDataSet query = new DicomDataSet();
query.Add(DicomTag.QueryRetrieveLevel, "SERIES");
// Study Level - must include study UID
query.Add(DicomTag.StudyInstanceUID, studyInstanceUID);
// Series Level matching and return keys
query.Add(DicomTag.SeriesInstanceUID, "");
query.Add(DicomTag.Modality, modality);
query.Add(DicomTag.SeriesNumber, "");
query.Add(DicomTag.SeriesDescription, "");
query.Add(DicomTag.NumberOfSeriesRelatedInstances, "");
// ... send request and collect results ...
return results;
}
Image-Level Query
Image-level queries return individual instance information within a series. This level requires both Study and Series Instance UIDs. Use image-level queries when you need to retrieve specific slices or verify which instances exist before retrieval.
public List<DicomDataSet> QueryImages(
string callingAE,
string calledAE,
string host,
int port,
string studyInstanceUID,
string seriesInstanceUID)
{
List<DicomDataSet> results = new List<DicomDataSet>();
// ... association setup ...
// Create image level query
DicomDataSet query = new DicomDataSet();
query.Add(DicomTag.QueryRetrieveLevel, "IMAGE");
// Required parent keys
query.Add(DicomTag.StudyInstanceUID, studyInstanceUID);
query.Add(DicomTag.SeriesInstanceUID, seriesInstanceUID);
// Image Level return keys
query.Add(DicomTag.SOPInstanceUID, "");
query.Add(DicomTag.InstanceNumber, "");
query.Add(DicomTag.SOPClassUID, "");
// ... send request and collect results ...
return results;
}
C-MOVE Retrieve Service
C-MOVE instructs the server to send images to a specified destination.
C-MOVE Architecture
Implementing C-MOVE
The following implementation sends a C-MOVE request to retrieve a complete study. The MoveDestination parameter specifies where images should be sent. Ensure the destination AE Title is configured on the PACS server before initiating the move.
public class DicomRetrieveService
{
public bool MoveStudy(
string callingAE,
string calledAE,
string host,
int port,
string moveDestinationAE, // Where to send the images
string studyInstanceUID)
{
DicomAssociation association = new DicomAssociation
{
CallingAETitle = callingAE,
CalledAETitle = calledAE,
Host = host,
Port = port
};
// Add Study Root Move presentation context
association.AddPresentationContext(
SOPClasses.StudyRootQueryRetrieve,
DicomTransferSyntax.ExplicitVRLittleEndian
);
try
{
association.Open();
// Create move dataset
DicomDataSet moveData = new DicomDataSet();
moveData.Add(DicomTag.QueryRetrieveLevel, "STUDY");
moveData.Add(DicomTag.StudyInstanceUID, studyInstanceUID);
// Create C-MOVE request
DicomCommandSet request = new DicomCommandSet(DicomCommandType.C_MOVE_RQ);
request.AffectedSOPClassUID = SOPClasses.StudyRootQueryRetrieve;
request.MessageID = 1;
request.Priority = 0;
request.MoveDestination = moveDestinationAE; // Critical: where to send
// Send move request
DicomCommandSet response;
DicomDataSet responseData;
do
{
response = association.SendRequest(request, moveData, out responseData);
if (response.Status == 0xFF00) // Pending
{
// Display progress
ushort remaining = response.NumberOfRemainingSuboperations;
ushort completed = response.NumberOfCompletedSuboperations;
ushort failed = response.NumberOfFailedSuboperations;
Console.WriteLine($"Progress: {completed} completed, " +
$"{remaining} remaining, {failed} failed");
}
} while (response.Status == 0xFF00);
if (response.Status == 0)
{
Console.WriteLine("C-MOVE completed successfully");
return true;
}
else
{
Console.WriteLine($"C-MOVE failed: Status 0x{response.Status:X4}");
return false;
}
}
catch (Exception ex)
{
Console.WriteLine($"C-MOVE error: {ex.Message}");
return false;
}
finally
{
if (association.IsOpen)
{
association.Close();
}
}
}
}
C-GET Retrieve Service
C-GET retrieves images directly back to the requester (simpler than C-MOVE).
C-GET Architecture
Implementing C-GET
C-GET requires handling incoming C-STORE sub-operations from the SCP. You must register storage presentation contexts for all image types you expect to receive. The handler must respond to each incoming image to acknowledge receipt.
public class DicomGetService
{
private List<DicomDataSet> retrievedInstances = new List<DicomDataSet>();
public List<DicomDataSet> GetStudy(
string callingAE,
string calledAE,
string host,
int port,
string studyInstanceUID)
{
retrievedInstances.Clear();
DicomAssociation association = new DicomAssociation
{
CallingAETitle = callingAE,
CalledAETitle = calledAE,
Host = host,
Port = port
};
// Add C-GET presentation context
association.AddPresentationContext(
SOPClasses.StudyRootQueryRetrieve,
DicomTransferSyntax.ExplicitVRLittleEndian
);
// IMPORTANT: Also need storage presentation contexts to receive images
association.AddPresentationContext(
SOPClasses.CTImageStorage,
DicomTransferSyntax.ExplicitVRLittleEndian
);
association.AddPresentationContext(
SOPClasses.MRImageStorage,
DicomTransferSyntax.ExplicitVRLittleEndian
);
// Handle incoming C-STORE requests from the SCP
association.StoreRequest += OnStoreRequest;
try
{
association.Open();
// Create C-GET dataset
DicomDataSet getData = new DicomDataSet();
getData.Add(DicomTag.QueryRetrieveLevel, "STUDY");
getData.Add(DicomTag.StudyInstanceUID, studyInstanceUID);
// Create C-GET request
DicomCommandSet request = new DicomCommandSet(DicomCommandType.C_GET_RQ);
request.AffectedSOPClassUID = SOPClasses.StudyRootQueryRetrieve;
request.MessageID = 1;
request.Priority = 0;
// Send C-GET request
DicomCommandSet response;
DicomDataSet responseData;
do
{
response = association.SendRequest(request, getData, out responseData);
if (response.Status == 0xFF00) // Pending
{
ushort remaining = response.NumberOfRemainingSuboperations;
ushort completed = response.NumberOfCompletedSuboperations;
Console.WriteLine($"C-GET Progress: {completed}/{completed + remaining}");
}
} while (response.Status == 0xFF00);
if (response.Status == 0)
{
Console.WriteLine($"Retrieved {retrievedInstances.Count} instances");
}
}
catch (Exception ex)
{
Console.WriteLine($"C-GET error: {ex.Message}");
}
finally
{
if (association.IsOpen)
{
association.Close();
}
}
return retrievedInstances;
}
private void OnStoreRequest(object sender, StoreRequestEventArgs e)
{
// Called when the server sends us an image via C-STORE
try
{
retrievedInstances.Add(e.DataSet);
Console.WriteLine($"Received: {e.DataSet[DicomTag.SOPInstanceUID]?.Value}");
// Send success response
DicomCommandSet response = new DicomCommandSet(DicomCommandType.C_STORE_RSP);
response.Status = 0; // Success
e.Association.SendResponse(response, null);
}
catch (Exception ex)
{
Console.WriteLine($"Error handling C-STORE: {ex.Message}");
DicomCommandSet response = new DicomCommandSet(DicomCommandType.C_STORE_RSP);
response.Status = 0xA700; // Out of resources
e.Association.SendResponse(response, null);
}
}
}
Comparison: C-MOVE vs C-GET
| Aspect | C-MOVE | C-GET |
|---|---|---|
| Destination | Third party | Back to requester |
| SCU SCP required | Destination must run SCP | Requester handles storage |
| Legacy support | Widely supported | Less common |
| Firewall friendly | Needs open ports on dest | More firewall friendly |
| Architecture | More complex | Simpler |
| Use case | Multi-destination routing | Direct retrieval |
Choose based on your architecture requirements and infrastructure constraints.
Related Articles
Learn more about DICOM query and retrieve operations:
Java Implementation:
- DICOM Basics using Java - Query and Retrieve Operations (C-FIND) - C-FIND implementation
- DICOM Basics using Java - Query and Retrieve Operations (C-MOVE) - C-MOVE implementation
- DICOM Basics using Java - Query and Retrieve Operations (C-GET) - C-GET implementation
.NET Implementation:
- DICOM Basics using .NET - Query and Retrieve Operations (C-FIND) - .NET C-FIND
- DICOM Basics using .NET - Query and Retrieve Operations (C-MOVE) - .NET C-MOVE
- DICOM Basics using .NET - Query and Retrieve Operations (C-GET) - .NET C-GET
Testing Tools:
- Orthanc DICOM Server for Testing - Test server setup