C-STORE is the fundamental DICOM service for sending images between systems.
Storage SCU Implementation
A Storage SCU sends DICOM objects to a Storage SCP. Building a robust storage client requires proper presentation context negotiation, error handling, and status code interpretation. The SCU must match the SOP Class of each image with an accepted presentation context.
Basic Storage Implementation
This implementation demonstrates sending a single DICOM file to a remote storage SCP. The presentation context is configured based on the image’s SOP Class to ensure compatibility.
public class DicomStorageService
{
public bool StoreImage(
string callingAE,
string calledAE,
string host,
int port,
string dicomFilePath)
{
// Read the DICOM file
DicomImage image = new DicomImage(dicomFilePath);
// Get SOP Class from the image
string sopClassUID = image.Attributes[DicomTag.SOPClassUID]?.Value.ToString();
string sopInstanceUID = image.Attributes[DicomTag.SOPInstanceUID]?.Value.ToString();
// Create association
DicomAssociation association = new DicomAssociation
{
CallingAETitle = callingAE,
CalledAETitle = calledAE,
Host = host,
Port = port
};
// Add presentation context for this SOP Class
association.AddPresentationContext(
sopClassUID,
DicomTransferSyntax.ExplicitVRLittleEndian,
DicomTransferSyntax.ImplicitVRLittleEndian
);
try
{
association.Open();
// Check if presentation context was accepted
if (!association.IsPresentationContextAccepted(sopClassUID))
{
Console.WriteLine($"SOP Class {sopClassUID} not supported by SCP");
return false;
}
// Create C-STORE request
DicomCommandSet request = new DicomCommandSet(DicomCommandType.C_STORE_RQ);
request.AffectedSOPClassUID = sopClassUID;
request.AffectedSOPInstanceUID = sopInstanceUID;
request.MessageID = 1;
request.Priority = 0; // MEDIUM
// Send the image
DicomCommandSet response = association.SendRequest(request, image.DataSet);
if (response.Status == 0)
{
Console.WriteLine($"Successfully stored: {sopInstanceUID}");
return true;
}
else
{
Console.WriteLine($"Storage failed: Status 0x{response.Status:X4}");
return false;
}
}
catch (Exception ex)
{
Console.WriteLine($"Storage error: {ex.Message}");
return false;
}
finally
{
if (association.IsOpen)
{
association.Close();
}
}
}
}
Batch Storage Implementation
When storing multiple images, reuse a single association for better performance rather than opening a new connection for each file. Group files by SOP Class to negotiate all required presentation contexts upfront.
public class BatchStorageService
{
public StorageResults StoreBatch(
string callingAE,
string calledAE,
string host,
int port,
List<string> dicomFiles)
{
StorageResults results = new StorageResults();
// Group files by SOP Class for efficient association usage
var filesBySopClass = GroupBySopClass(dicomFiles);
DicomAssociation association = new DicomAssociation
{
CallingAETitle = callingAE,
CalledAETitle = calledAE,
Host = host,
Port = port
};
// Add presentation contexts for all SOP Classes
foreach (string sopClass in filesBySopClass.Keys)
{
association.AddPresentationContext(
sopClass,
DicomTransferSyntax.ExplicitVRLittleEndian,
DicomTransferSyntax.ImplicitVRLittleEndian,
DicomTransferSyntax.JPEGLossless
);
}
try
{
association.Open();
ushort messageID = 1;
foreach (string filePath in dicomFiles)
{
try
{
DicomImage image = new DicomImage(filePath);
string sopClassUID = image.Attributes[DicomTag.SOPClassUID]?.Value.ToString();
string sopInstanceUID = image.Attributes[DicomTag.SOPInstanceUID]?.Value.ToString();
DicomCommandSet request = new DicomCommandSet(DicomCommandType.C_STORE_RQ);
request.AffectedSOPClassUID = sopClassUID;
request.AffectedSOPInstanceUID = sopInstanceUID;
request.MessageID = messageID++;
DicomCommandSet response = association.SendRequest(request, image.DataSet);
if (response.Status == 0)
{
results.SuccessfulFiles.Add(filePath);
}
else
{
results.FailedFiles.Add(filePath,
$"Status: 0x{response.Status:X4}");
}
}
catch (Exception ex)
{
results.FailedFiles.Add(filePath, ex.Message);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Association error: {ex.Message}");
}
finally
{
if (association.IsOpen)
{
association.Close();
}
}
Console.WriteLine($"Storage Results: {results.SuccessfulFiles.Count} success, " +
$"{results.FailedFiles.Count} failed");
return results;
}
private Dictionary<string, List<string>> GroupBySopClass(List<string> files)
{
var groups = new Dictionary<string, List<string>>();
foreach (string file in files)
{
try
{
using (DicomImage image = new DicomImage(file))
{
string sopClass = image.Attributes[DicomTag.SOPClassUID]?.Value.ToString();
if (!groups.ContainsKey(sopClass))
{
groups[sopClass] = new List<string>();
}
groups[sopClass].Add(file);
}
}
catch { }
}
return groups;
}
}
public class StorageResults
{
public List<string> SuccessfulFiles { get; set; } = new List<string>();
public Dictionary<string, string> FailedFiles { get; set; } = new Dictionary<string, string>();
}
Storage SCP Implementation
A Storage SCP receives and stores DICOM objects sent by modalities or other systems. The SCP must handle association negotiation, accept appropriate SOP Classes, receive image data, and persist files to storage. Proper error handling ensures graceful responses even when storage fails.
Basic Storage SCP
The following implementation creates a storage server that organizes received files by Patient ID, Study UID, and Series UID. This hierarchical structure facilitates efficient file management and retrieval.
public class DicomStorageServer
{
private DicomServer server;
private string storageDirectory;
private string aeTitle;
public DicomStorageServer(string aeTitle, int port, string storageDirectory)
{
this.aeTitle = aeTitle;
this.storageDirectory = storageDirectory;
Directory.CreateDirectory(storageDirectory);
// Create server
server = new DicomServer
{
AETitle = aeTitle,
Port = port
};
// Handle association requests
server.AssociationRequested += OnAssociationRequested;
// Handle C-STORE requests
server.StoreRequest += OnStoreRequest;
}
private void OnAssociationRequested(object sender, AssociationRequestEventArgs e)
{
Console.WriteLine($"Association request from {e.CallingAETitle}");
// Accept all common storage SOP Classes
foreach (var pc in e.PresentationContexts)
{
// Accept if we support the SOP Class
if (IsSupportedSOPClass(pc.AbstractSyntax))
{
pc.Result = PresentationContextResult.Accept;
// Select preferred transfer syntax
if (pc.TransferSyntaxes.Contains(DicomTransferSyntax.ExplicitVRLittleEndian))
{
pc.SelectedTransferSyntax = DicomTransferSyntax.ExplicitVRLittleEndian;
}
else
{
pc.SelectedTransferSyntax = pc.TransferSyntaxes[0];
}
}
else
{
pc.Result = PresentationContextResult.RejectAbstractSyntaxNotSupported;
}
}
e.Accept();
}
private void OnStoreRequest(object sender, StoreRequestEventArgs e)
{
try
{
// Extract key information
string patientID = e.DataSet[DicomTag.PatientID]?.Value?.ToString() ?? "UNKNOWN";
string studyUID = e.DataSet[DicomTag.StudyInstanceUID]?.Value?.ToString() ?? "UNKNOWN";
string seriesUID = e.DataSet[DicomTag.SeriesInstanceUID]?.Value?.ToString() ?? "UNKNOWN";
string sopInstanceUID = e.DataSet[DicomTag.SOPInstanceUID]?.Value?.ToString() ?? "UNKNOWN";
// Create directory structure: PatientID/StudyUID/SeriesUID/
string patientDir = Path.Combine(storageDirectory, SanitizeFileName(patientID));
string studyDir = Path.Combine(patientDir, SanitizeFileName(studyUID));
string seriesDir = Path.Combine(studyDir, SanitizeFileName(seriesUID));
Directory.CreateDirectory(seriesDir);
// Save the DICOM file
string filePath = Path.Combine(seriesDir, $"{sopInstanceUID}.dcm");
e.DataSet.Save(filePath);
Console.WriteLine($"Stored: {filePath}");
// Send success response
e.SendResponse(0); // Success status
}
catch (IOException ex)
{
Console.WriteLine($"Storage error: {ex.Message}");
e.SendResponse(0xA700); // Out of resources
}
catch (Exception ex)
{
Console.WriteLine($"Storage error: {ex.Message}");
e.SendResponse(0xC000); // Cannot understand
}
}
private bool IsSupportedSOPClass(string sopClassUID)
{
var supportedClasses = new[]
{
"1.2.840.10008.5.1.4.1.1.2", // CT Image Storage
"1.2.840.10008.5.1.4.1.1.4", // MR Image Storage
"1.2.840.10008.5.1.4.1.1.6.1", // US Image Storage
"1.2.840.10008.5.1.4.1.1.7", // Secondary Capture
"1.2.840.10008.5.1.4.1.1.1", // CR Image Storage
"1.2.840.10008.5.1.4.1.1.1.1", // Digital X-Ray
"1.2.840.10008.5.1.4.1.1.12.1", // X-Ray Angio
"1.2.840.10008.5.1.4.1.1.128" // PET Image Storage
};
return supportedClasses.Contains(sopClassUID);
}
private string SanitizeFileName(string name)
{
foreach (char c in Path.GetInvalidFileNameChars())
{
name = name.Replace(c, '_');
}
return name;
}
public void Start()
{
server.Start();
Console.WriteLine($"Storage SCP started: {aeTitle} on port {server.Port}");
}
public void Stop()
{
server.Stop();
Console.WriteLine("Storage SCP stopped");
}
}
Storage Status Codes
| Status | Name | Description |
|---|---|---|
| 0x0000 | Success | Storage completed successfully |
| 0xA700 | Out of Resources | Cannot store due to resource constraints |
| 0xA900 | Data Set Does Not Match SOP Class | Mismatch between data and declared SOP Class |
| 0xC000 | Cannot Understand | Unable to process the request |
| 0xB000 | Warning | Coercion of Data Elements |
| 0xB007 | Warning | Data Set Does Not Match SOP Class (Elements Coerced) |
Image Routing
Route images from sources to multiple destinations based on configurable rules. DICOM routers are essential components in enterprise imaging environments where images from modalities need to reach multiple systems like PACS, workstations, and cloud archives.
DICOM Router Implementation
This router receives images via its storage SCP, evaluates routing rules based on image metadata, and forwards matching images to configured destinations. Failed transmissions are queued for retry to ensure delivery.
public class DicomRouter
{
private DicomStorageServer inboundServer;
private List<RoutingRule> rules;
private string tempStorageDirectory;
public DicomRouter(string aeTitle, int port, string tempDirectory)
{
tempStorageDirectory = tempDirectory;
rules = new List<RoutingRule>();
inboundServer = new DicomStorageServer(aeTitle, port, tempDirectory);
inboundServer.ImageReceived += OnImageReceived;
}
public void AddRoutingRule(RoutingRule rule)
{
rules.Add(rule);
}
private async void OnImageReceived(object sender, ImageReceivedEventArgs e)
{
Console.WriteLine($"Routing image: {e.SOPInstanceUID}");
foreach (var rule in rules)
{
if (rule.Matches(e.DataSet))
{
await RouteToDestination(e.FilePath, rule.Destination);
}
}
}
private async Task RouteToDestination(string filePath, DicomDestination dest)
{
try
{
var storageService = new DicomStorageService();
bool success = storageService.StoreImage(
dest.CallingAE,
dest.AETitle,
dest.Host,
dest.Port,
filePath
);
if (success)
{
Console.WriteLine($"Routed to {dest.AETitle}");
}
else
{
// Queue for retry
QueueForRetry(filePath, dest);
}
}
catch (Exception ex)
{
Console.WriteLine($"Routing error: {ex.Message}");
QueueForRetry(filePath, dest);
}
}
private void QueueForRetry(string filePath, DicomDestination dest)
{
// Add to retry queue for later processing
Console.WriteLine($"Queued for retry: {filePath} to {dest.AETitle}");
}
public void Start()
{
inboundServer.Start();
Console.WriteLine("DICOM Router started");
}
public void Stop()
{
inboundServer.Stop();
}
}
public class RoutingRule
{
public string Name { get; set; }
public string ModalityFilter { get; set; }
public string BodyPartFilter { get; set; }
public DicomDestination Destination { get; set; }
public bool Matches(DicomDataSet dataSet)
{
// Check modality
if (!string.IsNullOrEmpty(ModalityFilter))
{
string modality = dataSet[DicomTag.Modality]?.Value?.ToString() ?? "";
if (modality != ModalityFilter)
return false;
}
// Check body part
if (!string.IsNullOrEmpty(BodyPartFilter))
{
string bodyPart = dataSet[DicomTag.BodyPartExamined]?.Value?.ToString() ?? "";
if (!bodyPart.Contains(BodyPartFilter))
return false;
}
return true;
}
}
public class DicomDestination
{
public string Name { get; set; }
public string CallingAE { get; set; }
public string AETitle { get; set; }
public string Host { get; set; }
public int Port { get; set; }
}
Router Usage Example
Configure the router with rules that match images based on modality, body part, or other DICOM attributes. Each rule specifies a destination where matching images should be forwarded.
// Create router
var router = new DicomRouter("ROUTER", 11112, @"C:\DICOM\Routing");
// Add routing rules
router.AddRoutingRule(new RoutingRule
{
Name = "All CT to PACS",
ModalityFilter = "CT",
Destination = new DicomDestination
{
Name = "Main PACS",
CallingAE = "ROUTER",
AETitle = "PACS_SERVER",
Host = "192.168.1.100",
Port = 104
}
});
router.AddRoutingRule(new RoutingRule
{
Name = "Chest X-Ray to Workstation",
ModalityFilter = "CR",
BodyPartFilter = "CHEST",
Destination = new DicomDestination
{
Name = "Reading Workstation",
CallingAE = "ROUTER",
AETitle = "WORKSTATION_1",
Host = "192.168.1.50",
Port = 11112
}
});
// Start router
router.Start();
Summary
C-STORE is fundamental to DICOM workflows:
- SCU Implementation: Send images to remote systems
- SCP Implementation: Receive and store incoming images
- Batch Processing: Efficiently send multiple images
- Image Routing: Forward images to multiple destinations
- Error Handling: Proper status code handling and retry logic
These capabilities form the backbone of medical imaging data distribution.
Related Articles
Learn more about DICOM storage operations:
Java Implementation:
- DICOM Basics using Java - Push and Store Operations (C-STORE) - Complete C-STORE implementation
- DICOM Basics using Java - Storage Commitment - Verifying successful storage
.NET Implementation:
- DICOM Basics using .NET - Push and Store Operations (C-STORE) - .NET storage operations
- DICOM Basics using .NET - Storage Commitment - .NET storage verification
Error Handling:
- DICOM Basics using Java - Handling Transient Errors - Retry strategies
- DICOM Basics using .NET - Handling Transient Errors - .NET error handling