LAUREL BRIDGE

LaurelBridge.DCFExamples.QuerySCU Namespace

DICOM Connectivity Framework V3.4
The QuerySCU example demonstrates how to use DCF to implement a query service class user.
Classes

  ClassDescription
Public classOptions
Command line options class for parsing user options for QuerySCU.
Public classProgram
Basic query service class user example which should be run with a Query SCP as well as a StoreSCP.

This example requires a Query SCP to be running to process FIND requests, and both a Query SCP and Store SCP to be running to process MOVE requests.

The Query SCP should be set up to respond to MOVE requests by sending instances to the Store SCP. The QuerySCPExtended and StoreSCPExtended example programs may be used for this purpose by entering these commands in two different command shells.

StoreSCPExtended -p 10105
QuerySCPExtended -p 10104 -m 10105
After the above servers are running, a FIND may be issued by this program with:
QuerySCU -p 10104 -q 0010,0010="MINER^STEPHEN"
And a MOVE may be issued by this program with: QuerySCU -p 10104 --domove -q 0020,000D=2.16.840.1.113662.4.8796818069641.798806497.93296077602350.10

Remarks

Supported OS Platforms:

  • Windows - .Net Framework 4.7.2 64-bit and 32-bit
  • Windows - .Net Core 2.1 64-bit and 32-bit
  • Linux - .Net Core 2.1 64-bit

Examples

QuerySCU Sample Code
public class Program : IQueryListener
{
    private QRSCU _scu;
    private AssociationInfo _ainfo;
    private readonly bool _relationalRetrieve = true;

    private List<string> _studiesFoundList = new List<string>();

    /// <summary>
    /// The main entry point for QuerySCU.
    /// </summary>
    /// <param name="args">Program arguments.</param>
    [STAThread]
    public static void Main(string[] args)
    {
        try
        {
            Options options;
            if (!Options.TryParse(args, out options))
            {
                throw new ArgumentException("bad command parameters");
            }

            if (!TryCreateRequestData(options, out var requestDataSet))
            {
                throw new ArgumentException(string.Format("invalid {0} parameters: {1}",
                    options.DoMove ? "MOVE" : "FIND", string.Join(", ", options.QueryTags)));
            }

            Console.WriteLine("Expecting a QRSCP to be listening on {0}...", options.Port);

            Program queryScu = new Program();

            if (options.DoMove)
            {
                DicomElement e = requestDataSet.GetElement(Tags.StudyInstanceUID);
                string uid = e.GetStringValueAt(0);
                Console.WriteLine("Performing a MOVE using {0}", uid);

                queryScu.ExecuteMove(options.Port, !options.UseEnumerable, uid);
                Console.WriteLine();
                Console.WriteLine("You may want to delete any instances just moved.");
            }
            else
            {
                Console.WriteLine("Performing a FIND using these query fields:{0}{1}", Environment.NewLine, requestDataSet);
                queryScu.ExecuteQuery(options.Port, !options.UseEnumerable, requestDataSet);
            }

        }
        catch (Exception e)
        {
            Console.WriteLine("Error during execution: {0}", e);
            Environment.ExitCode = 1;
        }
        finally
        {
            if (Debugger.IsAttached)
            {
                Console.Write("Press any key to continue . . . ");
                Console.ReadKey();
            }
        }
    }

    private static bool TryCreateRequestData(Options options, out DicomDataSet requestDataSet)
    {
        requestDataSet = new DicomDataSet();
        bool gotNonEmpty = false;
        foreach (string option in options.QueryTags)
        {
            string[] tokens = option.Split("=".ToCharArray(), 2);
            if (!AttributeTag.TryParse(tokens[0], out var queryTag))
            {
                throw new FormatException("invalid tag specification: " + tokens[0]);
            }

            string queryValue = tokens.Length > 1 ? tokens[1] : string.Empty;
            gotNonEmpty = gotNonEmpty || queryValue.Length > 0;
            requestDataSet.Insert(queryTag, queryValue);
        }

        // If we are doing a move, make sure we have a study instance uid which is required
        // for a study level move.  We could add options for different query/move levels and
        // validate the queryDataSet here.
        // 

        // For a MOVE at the study level, we really only need (and should probably only supply)
        // the Study Instance Uid with a non-empty value.
        if (options.DoMove)
        {
            DicomElement e = requestDataSet.GetElement(Tags.StudyInstanceUID);
            return e != null && !string.IsNullOrEmpty(e.GetValuesAsString(true));
        }

        // For a FIND at the study level, we should specify at least one study level tag that
        // has a non-empty value.  This example does not check that we are only supplied
        // Study level tags.
        return gotNonEmpty;
    }

