public TObservation MergeObservation(ILookupTemplate <IFhirTemplate> lookup, IObservationGroup observationGroup, TObservation existingObservation)
        {
            EnsureArg.IsNotNull(observationGroup, nameof(observationGroup));

            var(template, processor) = GetTemplateAndProcessor(observationGroup.Name, lookup);
            return(processor.MergeObservation(template, observationGroup, existingObservation));
        }
        public IEnumerable <IObservationGroup> CreateObservationGroups(ILookupTemplate <IFhirTemplate> lookup, IMeasurementGroup measurementGroup)
        {
            EnsureArg.IsNotNull(measurementGroup, nameof(measurementGroup));

            var(template, processor) = GetTemplateAndProcessor(measurementGroup.MeasureType, lookup);
            return(processor.CreateObservationGroups(template, measurementGroup));
        }
Example #3
0
        private void CheckForTemplateCompatibility(IContentTemplate contentTemplate, ILookupTemplate <IFhirTemplate> fhirTemplate, TemplateResult validationResult)
        {
            var deviceTemplates        = new List <MeasurementExtractor>();
            var fhirTemplates          = new List <CodeValueFhirTemplate>();
            var availableFhirTemplates = string.Empty;

            // TODO: Confirm that outer template factories are always collections for both Device and Fhir Mappings. This implies that
            // customers must always wrap their templates inside of a CollectionXXX Template.

            if (contentTemplate is CollectionContentTemplate collectionContentTemplate)
            {
                deviceTemplates.AddRange(collectionContentTemplate.Templates.Select(t => t as MeasurementExtractor));
            }

            if (fhirTemplate is FhirLookupTemplate fhirLookupTemplate)
            {
                fhirTemplates.AddRange(fhirLookupTemplate.Templates.Select(t => t as CodeValueFhirTemplate));
                availableFhirTemplates = string.Join(" ,", fhirTemplates.Select(t => t.TypeName));
            }

            foreach (var extractor in deviceTemplates)
            {
                try
                {
                    var innerTemplate                  = extractor.Template;
                    var matchingFhirTemplate           = fhirTemplate.GetTemplate(innerTemplate.TypeName) as CodeValueFhirTemplate;
                    var availableFhirValueNames        = new HashSet <string>(GetFhirValues(matchingFhirTemplate).Select(v => v.ValueName));
                    var availableFhirValueNamesDisplay = string.Join(" ,", availableFhirValueNames);

                    // Ensure all values are present
                    if (extractor.Template.Values != null)
                    {
                        foreach (var v in extractor.Template.Values)
                        {
                            if (!availableFhirValueNames.Contains(v.ValueName))
                            {
                                validationResult.CaptureWarning(
                                    $"The value [{v.ValueName}] in Device Mapping [{extractor.Template.TypeName}] is not represented within the Fhir Template of type [{innerTemplate.TypeName}]. Available values are: [{availableFhirValueNamesDisplay}]. No value will appear inside of Observations.",
                                    ValidationCategory.FHIRTRANSFORMATION,
                                    LineInfo.Default);
                            }
                        }
                    }
                }
                catch (TemplateNotFoundException)
                {
                    validationResult.CaptureWarning(
                        $"No matching Fhir Template exists for Device Mapping [{extractor.Template.TypeName}]. Ensure case matches. Available Fhir Templates: [{availableFhirTemplates}].",
                        ValidationCategory.FHIRTRANSFORMATION,
                        LineInfo.Default);
                }
                catch (Exception e)
                {
                    validationResult.CaptureException(e, ValidationCategory.FHIRTRANSFORMATION);
                }
            }
        }
Example #4
0
        public override async Task ProcessAsync(ILookupTemplate <IFhirTemplate> config, IMeasurementGroup data, Func <Exception, IMeasurementGroup, Task <bool> > errorConsumer = null)
        {
            // Get required ids
            var ids = await ResourceIdentityService.ResolveResourceIdentitiesAsync(data).ConfigureAwait(false);

            var grps = _fhirTemplateProcessor.CreateObservationGroups(config, data);

            foreach (var grp in grps)
            {
                _ = await SaveObservationAsync(config, grp, ids).ConfigureAwait(false);
            }
        }
