LAUREL BRIDGE

LaurelBridge.DCFExamples.QuerySCPCallback Namespace

DICOM Connectivity Framework V3.4
The QuerySCPCallback example demonstrates how to use the DCF to implement a simple Q/R service class provider, utilizing a callback to handle the CFindRequest, CMoveRequest, CGetRequest, and CCancelRequest DIMSE messages.
Classes

  ClassDescription
Public classProgram
Basic query/retrieve service class provider example which should be run with the QuerySCU. The Query Port is hardcoded to 10104, and the Move Port is hardcoded to 10105.
Public classProgramCallbackQueryServer
A class that demonstrates the callback style server for a QR SCP.
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

QuerySCPCallback Sample Code
public class Program
{
    /// <summary>
    /// The server port for the QR server.
    /// </summary>
    private const int QueryTcpPort = 10104;

    /// <summary>
    /// The port for the Store Server for move requests.
    /// </summary>
    private const int MoveTcpPort = 10105;

    /// <summary>
    /// Main entry point for QuerySCPCallback.
    /// </summary>
    [STAThread]
    public static void Main()
    {
        try
        {
            string imageStorageDirectory = ImageStorageDirectory;
            Console.WriteLine("Requested instances will be matched in directory: {0}", imageStorageDirectory);

            AllowedPresentationContext cFindContext = new AllowedPresentationContext(
                Uids.StudyRootQueryRetrieveInformationModelFIND,
                new List<string> { Uids.TransferSyntax.ExplicitVRLittleEndian, Uids.TransferSyntax.ImplicitVRLittleEndian });
            AllowedPresentationContext cMoveContext = new AllowedPresentationContext(
                Uids.StudyRootQueryRetrieveInformationModelMOVE,
                new List<string> { Uids.TransferSyntax.ExplicitVRLittleEndian, Uids.TransferSyntax.ImplicitVRLittleEndian });
            AllowedPresentationContext cGetContext = new AllowedPresentationContext(
                Uids.StudyRootQueryRetrieveInformationModelGET,
                new List<string> { Uids.TransferSyntax.ExplicitVRLittleEndian, Uids.TransferSyntax.ImplicitVRLittleEndian });
            AllowedPresentationContext mrStore = new AllowedPresentationContext(
                Uids.MRImageStorage,
                new List<string> { Uids.ILE });
            CallbackQueryServer queryServer = new CallbackQueryServer(
                new List<AllowedPresentationContext> { cFindContext, cMoveContext, cGetContext, mrStore });

            AssociationManager mgr = new AssociationManager();
            mgr.ServerTcpPort = QueryTcpPort;
            mgr.AssociationConfigPolicyMgr = queryServer;
            mgr.AddAssociationListener(queryServer);

            // AssociationManager.run() blocks, so start him listening for connections on a separate thread
            Thread t = new Thread(mgr.Run);
            t.Start();
            if (!mgr.WaitForRunning(2000))
            {
                throw new TimeoutException("AssociationManager did not start in an acceptable amount of time");
            }

            Console.WriteLine("listening on {0}...", QueryTcpPort);
            Console.WriteLine("Move requests will be sent to port {0}", MoveTcpPort);

        }
        catch (Exception e)
        {
            Console.WriteLine("Error during execution: {0}", e);
            Environment.ExitCode = 1;
        }
    }

    /// <summary>
    /// For purposes of this example, we look for images in the QueryImagesPath in the application
    /// directory which is created if it does not exist. We also prepopulate it with a few images
    /// that we know are supplied with the examples if they exist in the application directory.
    /// </summary>
    public static string ImageStorageDirectory
    {
        get
        {
            string basePath = AppDomain.CurrentDomain.BaseDirectory;
            string queryPath = Path.Combine(basePath, "QuerySCPImages");
            if (!Directory.Exists(queryPath))
            {
                Console.WriteLine("Creating and populating query directory: {0}", queryPath);
                Directory.CreateDirectory(queryPath);
                foreach (string demoFileName in DemoImages)
                {
                    string demoSrc = Path.Combine(basePath, demoFileName);
                    if (File.Exists(demoSrc))
                    {
                        string demoDst = Path.Combine(queryPath, demoFileName);
                        File.Copy(demoSrc, demoDst);
                    }
                }
            }
            return queryPath;
        }
    }

