Beispiel #1
0
        internal RetryableFhirTransactionPipeline(
            IFhirTransactionPipeline fhirTransactionPipeline,
            IExceptionStore exceptionStore,
            int maxRetryCount)
        {
            EnsureArg.IsNotNull(fhirTransactionPipeline, nameof(fhirTransactionPipeline));
            EnsureArg.IsNotNull(exceptionStore, nameof(exceptionStore));

            _fhirTransactionPipeline = fhirTransactionPipeline;
            _exceptionStore          = exceptionStore;
            _retryPolicy             = Policy
                                       .Handle <RetryableException>()
                                       .WaitAndRetryAsync(
                maxRetryCount,
                retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
                (exception, timeSpan, retryCount, context) =>
            {
                ChangeFeedEntry changeFeedEntry = (ChangeFeedEntry)context[nameof(ChangeFeedEntry)];

                return(_exceptionStore.WriteRetryableExceptionAsync(
                           changeFeedEntry,
                           retryCount,
                           exception));
            });
        }
Beispiel #2
0
        public async Task GivenAnInstance_WhenRetrievingChangeFeedWithoutMetadata_ThenMetadataIsNotReturned()
        {
            var studyInstanceUid  = TestUidGenerator.Generate();
            var seriesInstanceUid = TestUidGenerator.Generate();
            var sopInstanceUid    = TestUidGenerator.Generate();

            await CreateFile(studyInstanceUid, seriesInstanceUid, sopInstanceUid);

            long initialSequence;

            using (DicomWebResponse <ChangeFeedEntry> response = await _client.GetChangeFeedLatest("?includemetadata=false"))
            {
                ChangeFeedEntry changeFeedEntry = await response.GetValueAsync();

                initialSequence = changeFeedEntry.Sequence;

                Assert.Equal(studyInstanceUid, changeFeedEntry.StudyInstanceUid);
                Assert.Equal(seriesInstanceUid, changeFeedEntry.SeriesInstanceUid);
                Assert.Equal(sopInstanceUid, changeFeedEntry.SopInstanceUid);
                Assert.Null(changeFeedEntry.Metadata);
            }

            using (DicomWebAsyncEnumerableResponse <ChangeFeedEntry> response = await _client.GetChangeFeed($"?offset={initialSequence - 1}&includemetadata=false"))
            {
                ChangeFeedEntry[] changeFeedResults = await response.ToArrayAsync();

                Assert.Single(changeFeedResults);
                Assert.Null(changeFeedResults[0].Metadata);
                Assert.Equal(ChangeFeedState.Current, changeFeedResults[0].State);
            }
        }
Beispiel #3
0
        /// <inheritdoc/>
        public async Task WriteRetryableExceptionAsync(ChangeFeedEntry changeFeedEntry, int retryNum, Exception exceptionToStore, CancellationToken cancellationToken)
        {
            string tableName = Constants.TransientRetryTableName;

            DicomDataset dataset            = changeFeedEntry.Metadata;
            string       studyUid           = dataset.GetSingleValue <string>(DicomTag.StudyInstanceUID);
            string       seriesUid          = dataset.GetSingleValue <string>(DicomTag.SeriesInstanceUID);
            string       instanceUid        = dataset.GetSingleValue <string>(DicomTag.SOPInstanceUID);
            long         changeFeedSequence = changeFeedEntry.Sequence;

            CloudTable  table  = _client.GetTableReference(tableName);
            TableEntity entity = new RetryableEntity(studyUid, seriesUid, instanceUid, changeFeedSequence, retryNum, exceptionToStore);

            TableOperation operation = TableOperation.InsertOrMerge(entity);

            try
            {
                await table.ExecuteAsync(operation, cancellationToken);

                _logger.LogInformation("Retryable error when processsing changefeed entry: {ChangeFeedSequence} for DICOM instance with StudyUID: {StudyUID}, SeriesUID: {SeriesUID}, InstanceUID: {InstanceUID}. Tried {retryNum} time(s). Stored into table: {Table} in table storage.", changeFeedSequence, studyUid, seriesUid, instanceUid, retryNum, tableName);
            }
            catch
            {
                _logger.LogInformation("Retryable error when processsing changefeed entry: {ChangeFeedSequence} for DICOM instance with StudyUID: {StudyUID}, SeriesUID: {SeriesUID}, InstanceUID: {InstanceUID}. Tried {retryNum} time(s). Failed to store to table storage.", changeFeedSequence, studyUid, seriesUid, instanceUid, retryNum);
                throw;
            }
        }
Beispiel #4
0
        /// <inheritdoc/>
        public Task ProcessAsync(ChangeFeedEntry changeFeedEntry, CancellationToken cancellationToken)
        {
            var context = new Context
            {
                { nameof(ChangeFeedEntry), changeFeedEntry },
            };

            return(_timeoutPolicy.WrapAsync(_retryPolicy).ExecuteAsync(
                       async(ctx, tkn) =>
            {
                try
                {
                    await _fhirTransactionPipeline.ProcessAsync(changeFeedEntry, cancellationToken);
                }
                catch (TaskCanceledException ex) when(!cancellationToken.IsCancellationRequested)
                {
                    throw new RetryableException(ex);
                }
                catch (HttpRequestException ex)
                {
                    throw new RetryableException(ex);
                }
            },
                       context,
                       cancellationToken));
        }