Example #5
0
        public virtual async Task <string> SaveObservationAsync(ILookupTemplate <IFhirTemplate> config, IObservationGroup observationGroup, IDictionary <ResourceType, string> ids)
        {
            var identifier = GenerateObservationIdentifier(observationGroup, ids);
            var cacheKey   = $"{identifier.System}|{identifier.Value}";

            if (!_observationCache.TryGetValue(cacheKey, out Model.Observation existingObservation))
            {
                existingObservation = await GetObservationFromServerAsync(identifier).ConfigureAwait(false);
            }

            Model.Observation result;
            if (existingObservation == null)
            {
                var newObservation = GenerateObservation(config, observationGroup, identifier, ids);
                result = await _client.CreateAsync(newObservation).ConfigureAwait(false);

                _logger.LogMetric(IomtMetrics.FhirResourceSaved(ResourceType.Observation, ResourceOperation.Created), 1);
            }
            else
            {
                var policyResult = await Policy <Model.Observation>
                                   .Handle <FhirOperationException>(ex => ex.Status == System.Net.HttpStatusCode.Conflict || ex.Status == System.Net.HttpStatusCode.PreconditionFailed)
                                   .RetryAsync(2, async(polyRes, attempt) =>
                {
                    existingObservation = await GetObservationFromServerAsync(identifier).ConfigureAwait(false);
                })
                                   .ExecuteAndCaptureAsync(async() =>
                {
                    var mergedObservation = MergeObservation(config, existingObservation, observationGroup);
                    return(await _client.UpdateAsync(mergedObservation, versionAware: true).ConfigureAwait(false));
                }).ConfigureAwait(false);

                var exception = policyResult.FinalException;

                if (exception != null)
                {
                    throw exception;
                }

                result = policyResult.Result;
                _logger.LogMetric(IomtMetrics.FhirResourceSaved(ResourceType.Observation, ResourceOperation.Updated), 1);
            }

            _observationCache.CreateEntry(cacheKey)
            .SetAbsoluteExpiration(DateTimeOffset.UtcNow.AddHours(1))
            .SetSize(1)
            .SetValue(result)
            .Dispose();

            return(result.Id);
        }
Example #6
0
        public virtual async Task <string> SaveObservationAsync(ILookupTemplate <IFhirTemplate> config, IObservationGroup observationGroup, IDictionary <ResourceType, string> ids)
        {
            var identifier = GenerateObservationIdentifier(observationGroup, ids);
            var cacheKey   = $"{identifier.System}|{identifier.Value}";

            if (!_observationCache.TryGetValue(cacheKey, out Model.Observation existingObservation))
            {
                existingObservation = await GetObservationFromServerAsync(identifier).ConfigureAwait(false);
            }

            Model.Observation result;
            if (existingObservation == null)
            {
                var newObservation = GenerateObservation(config, observationGroup, identifier, ids);
                result = await _client.CreateAsync <Model.Observation>(newObservation).ConfigureAwait(false);
            }
            else
            {
                var policyResult = await Policy <Model.Observation>
                                   .Handle <FhirOperationException>(ex => ex.Status == System.Net.HttpStatusCode.Conflict)
                                   .FallbackAsync(async ct =>
                {
                    var refreshObservation = await GetObservationFromServerAsync(identifier).ConfigureAwait(false);
                    var mergedObservation  = MergeObservation(config, refreshObservation, observationGroup);
                    return(await _client.UpdateAsync(mergedObservation, versionAware: true).ConfigureAwait(false));
                })
                                   .ExecuteAndCaptureAsync(async() =>
                {
                    var mergedObservation = MergeObservation(config, existingObservation, observationGroup);
                    return(await _client.UpdateAsync(mergedObservation, versionAware: true).ConfigureAwait(false));
                }).ConfigureAwait(false);

                result = policyResult.Result;
            }

            _observationCache.CreateEntry(cacheKey)
            .SetAbsoluteExpiration(DateTimeOffset.UtcNow.AddHours(1))
            .SetSize(1)
            .SetValue(result)
            .Dispose();

            return(result.Id);
        }
