26 fo-dicom
I’ll walk you through implementing DICOM operations using fo-dicom (Fellow Oak DICOM), which is one of the most popular and well-maintained DICOM libraries for .NET. These examples will give you practical, working code that you can use to build DICOM applications.
26.1 Setting Up fo-dicom
First, install the NuGet package:
PackageReference Include="fo-dicom" Version="5.1.2" />
<PackageReference Include="fo-dicom.NetCore" Version="5.1.2" /> <
26.2 C-ECHO: Connectivity Verification
C-ECHO is the simplest operation - perfect for testing if your DICOM connection works:
using FellowOakDicom.Network;
using FellowOakDicom.Network.Client;
public async Task<bool> PerformEchoAsync(string serverHost, int serverPort,
string serverAeTitle, string clientAeTitle)
{
try
{
var client = DicomClientFactory.Create(serverHost, serverPort, false,
, serverAeTitle);
clientAeTitle
// Add C-ECHO request
var request = new DicomCEchoRequest();
// Track the response
= null;
DicomCEchoResponse echoResponse .OnResponseReceived += (req, response) =>
request{
= response;
echoResponse .WriteLine($"C-ECHO Response Status: {response.Status}");
Console};
.AddRequestAsync(request);
await client.SendAsync();
await client
return echoResponse?.Status == DicomStatus.Success;
}
catch (Exception ex)
{
.WriteLine($"C-ECHO failed: {ex.Message}");
Consolereturn false;
}
}
// Usage
bool isAlive = await PerformEchoAsync("192.168.1.100", 104,
"PACS_SERVER", "MY_WORKSTATION");
26.3 C-STORE SCU: Sending Images
Here’s how to send DICOM files to a PACS server:
using FellowOakDicom;
using FellowOakDicom.Network;
using FellowOakDicom.Network.Client;
public async Task SendDicomFileAsync(string filePath, string serverHost,
int serverPort, string serverAeTitle, string clientAeTitle)
{
try
{
// Load the DICOM file
var file = await DicomFile.OpenAsync(filePath);
// Create client
var client = DicomClientFactory.Create(serverHost, serverPort, false,
, serverAeTitle);
clientAeTitle
// Create C-STORE request
var request = new DicomCStoreRequest(file);
// Handle response
.OnResponseReceived += (req, response) =>
request{
if (response.Status == DicomStatus.Success)
{
.WriteLine($"Successfully stored: {file.Dataset.GetString(DicomTag.SOPInstanceUID)}");
Console}
else
{
.WriteLine($"Store failed with status: {response.Status}");
Console}
};
.AddRequestAsync(request);
await client.SendAsync();
await client}
catch (Exception ex)
{
.WriteLine($"C-STORE failed: {ex.Message}");
Console}
}
// Send multiple files in one association
public async Task SendMultipleDicomFilesAsync(List<string> filePaths,
string serverHost, int serverPort, string serverAeTitle, string clientAeTitle)
{
var client = DicomClientFactory.Create(serverHost, serverPort, false,
, serverAeTitle);
clientAeTitle
foreach (var filePath in filePaths)
{
var file = await DicomFile.OpenAsync(filePath);
var request = new DicomCStoreRequest(file)
{
= (req, response) =>
OnResponseReceived {
.WriteLine($"File {Path.GetFileName(filePath)}: {response.Status}");
Console}
};
.AddRequestAsync(request);
await client}
.SendAsync();
await client}
26.4 C-STORE SCP: Receiving Images
Creating a DICOM server that can receive images:
using FellowOakDicom.Network;
using FellowOakDicom.Log;
public class StorageProvider : DicomService, IDicomServiceProvider, IDicomCStoreProvider
{
private static readonly DicomTransferSyntax[] AcceptedTransferSyntaxes = new[]
{
.ExplicitVRLittleEndian,
DicomTransferSyntax.ImplicitVRLittleEndian,
DicomTransferSyntax.JPEGLSLossless,
DicomTransferSyntax.JPEG2000Lossless,
DicomTransferSyntax.JPEGProcess14
DicomTransferSyntax};
public StorageProvider(INetworkStream stream, Encoding fallbackEncoding,
, DicomServiceDependencies dependencies)
ILogger logger: base(stream, fallbackEncoding, logger, dependencies)
{
}
public async Task OnReceiveAssociationRequestAsync(DicomAssociation association)
{
// Accept all storage classes
foreach (var pc in association.PresentationContexts)
{
if (pc.AbstractSyntax.StorageCategory != DicomStorageCategory.None)
{
.AcceptTransferSyntaxes(pc, AcceptedTransferSyntaxes);
association}
}
SendAssociationAcceptAsync(association);
await }
public async Task<DicomCStoreResponse> OnCStoreRequestAsync(DicomCStoreRequest request)
{
try
{
// Save the file
var studyUid = request.Dataset.GetString(DicomTag.StudyInstanceUID);
var seriesUid = request.Dataset.GetString(DicomTag.SeriesInstanceUID);
var sopUid = request.Dataset.GetString(DicomTag.SOPInstanceUID);
// Create directory structure
var path = Path.Combine("DicomStorage", studyUid, seriesUid);
.CreateDirectory(path);
Directory
var filePath = Path.Combine(path, $"{sopUid}.dcm");
.File.SaveAsync(filePath);
await request
.WriteLine($"Stored file: {filePath}");
Console
return new DicomCStoreResponse(request, DicomStatus.Success);
}
catch (Exception ex)
{
.WriteLine($"Store failed: {ex.Message}");
Consolereturn new DicomCStoreResponse(request, DicomStatus.ProcessingFailure);
}
}
public async Task OnCStoreRequestExceptionAsync(string tempFileName, Exception e)
{
// Clean up temporary file
if (File.Exists(tempFileName))
.Delete(tempFileName);
File}
}
// Start the storage server
public async Task StartStorageServerAsync(int port, string aeTitle)
{
var server = DicomServerFactory.Create<StorageProvider>(port);
.Options.LogDimseDatasets = false;
server.Options.LogDataPDUs = false;
server
.WriteLine($"Storage SCP started on port {port} with AE Title: {aeTitle}");
Console
// Keep server running
.Delay(Timeout.Infinite);
await Task}
26.5 C-FIND: Querying Studies
C-FIND lets you search for studies, series, or images:
public async Task<List<DicomDataset>> FindStudiesAsync(
string patientId, DateTime? studyDate,
string serverHost, int serverPort,
string serverAeTitle, string clientAeTitle)
{
var client = DicomClientFactory.Create(serverHost, serverPort, false,
, serverAeTitle);
clientAeTitle
var results = new List<DicomDataset>();
// Create C-FIND request for STUDY level
var request = new DicomCFindRequest(DicomQueryRetrieveLevel.Study);
// Add query parameters - these are the matching keys
.Dataset.AddOrUpdate(DicomTag.PatientID, patientId ?? string.Empty);
request.Dataset.AddOrUpdate(DicomTag.PatientName, string.Empty);
request
// Study level attributes
.Dataset.AddOrUpdate(DicomTag.StudyInstanceUID, string.Empty);
request.Dataset.AddOrUpdate(DicomTag.StudyDescription, string.Empty);
request.Dataset.AddOrUpdate(DicomTag.StudyDate,
request?.ToString("yyyyMMdd") ?? string.Empty);
studyDate.Dataset.AddOrUpdate(DicomTag.StudyTime, string.Empty);
request.Dataset.AddOrUpdate(DicomTag.AccessionNumber, string.Empty);
request.Dataset.AddOrUpdate(DicomTag.Modality, string.Empty);
request.Dataset.AddOrUpdate(DicomTag.NumberOfStudyRelatedInstances, string.Empty);
request
// Handle responses
.OnResponseReceived = (req, response) =>
request{
if (response.Status == DicomStatus.Pending && response.HasDataset)
{
// Each pending response contains one match
.Add(response.Dataset);
results
.WriteLine($"Found study: " +
Console"{response.Dataset.GetString(DicomTag.PatientName)} - " +
$"{response.Dataset.GetString(DicomTag.StudyDescription)} - " +
$"{response.Dataset.GetString(DicomTag.StudyDate)}");
$}
else if (response.Status == DicomStatus.Success)
{
.WriteLine($"C-FIND completed. Found {results.Count} studies.");
Console}
};
.AddRequestAsync(request);
await client.SendAsync();
await client
return results;
}
// More complex query with date range
public async Task<List<DicomDataset>> FindStudiesWithDateRangeAsync(
string modality, DateTime startDate, DateTime endDate,
string serverHost, int serverPort,
string serverAeTitle, string clientAeTitle)
{
var client = DicomClientFactory.Create(serverHost, serverPort, false,
, serverAeTitle);
clientAeTitle
var results = new List<DicomDataset>();
var request = new DicomCFindRequest(DicomQueryRetrieveLevel.Study);
// Query parameters
.Dataset.AddOrUpdate(DicomTag.PatientName, string.Empty);
request.Dataset.AddOrUpdate(DicomTag.PatientID, string.Empty);
request.Dataset.AddOrUpdate(DicomTag.StudyInstanceUID, string.Empty);
request
// Date range query (DICOM format: YYYYMMDD-YYYYMMDD)
var dateRange = $"{startDate:yyyyMMdd}-{endDate:yyyyMMdd}";
.Dataset.AddOrUpdate(DicomTag.StudyDate, dateRange);
request
// Modality (CT, MR, CR, etc.)
.Dataset.AddOrUpdate(DicomTag.ModalitiesInStudy, modality ?? string.Empty);
request
.OnResponseReceived = (req, response) =>
request{
if (response.Status == DicomStatus.Pending && response.HasDataset)
{
.Add(response.Dataset);
results}
};
.AddRequestAsync(request);
await client.SendAsync();
await client
return results;
}
26.6 C-MOVE: Requesting Transfer to Another AE
C-MOVE tells the server to send images to a destination (which could be yourself):
public async Task MoveStudyAsync(
string studyInstanceUid, string destinationAe,
string serverHost, int serverPort,
string serverAeTitle, string clientAeTitle)
{
var client = DicomClientFactory.Create(serverHost, serverPort, false,
, serverAeTitle);
clientAeTitle
// Create C-MOVE request at STUDY level
var request = new DicomCMoveRequest(destinationAe, studyInstanceUid);
int completed = 0;
int warnings = 0;
int failures = 0;
.OnResponseReceived = (req, response) =>
request{
if (response.Status.State == DicomState.Pending)
{
.WriteLine($"Moving... Completed: {response.Completed}/" +
Console"{response.Remaining + response.Completed}");
$}
else if (response.Status.State == DicomState.Success)
{
.WriteLine($"C-MOVE completed successfully. " +
Console"Completed: {response.Completed}, " +
$"Warnings: {response.Warnings}, " +
$"Failures: {response.Failures}");
$}
else
{
.WriteLine($"C-MOVE failed with status: {response.Status}");
Console}
= response.Completed ?? 0;
completed = response.Warnings ?? 0;
warnings = response.Failures ?? 0;
failures };
.AddRequestAsync(request);
await client.SendAsync();
await client}
// Move at SERIES level
public async Task MoveSeriesAsync(
string studyInstanceUid, string seriesInstanceUid,
string destinationAe,
string serverHost, int serverPort,
string serverAeTitle, string clientAeTitle)
{
var client = DicomClientFactory.Create(serverHost, serverPort, false,
, serverAeTitle);
clientAeTitle
var request = new DicomCMoveRequest(destinationAe, studyInstanceUid,
);
seriesInstanceUid
.OnResponseReceived = (req, response) =>
request{
.WriteLine($"Status: {response.Status}, " +
Console"Completed: {response.Completed}/{response.Remaining + response.Completed}");
$};
.AddRequestAsync(request);
await client.SendAsync();
await client}
26.7 C-GET: Direct Retrieval
C-GET retrieves images directly (if the server supports it):
public async Task GetStudyAsync(
string studyInstanceUid,
string serverHost, int serverPort,
string serverAeTitle, string clientAeTitle)
{
var client = DicomClientFactory.Create(serverHost, serverPort, false,
, serverAeTitle);
clientAeTitle
var receivedFiles = new List<DicomDataset>();
// Create C-GET request
var request = new DicomCGetRequest(studyInstanceUid);
// Handle incoming C-STORE sub-operations
.OnCStoreRequest += async (req) =>
client{
try
{
.Add(req.Dataset);
receivedFiles
// Save the file
var sopUid = req.Dataset.GetString(DicomTag.SOPInstanceUID);
var filePath = Path.Combine("Downloads", $"{sopUid}.dcm");
.File.SaveAsync(filePath);
await req
.WriteLine($"Received and saved: {filePath}");
Console
return new DicomCStoreResponse(req, DicomStatus.Success);
}
catch (Exception ex)
{
.WriteLine($"Failed to save: {ex.Message}");
Consolereturn new DicomCStoreResponse(req, DicomStatus.ProcessingFailure);
}
};
// Handle C-GET responses
.OnResponseReceived = (req, response) =>
request{
if (response.Status == DicomStatus.Pending)
{
.WriteLine($"C-GET Progress: {response.Completed}/" +
Console"{response.Remaining + response.Completed}");
$}
else if (response.Status == DicomStatus.Success)
{
.WriteLine($"C-GET completed. Received {receivedFiles.Count} files");
Console}
else
{
.WriteLine($"C-GET failed: {response.Status}");
Console}
};
.AddRequestAsync(request);
await client.SendAsync();
await client}
26.8 Complete Working Example: Query and Retrieve Workflow
Here’s a practical example that combines operations:
public class DicomWorkflowService
{
private readonly string _serverHost;
private readonly int _serverPort;
private readonly string _serverAeTitle;
private readonly string _clientAeTitle;
public DicomWorkflowService(string serverHost, int serverPort,
string serverAeTitle, string clientAeTitle)
{
= serverHost;
_serverHost = serverPort;
_serverPort = serverAeTitle;
_serverAeTitle = clientAeTitle;
_clientAeTitle }
public async Task<bool> TestConnectionAsync()
{
var client = DicomClientFactory.Create(_serverHost, _serverPort, false,
, _serverAeTitle);
_clientAeTitle
var request = new DicomCEchoRequest();
bool success = false;
.OnResponseReceived += (req, response) =>
request{
= response.Status == DicomStatus.Success;
success };
.AddRequestAsync(request);
await client.SendAsync();
await client
return success;
}
public async Task<List<StudyInfo>> QueryStudiesForPatientAsync(string patientId)
{
var studies = new List<StudyInfo>();
var client = DicomClientFactory.Create(_serverHost, _serverPort, false,
, _serverAeTitle);
_clientAeTitle
var request = new DicomCFindRequest(DicomQueryRetrieveLevel.Study)
{
=
Dataset {
{ DicomTag.PatientID, patientId },
{ DicomTag.PatientName, string.Empty },
{ DicomTag.StudyInstanceUID, string.Empty },
{ DicomTag.StudyDate, string.Empty },
{ DicomTag.StudyDescription, string.Empty },
{ DicomTag.ModalitiesInStudy, string.Empty },
{ DicomTag.NumberOfStudyRelatedSeries, string.Empty },
{ DicomTag.NumberOfStudyRelatedInstances, string.Empty }
}
};
.OnResponseReceived = (req, response) =>
request{
if (response.Status == DicomStatus.Pending && response.HasDataset)
{
.Add(new StudyInfo
studies{
= response.Dataset.GetString(DicomTag.StudyInstanceUID),
StudyInstanceUID = response.Dataset.GetString(DicomTag.PatientName),
PatientName = response.Dataset.GetString(DicomTag.StudyDate),
StudyDate = response.Dataset.GetString(DicomTag.StudyDescription),
StudyDescription = response.Dataset.GetString(DicomTag.ModalitiesInStudy),
Modality = response.Dataset.GetString(DicomTag.NumberOfStudyRelatedInstances)
NumberOfImages });
}
};
.AddRequestAsync(request);
await client.SendAsync();
await client
return studies;
}
public async Task RetrieveStudyAsync(string studyInstanceUid, string destinationPath)
{
// First, set up a storage SCP to receive the images
var storagePort = 11113;
var storageAeTitle = _clientAeTitle;
// Start storage server in background
var storageTask = Task.Run(async () =>
{
StartTemporaryStorageServerAsync(storagePort, destinationPath);
await });
// Give the server time to start
.Delay(1000);
await Task
// Now request the move
MoveStudyAsync(studyInstanceUid, storageAeTitle);
await }
private async Task StartTemporaryStorageServerAsync(int port, string storagePath)
{
// Implementation of temporary storage server
// (Similar to the StorageProvider shown earlier)
}
}
public class StudyInfo
{
public string StudyInstanceUID { get; set; }
public string PatientName { get; set; }
public string StudyDate { get; set; }
public string StudyDescription { get; set; }
public string Modality { get; set; }
public string NumberOfImages { get; set; }
}
26.9 Error Handling and Connection Options
public class RobustDicomClient
{
public async Task<T> ExecuteWithRetryAsync<T>(
<Task<T>> operation,
Funcint maxRetries = 3,
int delayMilliseconds = 1000)
{
for (int i = 0; i < maxRetries; i++)
{
try
{
return await operation();
}
catch (DicomAssociationRejectedException ex)
{
.WriteLine($"Association rejected: {ex.RejectReason}");
Consoleif (i == maxRetries - 1) throw;
}
catch (DicomNetworkException ex)
{
.WriteLine($"Network error: {ex.Message}");
Consoleif (i == maxRetries - 1) throw;
}
.Delay(delayMilliseconds * (i + 1));
await Task}
throw new Exception("Max retries exceeded");
}
public IDicomClient CreateClientWithOptions(
string host, int port, string callingAe, string calledAe)
{
var client = DicomClientFactory.Create(host, port, false, callingAe, calledAe);
// Set client options
.AssociationTimeout = TimeSpan.FromSeconds(30);
client.AssociationLingerTimeout = TimeSpan.FromSeconds(1);
client.MaximumPDULength = 16384;
client.ClientOptions.AssociationRequestTimeoutInMs = 5000;
client
// Add logging
.Logger = new ConsoleLogger();
client
return client;
}
}
These examples should give you a solid foundation for implementing DICOM operations in C# using fo-dicom. The library handles most of the low-level DICOM protocol details, letting you focus on your application logic.
Remember that in a hospital environment like Ramathibodi, you’ll need to coordinate with your PACS administrators to get the proper AE Titles configured and network access established before these operations will work.