The QuerySCU example demonstrates how to use DCF to implement a query service class user.
Classes
Class | Description | |
---|---|---|
Options |
Command line options class for parsing user options for QuerySCU.
| |
Program |
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 QuerySCU -p 10104 -q 0010,0010="MINER^STEPHEN" |
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; } } } }