Example #7
0
        public ValidationResult PerformValidation(
            IEnumerable <JToken> deviceEvents,
            string deviceMappingContent,
            string fhirMappingContent,
            bool aggregateDeviceEvents = false)
        {
            if (string.IsNullOrWhiteSpace(deviceMappingContent) && string.IsNullOrWhiteSpace(fhirMappingContent))
            {
                throw new ArgumentException($"At least one of [{nameof(deviceMappingContent)}] or [{nameof(fhirMappingContent)}] must be provided");
            }

            var validationResult = new ValidationResult();

            IContentTemplate contentTemplate             = null;
            ILookupTemplate <IFhirTemplate> fhirTemplate = null;

            if (!string.IsNullOrEmpty(deviceMappingContent))
            {
                contentTemplate = LoadDeviceTemplate(deviceMappingContent, validationResult.TemplateResult);
            }

            if (!string.IsNullOrEmpty(fhirMappingContent))
            {
                fhirTemplate = LoadFhirTemplate(fhirMappingContent, validationResult.TemplateResult);
            }

            if (contentTemplate != null && fhirTemplate != null)
            {
                CheckForTemplateCompatibility(contentTemplate, fhirTemplate, validationResult.TemplateResult);
            }

            if (validationResult.TemplateResult.GetErrors(ErrorLevel.ERROR).Count() > 0)
            {
                // Fail early since there are errors with the template.
                return(validationResult);
            }

            ValidateDeviceEvents(deviceEvents, contentTemplate, fhirTemplate, validationResult, aggregateDeviceEvents);

            return(validationResult);
        }
Example #8
0
        public virtual Model.Observation GenerateObservation(ILookupTemplate <IFhirTemplate> config, IObservationGroup grp, Model.Identifier observationId, IDictionary <ResourceType, string> ids)
        {
            EnsureArg.IsNotNull(grp, nameof(grp));
            EnsureArg.IsNotNull(observationId, nameof(observationId));

            var patientId = Ensure.String.IsNotNullOrWhiteSpace(ids[ResourceType.Patient], nameof(ResourceType.Patient));
            var deviceId  = Ensure.String.IsNotNullOrWhiteSpace(ids[ResourceType.Device], nameof(ResourceType.Device));

            var observation = _fhirTemplateProcessor.CreateObservation(config, grp);

            observation.Subject    = patientId.ToReference <Model.Patient>();
            observation.Device     = deviceId.ToReference <Model.Device>();
            observation.Identifier = new List <Model.Identifier> {
                observationId
            };

            if (ids.TryGetValue(ResourceType.Encounter, out string encounterId))
            {
                observation.Encounter = encounterId.ToReference <Model.Encounter>();
            }

            return(observation);
        }
Example #9
0
        protected virtual void ProcessNormalizedEvent(Measurement normalizedEvent, ILookupTemplate <IFhirTemplate> fhirTemplate, DeviceResult validationResult)
        {
            var measurementGroup = new MeasurementGroup
            {
                MeasureType   = normalizedEvent.Type,
                CorrelationId = normalizedEvent.CorrelationId,
                DeviceId      = normalizedEvent.DeviceId,
                EncounterId   = normalizedEvent.EncounterId,
                PatientId     = normalizedEvent.PatientId,
                Data          = new List <Measurement>()
                {
                    normalizedEvent
                },
            };

            try
            {
                // Convert Measurement to Observation Group
                var observationGroup = _fhirTemplateProcessor.CreateObservationGroups(fhirTemplate, measurementGroup).First();

                // Build HL7 Observation
                validationResult.Observations.Add(_fhirTemplateProcessor.CreateObservation(fhirTemplate, observationGroup));
            }
            catch (TemplateNotFoundException e)
            {
                validationResult.CaptureError(
                    $"No Fhir Template exists with the type name [{e.Message}]. Ensure that all Fhir Template type names match Device Mapping type names (including casing)",
                    ErrorLevel.ERROR,
                    ValidationCategory.FHIRTRANSFORMATION,
                    LineInfo.Default);
            }
            catch (Exception e)
            {
                validationResult.CaptureException(e, ValidationCategory.FHIRTRANSFORMATION);
            }
        }