Beispiel #5
0
        /// <inheritdoc/>
        public async Task WriteRetryableExceptionAsync(ChangeFeedEntry changeFeedEntry, int retryNum, TimeSpan nextDelayTimeSpan, Exception exceptionToStore, CancellationToken cancellationToken)
        {
            EnsureArg.IsNotNull(changeFeedEntry, nameof(changeFeedEntry));

            DicomDataset dataset            = changeFeedEntry.Metadata;
            string       studyInstanceUid   = dataset.GetSingleValue <string>(DicomTag.StudyInstanceUID);
            string       seriesInstanceUid  = dataset.GetSingleValue <string>(DicomTag.SeriesInstanceUID);
            string       sopInstanceUid     = dataset.GetSingleValue <string>(DicomTag.SOPInstanceUID);
            long         changeFeedSequence = changeFeedEntry.Sequence;

            var tableClient = _tableServiceClient.GetTableClient(Constants.TransientRetryTableName);
            var entity      = new RetryableEntity(studyInstanceUid, seriesInstanceUid, sopInstanceUid, changeFeedSequence, retryNum, exceptionToStore);

            try
            {
                await tableClient.UpsertEntityAsync(entity, cancellationToken : cancellationToken);

                _logger.LogInformation("Retryable error when processing changefeed entry: {ChangeFeedSequence} for DICOM instance with StudyUID: {StudyInstanceUid}, SeriesUID: {SeriesInstanceUid}, InstanceUID: {SopInstanceUid}. Tried {RetryNum} time(s). Waiting {Milliseconds} milliseconds . Stored into table: {Table} in table storage.", changeFeedSequence, studyInstanceUid, seriesInstanceUid, sopInstanceUid, retryNum, nextDelayTimeSpan.TotalMilliseconds, Constants.TransientRetryTableName);
            }
            catch
            {
                _logger.LogInformation("Retryable error when processing changefeed entry: {ChangeFeedSequence} for DICOM instance with StudyUID: {StudyInstanceUid}, SeriesUID: {SeriesInstanceUid}, InstanceUID: {SopInstanceUid}. Tried {RetryNum} time(s). Failed to store to table storage.", changeFeedSequence, studyInstanceUid, seriesInstanceUid, sopInstanceUid, retryNum);
                throw;
            }
        }
        private async Task <FhirTransactionRequestEntry> PrepareRequestAsync(ChangeFeedEntry changeFeedEntry, string patientResourceId)
        {
            _fhirTransactionContext = new FhirTransactionContext(changeFeedEntry);

            _fhirTransactionContext.Request.Patient = FhirTransactionRequestEntryGenerator.GenerateDefaultNoChangeRequestEntry <Patient>(new ServerResourceId(ResourceType.Patient, patientResourceId));

            return(await _imagingStudyDeleteHandler.BuildAsync(_fhirTransactionContext, CancellationToken.None));
        }
        private async Task ExecuteAndValidateRetryThenThrowTimeOut(Exception ex)
        {
            ChangeFeedEntry changeFeedEntry = ChangeFeedGenerator.Generate();

            _fhirTransactionPipeline.ProcessAsync(changeFeedEntry, DefaultCancellationToken).Throws(ex);

            await Assert.ThrowsAsync <TimeoutRejectedException>(() => _retryableFhirTransactionPipeline.ProcessAsync(changeFeedEntry, DefaultCancellationToken));
        }
        /// <inheritdoc/>
        public async Task ProcessAsync(ChangeFeedEntry changeFeedEntry, CancellationToken cancellationToken)
        {
            EnsureArg.IsNotNull(changeFeedEntry, nameof(changeFeedEntry));

            // Create a context used throughout this process.
            var context = new FhirTransactionContext(changeFeedEntry);

            // Prepare all required objects for the transaction.
            foreach (IFhirTransactionPipelineStep pipeline in _fhirTransactionPipelines)
            {
                await pipeline.PrepareRequestAsync(context, cancellationToken);
            }

            // Check to see if any resource needs to be created/updated.
            var bundle = new Bundle()
            {
                Type = Bundle.BundleType.Transaction,
            };

            var usedPropertyAccessors = new List <FhirTransactionRequestResponsePropertyAccessor>(_requestResponsePropertyAccessors.Count);

            foreach (FhirTransactionRequestResponsePropertyAccessor propertyAccessor in _requestResponsePropertyAccessors)
            {
                FhirTransactionRequestEntry requestEntry = propertyAccessor.RequestEntryGetter(context.Request);

                if (requestEntry == null || requestEntry.RequestMode == FhirTransactionRequestMode.None)
                {
                    // No associated request, skip it.
                    continue;
                }

                // There is a associated request, add to the list so it gets processed.
                usedPropertyAccessors.Add(propertyAccessor);
                bundle.Entry.Add(CreateRequestBundleEntryComponent(requestEntry));
            }

            if (!bundle.Entry.Any())
            {
                // Nothing to update.
                return;
            }

            // Execute the transaction.
            Bundle responseBundle = await _fhirTransactionExecutor.ExecuteTransactionAsync(bundle, cancellationToken);

            // Process the response.
            for (int i = 0; i < usedPropertyAccessors.Count; i++)
            {
                FhirTransactionResponseEntry responseEntry = CreateResponseEntry(responseBundle.Entry[i]);

                usedPropertyAccessors[i].ResponseEntrySetter(context.Response, responseEntry);
            }

            // Execute any additional checks of the response.
            foreach (IFhirTransactionPipelineStep pipeline in _fhirTransactionPipelines)
            {
                pipeline.ProcessResponse(context);
            }
        /// <inheritdoc/>
        public async Task <FhirTransactionRequestEntry> BuildAsync(FhirTransactionContext context, CancellationToken cancellationToken)
        {
            EnsureArg.IsNotNull(context, nameof(context));
            EnsureArg.IsNotNull(context.ChangeFeedEntry, nameof(context.ChangeFeedEntry));
            EnsureArg.IsNotNull(context.Request, nameof(context.Request));

            IResourceId patientId = context.Request.Patient.ResourceId;

            ChangeFeedEntry changeFeedEntry = context.ChangeFeedEntry;

            Identifier imagingStudyIdentifier = ImagingStudyIdentifierUtility.CreateIdentifier(changeFeedEntry.StudyInstanceUid);

            ImagingStudy existingImagingStudy = await _fhirService.RetrieveImagingStudyAsync(imagingStudyIdentifier, cancellationToken);

            ImagingStudy imagingStudy = (ImagingStudy)existingImagingStudy?.DeepCopy();

            FhirTransactionRequestMode requestMode = FhirTransactionRequestMode.None;

            if (existingImagingStudy == null)
            {
                imagingStudy = new ImagingStudy()
                {
                    Status  = ImagingStudy.ImagingStudyStatus.Available,
                    Subject = patientId.ToResourceReference(),
                };

                imagingStudy.Identifier.Add(imagingStudyIdentifier);
                requestMode = FhirTransactionRequestMode.Create;
            }

            SynchronizeImagingStudyProperties(context, imagingStudy);

            if (requestMode != FhirTransactionRequestMode.Create &&
                !existingImagingStudy.IsExactly(imagingStudy))
            {
                requestMode = FhirTransactionRequestMode.Update;
            }

            Bundle.RequestComponent request = requestMode switch
            {
                FhirTransactionRequestMode.Create => ImagingStudyPipelineHelper.GenerateCreateRequest(imagingStudyIdentifier),
                FhirTransactionRequestMode.Update => ImagingStudyPipelineHelper.GenerateUpdateRequest(imagingStudy),
                _ => null,
            };

            IResourceId resourceId = requestMode switch
            {
                FhirTransactionRequestMode.Create => new ClientResourceId(),
                _ => existingImagingStudy.ToServerResourceId(),
            };

            return(new FhirTransactionRequestEntry(
                       requestMode,
                       request,
                       resourceId,
                       imagingStudy));
        }
Beispiel #10
0
        public FhirTransactionContext(ChangeFeedEntry changeFeedEntry)
        {
            EnsureArg.IsNotNull(changeFeedEntry, nameof(changeFeedEntry));

            ChangeFeedEntry = changeFeedEntry;

            Request  = new FhirTransactionRequest();
            Response = new FhirTransactionResponse();
        }
Beispiel #11
0
        private async Task ExecuteAndValidate(Exception ex, int expectedNumberOfCalls)
        {
            ChangeFeedEntry changeFeedEntry = ChangeFeedGenerator.Generate();

            _fhirTransactionPipeline.ProcessAsync(changeFeedEntry, DefaultCancellationToken).Throws(ex);

            await Assert.ThrowsAsync(ex.GetType(), () => _retryableFhirTransactionPipeline.ProcessAsync(changeFeedEntry, DefaultCancellationToken));

            await _fhirTransactionPipeline.Received(expectedNumberOfCalls).ProcessAsync(changeFeedEntry, DefaultCancellationToken);
        }
        private async Task <FhirTransactionRequestEntry> BuildImagingStudyEntryComponent(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, string patientResourceId, bool addMetadata = true)
        {
            ChangeFeedEntry changeFeedEntry = ChangeFeedGenerator.Generate(
                action: ChangeFeedAction.Create,
                studyInstanceUid: studyInstanceUid,
                seriesInstanceUid: seriesInstanceUid,
                sopInstanceUid: sopInstanceUid,
                metadata: addMetadata ? FhirTransactionContextBuilder.CreateDicomDataset() : null);

            return(await PrepareRequestAsync(changeFeedEntry, patientResourceId));
        }
Beispiel #13
0
        public Task WriteRetryableExceptionAsync(ChangeFeedEntry changeFeedEntry, int retryNum, Exception exceptionToStore, CancellationToken cancellationToken = default)
        {
            DicomDataset dataset            = changeFeedEntry.Metadata;
            string       studyUid           = dataset.GetSingleValue <string>(DicomTag.StudyInstanceUID);
            string       seriesUid          = dataset.GetSingleValue <string>(DicomTag.SeriesInstanceUID);
            string       instanceUid        = dataset.GetSingleValue <string>(DicomTag.SOPInstanceUID);
            long         changeFeedSequence = changeFeedEntry.Sequence;

            _logger.LogInformation("Retryable error when processsing changefeed entry: {ChangeFeedSequence} for DICOM instance with StudyUID: {StudyUID}, SeriesUID: {SeriesUID}, InstanceUID: {InstanceUID}. Tried {retryNum} time(s).", changeFeedSequence, studyUid, seriesUid, instanceUid, retryNum);
            return(Task.CompletedTask);
        }
        /// <inheritdoc/>
        public async Task WriteExceptionAsync(ChangeFeedEntry changeFeedEntry, Exception exceptionToStore, ErrorType errorType, CancellationToken cancellationToken)
        {
            CloudTable  table;
            TableEntity entity;
            string      tableName;

            EnsureArg.IsNotNull(changeFeedEntry, nameof(changeFeedEntry));

            switch (errorType)
            {
            case ErrorType.FhirError:
                tableName = Constants.FhirExceptionTableName;
                break;

            case ErrorType.DicomError:
                tableName = Constants.DicomExceptionTableName;
                break;

            case ErrorType.DicomValidationError:
                tableName = Constants.DicomValidationTableName;
                break;

            case ErrorType.TransientFailure:
                tableName = Constants.TransientFailureTableName;
                break;

            default:
                return;
            }

            DicomDataset dataset            = changeFeedEntry.Metadata;
            string       studyUid           = dataset.GetSingleValue <string>(DicomTag.StudyInstanceUID);
            string       seriesUid          = dataset.GetSingleValue <string>(DicomTag.SeriesInstanceUID);
            string       instanceUid        = dataset.GetSingleValue <string>(DicomTag.SOPInstanceUID);
            long         changeFeedSequence = changeFeedEntry.Sequence;

            table  = _client.GetTableReference(tableName);
            entity = new IntransientEntity(studyUid, seriesUid, instanceUid, changeFeedSequence, exceptionToStore);

            var operation = TableOperation.InsertOrMerge(entity);

            try
            {
                await table.ExecuteAsync(operation, cancellationToken);

                _logger.LogInformation("Error when processsing changefeed entry: {ChangeFeedSequence} for DICOM instance with StudyUID: {StudyUID}, SeriesUID: {SeriesUID}, InstanceUID: {InstanceUID}. Stored into table: {Table} in table storage.", changeFeedSequence, studyUid, seriesUid, instanceUid, tableName);
            }
            catch
            {
                _logger.LogInformation("Error when processsing changefeed entry: {ChangeFeedSequence} for DICOM instance with StudyUID: {StudyUID}, SeriesUID: {SeriesUID}, InstanceUID: {InstanceUID}. Failed to store to table storage.", changeFeedSequence, studyUid, seriesUid, instanceUid);
                throw;
            }
        }
        /// <inheritdoc/>
        public async Task <FhirTransactionRequestEntry> BuildAsync(FhirTransactionContext context, CancellationToken cancellationToken)
        {
            EnsureArg.IsNotNull(context, nameof(context));
            EnsureArg.IsNotNull(context.ChangeFeedEntry, nameof(context.ChangeFeedEntry));

            ChangeFeedEntry changeFeedEntry = context.ChangeFeedEntry;

            Identifier   imagingStudyIdentifier = ImagingStudyIdentifierUtility.CreateIdentifier(changeFeedEntry.StudyInstanceUid);
            ImagingStudy imagingStudy           = await _fhirService.RetrieveImagingStudyAsync(imagingStudyIdentifier, cancellationToken);

            // Returns null if imagingStudy does not exists for given studyInstanceUid
            if (imagingStudy == null)
            {
                return(null);
            }

            string imagingStudySource = imagingStudy.Meta.Source;

            ImagingStudy.SeriesComponent   series   = ImagingStudyPipelineHelper.GetSeriesWithinAStudy(changeFeedEntry.SeriesInstanceUid, imagingStudy.Series);
            ImagingStudy.InstanceComponent instance = ImagingStudyPipelineHelper.GetInstanceWithinASeries(changeFeedEntry.SopInstanceUid, series);

            // Return null if the given instance is not present in ImagingStudy
            if (instance == null)
            {
                return(null);
            }

            // Removes instance from series collection
            series.Instance.Remove(instance);

            // Removes series from ImagingStudy if its instance collection is empty
            if (series.Instance.Count == 0)
            {
                imagingStudy.Series.Remove(series);
            }

            if (imagingStudy.Series.Count == 0 && _dicomWebEndpoint.Equals(imagingStudySource, System.StringComparison.Ordinal))
            {
                return(new FhirTransactionRequestEntry(
                           FhirTransactionRequestMode.Delete,
                           ImagingStudyPipelineHelper.GenerateDeleteRequest(imagingStudy),
                           imagingStudy.ToServerResourceId(),
                           imagingStudy));
            }

            return(new FhirTransactionRequestEntry(
                       FhirTransactionRequestMode.Update,
                       ImagingStudyPipelineHelper.GenerateUpdateRequest(imagingStudy),
                       imagingStudy.ToServerResourceId(),
                       imagingStudy));
        }
        public async Task GivenAValidCreateChangeFeed_WhenBuilt_ThenCorrectEntryComponentShouldBeCreated()
        {
            const string studyInstanceUid  = "1";
            const string seriesInstanceUid = "2";
            const string sopInstanceUid    = "3";
            const string patientResourceId = "p1";

            ChangeFeedEntry changeFeedEntry = ChangeFeedGenerator.Generate(
                action: ChangeFeedAction.Create,
                studyInstanceUid: studyInstanceUid,
                seriesInstanceUid: seriesInstanceUid,
                sopInstanceUid: sopInstanceUid);

            FhirTransactionRequestEntry entry = await BuildImagingStudyEntryComponent(studyInstanceUid, seriesInstanceUid, sopInstanceUid, patientResourceId);

            ValidationUtility.ValidateRequestEntryMinimumRequirementForWithChange(FhirTransactionRequestMode.Create, "ImagingStudy", Bundle.HTTPVerb.POST, entry);

            ImagingStudy imagingStudy = Assert.IsType <ImagingStudy>(entry.Resource);

            string jsonString;

            jsonString = JsonSerializer.Serialize(entry);

            Assert.IsType <ClientResourceId>(entry.ResourceId);
            Assert.Equal(ImagingStudy.ImagingStudyStatus.Available, imagingStudy.Status);
            Assert.Null(entry.Request.IfMatch);

            ValidationUtility.ValidateResourceReference("Patient/p1", imagingStudy.Subject);

            Assert.Collection(
                imagingStudy.Identifier,
                identifier => ValidationUtility.ValidateIdentifier("urn:dicom:uid", $"urn:oid:{studyInstanceUid}", identifier),
                identifier => ValidationUtility.ValidateAccessionNumber(null, FhirTransactionContextBuilder.DefaultAccessionNumber, identifier));

            Assert.Collection(
                imagingStudy.Series,
                series =>
            {
                Assert.Equal(seriesInstanceUid, series.Uid);

                Assert.Collection(
                    series.Instance,
                    instance => Assert.Equal(sopInstanceUid, instance.Uid));
            });

            ValidateDicomFilePropertiesAreCorrectlyMapped(imagingStudy, series: imagingStudy.Series.First(), instance: imagingStudy.Series.First().Instance.First());
        }
Beispiel #17
0
        /// <inheritdoc/>
        public async Task PrepareRequestAsync(FhirTransactionContext context, CancellationToken cancellationToken)
        {
            EnsureArg.IsNotNull(context, nameof(context));

            ChangeFeedEntry changeFeedEntry = context.ChangeFeedEntry;

            context.Request.ImagingStudy = changeFeedEntry.Action switch
            {
                ChangeFeedAction.Create => await _imagingStudyUpsertHandler.BuildAsync(context, cancellationToken),
                ChangeFeedAction.Delete => await _imagingStudyDeleteHandler.BuildAsync(context, cancellationToken),
                _ => throw new NotSupportedException(
                          string.Format(
                              CultureInfo.InvariantCulture,
                              DicomCastCoreResource.NotSupportedChangeFeedAction,
                              changeFeedEntry.Action)),
            };
        }
Beispiel #18
0
        public async Task GivenASetOfDicomInstances_WhenRetrievingChangeFeed_ThenTheExpectedInstanceAreReturned()
        {
            var  studyInstanceUid  = TestUidGenerator.Generate();
            var  seriesInstanceUid = TestUidGenerator.Generate();
            var  sopInstanceUids   = Enumerable.Range(1, 10).Select(x => TestUidGenerator.Generate()).ToArray();
            long initialSequence   = -1;

            for (int i = 0; i < 10; i++)
            {
                await CreateFile(studyInstanceUid, seriesInstanceUid, sopInstanceUids[i]);

                if (initialSequence == -1)
                {
                    using DicomWebResponse <ChangeFeedEntry> latestResponse = await _client.GetChangeFeedLatest();

                    ChangeFeedEntry result = await latestResponse.GetValueAsync();

                    initialSequence = result.Sequence;
                    Assert.Equal(studyInstanceUid, result.StudyInstanceUid);
                    Assert.Equal(seriesInstanceUid, result.SeriesInstanceUid);
                    Assert.Equal(sopInstanceUids[i], result.SopInstanceUid);
                    Assert.Equal(ChangeFeedAction.Create, result.Action);
                    Assert.Equal(ChangeFeedState.Current, result.State);
                }
            }

            using (DicomWebAsyncEnumerableResponse <ChangeFeedEntry> response = await _client.GetChangeFeed())
            {
                Assert.Equal(10, await response.CountAsync());
            }

            using (DicomWebAsyncEnumerableResponse <ChangeFeedEntry> response = await _client.GetChangeFeed($"?offset={initialSequence - 1}"))
            {
                ChangeFeedEntry[] changeFeedResults = await response.ToArrayAsync();

                Assert.Equal(10, changeFeedResults.Length);

                for (int i = 0; i < 10; i++)
                {
                    Assert.Equal(studyInstanceUid, changeFeedResults[i].StudyInstanceUid);
                    Assert.Equal(seriesInstanceUid, changeFeedResults[i].SeriesInstanceUid);
                    Assert.Equal(sopInstanceUids[i], changeFeedResults[i].SopInstanceUid);
                    Assert.NotNull(changeFeedResults[i].Metadata);
                }
            }
        }
Beispiel #19
0
        public async Task <IEnumerable <FhirTransactionRequestEntry> > BuildAsync(FhirTransactionContext context, CancellationToken cancellationToken)
        {
            EnsureArg.IsNotNull(context?.ChangeFeedEntry, nameof(context.ChangeFeedEntry));
            EnsureArg.IsNotNull(context.Request, nameof(context.Request));

            IResourceId     patientId       = context.Request.Patient.ResourceId;
            IResourceId     imagingStudyId  = context.Request.ImagingStudy.ResourceId;
            ChangeFeedEntry changeFeedEntry = context.ChangeFeedEntry;

            Identifier identifier = IdentifierUtility.CreateIdentifier(changeFeedEntry.StudyInstanceUid);

            IReadOnlyCollection <Observation> observations = _observationParser.Parse(changeFeedEntry.Metadata, patientId.ToResourceReference(), imagingStudyId.ToResourceReference(), identifier);

            if (observations.Count == 0)
            {
                return(Enumerable.Empty <FhirTransactionRequestEntry>());
            }

            Identifier imagingStudyIdentifier = imagingStudyId.ToResourceReference().Identifier;
            IEnumerable <Observation> existingDoseSummariesAsync = imagingStudyIdentifier != null
                ? await _fhirService
                                                                   .RetrieveObservationsAsync(
                imagingStudyId.ToResourceReference().Identifier,
                cancellationToken)
                : new List <Observation>();

            // TODO: Figure out a way to match existing observations with newly created ones.

            List <FhirTransactionRequestEntry> fhirRequests = new List <FhirTransactionRequestEntry>();

            foreach (var observation in observations)
            {
                fhirRequests.Add(new FhirTransactionRequestEntry(
                                     FhirTransactionRequestMode.Create,
                                     new Bundle.RequestComponent()
                {
                    Method = Bundle.HTTPVerb.POST,
                    Url    = ResourceType.Observation.GetLiteral()
                },
                                     new ClientResourceId(),
                                     observation));
            }

            return(fhirRequests);
        }
        public async Task GivenAChangeFeedEntryForCreate_WhenPreparingTheRequest_ThenCreateHandlerIsCalled()
        {
            const string studyInstanceUid  = "1";
            const string seriesInstanceUid = "2";
            const string sopInstanceUid    = "3";

            ChangeFeedEntry changeFeed = ChangeFeedGenerator.Generate(
                action: ChangeFeedAction.Create,
                studyInstanceUid: studyInstanceUid,
                seriesInstanceUid: seriesInstanceUid,
                sopInstanceUid: sopInstanceUid);

            var fhirTransactionContext = new FhirTransactionContext(changeFeed);

            await _imagingStudyPipeline.PrepareRequestAsync(fhirTransactionContext, DefaultCancellationToken);

            await _imagingStudyUpsertHandler.Received(1).BuildAsync(fhirTransactionContext, DefaultCancellationToken);
        }
        public async Task PrepareRequestAsync(FhirTransactionContext context, CancellationToken cancellationToken = default)
        {
            EnsureArg.IsNotNull(context, nameof(context));

            if (_dicomCastConfiguration.Features.GenerateObservations)
            {
                ChangeFeedEntry changeFeedEntry = context.ChangeFeedEntry;
                context.Request.Observation = changeFeedEntry.Action switch
                {
                    ChangeFeedAction.Create => await _observationUpsertHandler.BuildAsync(context, cancellationToken),
                    ChangeFeedAction.Delete => await _observationDeleteHandler.BuildAsync(context, cancellationToken),
                    _ => throw new NotSupportedException(
                              string.Format(
                                  CultureInfo.InvariantCulture,
                                  DicomCastCoreResource.NotSupportedChangeFeedAction,
                                  changeFeedEntry.Action))
                };
            }
        }
Beispiel #22
0
        private async Task AddSeriesToImagingStudyAsync(FhirTransactionContext context, ImagingStudy imagingStudy, CancellationToken cancellationToken)
        {
            ChangeFeedEntry changeFeedEntry = context.ChangeFeedEntry;

            List <ImagingStudy.SeriesComponent> existingSeriesCollection = imagingStudy.Series;

            ImagingStudy.InstanceComponent instance = new ImagingStudy.InstanceComponent()
            {
                Uid = changeFeedEntry.SopInstanceUid,
            };

            // Checks if the given series already exists within a study
            ImagingStudy.SeriesComponent series = ImagingStudyPipelineHelper.GetSeriesWithinAStudy(changeFeedEntry.SeriesInstanceUid, existingSeriesCollection);

            if (series == null)
            {
                series = new ImagingStudy.SeriesComponent()
                {
                    Uid = changeFeedEntry.SeriesInstanceUid,
                };

                series.Instance.Add(instance);
                imagingStudy.Series.Add(series);
            }
            else
            {
                ImagingStudy.InstanceComponent existingInstance = ImagingStudyPipelineHelper.GetInstanceWithinASeries(changeFeedEntry.SopInstanceUid, series);

                if (existingInstance == null)
                {
                    series.Instance.Add(instance);
                }
                else
                {
                    instance = existingInstance;
                }
            }

            await _imagingStudySynchronizer.SynchronizeSeriesPropertiesAsync(context, series, cancellationToken);

            await _imagingStudySynchronizer.SynchronizeInstancePropertiesAsync(context, instance, cancellationToken);
        }
Beispiel #23
0
        /// <inheritdoc/>
        public Task ProcessAsync(ChangeFeedEntry changeFeedEntry, CancellationToken cancellationToken)
        {
            Context context = new Context();

            context[nameof(ChangeFeedEntry)] = changeFeedEntry;

            return(_retryPolicy.ExecuteAsync(
                       (ctx, tkn) =>
            {
                try
                {
                    return _fhirTransactionPipeline.ProcessAsync(changeFeedEntry, cancellationToken);
                }
                catch (TaskCanceledException ex) when(!cancellationToken.IsCancellationRequested)
                {
                    throw new RetryableException(ex);
                }
            },
                       context,
                       cancellationToken));
        }
Beispiel #24
0
        /// <inheritdoc/>
        public async Task WriteExceptionAsync(ChangeFeedEntry changeFeedEntry, Exception exceptionToStore, ErrorType errorType, CancellationToken cancellationToken)
        {
            EnsureArg.IsNotNull(changeFeedEntry, nameof(changeFeedEntry));

            string tableName = errorType switch
            {
                ErrorType.FhirError => Constants.FhirExceptionTableName,
                ErrorType.DicomError => Constants.DicomExceptionTableName,
                ErrorType.DicomValidationError => Constants.DicomValidationTableName,
                ErrorType.TransientFailure => Constants.TransientFailureTableName,
                _ => null,
            };

            if (tableName == null)
            {
                return;
            }

            DicomDataset dataset            = changeFeedEntry.Metadata;
            string       studyInstanceUid   = dataset.GetSingleValue <string>(DicomTag.StudyInstanceUID);
            string       seriesInstanceUid  = dataset.GetSingleValue <string>(DicomTag.SeriesInstanceUID);
            string       sopInstanceUid     = dataset.GetSingleValue <string>(DicomTag.SOPInstanceUID);
            long         changeFeedSequence = changeFeedEntry.Sequence;

            var tableClient = _tableServiceClient.GetTableClient(tableName);
            var entity      = new IntransientEntity(studyInstanceUid, seriesInstanceUid, sopInstanceUid, changeFeedSequence, exceptionToStore);

            try
            {
                await tableClient.UpsertEntityAsync(entity, cancellationToken : cancellationToken);

                _logger.LogInformation("Error when processing changefeed entry: {ChangeFeedSequence} for DICOM instance with StudyUID: {StudyInstanceUid}, SeriesUID: {SeriesInstanceUid}, InstanceUID: {SopInstanceUid}. Stored into table: {Table} in table storage.", changeFeedSequence, studyInstanceUid, seriesInstanceUid, sopInstanceUid, tableName);
            }
            catch
            {
                _logger.LogInformation("Error when processing changefeed entry: {ChangeFeedSequence} for DICOM instance with StudyUID: {StudyInstanceUid}, SeriesUID: {SeriesInstanceUid}, InstanceUID: {SopInstanceUid}. Failed to store to table storage.", changeFeedSequence, studyInstanceUid, seriesInstanceUid, sopInstanceUid);
                throw;
            }
        }
        public async Task GivenAChangeFeedEntryForDelete_WhenBuilt_ThenDeleteHandlerIsCalled()
        {
            const string studyInstanceUid  = "1";
            const string seriesInstanceUid = "2";
            const string sopInstanceUid    = "3";
            const string patientResourceId = "p1";

            ChangeFeedEntry changeFeed = ChangeFeedGenerator.Generate(
                action: ChangeFeedAction.Delete,
                studyInstanceUid: studyInstanceUid,
                seriesInstanceUid: seriesInstanceUid,
                sopInstanceUid: sopInstanceUid);

            var fhirTransactionContext = new FhirTransactionContext(changeFeed);

            fhirTransactionContext.Request.Patient = FhirTransactionRequestEntryGenerator.GenerateDefaultNoChangeRequestEntry <Patient>(
                new ServerResourceId(ResourceType.Patient, patientResourceId));

            await _imagingStudyPipeline.PrepareRequestAsync(fhirTransactionContext, DefaultCancellationToken);

            await _imagingStudyDeleteHandler.Received(1).BuildAsync(fhirTransactionContext, DefaultCancellationToken);
        }
        public static ChangeFeedEntry Generate(long?sequence = null, ChangeFeedAction?action = null, string studyInstanceUid = null, string seriesInstanceUid = null, string sopInstanceUid = null, ChangeFeedState?state = null, DicomDataset metadata = null)
        {
            if (sequence == null)
            {
                sequence = 1;
            }

            if (action == null)
            {
                action = ChangeFeedAction.Create;
            }

            if (string.IsNullOrEmpty(studyInstanceUid))
            {
                studyInstanceUid = DicomUID.Generate().UID;
            }

            if (string.IsNullOrEmpty(seriesInstanceUid))
            {
                seriesInstanceUid = DicomUID.Generate().UID;
            }

            if (string.IsNullOrEmpty(sopInstanceUid))
            {
                sopInstanceUid = DicomUID.Generate().UID;
            }

            if (state == null)
            {
                state = ChangeFeedState.Current;
            }

            var changeFeedEntry = new ChangeFeedEntry(sequence.Value, DateTime.UtcNow, action.Value, studyInstanceUid, seriesInstanceUid, sopInstanceUid, state.Value)
            {
                Metadata = metadata,
            };

            return(changeFeedEntry);
        }
Beispiel #27
0
        public async Task GivenAnInstance_WhenRetrievingChangeFeedWithPartition_ThenPartitionNameIsReturned()
        {
            var    newPartition      = TestUidGenerator.Generate();
            string studyInstanceUID  = TestUidGenerator.Generate();
            string seriesInstanceUID = TestUidGenerator.Generate();
            string sopInstanceUID    = TestUidGenerator.Generate();

            DicomFile dicomFile = Samples.CreateRandomDicomFile(studyInstanceUID, seriesInstanceUID, sopInstanceUID);

            using DicomWebResponse <DicomDataset> response1 = await _instancesManager.StoreAsync(new[] { dicomFile }, partitionName : newPartition);

            Assert.True(response1.IsSuccessStatusCode);

            long initialSequence;

            using (DicomWebResponse <ChangeFeedEntry> response = await _client.GetChangeFeedLatest("?includemetadata=false"))
            {
                ChangeFeedEntry changeFeedEntry = await response.GetValueAsync();

                initialSequence = changeFeedEntry.Sequence;

                Assert.Equal(newPartition, changeFeedEntry.PartitionName);
                Assert.Equal(studyInstanceUID, changeFeedEntry.StudyInstanceUid);
                Assert.Equal(seriesInstanceUID, changeFeedEntry.SeriesInstanceUid);
                Assert.Equal(sopInstanceUID, changeFeedEntry.SopInstanceUid);
            }

            using (DicomWebAsyncEnumerableResponse <ChangeFeedEntry> response = await _client.GetChangeFeed($"?offset={initialSequence - 1}&includemetadata=false"))
            {
                ChangeFeedEntry[] changeFeedResults = await response.ToArrayAsync();

                Assert.Single(changeFeedResults);
                Assert.Null(changeFeedResults[0].Metadata);
                Assert.Equal(newPartition, changeFeedResults[0].PartitionName);
                Assert.Equal(ChangeFeedState.Current, changeFeedResults[0].State);
            }
        }
        /// <inheritdoc/>
        public async Task ProcessAsync(ChangeFeedEntry changeFeedEntry, CancellationToken cancellationToken)
        {
            EnsureArg.IsNotNull(changeFeedEntry, nameof(changeFeedEntry));

            using (LogProcessingDelegate(_logger, changeFeedEntry.Sequence))
            {
                try
                {
                    await _fhirTransactionPipeline.ProcessAsync(changeFeedEntry, cancellationToken);

                    LogSuccessfullyProcessedDelegate(_logger, null);
                }
                catch (OperationCanceledException) when(cancellationToken.IsCancellationRequested)
                {
                    // Cancel requested
                    throw;
                }
                catch (Exception ex)
                {
                    LogExceptionDelegate(_logger, ex);
                    throw;
                }
            }
        }
        private async Task <FhirTransactionRequestEntry> BuildImagingStudyEntryComponent(string studyInstanceUid, string seriesInstanceUid, string sopInstanceUid, string patientResourceId)
        {
            ChangeFeedEntry changeFeedEntry = ChangeFeedGenerator.Generate(action: ChangeFeedAction.Delete, studyInstanceUid: studyInstanceUid, seriesInstanceUid: seriesInstanceUid, sopInstanceUid: sopInstanceUid);

            return(await PrepareRequestAsync(changeFeedEntry, patientResourceId));
        }
 public ChangeFeedLatestResponse(ChangeFeedEntry entry)
 {
     Entry = entry;
 }