    /// <summary>
    /// Execute a c-find request.
    /// </summary>
    /// <param name="qrScpPort">The port on which the Q/R SCP is expected to be listening.</param>
    /// <param name="useListener">
    /// Whether to use the FIND or MOVE method type that uses an IQueryListener or that directly returns an IEnumerable.
    /// When <c>True</c>, use an IQueryListener; when <c>False</c>, return an IEnumerable.
    /// </param>
    /// <param name="requestDataSet">The string to send in a C-FIND request for the Patient's Name (0010,0010) to match.</param>
    /// <remarks>
    /// We take the simple approach in this example of specifying an ISO_IR 100 character set for the
    /// encoding of our query request, which is probably fine for 90% of the Western languages.  By a strict
    /// reading of the spec, the character set should only be specified if we actually require it in our
    /// query.  Note that the SCP is free to ignore and/or not support our character set.
    /// <para>
    /// If you need to encode additional and/or multi-byte character sets, you may use the EncodingUtils.AnalyzeEncodings
    /// method in the CharacterSets namespace.
    /// </para>
    /// </remarks>
    public void ExecuteQuery(int qrScpPort, bool useListener, DicomDataSet requestDataSet)
    {
        try
        {
            _ainfo = new AssociationInfo();
            _ainfo.CallingTitle = "SCU";
            _ainfo.CalledTitle = "SCP";
            _ainfo.CalledPresentationAddress = string.Format("localhost:{0}", qrScpPort);

            // Add the requested presentation context
            RequestedPresentationContext ctx = new RequestedPresentationContext(
                1,
                Uids.StudyRootQueryRetrieveInformationModelFIND,
                Uids.ELE, Uids.ILE);
            if (_relationalRetrieve)
            {
                byte[] extNeg = new byte[1];
                extNeg[0] = 1;
                ctx.SOPSpecificData = extNeg;
            }
            _ainfo.AddRequestedPresentationContext(ctx);

            // Make the SCU here
            _scu = new QRSCU(_ainfo);
            _scu.MaxReturnedResults = 100;
            _scu.QueryTimeoutSeconds = -1; // We only use the progress timer, not the absolute timer
            _scu.SendDimseTimeoutSeconds = 30;
            _scu.ReceiveDimseTimeoutSeconds = 30;
            _scu.ProgressTimeoutSeconds = 30;

            // Make the query identifier used in the C-Find
            QRIdentifier query = new QRIdentifier();
            query.DataSet.Insert(requestDataSet);
            // Fields to query on.
            query.QueryRetrieveLevel = "STUDY";

            // Default fields to return if not specified in requestDataSet.
            if (!query.DataSet.ContainsElement(Tags.PatientID))
                query.PatientId = "";
            if (!query.DataSet.ContainsElement(Tags.PatientSex))
                query.PatientsSex = "";
            if (!query.DataSet.ContainsElement(Tags.Modality))
                query.Modality = "";
            if (!query.DataSet.ContainsElement(Tags.StudyDate))
                query.StudyDate = "";
            if (!query.DataSet.ContainsElement(Tags.StudyInstanceUID))
                query.StudyInstanceUid = "";  // The value returned here is recorded for use later for a C-Move

            // Specify that this request contains ISO_IR 100 encoded data
            query.DataSet.Insert(Tags.SpecificCharacterSet, EncodingUtils.GetCharacterSetName(CharacterSetID.ISO_IR_100));

            Console.WriteLine("Query Data set is:{0}{1}{0}", Environment.NewLine, query.DataSet);

            _scu.RequestAssociation();
            if (useListener)
            {
                Console.WriteLine("Performing callback style C-Find ...{0}", Environment.NewLine);
                _scu.CFind(query.DataSet, this, true);
            }
            else
            {
                Console.WriteLine("Performing iterator style C-Find ...{0}", Environment.NewLine);
                IEnumerable<CFindResponse> results = _scu.CFindWithResults(query.DataSet);
                foreach (CFindResponse cFindResponse in results)
                {
                    if (cFindResponse is CFindPendingResponse)
                    {
                        ProcessPendingOrFinalResponse(cFindResponse);
                    }
                    else
                    {
                        ProcessOperationCompleteStatus(cFindResponse.Status);
                    }
                }
            }
            _scu.ReleaseAssociation();

        }
        catch (Exception ex)
        {
            Console.WriteLine("Error executing query: {0}{1}{1}Please verify the Query SCP is up and running.", ex, Environment.NewLine);
        }
    }