Example #10
0
 public virtual Model.Observation MergeObservation(ILookupTemplate <IFhirTemplate> config, Model.Observation observation, IObservationGroup grp)
 {
     return(_fhirTemplateProcessor.MergeObservation(config, grp, observation));
 }
        private (IFhirTemplate template, IFhirTemplateProcessor <IFhirTemplate, TObservation> processor) GetTemplateAndProcessor(string lookupValue, ILookupTemplate <IFhirTemplate> lookup)
        {
            EnsureArg.IsNotNullOrWhiteSpace(lookupValue, nameof(lookupValue));
            EnsureArg.IsNotNull(lookup, nameof(lookup));

            var template     = lookup.GetTemplate(lookupValue);
            var templateType = template.GetType();

            if (!_registeredTemplateProcessors.TryGetValue(templateType, out var processor))
            {
                throw new NotSupportedException($"No processor registered for template type {templateType}.");
            }

            return(template, processor);
        }
 public override Task ProcessAsync(ILookupTemplate <IFhirTemplate> config, IMeasurementGroup data, Func <Exception, IMeasurementGroup, Task <bool> > errorConsumer = null)
 {
     throw new NotImplementedException();
 }
        private async Task ProcessMeasurementGroups(IEnumerable <IMeasurementGroup> measurementGroups, ILookupTemplate <IFhirTemplate> template, ITelemetryLogger log)
        {
            // Group work by device to avoid race conditions when resource creation is enabled.
            var workItems = measurementGroups.GroupBy(grp => grp.DeviceId)
                            .Select(grp => new Func <Task>(
                                        async() =>
            {
                foreach (var m in grp)
                {
                    try
                    {
                        await _fhirImportService.ProcessAsync(template, m).ConfigureAwait(false);
                    }
                    catch (Exception ex)
                    {
                        if (!Options.ExceptionService.HandleException(ex, log))
                        {
                            throw;
                        }
                    }
                }
            }));

            await StartWorker(workItems).ConfigureAwait(false);
        }
Example #14
0
 public abstract Task ProcessAsync(ILookupTemplate <IFhirTemplate> config, IMeasurementGroup data, Func <Exception, IMeasurementGroup, Task <bool> > errorConsumer = null);
Example #15
0
        /// <summary>
        /// Validates device events. This method then enriches the passed in ValidationResult object with DeviceResults.
        /// </summary>
        /// <param name="deviceEvents">The device events to validate</param>
        /// <param name="contentTemplate">The device mapping template</param>
        /// <param name="fhirTemplate">The fhir mapping template</param>
        /// <param name="validationResult">The ValidationResult</param>
        /// <param name="aggregateDeviceEvents">Indicates if DeviceResults should be aggregated</param>
        protected virtual void ValidateDeviceEvents(
            IEnumerable <JToken> deviceEvents,
            IContentTemplate contentTemplate,
            ILookupTemplate <IFhirTemplate> fhirTemplate,
            ValidationResult validationResult,
            bool aggregateDeviceEvents)
        {
            var aggregatedDeviceResults = new Dictionary <string, DeviceResult>();

            foreach (var payload in deviceEvents)
            {
                if (payload != null && contentTemplate != null)
                {
                    var deviceResult = new DeviceResult();
                    deviceResult.DeviceEvent = payload;

                    ProcessDeviceEvent(payload, contentTemplate, deviceResult);

                    if (fhirTemplate != null)
                    {
                        foreach (var m in deviceResult.Measurements)
                        {
                            ProcessNormalizedEvent(m, fhirTemplate, deviceResult);
                        }
                    }

                    if (aggregateDeviceEvents)
                    {
                        /*
                         * During aggregation we group DeviceEvents by the exceptions that they produce.
                         * This allows us to return a DeviceResult with a sample Device Event payload,
                         * the running count grouped DeviceEvents and the exception that they are grouped by.
                         */
                        foreach (var exception in deviceResult.Exceptions)
                        {
                            if (aggregatedDeviceResults.TryGetValue(exception.Message, out DeviceResult result))
                            {
                                // If we've already seen this error message before, simply increment the running total
                                result.AggregatedCount++;
                            }
                            else
                            {
                                // Create a new DeviceResult to hold details about this new exception.
                                var aggregatedDeviceResult = new DeviceResult()
                                {
                                    DeviceEvent     = deviceResult.DeviceEvent, // A sample device event which exhibits the error
                                    AggregatedCount = 1,
                                    Exceptions      = new List <ValidationError>()
                                    {
                                        exception
                                    },
                                };

                                aggregatedDeviceResults[exception.Message] = aggregatedDeviceResult;
                                validationResult.DeviceResults.Add(aggregatedDeviceResult);
                            }
                        }
                    }
                    else
                    {
                        validationResult.DeviceResults.Add(deviceResult);
                    }
                }
            }
        }
