LAUREL BRIDGE

LaurelBridge.DCFExamples.QuerySCPExtended Namespace

DICOM Connectivity Framework V3.4
The QuerySCPExtended example demonstrates how to use the DCF to implement a simple Query/Retrieve service class provider that extends the stock QRSCP, overriding methods to handle the CFindRequest, CMoveRequest, CGetRequest, and CCancelRequest DIMSE messages.
Classes

  ClassDescription
Public classOptions
Command line options class for parsing user options for QuerySCPExtended.
Public classOverrideQRSCP
The OverrideQRSCP class provides an alternate implementation of the QRSCP that overrides the QRSCP methods directly, instead of using the callback techniques used in the QuerySCPCallback.
Public classProgram
Basic query/retrieve service class provider example which should be run with the QuerySCU. The QuerySCPExtended looks for images in the QuerySCPImages folder which is created and populated with some demo datasets if it does not exist.

This example creates a class that extends the QuerySCP class and overrides the CFindRq, CGetRq, CMoveRq, CCancelRq and CStoreRsp methods to handle the required Query/Retrieve service DIMSE messages. See the SampleVNA example for a more full-featured example of a Query/Retrieve Service implementation.

When responding to a MOVE request from a Query SCU, this Query/Retrieve SCP will send instances to a Store SCP. A Store SCP may be run by using the StoreSCPExtended code.

StoreSCPExtended -p 10105
QuerySCPExtended -p 10104 -m 10105
The QuerySCU example code may then be run to issue a FIND or MOVE request.
QuerySCU -p 10104 FIND "Stephen^Miner"
QuerySCU -p 10104 MOVE 1.2.123.4.454.62341.246.2

Public classProgramExtendedQueryServer
A class that demonstrates the extended style server for a Query/Retrieve 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

QuerySCPExtended Sample Code
public class Program
{
    /// <summary>
    /// Main entry point for QuerySCPExtended.
    /// </summary>
    /// <param name="args">Program arguments.</param>
    [STAThread]
    public static void Main(string[] args)
    {
        try
        {
            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 });

            Options options;
            if (!Options.TryParse(args, out options))
            {
                throw new ArgumentException("bad command parameters");
            }

            if (string.IsNullOrEmpty(options.ImageStorageDirectory))
                options.ImageStorageDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "QuerySCPImages");

            InitializeImageStorage(options.ImageStorageDirectory);

            ExtendedQueryServer queryServer = new ExtendedQueryServer(
                options.ImageStorageDirectory, options.MoveDestinationPort,
                new List<AllowedPresentationContext> { cFindContext, cMoveContext, cGetContext, mrStore });

            AssociationManager mgr = new AssociationManager();
            mgr.ServerTcpPort = options.Port;
            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}...", mgr.ServerTcpPort);
            Console.WriteLine("Requested instances will be matched in directory: {0}", options.ImageStorageDirectory);
            Console.WriteLine("Requested instances will be sent to port {0}", options.MoveDestinationPort);
        }
        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 void InitializeImageStorage(string path)
    {
        string imageStorageDirectory = Path.GetFullPath(path);
        if (!Directory.Exists(imageStorageDirectory))
        {
            Console.WriteLine("Creating and populating query directory: {0}", imageStorageDirectory);
            Directory.CreateDirectory(imageStorageDirectory);
            foreach (string demoFileName in DemoImages)
            {
                string demoSrc = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, demoFileName);
                if (File.Exists(demoSrc))
                {
                    string demoDst = Path.Combine(imageStorageDirectory, demoFileName);
                    File.Copy(demoSrc, demoDst);
                }
            }
        }
    }

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

    /// <summary>
    /// A class that demonstrates the extended style server for a Query/Retrieve 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 ExtendedQueryServer : AssociationListenerAdapter, IAssociationConfigPolicyManager
    {
        readonly IList<AllowedPresentationContext> _presentationContexts;
        private readonly string _imageStorageDirectory;
        private readonly int _moveDestinationPort;

        /// <summary>
        /// Constructs an ExtendedQueryServer that will service incoming associations.
        /// </summary>
        /// <param name="imageStorageDirectory">The directory in which to look for matching instance.</param>
        /// <param name="moveDestinationPort">The port on which matching C-MOVE instances are to be sent back; assumed to be on same address, "localhost".</param>
        /// <param name="allowedPresentationContexts">The list of presentation contexts that will be allowed</param>
        public ExtendedQueryServer(string imageStorageDirectory, int moveDestinationPort, IList<AllowedPresentationContext> allowedPresentationContexts)
        {
            _presentationContexts = allowedPresentationContexts;
            _imageStorageDirectory = imageStorageDirectory;
            _moveDestinationPort = moveDestinationPort;
        }

        #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 AssociationListener 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 OverrideQRSCP(_imageStorageDirectory, _moveDestinationPort, assoc, _presentationContexts));
        }
        #endregion
    }
}