Modality Worklist SCP (Provider)

Section 15 of 27
56% complete

A Modality Worklist SCP provides scheduled procedure information to querying modalities.

Worklist SCP Architecture

Modality Worklist SCP Architecturesaravanansubramanian.comhow a worklist provider fields C-FIND requests and returns scheduled proceduresModality (SCU)C-FIND RQ - incoming queryModality (SCU)C-FIND RSP - streamed resultsWorklist SCP (Provider)DICOM Network LayerTCP listener - association negotiation - PDU framing - MWL Find SOP Classport 104 (typical) - Explicit VR Little EndianAssociation HandlerAccept / reject presentationcontexts, verify AE title,hand off to query processorQuery ProcessorParse identifier dataset,translate DICOM matchingkeys into query criteriaDatabase AccessRIS / HIS schema queries,wildcards, date ranges,Orders + SPS joinResult SenderPending 0xFF00 per match,Success 0x0000 to closeError paths0xA700 out of resources,0xA900 identifier mismatch

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.

Quiz: Modality Worklist SCP (Provider)

Question 1 of 4

What role does a Modality Worklist SCP play in healthcare?