    private static readonly string[] DemoImages =
    {
        "BasicTextSR.dcm",
        "mr-knee.dcm",
        "MultiFrameMonoJpeg50US.dcm",
        "SingleFrameJ2k90CT.dcm",
        "SingleFrameJpeg50US.dcm"
    };

    /// <summary>
    /// A class that demonstrates the callback style server for a QR SCP.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The <see cref="IAssociationConfigPolicyManager"/> allows us to get a callback to set our
    /// session settings for the association.
    /// </para>
    /// <para>
    /// This class overrides the <see cref="AssociationListenerAdapter.BeginAssociation"/> method
    /// to install a QRSCP.
    /// </para>
    /// <para>
    /// Other <see cref="AssociationListenerAdapter"/> methods may be overridden to implement
    /// additional functionality.
    /// </para>
    /// </remarks>
    public class CallbackQueryServer : AssociationListenerAdapter, IAssociationConfigPolicyManager
    {
        readonly IList<AllowedPresentationContext> _presentationContexts;

        private bool CancelRequested { get; set; }

        /// <summary>
        /// Constructs an CallbackStoreServer will service incoming associations.
        /// </summary>
        /// <param name="allowedPresentationContexts">The list of presentation contexts that will be allowed</param>
        public CallbackQueryServer(IList<AllowedPresentationContext> allowedPresentationContexts)
        {
            _presentationContexts = allowedPresentationContexts;
        }

        #region IAssociationConfigPolicyManager Overrides
        /// <summary>
        /// Returns a DicomSessionSettings object to be used for the association.
        /// </summary>
        /// <param name="assoc">The AssociationAcceptor for the given association</param>
        /// <returns>The DicomSessionSettings for the given association.</returns>
        public DicomSessionSettings GetSessionSettings(AssociationAcceptor assoc)
        {
            return new DicomSessionSettings() { SessionName = String.Format("QRServer: {0}", assoc.DicomSocket.ConnectionID) };
        }
        #endregion

        #region AssociationListenerAdapter Overrides
        /// <summary>
        /// Creates the QRSCP to handle the association that caused this method to be called.
        /// </summary>
        /// <param name="assoc">The AssociationAcceptor for the given association</param>
        public override void BeginAssociation(AssociationAcceptor assoc)
        {
            assoc.RegisterServiceClassProvider(new QRSCP(assoc, _presentationContexts, CFind, CMove, CGet, CCancel));
        }
        #endregion

        #region QRSCP Callback delegates
        /// <summary>
        /// Create and send some canned responses to the SCU.
        /// </summary>
        /// <param name="acceptor">The AssociationAcceptor for the given association</param>
        /// <param name="request">The inbound CFindRequest</param>
        /// <returns>An IEnumerable list of zero or more pending responses and one final response</returns>
        public IEnumerable<CFindResponse> CFind(AssociationAcceptor acceptor, CFindRequest request)
        {
            Console.WriteLine("CallbackQueryServer.CFind: {0} to port {1}:{2}{3}",
                acceptor.AssociationInfo.CallingTitle,
                acceptor.AssociationInfo.CalledPresentationAddress,
                Environment.NewLine,
                request.Data);

            foreach (DicomDataSet result in FindMatchingDataSets(request.Data))
            {
                if (CancelRequested)
                    break;

                CFindPendingResponse pending = new CFindPendingResponse(request, result);
                Console.WriteLine("CallbackQueryServer: sending C-Find pending response dataset:{0}{1}{0}", Environment.NewLine, pending);
                yield return pending;
            }

            CFindFinalResponse finalResponse = new CFindFinalResponse(request);
            if (CancelRequested)
                finalResponse.Status = DimseStatus.CANCEL;

            Console.WriteLine("CallbackQueryServer: sending the C-Find final response dataset:{0}{1}", Environment.NewLine, finalResponse);
            yield return finalResponse;
        }

