A Modality Worklist SCP provides scheduled procedure information to querying modalities.
Worklist SCP Architecture
Basic Worklist SCP Implementation
Building a worklist SCP involves handling incoming C-FIND queries, executing searches against your data source, and returning matching results. Each matching item is sent as a pending response before a final success response completes the query.
public class ModalityWorklistProvider
{
private DicomServer server;
private IWorklistDataSource dataSource;
private string aeTitle;
public ModalityWorklistProvider(string aeTitle, int port, IWorklistDataSource dataSource)
{
this.aeTitle = aeTitle;
this.dataSource = dataSource;
server = new DicomServer
{
AETitle = aeTitle,
Port = port
};
// Handle association requests
server.AssociationRequested += OnAssociationRequested;
// Handle C-FIND requests for worklist
server.FindRequest += OnFindRequest;
}
private void OnAssociationRequested(object sender, AssociationRequestEventArgs e)
{
Console.WriteLine($"Association request from {e.CallingAETitle}");
foreach (var pc in e.PresentationContexts)
{
// Only accept MWL SOP Class
if (pc.AbstractSyntax == SOPClasses.ModalityWorklistFind)
{
pc.Result = PresentationContextResult.Accept;
pc.SelectedTransferSyntax = DicomTransferSyntax.ExplicitVRLittleEndian;
}
else
{
pc.Result = PresentationContextResult.RejectAbstractSyntaxNotSupported;
}
}
e.Accept();
}
private void OnFindRequest(object sender, FindRequestEventArgs e)
{
Console.WriteLine("Received MWL query");
try
{
// Parse the query criteria
WorklistQueryCriteria criteria = ParseQueryCriteria(e.DataSet);
// Get matching worklist items from data source
List<WorklistItem> matches = dataSource.QueryWorklist(criteria);
Console.WriteLine($"Found {matches.Count} matching items");
// Send each matching item
foreach (WorklistItem item in matches)
{
// Create response dataset
DicomDataSet responseData = CreateWorklistResponse(item);
// Send with Pending status (0xFF00)
e.SendPendingResponse(responseData);
}
// Send final success response
e.SendFinalResponse(0); // Success
}
catch (Exception ex)
{
Console.WriteLine($"Query error: {ex.Message}");
e.SendFinalResponse(0xA700); // Out of resources
}
}
private WorklistQueryCriteria ParseQueryCriteria(DicomDataSet query)
{
var criteria = new WorklistQueryCriteria();
// Patient level criteria
if (query.Contains(DicomTag.PatientName))
{
criteria.PatientName = query[DicomTag.PatientName]?.Value?.ToString();
}
if (query.Contains(DicomTag.PatientID))
{
criteria.PatientID = query[DicomTag.PatientID]?.Value?.ToString();
}
// Study level criteria
if (query.Contains(DicomTag.AccessionNumber))
{
criteria.AccessionNumber = query[DicomTag.AccessionNumber]?.Value?.ToString();
}
// Scheduled Procedure Step criteria
var spsSequence = query[DicomTag.ScheduledProcedureStepSequence] as DicomSequence;
if (spsSequence != null && spsSequence.Items.Count > 0)
{
var sps = spsSequence.Items[0];
if (sps.Contains(DicomTag.ScheduledProcedureStepStartDate))
{
criteria.ScheduledDate = sps[DicomTag.ScheduledProcedureStepStartDate]?.Value?.ToString();
}
if (sps.Contains(DicomTag.Modality))
{
criteria.Modality = sps[DicomTag.Modality]?.Value?.ToString();
}
if (sps.Contains(DicomTag.ScheduledStationAETitle))
{
criteria.ScheduledStationAE = sps[DicomTag.ScheduledStationAETitle]?.Value?.ToString();
}
}
return criteria;
}
private DicomDataSet CreateWorklistResponse(WorklistItem item)
{
DicomDataSet ds = new DicomDataSet();
// Patient Module
ds.Add(DicomTag.PatientName, item.PatientName);
ds.Add(DicomTag.PatientID, item.PatientID);
ds.Add(DicomTag.PatientBirthDate, item.PatientBirthDate ?? "");
ds.Add(DicomTag.PatientSex, item.PatientSex ?? "");
ds.Add(DicomTag.PatientWeight, item.PatientWeight ?? "");
// Study Module
ds.Add(DicomTag.StudyInstanceUID, item.StudyInstanceUID);
ds.Add(DicomTag.AccessionNumber, item.AccessionNumber);
ds.Add(DicomTag.ReferringPhysicianName, item.ReferringPhysicianName ?? "");
ds.Add(DicomTag.RequestedProcedureID, item.RequestedProcedureID);
ds.Add(DicomTag.RequestedProcedureDescription, item.RequestedProcedureDescription ?? "");
// Scheduled Procedure Step Sequence
DicomSequence spsSeq = new DicomSequence(DicomTag.ScheduledProcedureStepSequence);
DicomDataSet spsItem = new DicomDataSet();
spsItem.Add(DicomTag.Modality, item.Modality);
spsItem.Add(DicomTag.ScheduledStationAETitle, item.ScheduledStationAE ?? "");
spsItem.Add(DicomTag.ScheduledStationName, item.ScheduledStationName ?? "");
spsItem.Add(DicomTag.ScheduledProcedureStepStartDate, item.ScheduledDate);
spsItem.Add(DicomTag.ScheduledProcedureStepStartTime, item.ScheduledTime ?? "");
spsItem.Add(DicomTag.ScheduledPerformingPhysicianName, item.ScheduledPerformingPhysician ?? "");
spsItem.Add(DicomTag.ScheduledProcedureStepDescription, item.ScheduledProcedureStepDescription ?? "");
spsItem.Add(DicomTag.ScheduledProcedureStepID, item.ScheduledProcedureStepID);
spsItem.Add(DicomTag.ScheduledProcedureStepLocation, item.ScheduledLocation ?? "");
spsSeq.Items.Add(spsItem);
ds.Add(spsSeq);
return ds;
}
public void Start()
{
server.Start();
Console.WriteLine($"Worklist SCP started: {aeTitle} on port {server.Port}");
}
public void Stop()
{
server.Stop();
Console.WriteLine("Worklist SCP stopped");
}
}
public class WorklistQueryCriteria
{
public string PatientName { get; set; }
public string PatientID { get; set; }
public string AccessionNumber { get; set; }
public string ScheduledDate { get; set; }
public string Modality { get; set; }
public string ScheduledStationAE { get; set; }
}
Database Integration
A production worklist SCP retrieves scheduled procedures from a database, typically connected to the RIS or HIS system. The data source abstraction allows different database implementations while maintaining a consistent interface for the SCP.
Data Source Interface
Define a clean interface for data access to enable testing with mock implementations and support different database backends.
public interface IWorklistDataSource
{
List<WorklistItem> QueryWorklist(WorklistQueryCriteria criteria);
void AddWorklistItem(WorklistItem item);
void UpdateWorklistItem(WorklistItem item);
void RemoveWorklistItem(string accessionNumber);
}
SQL Server Implementation
This SQL Server implementation demonstrates querying scheduled procedures with support for DICOM wildcard matching. The implementation handles date ranges and converts DICOM wildcards to SQL LIKE patterns.
public class SqlWorklistDataSource : IWorklistDataSource
{
private string connectionString;
public SqlWorklistDataSource(string connectionString)
{
this.connectionString = connectionString;
}
public List<WorklistItem> QueryWorklist(WorklistQueryCriteria criteria)
{
var results = new List<WorklistItem>();
using (SqlConnection conn = new SqlConnection(connectionString))
{
conn.Open();
StringBuilder sql = new StringBuilder(@"
SELECT
o.PatientName,
o.PatientID,
o.PatientBirthDate,
o.PatientSex,
o.StudyInstanceUID,
o.AccessionNumber,
o.RequestedProcedureID,
o.RequestedProcedureDescription,
o.ReferringPhysicianName,
s.Modality,
s.ScheduledStationAE,
s.ScheduledDate,
s.ScheduledTime,
s.ScheduledProcedureStepID,
s.ScheduledProcedureStepDescription,
s.ScheduledPerformingPhysician
FROM Orders o
INNER JOIN ScheduledProcedureSteps s ON o.OrderID = s.OrderID
WHERE o.Status = 'SCHEDULED'
");
var parameters = new List<SqlParameter>();
// Build WHERE clause based on criteria
if (!string.IsNullOrEmpty(criteria.PatientName))
{
sql.Append(" AND o.PatientName LIKE @PatientName");
parameters.Add(new SqlParameter("@PatientName",
criteria.PatientName.Replace("*", "%").Replace("?", "_")));
}
if (!string.IsNullOrEmpty(criteria.PatientID))
{
sql.Append(" AND o.PatientID = @PatientID");
parameters.Add(new SqlParameter("@PatientID", criteria.PatientID));
}
if (!string.IsNullOrEmpty(criteria.AccessionNumber))
{
sql.Append(" AND o.AccessionNumber = @AccessionNumber");
parameters.Add(new SqlParameter("@AccessionNumber", criteria.AccessionNumber));
}
if (!string.IsNullOrEmpty(criteria.ScheduledDate))
{
// Handle date range (e.g., "20240101-20240131")
if (criteria.ScheduledDate.Contains("-"))
{
var dates = criteria.ScheduledDate.Split('-');
sql.Append(" AND s.ScheduledDate BETWEEN @StartDate AND @EndDate");
parameters.Add(new SqlParameter("@StartDate", dates[0]));
parameters.Add(new SqlParameter("@EndDate", dates[1]));
}
else
{
sql.Append(" AND s.ScheduledDate = @ScheduledDate");
parameters.Add(new SqlParameter("@ScheduledDate", criteria.ScheduledDate));
}
}
if (!string.IsNullOrEmpty(criteria.Modality))
{
sql.Append(" AND s.Modality = @Modality");
parameters.Add(new SqlParameter("@Modality", criteria.Modality));
}
if (!string.IsNullOrEmpty(criteria.ScheduledStationAE))
{
sql.Append(" AND s.ScheduledStationAE = @ScheduledStationAE");
parameters.Add(new SqlParameter("@ScheduledStationAE", criteria.ScheduledStationAE));
}
sql.Append(" ORDER BY s.ScheduledDate, s.ScheduledTime");
using (SqlCommand cmd = new SqlCommand(sql.ToString(), conn))
{
cmd.Parameters.AddRange(parameters.ToArray());
using (SqlDataReader reader = cmd.ExecuteReader())
{
while (reader.Read())
{
results.Add(new WorklistItem
{
PatientName = reader["PatientName"].ToString(),
PatientID = reader["PatientID"].ToString(),
PatientBirthDate = reader["PatientBirthDate"].ToString(),
PatientSex = reader["PatientSex"].ToString(),
StudyInstanceUID = reader["StudyInstanceUID"].ToString(),
AccessionNumber = reader["AccessionNumber"].ToString(),
RequestedProcedureID = reader["RequestedProcedureID"].ToString(),
RequestedProcedureDescription = reader["RequestedProcedureDescription"].ToString(),
ReferringPhysicianName = reader["ReferringPhysicianName"].ToString(),
Modality = reader["Modality"].ToString(),
ScheduledStationAE = reader["ScheduledStationAE"].ToString(),
ScheduledDate = reader["ScheduledDate"].ToString(),
ScheduledTime = reader["ScheduledTime"].ToString(),
ScheduledProcedureStepID = reader["ScheduledProcedureStepID"].ToString(),
ScheduledProcedureStepDescription = reader["ScheduledProcedureStepDescription"].ToString(),
ScheduledPerformingPhysician = reader["ScheduledPerformingPhysician"].ToString()
});
}
}
}
}
return results;
}
public void AddWorklistItem(WorklistItem item) { /* Implementation */ }
public void UpdateWorklistItem(WorklistItem item) { /* Implementation */ }
public void RemoveWorklistItem(string accessionNumber) { /* Implementation */ }
}
Usage Example
Deploy the worklist SCP as a Windows service or console application. Configure it with your database connection string and network settings appropriate for your environment.
// Create data source
IWorklistDataSource dataSource = new SqlWorklistDataSource(
"Server=localhost;Database=RIS;Trusted_Connection=True;"
);
// Create and start the worklist SCP
ModalityWorklistProvider worklistSCP = new ModalityWorklistProvider(
aeTitle: "WORKLIST_SCP",
port: 104,
dataSource: dataSource
);
worklistSCP.Start();
Console.WriteLine("Worklist SCP is running. Press Enter to stop.");
Console.ReadLine();
worklistSCP.Stop();
Database Schema
The following schema supports the worklist SCP with tables for orders and scheduled procedure steps. Indexes on commonly searched fields optimize query performance for high-volume environments.
-- Orders table
CREATE TABLE Orders (
OrderID INT PRIMARY KEY IDENTITY,
PatientName NVARCHAR(100),
PatientID NVARCHAR(50),
PatientBirthDate VARCHAR(8),
PatientSex VARCHAR(1),
StudyInstanceUID VARCHAR(64) UNIQUE,
AccessionNumber VARCHAR(50) UNIQUE,
RequestedProcedureID VARCHAR(50),
RequestedProcedureDescription NVARCHAR(200),
ReferringPhysicianName NVARCHAR(100),
Status VARCHAR(20) DEFAULT 'SCHEDULED',
CreatedDate DATETIME DEFAULT GETDATE()
);
-- Scheduled Procedure Steps table
CREATE TABLE ScheduledProcedureSteps (
SPSID INT PRIMARY KEY IDENTITY,
OrderID INT FOREIGN KEY REFERENCES Orders(OrderID),
Modality VARCHAR(16),
ScheduledStationAE VARCHAR(16),
ScheduledStationName NVARCHAR(100),
ScheduledDate VARCHAR(8),
ScheduledTime VARCHAR(14),
ScheduledProcedureStepID VARCHAR(50),
ScheduledProcedureStepDescription NVARCHAR(200),
ScheduledPerformingPhysician NVARCHAR(100),
ScheduledLocation NVARCHAR(100)
);
-- Index for common queries
CREATE INDEX IX_SPS_ScheduledDate ON ScheduledProcedureSteps(ScheduledDate);
CREATE INDEX IX_SPS_Modality ON ScheduledProcedureSteps(Modality);
CREATE INDEX IX_SPS_ScheduledStationAE ON ScheduledProcedureSteps(ScheduledStationAE);
Building a Modality Worklist SCP enables seamless integration between RIS and imaging modalities, streamlining radiology workflows.