DICOM Basics using .NET and C# - Storage Commitment Service
Introduction
This is part of my series of articles on the DICOM standard. In this tutorial, we'll explore the DICOM Storage Commitment service, which provides a mechanism for confirming that images have been safely stored in a PACS or archive before the sending system (typically a modality) deletes its local copies.
Storage Commitment is a critical workflow in healthcare imaging environments. When a CT scanner generates images, it needs confirmation that those images are safely stored in the PACS before it can free up local storage space. Without this confirmation, there's a risk of data loss if images are deleted prematurely.
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 Storage Commitment-capable PACS server (DCM4CHEE or commercial PACS)
- Note: Orthanc does NOT support Storage Commitment natively
- You can find all the code demonstrated in this tutorial on GitHub here
“Time reveals all things.” ~ Erasmus
The Theory Behind Storage Commitment
To understand why Storage Commitment exists, consider the fundamental challenge of distributed systems: how do you reliably coordinate state between independent nodes? When a CT scanner generates images, those images exist only on the scanner's local storage. The scanner needs to transmit these images to an archive, but network transfers can fail, and storage systems can have errors. Without a confirmation mechanism, you face a dangerous uncertainty.
Storage Commitment implements an asynchronous request-confirmation exchange adapted for medical imaging. The modality first stores images via C-STORE, then sends an N-ACTION request asking the archive to confirm durable storage of those instances. The archive later responds with an N-EVENT-REPORT confirming (or denying) commitment. Only after receiving positive confirmation can the modality safely delete local copies. Note that this is not a true two-phase commit protocol (which requires a prepare/vote phase, atomicity guarantees, and rollback capabilities), but rather a simple request-confirm pattern where the SCU asks for commitment and the SCP eventually responds.
The asynchronous nature of N-EVENT-REPORT is a deliberate design choice. Archives may need to verify data integrity, replicate to backup systems, or perform other validation before confirming commitment. By allowing the response to arrive hours later on a separate association, DICOM accommodates real-world archive architectures where immediate confirmation isn't possible. This asynchrony also means the modality must maintain a pending-commitment state machine, tracking which images are awaiting confirmation and timing out stale requests.
The Transaction UID serves as a correlation identifier that links the request to its eventual response across time and network sessions. This is essential because the response may arrive on a completely different TCP connection, potentially to a different process or even machine if the modality software has been restarted. The Transaction UID provides the semantic link that allows matching responses to requests in a stateless manner.
Understanding Storage Commitment
The Storage Commitment service uses the N-ACTION and N-EVENT-REPORT DIMSE services to request and confirm storage commitment. Here's the typical workflow:
- C-STORE: Modality sends images to PACS using standard C-STORE operations
- N-ACTION: Modality sends a Storage Commitment request listing the SOP instances to commit
- Verification: PACS verifies that all referenced images are safely stored
- N-EVENT-REPORT: PACS sends back a response indicating success or failure for each instance
- Cleanup: Modality can safely delete local copies for successfully committed images
Key SOP Classes and Attributes
Storage Commitment uses the following SOP Class:
| SOP Class | UID |
|---|---|
| Storage Commitment Push Model | 1.2.840.10008.1.20.1 |
The N-ACTION request contains:
| Attribute | Tag | Description |
|---|---|---|
| Transaction UID | (0008,1195) | Links request to response |
| Referenced SOP Sequence | (0008,1199) | List of instances to commit |
Each item in the Referenced SOP Sequence contains:
| Attribute | Tag | Description |
|---|---|---|
| Referenced SOP Class UID | (0008,1150) | SOP Class of the stored image |
| Referenced SOP Instance UID | (0008,1155) | SOP Instance UID of the stored image |
Storage Commitment Response
The N-EVENT-REPORT response uses Event Type IDs to indicate the overall result:
| Event Type ID | Meaning |
|---|---|
| 1 | All instances successfully committed |
| 2 | Some or all instances failed (check sequences) |
The response includes:
| Attribute | Tag | Description |
|---|---|---|
| Referenced SOP Sequence | (0008,1199) | Successfully committed instances |
| Failed SOP Sequence | (0008,1198) | Failed instances with reasons |
Failure Reasons
When commitment fails, the Failure Reason attribute indicates why:
| Code | Meaning |
|---|---|
| 0110 | Processing failure |
| 0112 | No such object instance |
| 0213 | Resource limitation |
Implementation Overview
A complete Storage Commitment implementation requires both SCU (requester) and SCP (responder) components. Here's a conceptual overview:
using System;
using FellowOakDicom;
using FellowOakDicom.Network;
using System.Threading;
using System.Threading.Tasks;
using FellowOakDicom.Network.Client;
namespace StorageCommitmentExample
{
public class StorageCommitmentClient
{
private readonly string _serverHost = "localhost";
private readonly int _serverPort = 11112;
private readonly string _remoteAeTitle = "STORAGECMT_SCP";
private readonly string _localAeTitle = "FODICOM_SCU";
/// <summary>
/// Requests storage commitment for a list of stored images.
/// </summary>
public void RequestStorageCommitment(List<StoredImage> storedImages)
{
// Step 1: Generate a unique Transaction UID
var transactionUid = DicomUID.Generate();
// Step 2: Build the Referenced SOP Sequence
var referencedSopSequence = new DicomSequence(DicomTag.ReferencedSOPSequence);
foreach (var image in storedImages)
{
var item = new DicomDataset();
item.Add(DicomTag.ReferencedSOPClassUID, image.SopClassUid);
item.Add(DicomTag.ReferencedSOPInstanceUID, image.SopInstanceUid);
referencedSopSequence.Items.Add(item);
}
// Step 3: Create the N-ACTION request dataset
var actionDataset = new DicomDataset();
actionDataset.Add(DicomTag.TransactionUID, transactionUid);
actionDataset.Add(referencedSopSequence);
// Step 4: Create the N-ACTION request
var actionRequest = new DicomNActionRequest(
DicomUID.StorageCommitmentPushModelSOPClass,
DicomUID.StorageCommitmentPushModelSOPInstance,
1); // Action Type ID = 1 (Request Storage Commitment)
actionRequest.Dataset = actionDataset;
// Step 5: Handle the response
actionRequest.OnResponseReceived += (request, response) =>
{
if (response.Status == DicomStatus.Success)
{
Console.WriteLine("Storage Commitment request accepted");
Console.WriteLine($"Transaction UID: {transactionUid}");
}
else
{
Console.WriteLine($"Request failed: {response.Status}");
}
};
// Step 6: Send the request
// Note: The actual N-EVENT-REPORT will arrive asynchronously
}
}
public class StoredImage
{
public DicomUID SopClassUid { get; set; }
public DicomUID SopInstanceUid { get; set; }
}
}
Implementing an Event Report Handler
The N-EVENT-REPORT arrives asynchronously, often on a separate association. You'll need an SCP to receive it:
/// <summary>
/// Handles the N-EVENT-REPORT response from the Storage Commitment SCP.
/// </summary>
private void HandleEventReport(DicomNEventReportRequest request)
{
var eventTypeId = request.EventTypeID;
var dataset = request.Dataset;
Console.WriteLine($"Received N-EVENT-REPORT, Event Type: {eventTypeId}");
// Check for successfully committed instances
var successSequence = dataset.GetSequence(DicomTag.ReferencedSOPSequence);
if (successSequence != null)
{
Console.WriteLine($"Successfully committed: {successSequence.Items.Count} instances");
foreach (var item in successSequence.Items)
{
var sopInstanceUid = item.GetSingleValue<string>(DicomTag.ReferencedSOPInstanceUID);
Console.WriteLine($" - {sopInstanceUid}: COMMITTED");
// Safe to delete local copy
}
}
// Check for failed instances
var failedSequence = dataset.GetSequence(DicomTag.FailedSOPSequence);
if (failedSequence != null)
{
Console.WriteLine($"Failed to commit: {failedSequence.Items.Count} instances");
foreach (var item in failedSequence.Items)
{
var sopInstanceUid = item.GetSingleValue<string>(DicomTag.ReferencedSOPInstanceUID);
var failureReason = item.GetSingleValue<ushort>(DicomTag.FailureReason);
Console.WriteLine($" - {sopInstanceUid}: FAILED (reason: {failureReason:X4})");
// DO NOT delete local copy - retry or alert
}
}
}
Important Considerations
When implementing Storage Commitment:
- Asynchronous Nature: The N-EVENT-REPORT may arrive on a separate association, minutes or hours later
- SCP Requirement: You need to implement an SCP to receive the event report callback
- Transaction Tracking: Store the Transaction UID to correlate requests with responses
- Retry Logic: Implement retry logic for failed commitments
- Timeout Handling: Handle cases where no response is received
Testing Environment
For testing Storage Commitment:
- DCM4CHEE: Open-source PACS with full Storage Commitment support
- Commercial PACS: Most enterprise PACS systems support Storage Commitment
- Orthanc: Does NOT support Storage Commitment natively (as of current versions)
Conclusion
Storage Commitment is an important DICOM service for ensuring data integrity in medical imaging workflows. It provides a reliable mechanism for modalities to confirm that their images are safely stored before freeing up local storage space.
While fo-dicom provides the building blocks for Storage Commitment, implementing a complete solution requires careful handling of the asynchronous N-EVENT-REPORT and proper integration with your workflow. This service is typically used in production environments where data integrity is critical.
Please check out the next tutorial in this series where we cover DICOM Modality Worklist and MPPS operations.