    private void ExecuteMove(int qrScpPort, bool useListener, string studyInstanceUid)
    {
        try
        {
            string moveDestination = "STORE_SCP";  // Populating with an example value; A Store SCP should be listening on port localhost:10105 and accept this AE

            _ainfo = new AssociationInfo();
            _ainfo.CallingTitle = "SCU";
            _ainfo.CalledTitle = "SCP";
            _ainfo.CalledPresentationAddress = string.Format("localhost:{0}", qrScpPort);

            // Add the requested presentation context
            RequestedPresentationContext ctx = new RequestedPresentationContext(
                1,
                Uids.StudyRootQueryRetrieveInformationModelMOVE,
                Uids.ELE, Uids.ILE);
            if (_relationalRetrieve)
            {
                byte[] extNeg = new byte[1];
                extNeg[0] = 1;
                ctx.SOPSpecificData = extNeg;
            }
            _ainfo.AddRequestedPresentationContext(ctx);

            // Make the SCU here
            _scu = new QRSCU(_ainfo);
            _scu.MaxReturnedResults = 100;
            _scu.QueryTimeoutSeconds = -1; // We only use the progress timer, not the absolute timer
            _scu.SendDimseTimeoutSeconds = 30;
            _scu.ReceiveDimseTimeoutSeconds = 30;
            _scu.ProgressTimeoutSeconds = 30;

            // Make the query identifier used in the C-Move
            QRIdentifier query = new QRIdentifier();
            // Add C-Move level
            query.QueryRetrieveLevel = "STUDY";

            // Add C-Move Unique Key
            query.StudyInstanceUid = studyInstanceUid;

            Console.WriteLine("Query Data set is:{0}{1}{0}", Environment.NewLine, query.DataSet);
            Console.WriteLine("Move Destination is:{0}{1}", moveDestination, Environment.NewLine);


            _scu.RequestAssociation();
            if (useListener)
            {
                Console.WriteLine("Performing callback style C-Move ...{0}", Environment.NewLine);
                _scu.CMove(query.DataSet, moveDestination, this, true);
            }
            else
            {
                Console.WriteLine("Performing iterator style C-Move ...{0}", Environment.NewLine);
                IEnumerable<CMoveResponse> results = _scu.CMoveWithResults(query.DataSet, moveDestination);
                foreach (CMoveResponse cMoveResponse in results)
                {
                    if (cMoveResponse is CMovePendingResponse)
                    {
                        ProcessPendingOrFinalResponse(cMoveResponse);
                    }
                    else
                    {
                        ProcessOperationCompleteStatus(cMoveResponse.Status);
                    }
                }
            }
            _scu.ReleaseAssociation();

        }
        catch (Exception ex)
        {
            Console.WriteLine("Error executing query: {0}{1}{1}Please verify the Query SCP is up and running.", ex, Environment.NewLine);
        }
    }

    #region QueryListener Members
    /// <summary>
    /// This method is called for all incoming DIMSE response messages.
    /// </summary>
    /// <param name="rsp">The DIMSE response messages.</param>
    public virtual void QueryEvent(DimseMessage rsp)
    {
        ProcessPendingOrFinalResponse(rsp);
    }

    /// <summary>
    /// This method is called at the completion of the query.
    /// </summary>
    /// <param name="status">The DIMSE status of the final response.</param>
    public virtual void QueryComplete(int status)
    {
        ProcessOperationCompleteStatus(status);
    }
    #endregion

    private void ProcessPendingOrFinalResponse(DimseMessage rsp)
    {
        // This may be either a pending response or a final response
        Console.WriteLine("Got a reply:{0}{1}{0}", Environment.NewLine, rsp);

        string studyFound = rsp.Data.GetElementStringValue(Tags.StudyInstanceUID, string.Empty);

        if (!string.IsNullOrEmpty(studyFound))
        {
            _studiesFoundList.Add(studyFound);
        }
    }

    private void ProcessOperationCompleteStatus(int status)
    {
        Console.WriteLine("Received queryComplete event status = {0}", status);

        switch (status)
        {
            default:
                {
                    Console.WriteLine("Operation status({0})", status);
                    break;
                }
            case QueryListenerStatus.QUERY_LISTENER_SUCCESS:
                {
                    Console.WriteLine("Operation finished with no errors.");
                    break;
                }
            case QueryListenerStatus.QUERY_LISTENER_CANCELED:
                {
                    Console.WriteLine("Operation was Canceled successfully");
                    break;
                }
            case QueryListenerStatus.QUERY_LISTENER_WARNING:
                {
                    Console.WriteLine("Warning: some or all subops failed for C-MOVE");
                    break;
                }
            case QueryListenerStatus.QUERY_LISTENER_ERROR:
                {
                    Console.WriteLine("Failure.");
                    break;
                }
            case QueryListenerStatus.QUERY_LISTENER_INTERNAL_ERROR:
                {
                    Console.WriteLine("Internal Error. Association may still be connected. Aborting");

                    if (_scu.Connected)
                    {
                        _scu.AbortAssociation();
                    }

                    break;
                }
            case QueryListenerStatus.QUERY_LISTENER_TIMEOUT:
                {
                    Console.WriteLine("Operation timed out");

                    if (_scu.Connected)
                    {
                        _scu.AbortAssociation();
                    }

                    break;
                }
            case QueryListenerStatus.QUERY_LISTENER_SUCCESS_MAX_RETURNED_RESULTS_REACHED:
                {
                    Console.WriteLine("Success, Max Returned Results Reached");

                    if (_scu.Connected)
                    {
                        _scu.AbortAssociation();
                    }

                    break;
                }
            case QueryListenerStatus.QUERY_LISTENER_DIMSE_RECEIVE_TIMEOUT:
                {
                    Console.WriteLine("Operation DIMSE receive timed out");

                    if (_scu.Connected)
                    {
                        _scu.AbortAssociation();
                    }

                    break;
                }
        }
    }

}