Example #16
0
        public virtual async Task <string> SaveObservationAsync(ILookupTemplate <IFhirTemplate> config, IObservationGroup observationGroup, IDictionary <ResourceType, string> ids)
        {
            var identifier = GenerateObservationIdentifier(observationGroup, ids);
            var cacheKey   = $"{identifier.System}|{identifier.Value}";

            if (!_observationCache.TryGetValue(cacheKey, out Model.Observation existingObservation))
            {
                existingObservation = await GetObservationFromServerAsync(identifier).ConfigureAwait(false);

                // Discovered an issue where FHIR Service is only matching on first 128 characters of the identifier.  This is a temporary measure to prevent merging of different observations until a fix is available.
                if (existingObservation != null && !existingObservation.Identifier.Exists(i => i.IsExactly(identifier)))
                {
                    throw new NotSupportedException("FHIR Service returned matching observation but expected identifier was not present.");
                }
            }

            var policyResult = await Policy <(Model.Observation observation, ResourceOperation operationType)>
                               .Handle <FhirException>(ex => ex.StatusCode == System.Net.HttpStatusCode.Conflict || ex.StatusCode == System.Net.HttpStatusCode.PreconditionFailed)
                               .RetryAsync(2, async(polyRes, attempt) =>
            {
                // 409 Conflict or 412 Precondition Failed can occur if the Observation.meta.versionId does not match the update request.
                // This can happen if 2 independent processes are updating the same Observation simultaneously.
                // or
                // The update operation failed because the Observation no longer exists.
                // This can happen if a cached Observation was deleted from the FHIR Server.

                _logger.LogTrace("A conflict or precondition caused an Observation update to fail. Getting the most recent Observation.");

                // Attempt to get the most recent version of the Observation.
                existingObservation = await GetObservationFromServerAsync(identifier).ConfigureAwait(false);

                // If the Observation no longer exists on the FHIR Server, it was most likely deleted.
                if (existingObservation == null)
                {
                    _logger.LogTrace("A cached version of an Observation was deleted. Creating a new Observation.");

                    // Remove the Observation from the cache (this version no longer exists on the FHIR Server.
                    _observationCache.Remove(cacheKey);
                }
            })
                               .ExecuteAndCaptureAsync(async() =>
            {
                if (existingObservation == null)
                {
                    var newObservation = GenerateObservation(config, observationGroup, identifier, ids);
                    return(await _fhirService.CreateResourceAsync(newObservation).ConfigureAwait(false), ResourceOperation.Created);
                }

                // Merge the new data with the existing Observation.
                var mergedObservation = MergeObservation(config, existingObservation, observationGroup);

                // Check to see if there are any changes after merging and update the Status to amended if changed.
                if (!existingObservation.AmendIfChanged(mergedObservation))
                {
                    // There are no changes to the Observation - Do not update.
                    return(existingObservation, ResourceOperation.NoOperation);
                }

                // Update the Observation. Some failures will be handled in the RetryAsync block above.
                return(await _fhirService.UpdateResourceAsync(mergedObservation).ConfigureAwait(false), ResourceOperation.Updated);
            }).ConfigureAwait(false);

            var exception = policyResult.FinalException;

            if (exception != null)
            {
                throw exception;
            }

            var observation = policyResult.Result.observation;

            _logger.LogMetric(IomtMetrics.FhirResourceSaved(ResourceType.Observation, policyResult.Result.operationType), 1);

            _observationCache.CreateEntry(cacheKey)
            .SetAbsoluteExpiration(DateTimeOffset.UtcNow.AddHours(1))
            .SetSize(1)
            .SetValue(observation)
            .Dispose();

            return(observation.Id);
        }