        /// <summary>
        /// Implementation of the CCancel handler.
        /// </summary>
        /// <param name="acceptor">The Association acceptor.</param>
        /// <param name="request">The CCancelRequest DIMSE message.</param>
        public void CCancel(AssociationAcceptor acceptor, CCancelRequest request)
        {
            Console.WriteLine("CallbackQueryServer.CCancel: got a cancel request from {0} {1}",
                acceptor.AssociationInfo.CallingTitle,
                acceptor.AssociationInfo.CalledPresentationAddress);
            CancelRequested = true;
        }

        /// <summary>
        /// Move some canned responses to a Store SCP and send CMoveResponses the SCU.
        /// </summary>
        /// <param name="acceptor">The AssociationAcceptor for the given association</param>
        /// <param name="request">The inbound CMoveRequest</param>
        /// <returns>An IEnumerable list of zero or more pending responses and one final response</returns>
        public IEnumerable<CMoveResponse> CMove(AssociationAcceptor acceptor, CMoveRequest request)
        {
            Console.WriteLine("CallbackQueryServer.CMove: {0} to port {1}:{2}{3}",
                acceptor.AssociationInfo.CallingTitle,
                acceptor.AssociationInfo.CalledPresentationAddress,
                Environment.NewLine,
                request.Data);

            AssociationInfo ainfo = new AssociationInfo();
            ainfo.CalledTitle = request.MoveDestination;
            ainfo.CallingTitle = acceptor.AssociationInfo.CalledTitle;
            ainfo.CalledPresentationAddress = "localhost:" + MoveTcpPort;

            byte ctxId = 1;
            IList<DicomDataSet> dataSets = new List<DicomDataSet>();
            foreach (DicomDataSet dds in FindMatchingDataSets(request.Data))
            {
                if (CancelRequested)
                    break;

                bool foundContext = false;
                foreach (RequestedPresentationContext ctx in ainfo.RequestedPresentationContextList)
                {
                    if (dds.GetElementStringValue(Tags.SOPClassUID).Equals(ctx.AbstractSyntax))
                    {
                        foundContext = true;
                        break;
                    }
                }

                if (!foundContext)
                {
                    ainfo.AddRequestedPresentationContext(
                        new RequestedPresentationContext(
                            ctxId,
                            dds.GetElementStringValue(Tags.SOPClassUID),
                            dds.GetElementStringValue(Tags.TransferSyntaxUID)));
                    ctxId += 2;
                }

                dataSets.Add(dds);
            }

            // The SubOps class handles gathering DIMSE status for the CStore responses we will receive
            // so that we may return a correct final response to the QRSCU.
            SubOps counts = new SubOps();
            counts.RemainingSubOps = dataSets.Count;

            StoreSCU scu = new StoreSCU(ainfo, acceptor.SessionSettings);
            scu.RequestAssociation();
            foreach (DicomDataSet dds in dataSets)
            {
                if (CancelRequested)
                    break;

                DimseMessage dimseResponse = scu.CStore(dds, acceptor.SessionSettings.SendDimseTimeoutSeconds);
                counts.Update(dimseResponse.Status, dds.GetElementStringValue(Tags.SOPInstanceUID));

                CMoveResponse response = new CMovePendingResponse(request);
                response.UpdateSubOps(counts);
                Console.WriteLine("CallbackQueryServer: sending C-Move pending response dataset:{0}{1}{0}", Environment.NewLine, response);
                yield return response;
            }
            scu.ReleaseAssociation();

            CMoveFinalResponse finalResponse = new CMoveFinalResponse(request);
            finalResponse.UpdateSubOps(counts);
            if (CancelRequested)
                finalResponse.SetCancelled();

            Console.WriteLine("CallbackQueryServer: sending the C-Move final response dataset:{0}{1}", Environment.NewLine, finalResponse);
            yield return finalResponse;
        }

