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
Class | Description | |
---|---|---|
Program |
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.
| |
ProgramCallbackQueryServer |
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<DicomInfoRecord>, 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 } }