        /// <summary>
        /// Implementation of the CGet DIMSE handler.
        /// </summary>
        /// <param name="acceptor">The AssociationAcceptor for this request.</param>
        /// <param name="request">The CGet DIMSE message.</param>
        /// <returns>An enumeration of CGetResponse messages.</returns>
        public IEnumerable<CGetResponse> CGet(AssociationAcceptor acceptor, CGetRequest request)
        {
            Console.WriteLine("CallbackQueryServer.CGet: {0} to port {1}:{2}{3}",
                acceptor.AssociationInfo.CallingTitle, acceptor.AssociationInfo.CalledPresentationAddress,
                Environment.NewLine,
                request.Data);

            IList<DicomDataSet> dataSets = new List<DicomDataSet>();
            foreach (DicomDataSet dds in FindMatchingDataSets(request.Data))
            {
                dataSets.Add(dds);
            }

            // The SubOps class handles gathering DIMSE status for the CStore responses we will receive
            // so that we may return a correct final response to the QRSCU.
            SubOps counts = new SubOps();
            counts.RemainingSubOps = dataSets.Count;
            foreach (DicomDataSet dds in dataSets)
            {
                CStoreRequest storeRequest = new CStoreRequest();
                storeRequest.Data = dds;
                acceptor.SendDimseMessage(storeRequest, acceptor.SessionSettings.SendDimseTimeoutSeconds);
                DimseMessage dimseResponse = acceptor.ReceiveDimseMessage(0, acceptor.SessionSettings.ReceiveDimseTimeoutSeconds);
                counts.Update(dimseResponse.Status, dds.GetElementStringValue(Tags.SOPInstanceUID));
                CGetResponse response = new CGetPendingResponse(request);
                response.UpdateSubOps(counts);
                yield return response;
            }

            yield return new CGetFinalResponse(request);
        }
        #endregion

        #region Application Overrides
        /// <summary>
        /// This method is where you would find your matching datasets based upon the query request.
        /// </summary>
        /// <remarks>
        /// <para>
        /// Please note that the implementation of this method in this example has been designed for simplicity as it
        /// pertains to the user of the DCF running the examples without needing to set up / configure a database or some
        /// other backing store. This example implementation should NOT be used in a real world use case.
        /// </para>
        /// <para>
        /// One of the reasons this implementation should NOT be used is because it loads full DicomDataSet objects
        /// into memory, including the pixel data via the line 'dds.ExpandStreamingModeData(true);'.
        /// </para>
        /// <para>
        /// Another reason to not use this is because this method does not consult the <see cref="Tags.QueryRetrieveLevel"/>
        /// to filter the list of possible responses.  See the SampleVNA example for an implementation that consults
        /// the QueryRetrieveLevel.
        /// </para>
        /// <para>
        /// A more real-world implementation would return not full DicomDataSets, but information that represented records
        /// from a database or some other backing store; for example, IEnumerable&lt;DicomInfoRecord&gt;, where DicomInfoRecord
        /// objects were defined by the application developer and contained the information necessary to populate the
        /// requested presentation contexts in an AssociateRequestPdu. The rest of the header and pixel data would be loaded
        /// only when it came time to send that DimseMessage to the recipient.
        /// </para>
        /// <para>
        /// See the DICOM specification, chapter 4, annex C for Query/Retrieve information. Specifically, see section C.6 for
        /// a description of the unique, required, and optional tags.
        /// </para>
        /// </remarks>
        /// <param name="query">The CFind query request.</param>
        /// <returns>An enumeration of DicomDataSet objects that match the query.</returns>
        private IEnumerable<DicomDataSet> FindMatchingDataSets(DicomDataSet query)
        {
            if (query == null)
                throw new ArgumentNullException("query");
            // TODO: replace with code to find matching datasets using the query dataset
            string location = ImageStorageDirectory;
            foreach (string fileName in Directory.GetFiles(location, "*.dcm"))
            {
                using (DicomFileInput dfi = new DicomFileInput(fileName))
                {
                    DicomDataSet dds = dfi.ReadDataSet();
                    // since we are forcing the DicomFileInput to close (ala using) expand streaming mode data here
                    dds.ExpandStreamingModeData(true);

                    // Use the DicomDataSet Compare method in Query Mode (2) 
                    if (dds.Compare(query, 2))
                    {
                        yield return dds;
                    }
                }
            }
        }
        #endregion
    }
}