From a31879d35351af9ee8ba737f3c72d61ea87069f0 Mon Sep 17 00:00:00 2001 From: Isao Yamauchi Date: Thu, 31 Jul 2025 14:30:11 -0700 Subject: [PATCH 1/7] Initial change of clinical reference duplication. --- .../Configs/CoreFeatureConfiguration.cs | 5 + .../Modules/FhirModule.cs | 9 +- ...DuplicateClinicalReferenceBehaviorTests.cs | 111 ++++++++++ ...ealth.Fhir.Shared.Core.UnitTests.projitems | 1 + .../DuplicateClinicalReferenceBehavior.cs | 194 ++++++++++++++++++ ...icrosoft.Health.Fhir.Shared.Core.projitems | 1 + 6 files changed, 319 insertions(+), 2 deletions(-) create mode 100644 src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/DuplicateClinicalReferenceBehaviorTests.cs create mode 100644 src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/DuplicateClinicalReferenceBehavior.cs diff --git a/src/Microsoft.Health.Fhir.Core/Configs/CoreFeatureConfiguration.cs b/src/Microsoft.Health.Fhir.Core/Configs/CoreFeatureConfiguration.cs index 738615f5e7..c23acca82b 100644 --- a/src/Microsoft.Health.Fhir.Core/Configs/CoreFeatureConfiguration.cs +++ b/src/Microsoft.Health.Fhir.Core/Configs/CoreFeatureConfiguration.cs @@ -96,5 +96,10 @@ public class CoreFeatureConfiguration /// Gets or sets a value indicating whether the server supports the includes. /// public bool SupportsIncludes { get; set; } = false; + + /// + /// Gets or sets a value indicating whether the server enables clinical reference duplication. + /// + public bool EnableClinicalReferenceDuplication { get; set; } = false; } } diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs b/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs index 3c8408e7e9..e3fdc6803f 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs @@ -27,7 +27,6 @@ using Microsoft.Health.Fhir.Api.Features.Resources; using Microsoft.Health.Fhir.Api.Features.Resources.Bundle; using Microsoft.Health.Fhir.Core.Extensions; -using Microsoft.Health.Fhir.Core.Features; using Microsoft.Health.Fhir.Core.Features.Conformance; using Microsoft.Health.Fhir.Core.Features.Context; using Microsoft.Health.Fhir.Core.Features.Health; @@ -35,9 +34,12 @@ using Microsoft.Health.Fhir.Core.Features.Persistence; using Microsoft.Health.Fhir.Core.Features.Security; using Microsoft.Health.Fhir.Core.Messages.CapabilityStatement; +using Microsoft.Health.Fhir.Core.Messages.Create; +using Microsoft.Health.Fhir.Core.Messages.Delete; using Microsoft.Health.Fhir.Core.Messages.Search; -using Microsoft.Health.Fhir.Core.Messages.Storage; +using Microsoft.Health.Fhir.Core.Messages.Upsert; using Microsoft.Health.Fhir.Core.Models; +using Microsoft.Health.Fhir.Shared.Core.Features.Guidance; namespace Microsoft.Health.Fhir.Api.Modules { @@ -193,6 +195,9 @@ ResourceElement SetMetadata(Resource resource, string versionId, DateTimeOffset services.AddScoped(); services.AddTransient(typeof(IScopeProvider<>), typeof(ScopeProvider<>)); + services.AddTransient, DuplicateClinicalReferenceBehavior>(); + services.AddTransient, DuplicateClinicalReferenceBehavior>(); + services.AddTransient, DuplicateClinicalReferenceBehavior>(); } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/DuplicateClinicalReferenceBehaviorTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/DuplicateClinicalReferenceBehaviorTests.cs new file mode 100644 index 0000000000..bcdcb2fc5c --- /dev/null +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/DuplicateClinicalReferenceBehaviorTests.cs @@ -0,0 +1,111 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using Hl7.Fhir.Model; +using Hl7.Fhir.Serialization; +using MediatR; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Health.Fhir.Core.Configs; +using Microsoft.Health.Fhir.Core.Extensions; +using Microsoft.Health.Fhir.Core.Features.Persistence; +using Microsoft.Health.Fhir.Core.Features.Search; +using Microsoft.Health.Fhir.Core.Messages.Create; +using Microsoft.Health.Fhir.Core.Messages.Upsert; +using Microsoft.Health.Fhir.Core.Models; +using Microsoft.Health.Fhir.Shared.Core.Features.Guidance; +using NSubstitute; +using Xunit; +using Task = System.Threading.Tasks.Task; + +namespace Microsoft.Health.Fhir.Shared.Core.UnitTests.Features.Guidance +{ + public class DuplicateClinicalReferenceBehaviorTests + { + private readonly DuplicateClinicalReferenceBehavior _behavior; + private readonly IMediator _mediator; + private readonly ISearchService _searchService; + private readonly CoreFeatureConfiguration _coreFeatureConfiguration; + private readonly ILogger _logger; + private readonly IRawResourceFactory _rawResourceFactory; + private readonly IResourceWrapperFactory _resourceWrapperFactory; + + public DuplicateClinicalReferenceBehaviorTests() + { + _mediator = Substitute.For(); + _searchService = Substitute.For(); + _logger = Substitute.For>(); + _coreFeatureConfiguration = new CoreFeatureConfiguration + { + EnableClinicalReferenceDuplication = true, + }; + + _behavior = new DuplicateClinicalReferenceBehavior( + _mediator, + _searchService, + Options.Create(_coreFeatureConfiguration), + _logger); + + _rawResourceFactory = Substitute.For(new FhirJsonSerializer()); + _resourceWrapperFactory = Substitute.For(); + _resourceWrapperFactory + .Create(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(x => CreateResourceWrapper(x.ArgAt(0), x.ArgAt(1))); + } + + [Fact] + public async Task GivenCreateRequest_WhenResourceIsCreated_ThenDuplicateResourceShouldBeCreated() + { + var diagnosticReport = new DiagnosticReport + { + Id = Guid.NewGuid().ToString(), + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/1e404af3-077f-4bee-b7a6-a9be97e1ce32", + }, + }, + }; + + var resourceElement = diagnosticReport.ToResourceElement(); + var resourceWrapper = CreateResourceWrapper(resourceElement); + var request = new CreateResourceRequest(resourceElement); + var response = new UpsertResourceResponse(new SaveOutcome(new RawResourceElement(resourceWrapper), SaveOutcomeType.Created)); + await _behavior.Handle(request, async (ct) => await Task.Run(() => response), CancellationToken.None); + } + + private ResourceWrapper CreateResourceWrapper(ResourceElement resource, bool isDeleted = false) + { + return new ResourceWrapper( + resource, + _rawResourceFactory.Create(resource, keepMeta: true), + new ResourceRequest(HttpMethod.Post, "http://fhir"), + isDeleted, + null, + null, + null, + null, + 0); + } + } +} diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Microsoft.Health.Fhir.Shared.Core.UnitTests.projitems b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Microsoft.Health.Fhir.Shared.Core.UnitTests.projitems index 0824808a8c..2aaf61340a 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Microsoft.Health.Fhir.Shared.Core.UnitTests.projitems +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Microsoft.Health.Fhir.Shared.Core.UnitTests.projitems @@ -16,6 +16,7 @@ + diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/DuplicateClinicalReferenceBehavior.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/DuplicateClinicalReferenceBehavior.cs new file mode 100644 index 0000000000..c4f27b314e --- /dev/null +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/DuplicateClinicalReferenceBehavior.cs @@ -0,0 +1,194 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EnsureThat; +using Hl7.Fhir.Model; +using MediatR; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Health.Fhir.Core.Configs; +using Microsoft.Health.Fhir.Core.Extensions; +using Microsoft.Health.Fhir.Core.Features.Persistence; +using Microsoft.Health.Fhir.Core.Features.Search; +using Microsoft.Health.Fhir.Core.Messages.Create; +using Microsoft.Health.Fhir.Core.Messages.Delete; +using Microsoft.Health.Fhir.Core.Messages.Upsert; +using Microsoft.Health.Fhir.Core.Models; + +namespace Microsoft.Health.Fhir.Shared.Core.Features.Guidance +{ + public class DuplicateClinicalReferenceBehavior : IPipelineBehavior, + IPipelineBehavior, + IPipelineBehavior + { + private readonly IMediator _mediator; + private readonly ISearchService _searchService; + private readonly CoreFeatureConfiguration _coreFeatureConfiguration; + private readonly ILogger _logger; + + public DuplicateClinicalReferenceBehavior( + IMediator mediator, + ISearchService searchService, + IOptions coreFeatureConfiguration, + ILogger logger) + { + EnsureArg.IsNotNull(mediator, nameof(mediator)); + EnsureArg.IsNotNull(searchService, nameof(searchService)); + EnsureArg.IsNotNull(coreFeatureConfiguration?.Value, nameof(coreFeatureConfiguration)); + EnsureArg.IsNotNull(logger, nameof(logger)); + + _mediator = mediator; + _searchService = searchService; + _coreFeatureConfiguration = coreFeatureConfiguration.Value; + _logger = logger; + } + + public async Task Handle( + CreateResourceRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken) + { + var response = await next(cancellationToken); + var resource = response?.Outcome?.RawResourceElement?.RawResource?.ToITypedElement(ModelInfoProvider.Instance)?.ToResourceElement(); + if (_coreFeatureConfiguration.EnableClinicalReferenceDuplication + && resource != null + && (string.Equals(resource.InstanceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase) + || string.Equals(resource.InstanceType, KnownResourceTypes.DocumentReference, StringComparison.OrdinalIgnoreCase))) + { + // TODO: need to differentiate urls since not all urls are of clinical notes (https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#fhir-resources-to-exchange-clinical-notes) + var url = GetUrl(resource); + if (!string.IsNullOrWhiteSpace(url)) + { + _logger.LogInformation($"A url found in '{resource.InstanceType}' resource: {url}"); + + var searchResult = await SearchDeplicateResourceAsync( + resource, + url, + cancellationToken); + var found = searchResult?.Results?.Any() ?? false; + if (!found) + { + _logger.LogInformation($"Creating a duplicate resource of '{resource.InstanceType}' resource..."); + var duplicateResource = CreateDuplicateResource(resource, url); + var createRequest = new CreateResourceRequest(duplicateResource); + var createResponse = await _mediator.Send( + createRequest, + cancellationToken); + if (createResponse?.Outcome?.Outcome == SaveOutcomeType.Created) + { + _logger.LogInformation($"A duplicate resource of '{resource.InstanceType}' resource created."); + } + } + else + { + _logger.LogInformation($"A duplicate resource of '{resource.InstanceType}' resource already exists."); + } + } + } + + return response; + } + + public Task Handle( + UpsertResourceRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken) + { + return next(cancellationToken); + } + + public Task Handle( + DeleteResourceRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken) + { + return next(cancellationToken); + } + + private Task SearchDeplicateResourceAsync( + ResourceElement resourceElement, + string url, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(resourceElement, nameof(resourceElement)); + EnsureArg.IsNotNull(url, nameof(url)); + + var isDiagnosticReport = string.Equals(resourceElement.InstanceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase); + var queryParameters = new List> + { + Tuple.Create("_tag", url), + }; + + return _searchService.SearchAsync( + isDiagnosticReport ? KnownResourceTypes.DocumentReference : KnownResourceTypes.DiagnosticReport, + queryParameters, + cancellationToken); + } + + private static ResourceElement CreateDuplicateResource( + ResourceElement resourceElement, + string url) + { + EnsureArg.IsNotNull(resourceElement, nameof(resourceElement)); + EnsureArg.IsNotNull(url, nameof(url)); + + var isDiagnosticReport = string.Equals(resourceElement.InstanceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase); + if (isDiagnosticReport) + { + // TODO: more fields need to be populated? + var documentReference = new DocumentReference + { + Meta = new Meta + { + Tag = new List + { + new Coding("url", url), + }, + }, +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }; + + return documentReference.ToResourceElement(); + } + + // TODO: more fields need to be populated? + var diagnosticReport = new DiagnosticReport + { + Meta = new Meta + { + Tag = new List + { + new Coding("url", url), + }, + }, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + }; + + return diagnosticReport.ToResourceElement(); + } + + private static string GetUrl(ResourceElement resourceElement) + { + if (resourceElement == null) + { + return null; + } + + var path = string.Equals(resourceElement.InstanceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase) + ? "DiagnosticReport.presentedForm.url" + : "DocumentReference.content.attachment.url"; + return resourceElement.Scalar(path); + } + } +} diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Microsoft.Health.Fhir.Shared.Core.projitems b/src/Microsoft.Health.Fhir.Shared.Core/Microsoft.Health.Fhir.Shared.Core.projitems index 22154ac536..ba72682b1b 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Microsoft.Health.Fhir.Shared.Core.projitems +++ b/src/Microsoft.Health.Fhir.Shared.Core/Microsoft.Health.Fhir.Shared.Core.projitems @@ -16,6 +16,7 @@ + From eb2bbcf8f3cb9211bc9bf7f180429e5e4ceb6739 Mon Sep 17 00:00:00 2001 From: Isao Yamauchi Date: Tue, 5 Aug 2025 13:02:59 -0700 Subject: [PATCH 2/7] Approach 1 prototype --- ...DuplicateClinicalReferenceBehaviorTests.cs | 241 +++++++++-- .../DuplicateClinicalReferenceBehavior.cs | 395 +++++++++++++++--- 2 files changed, 553 insertions(+), 83 deletions(-) diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/DuplicateClinicalReferenceBehaviorTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/DuplicateClinicalReferenceBehaviorTests.cs index bcdcb2fc5c..06d573e937 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/DuplicateClinicalReferenceBehaviorTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/DuplicateClinicalReferenceBehaviorTests.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Threading; using Hl7.Fhir.Model; @@ -21,6 +22,7 @@ using Microsoft.Health.Fhir.Core.Models; using Microsoft.Health.Fhir.Shared.Core.Features.Guidance; using NSubstitute; +using NSubstitute.ExceptionExtensions; using Xunit; using Task = System.Threading.Tasks.Task; @@ -59,38 +61,112 @@ public DuplicateClinicalReferenceBehaviorTests() .Returns(x => CreateResourceWrapper(x.ArgAt(0), x.ArgAt(1))); } - [Fact] - public async Task GivenCreateRequest_WhenResourceIsCreated_ThenDuplicateResourceShouldBeCreated() + [Theory] + [MemberData(nameof(GetCreateResourceData))] + public async Task GivenCreateRequest_WhenResourceIsCreated_ThenDuplicateResourceShouldBeCreated( + Resource resource, + bool shouldDuplicate) { - var diagnosticReport = new DiagnosticReport - { - Id = Guid.NewGuid().ToString(), - Status = DiagnosticReport.DiagnosticReportStatus.Registered, - Code = new CodeableConcept() - { - Coding = new List() + var resourceType = resource.TypeName; + var duplicateResourceType = string.Equals(resource, KnownResourceTypes.DiagnosticReport) + ? KnownResourceTypes.DocumentReference : KnownResourceTypes.DiagnosticReport; + + // Set up a validation on a request for creating the original resource. + var resourceElement = resource.ToResourceElement(); + var resourceWrapper = CreateResourceWrapper(resourceElement); + var request = new CreateResourceRequest(resourceElement); + var response = new UpsertResourceResponse( + new SaveOutcome(new RawResourceElement(resourceWrapper), SaveOutcomeType.Created)); + _mediator.Send( + Arg.Is(x => string.Equals(x.Resource.InstanceType, resourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any()) + .Throws(new Exception($"Shouldn't be called to create a {resourceType} resource.")); + + // Set up a validation on a request for creating a duplicate resource. + Resource duplicateResource = null; + _mediator.Send( + Arg.Is(x => string.Equals(x.Resource.InstanceType, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any()) + .Returns( + x => { - new Coding() + Assert.True(shouldDuplicate, "Duplicating a resource is unnecessary."); + + var re = ((CreateResourceRequest)x[0]).Resource; + Assert.NotNull(re); + Assert.Equal(duplicateResourceType, re.InstanceType, true); + + var r = re.ToPoco(); + Assert.NotNull(r.Meta?.Tag); + Assert.Contains( + r.Meta.Tag, + x => string.Equals(x.System, DuplicateClinicalReferenceBehavior.TagDuplicateOf, StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Code, resource.Id, StringComparison.OrdinalIgnoreCase)); + + if (string.Equals(duplicateResourceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase)) { - Code = "12345", - }, - }, - }, - PresentedForm = new List - { - new Attachment() + var original = (DocumentReference)resource; + var duplicate = (DiagnosticReport)r; + Assert.NotNull(duplicate.PresentedForm); + foreach (var a in original.Content.Select(x => x.Attachment)) + { + Assert.Contains( + duplicate.PresentedForm, + x => string.Equals(x.Url, a.Url, StringComparison.OrdinalIgnoreCase)); + } + + duplicateResource = duplicate; + } + else + { + var original = (DiagnosticReport)resource; + var duplicate = (DocumentReference)r; + Assert.NotNull(duplicate.Content); + foreach (var a in original.PresentedForm) + { + Assert.Contains( + duplicate.Content, + x => string.Equals(x.Attachment?.Url, a.Url, StringComparison.OrdinalIgnoreCase)); + } + + duplicateResource = duplicate; + } + + if (string.IsNullOrEmpty(r.Id)) + { + r.Id = Guid.NewGuid().ToString(); + } + + return Task.FromResult( + new UpsertResourceResponse( + new SaveOutcome(new RawResourceElement(CreateResourceWrapper(r.ToResourceElement())), SaveOutcomeType.Created))); + }); + + // Set up a validation on a request for updating the initial resource with the id of the duplicate resource. + _mediator.Send( + Arg.Is(x => string.Equals(x.Resource.InstanceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase)), + Arg.Any()) + .Returns( + x => { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/1e404af3-077f-4bee-b7a6-a9be97e1ce32", - }, - }, - }; + Assert.True(shouldDuplicate, "Duplicating a resource is unnecessary."); + + var re = ((UpsertResourceRequest)x[0]).Resource; + Assert.NotNull(re); + Assert.Equal(KnownResourceTypes.DiagnosticReport, re.InstanceType, true); + + var r = re.ToPoco(); + Assert.NotNull(r.Meta?.Tag); + Assert.Contains( + r.Meta.Tag, + t => string.Equals(t.System, DuplicateClinicalReferenceBehavior.TagDuplicateOf, StringComparison.OrdinalIgnoreCase) + && string.Equals(t.Code, duplicateResource.Id, StringComparison.OrdinalIgnoreCase)); + + return Task.FromResult( + new UpsertResourceResponse( + new SaveOutcome(new RawResourceElement(CreateResourceWrapper(r.ToResourceElement())), SaveOutcomeType.Updated))); + }); - var resourceElement = diagnosticReport.ToResourceElement(); - var resourceWrapper = CreateResourceWrapper(resourceElement); - var request = new CreateResourceRequest(resourceElement); - var response = new UpsertResourceResponse(new SaveOutcome(new RawResourceElement(resourceWrapper), SaveOutcomeType.Created)); await _behavior.Handle(request, async (ct) => await Task.Run(() => response), CancellationToken.None); } @@ -107,5 +183,116 @@ private ResourceWrapper CreateResourceWrapper(ResourceElement resource, bool isD null, 0); } + + public static IEnumerable GetCreateResourceData() + { + var data = new[] + { + new object[] + { + // Create a new DiagnosticReport resource with one attachment. + new DiagnosticReport + { + Id = Guid.NewGuid().ToString(), + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, + }, + }, + true, + }, + new object[] + { + // Create a new DiagnosticReport resource with multiple attachments. + new DiagnosticReport + { + Id = Guid.NewGuid().ToString(), + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment1", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/Binary/attachment2", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment3", + }, + }, + }, + true, + }, + new object[] + { + // Create a new DiagnosticReport resource with one attachment. + new DiagnosticReport + { + Id = Guid.NewGuid().ToString(), + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + }, + false, + }, + }; + + foreach (var d in data) + { + yield return d; + } + } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/DuplicateClinicalReferenceBehavior.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/DuplicateClinicalReferenceBehavior.cs index c4f27b314e..6a93ab0fd8 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/DuplicateClinicalReferenceBehavior.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/DuplicateClinicalReferenceBehavior.cs @@ -28,6 +28,8 @@ public class DuplicateClinicalReferenceBehavior : IPipelineBehavior, IPipelineBehavior { + internal const string TagDuplicateOf = "duplicateOf"; + private readonly IMediator _mediator; private readonly ISearchService _searchService; private readonly CoreFeatureConfiguration _coreFeatureConfiguration; @@ -55,93 +57,266 @@ public async Task Handle( RequestHandlerDelegate next, CancellationToken cancellationToken) { + EnsureArg.IsNotNull(request, nameof(request)); + EnsureArg.IsNotNull(next, nameof(next)); + var response = await next(cancellationToken); - var resource = response?.Outcome?.RawResourceElement?.RawResource?.ToITypedElement(ModelInfoProvider.Instance)?.ToResourceElement(); - if (_coreFeatureConfiguration.EnableClinicalReferenceDuplication - && resource != null - && (string.Equals(resource.InstanceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase) - || string.Equals(resource.InstanceType, KnownResourceTypes.DocumentReference, StringComparison.OrdinalIgnoreCase))) + var resource = response?.Outcome?.RawResourceElement?.RawResource? + .ToITypedElement(ModelInfoProvider.Instance)? + .ToResourceElement()? + .ToPoco(); + if (_coreFeatureConfiguration.EnableClinicalReferenceDuplication && ShouldDuplicate(resource)) { - // TODO: need to differentiate urls since not all urls are of clinical notes (https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#fhir-resources-to-exchange-clinical-notes) - var url = GetUrl(resource); - if (!string.IsNullOrWhiteSpace(url)) + try { - _logger.LogInformation($"A url found in '{resource.InstanceType}' resource: {url}"); - - var searchResult = await SearchDeplicateResourceAsync( + var resourceTypeToCreate = (resource is DiagnosticReport) ? KnownResourceTypes.DocumentReference : KnownResourceTypes.DiagnosticReport; + _logger.LogInformation( + "Creating a '{DuplicateResourceType}' resource to duplicate a '{ResourceType}' resource...", + resourceTypeToCreate, + resource.TypeName); + var createResponse = await CreateResourceAsync( resource, - url, cancellationToken); - var found = searchResult?.Results?.Any() ?? false; - if (!found) + _logger.LogInformation( + "A '{DuplicateResourceType}' resource {Outcome}.", + resourceTypeToCreate, + createResponse?.Outcome?.Outcome.ToString().ToLowerInvariant() ?? "not created"); + + var duplicateResource = createResponse?.Outcome?.RawResourceElement?.RawResource? + .ToITypedElement(ModelInfoProvider.Instance)? + .ToResourceElement()? + .ToPoco(); + if (duplicateResource != null) { - _logger.LogInformation($"Creating a duplicate resource of '{resource.InstanceType}' resource..."); - var duplicateResource = CreateDuplicateResource(resource, url); - var createRequest = new CreateResourceRequest(duplicateResource); - var createResponse = await _mediator.Send( - createRequest, + _logger.LogInformation( + "Updating a '{ResourceType}' resource with the id of a '{DuplicateResourceType}' resource...", + resource.TypeName, + resourceTypeToCreate); + await UpdateResourceAsync( + resource, + duplicateResource, cancellationToken); - if (createResponse?.Outcome?.Outcome == SaveOutcomeType.Created) - { - _logger.LogInformation($"A duplicate resource of '{resource.InstanceType}' resource created."); - } } else { - _logger.LogInformation($"A duplicate resource of '{resource.InstanceType}' resource already exists."); + _logger.LogWarning("A response for a create request contains an outcome with a null resource."); } } + catch (Exception ex) + { + _logger.LogError( + ex, + "Failed to create a duplicate of a '{ResourceType}' resource.", + resource.TypeName); + throw; + } } return response; } - public Task Handle( + public async Task Handle( UpsertResourceRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { - return next(cancellationToken); + EnsureArg.IsNotNull(request, nameof(request)); + EnsureArg.IsNotNull(next, nameof(next)); + + var response = await next(cancellationToken); + var resource = response?.Outcome?.RawResourceElement?.RawResource? + .ToITypedElement(ModelInfoProvider.Instance)? + .ToResourceElement()? + .ToPoco(); + var duplicateResourceId = GetDuplicateResourceId(resource); + if (_coreFeatureConfiguration.EnableClinicalReferenceDuplication) + { + try + { + if (!string.IsNullOrEmpty(duplicateResourceId)) + { + _logger.LogInformation("Searching a duplicate resource for {Id}...", duplicateResourceId); + var searchResult = await SearchResourceAsync( + (resource is DiagnosticReport) ? KnownResourceTypes.DocumentReference : KnownResourceTypes.DiagnosticReport, + duplicateResourceId, + cancellationToken); + + var duplicateResources = searchResult?.Results?.Where(x => x.Resource != null).Select(x => x.Resource).ToList(); + _logger.LogInformation("Updating {Count} duplicate resources...", duplicateResources.Count); + if (duplicateResources.Count > 1) + { + _logger.LogWarning("More than one duplicate resource found."); + } + + foreach (var duplicate in duplicateResources) + { + var duplicateResource = duplicate.RawResource? + .ToITypedElement(ModelInfoProvider.Instance)? + .ToResourceElement()? + .ToPoco(); + if (duplicateResource == null) + { + _logger.LogWarning("Failed to convert a resource element to a poco: {Id}", duplicate.ResourceId); + continue; + } + + UpdateDuplicateResource( + resource, + duplicateResource); + await _mediator.Send( + new UpsertResourceRequest(duplicateResource.ToResourceElement()), + cancellationToken); + } + + return response; + } + + var resourceTypeToCreate = (resource is DiagnosticReport) ? KnownResourceTypes.DocumentReference : KnownResourceTypes.DiagnosticReport; + _logger.LogInformation( + "Creating a '{DuplicateResourceType}' resource to duplicate a '{ResourceType}' resource...", + resourceTypeToCreate, + resource.TypeName); + await CreateResourceAsync( + resource, + cancellationToken); + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Failed to update a duplicate of a '{ResourceType}' resource.", + resource.TypeName); + throw; + } + } + + return response; } - public Task Handle( + public async Task Handle( DeleteResourceRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { - return next(cancellationToken); + EnsureArg.IsNotNull(request, nameof(request)); + EnsureArg.IsNotNull(next, nameof(next)); + + var response = await next(cancellationToken); + var resourceKey = request?.ResourceKey; + if (_coreFeatureConfiguration.EnableClinicalReferenceDuplication + && (string.Equals(resourceKey?.ResourceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase) + || string.Equals(resourceKey?.ResourceType, KnownResourceTypes.DocumentReference, StringComparison.OrdinalIgnoreCase))) + { + try + { + _logger.LogInformation("Searching a duplicate resource for {Id}...", resourceKey.Id); + var searchResult = await SearchResourceAsync( + string.Equals(resourceKey.ResourceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase) + ? KnownResourceTypes.DocumentReference : KnownResourceTypes.DiagnosticReport, + resourceKey.Id, + cancellationToken); + + var duplicateResources = searchResult.Results.Where(x => x.Resource != null).Select(x => x.Resource).ToList(); + _logger.LogInformation("Deleting {Count} duplicate resources...", duplicateResources.Count); + if (duplicateResources.Count > 1) + { + _logger.LogWarning("More than one duplicate resource found."); + } + + foreach (var duplicate in duplicateResources) + { + _logger.LogInformation("Deleting a duplicate resource: {Id}...", duplicate.ResourceId); + await _mediator.Send( + new DeleteResourceRequest( + new ResourceKey(duplicate.ResourceTypeName, duplicate.ResourceId), + request.DeleteOperation), + cancellationToken); + } + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Failed to delete a duplicate of a '{ResourceType}' resource.", + resourceKey.ResourceType); + throw; + } + } + + return response; + } + + private Task CreateResourceAsync( + Resource resource, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(resource, nameof(resource)); + + var duplicateResource = CreateDuplicateResource(resource); + var request = new CreateResourceRequest(duplicateResource.ToResourceElement()); + + return _mediator.Send( + request, + cancellationToken); + } + + private Task UpdateResourceAsync( + Resource resource, + Resource duplicateResource, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(resource, nameof(resource)); + EnsureArg.IsNotNull(duplicateResource, nameof(duplicateResource)); + + if (resource.Meta == null) + { + resource.Meta = new Meta(); + } + + if (resource.Meta.Tag == null) + { + resource.Meta.Tag = new List(); + } + + resource.Meta.Tag.Add(new Coding(TagDuplicateOf, duplicateResource.Id)); + return _mediator.Send( + new UpsertResourceRequest(resource.ToResourceElement()), + cancellationToken); } - private Task SearchDeplicateResourceAsync( - ResourceElement resourceElement, - string url, + private Task SearchResourceAsync( + string resourceType, + string resourceId, CancellationToken cancellationToken) { - EnsureArg.IsNotNull(resourceElement, nameof(resourceElement)); - EnsureArg.IsNotNull(url, nameof(url)); + EnsureArg.IsNotNull(resourceType, nameof(resourceType)); + EnsureArg.IsNotNull(resourceId, nameof(resourceId)); - var isDiagnosticReport = string.Equals(resourceElement.InstanceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase); var queryParameters = new List> { - Tuple.Create("_tag", url), + Tuple.Create("_tag", $"{TagDuplicateOf}|{resourceId}"), }; return _searchService.SearchAsync( - isDiagnosticReport ? KnownResourceTypes.DocumentReference : KnownResourceTypes.DiagnosticReport, + resourceType, queryParameters, cancellationToken); } - private static ResourceElement CreateDuplicateResource( - ResourceElement resourceElement, - string url) + private static Resource CreateDuplicateResource(Resource resource) { - EnsureArg.IsNotNull(resourceElement, nameof(resourceElement)); - EnsureArg.IsNotNull(url, nameof(url)); + EnsureArg.IsNotNull(resource, nameof(resource)); + + if (!(resource is DiagnosticReport || resource is DocumentReference)) + { + throw new ArgumentException( + $"A resource to be duplicated must be of type '{nameof(DiagnosticReport)}' or '{nameof(DocumentReference)}'."); + } - var isDiagnosticReport = string.Equals(resourceElement.InstanceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase); - if (isDiagnosticReport) + if (resource is DiagnosticReport) { + var diagnosticReportToDuplicate = (DiagnosticReport)resource; + // TODO: more fields need to be populated? var documentReference = new DocumentReference { @@ -149,9 +324,11 @@ private static ResourceElement CreateDuplicateResource( { Tag = new List { - new Coding("url", url), + new Coding(TagDuplicateOf, diagnosticReportToDuplicate.Id), }, }, + Content = new List(), + Subject = diagnosticReportToDuplicate.Subject, #if R4 || R4B || Stu3 Status = DocumentReferenceStatus.Current, #else @@ -159,36 +336,142 @@ private static ResourceElement CreateDuplicateResource( #endif }; - return documentReference.ToResourceElement(); + if (diagnosticReportToDuplicate.PresentedForm?.Any(x => x.Url != null) ?? false) + { + // TODO: need to be more selective about whether the resource should be duplicated based on urls. + // (https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#clinical-notes-1) + foreach (var attachment in diagnosticReportToDuplicate.PresentedForm.Where(x => x.Url != null)) + { + documentReference.Content.Add( + new DocumentReference.ContentComponent + { + Attachment = attachment, + }); + } + } + + return documentReference; } + var documentReferenceToDuplicate = (DocumentReference)resource; + // TODO: more fields need to be populated? var diagnosticReport = new DiagnosticReport { Meta = new Meta { Tag = new List - { - new Coding("url", url), - }, + { + new Coding(TagDuplicateOf, resource.Id), + }, }, + PresentedForm = new List(), + Subject = documentReferenceToDuplicate.Subject, Status = DiagnosticReport.DiagnosticReportStatus.Registered, }; - return diagnosticReport.ToResourceElement(); + if (documentReferenceToDuplicate.Content?.Any(x => x.Attachment?.Url != null) ?? false) + { + // TODO: need to be more selective about whether the resource should be duplicated based on urls. + // (https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#clinical-notes-1) + foreach (var attachment in documentReferenceToDuplicate.Content?.Where(x => x.Attachment?.Url != null).Select(x => x.Attachment)) + { + diagnosticReport.PresentedForm.Add(attachment); + } + } + + return diagnosticReport; } - private static string GetUrl(ResourceElement resourceElement) + private static string GetDuplicateResourceId(Resource resource) { - if (resourceElement == null) + return resource?.Meta?.Tag?.SingleOrDefault(x => x.System == TagDuplicateOf)?.Code; + } + + private static bool ShouldDuplicate(Resource resource) + { + if (resource == null) { - return null; + return false; } - var path = string.Equals(resourceElement.InstanceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase) - ? "DiagnosticReport.presentedForm.url" - : "DocumentReference.content.attachment.url"; - return resourceElement.Scalar(path); + if (resource is DiagnosticReport) + { + // TODO: need to be more selective about whether the resource should be duplicated based on urls. + // (https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#clinical-notes-1) + return ((DiagnosticReport)resource).PresentedForm?.Any(x => x.Url != null) ?? false; + } + else if (resource is DocumentReference) + { + // TODO: need to be more selective about whether the resource should be duplicated based on urls. + // (https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#clinical-notes-1) + return ((DocumentReference)resource).Content?.Any(x => x.Attachment?.Url != null) ?? false; + } + + return false; + } + + private static void UpdateDuplicateResource( + Resource resource, + Resource duplicateResource) + { + EnsureArg.IsNotNull(resource, nameof(resource)); + EnsureArg.IsNotNull(duplicateResource, nameof(duplicateResource)); + + if (!(resource is DiagnosticReport || resource is DocumentReference) + || !(duplicateResource is DiagnosticReport || duplicateResource is DocumentReference) + || string.Equals(resource?.TypeName, duplicateResource?.TypeName, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException( + $"A source/target resource to be updated must be of type '{nameof(DiagnosticReport)}' and '{nameof(DocumentReference)}'."); + } + + if (resource is DiagnosticReport) + { + var diagnosticReport = (DiagnosticReport)resource; + var documentReference = (DocumentReference)duplicateResource; + + documentReference.Subject = diagnosticReport.Subject; + if (documentReference.Content == null) + { + documentReference.Content = new List(); + } + + // TODO: is clearing the entire content necessary and right? + documentReference.Content.Clear(); + if (diagnosticReport.PresentedForm?.Any(x => x.Url != null) ?? false) + { + foreach (var attachment in diagnosticReport.PresentedForm.Where(x => x.Url != null)) + { + documentReference.Content.Add( + new DocumentReference.ContentComponent + { + Attachment = attachment, + }); + } + } + } + else + { + var documentReference = (DocumentReference)resource; + var diagnosticReport = (DiagnosticReport)duplicateResource; + + diagnosticReport.Subject = documentReference.Subject; + if (diagnosticReport.PresentedForm == null) + { + diagnosticReport.PresentedForm = new List(); + } + + // TODO: is clearing the entire presented-form necessary and right? + diagnosticReport.PresentedForm.Clear(); + if (documentReference.Content?.Any(x => x.Attachment?.Url != null) ?? false) + { + foreach (var attachment in documentReference.Content?.Where(x => x.Attachment?.Url != null).Select(x => x.Attachment)) + { + diagnosticReport.PresentedForm.Add(attachment); + } + } + } } } } From bcee283eaa8c87b57bbebed56c8ab97215b348ca Mon Sep 17 00:00:00 2001 From: Isao Yamauchi Date: Fri, 8 Aug 2025 15:55:06 -0700 Subject: [PATCH 3/7] Refactoring and adding more tests. --- .../Modules/FhirModule.cs | 2 + .../ClinicalReferenceDuplicatorTests.cs | 1137 +++++++++++++++++ ...DuplicateClinicalReferenceBehaviorTests.cs | 322 ++--- ...ealth.Fhir.Shared.Core.UnitTests.projitems | 1 + .../Guidance/ClinicalReferenceDuplicator.cs | 543 ++++++++ .../DuplicateClinicalReferenceBehavior.cs | 406 +----- .../Guidance/IClinicalReferenceDuplicator.cs | 40 + ...icrosoft.Health.Fhir.Shared.Core.projitems | 2 + 8 files changed, 1861 insertions(+), 592 deletions(-) create mode 100644 src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/ClinicalReferenceDuplicatorTests.cs create mode 100644 src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/ClinicalReferenceDuplicator.cs create mode 100644 src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/IClinicalReferenceDuplicator.cs diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs b/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs index 7545e9bea4..6cf4a1de68 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs @@ -29,6 +29,7 @@ using Microsoft.Health.Fhir.Core.Extensions; using Microsoft.Health.Fhir.Core.Features.Conformance; using Microsoft.Health.Fhir.Core.Features.Context; +using Microsoft.Health.Fhir.Core.Features.Guidance; using Microsoft.Health.Fhir.Core.Features.Health; using Microsoft.Health.Fhir.Core.Features.Operations; using Microsoft.Health.Fhir.Core.Features.Persistence; @@ -220,6 +221,7 @@ ResourceElement SetMetadata(Resource resource, string versionId, DateTimeOffset services.AddTransient, DuplicateClinicalReferenceBehavior>(); services.AddTransient, DuplicateClinicalReferenceBehavior>(); services.AddTransient, DuplicateClinicalReferenceBehavior>(); + services.AddSingleton(); } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/ClinicalReferenceDuplicatorTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/ClinicalReferenceDuplicatorTests.cs new file mode 100644 index 0000000000..e095c4ecb6 --- /dev/null +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/ClinicalReferenceDuplicatorTests.cs @@ -0,0 +1,1137 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using Hl7.Fhir.Model; +using Hl7.Fhir.Serialization; +using MediatR; +using Microsoft.Extensions.Logging; +using Microsoft.Health.Fhir.Core.Extensions; +using Microsoft.Health.Fhir.Core.Features.Guidance; +using Microsoft.Health.Fhir.Core.Features.Persistence; +using Microsoft.Health.Fhir.Core.Features.Search; +using Microsoft.Health.Fhir.Core.Messages.Create; +using Microsoft.Health.Fhir.Core.Messages.Upsert; +using Microsoft.Health.Fhir.Core.Models; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; +using Task = System.Threading.Tasks.Task; + +namespace Microsoft.Health.Fhir.Shared.Core.UnitTests.Features.Guidance +{ + public class ClinicalReferenceDuplicatorTests + { + private readonly ClinicalReferenceDuplicator _duplicator; + private readonly IMediator _mediator; + private readonly ISearchService _searchService; + private readonly IRawResourceFactory _rawResourceFactory; + private readonly IResourceWrapperFactory _resourceWrapperFactory; + private readonly ILogger _logger; + + public ClinicalReferenceDuplicatorTests() + { + _mediator = Substitute.For(); + _searchService = Substitute.For(); + _logger = Substitute.For>(); + + _duplicator = new ClinicalReferenceDuplicator( + _mediator, + _searchService, + _logger); + + _rawResourceFactory = Substitute.For(new FhirJsonSerializer()); + _resourceWrapperFactory = Substitute.For(); + _resourceWrapperFactory + .Create(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(x => CreateResourceWrapper(x.ArgAt(0), x.ArgAt(1))); + } + + [Theory] + [MemberData(nameof(GetCreateResourceData))] + public async Task GivenResource_WhenCreating_ThenDuplicateResourceShouldBeCreated( + Resource resource) + { + var resourceType = resource.TypeName; + var duplicateResourceType = string.Equals(resourceType, KnownResourceTypes.DiagnosticReport) + ? KnownResourceTypes.DocumentReference : KnownResourceTypes.DiagnosticReport; + + // Set up a validation on a request for creating the original resource. + var resourceElement = resource.ToResourceElement(); + _mediator.Send( + Arg.Is(x => string.Equals(x.Resource.InstanceType, resourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any()) + .Throws(new Exception($"Shouldn't be called to create a {resourceType} resource.")); + + // Set up a validation on a request for creating a duplicate resource. + Resource duplicateResource = null; + _mediator.Send( + Arg.Is(x => string.Equals(x.Resource.InstanceType, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any()) + .Returns( + x => + { + var re = ((CreateResourceRequest)x[0]).Resource; + Assert.NotNull(re); + Assert.Equal(duplicateResourceType, re.InstanceType, true); + + var r = re.ToPoco(); + Assert.NotNull(r.Meta?.Tag); + Assert.Contains( + r.Meta.Tag, + x => string.Equals(x.System, ClinicalReferenceDuplicator.TagDuplicateOf, StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Code, resource.Id, StringComparison.OrdinalIgnoreCase)); + Assert.Contains( + r.Meta.Tag, + x => string.Equals(x.System, ClinicalReferenceDuplicator.TagIsDuplicate, StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Code, bool.TrueString, StringComparison.OrdinalIgnoreCase)); + + if (string.Equals(duplicateResourceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase)) + { + var original = (DocumentReference)resource; + var duplicate = (DiagnosticReport)r; + + Assert.Equal(original.Subject?.Reference, duplicate.Subject?.Reference); + Assert.NotNull(duplicate.PresentedForm); + foreach (var a in original.Content.Select(x => x.Attachment)) + { + Assert.Contains( + duplicate.PresentedForm, + x => string.Equals(x.Url, a.Url, StringComparison.OrdinalIgnoreCase)); + } + + duplicateResource = duplicate; + } + else + { + var original = (DiagnosticReport)resource; + var duplicate = (DocumentReference)r; + + Assert.Equal(original.Subject?.Reference, duplicate.Subject?.Reference); + Assert.NotNull(duplicate.Content); + foreach (var a in original.PresentedForm) + { + Assert.Contains( + duplicate.Content, + x => string.Equals(x.Attachment?.Url, a.Url, StringComparison.OrdinalIgnoreCase)); + } + + duplicateResource = duplicate; + } + + if (string.IsNullOrEmpty(r.Id)) + { + r.Id = Guid.NewGuid().ToString(); + } + + return Task.FromResult( + new UpsertResourceResponse( + new SaveOutcome(new RawResourceElement(CreateResourceWrapper(r.ToResourceElement())), SaveOutcomeType.Created))); + }); + + // Set up a validation on a request for updating the original resource with the id of the duplicate resource. + _mediator.Send( + Arg.Is(x => string.Equals(x.Resource.InstanceType, resourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any()) + .Returns( + x => + { + var re = ((UpsertResourceRequest)x[0]).Resource; + Assert.NotNull(re); + Assert.Equal(resourceType, re.InstanceType, true); + + var r = re.ToPoco(); + Assert.NotNull(r.Meta?.Tag); + Assert.Contains( + r.Meta.Tag, + t => string.Equals(t.System, ClinicalReferenceDuplicator.TagDuplicateOf, StringComparison.OrdinalIgnoreCase) + && string.Equals(t.Code, duplicateResource.Id, StringComparison.OrdinalIgnoreCase)); + + return Task.FromResult( + new UpsertResourceResponse( + new SaveOutcome(new RawResourceElement(CreateResourceWrapper(r.ToResourceElement())), SaveOutcomeType.Updated))); + }); + + await _duplicator.CreateResourceAsync( + resource, + CancellationToken.None); + + // Check how many times create/update was invoked. + await _mediator.Received(1).Send( + Arg.Any(), + Arg.Any()); + await _mediator.Received(1).Send( + Arg.Any(), + Arg.Any()); + } + + [Theory] + [MemberData(nameof(GetUpdateResourceData))] + public async Task GivenResource_WhenUpdating_ThenDuplicateResourceShouldBeUpdated( + Resource resource, + List duplicateResources) + { + var resourceType = resource.TypeName; + var duplicateResourceType = string.Equals(resourceType, KnownResourceTypes.DiagnosticReport) + ? KnownResourceTypes.DocumentReference : KnownResourceTypes.DiagnosticReport; + + // Set up a validation on a request for searching duplicate resources. + var entries = new List(); + if (duplicateResources?.Any() ?? false) + { + foreach (var r in duplicateResources) + { + var wrapper = new ResourceWrapper( + r.ToResourceElement(), + new RawResource(r.ToJson(), FhirResourceFormat.Json, false), + null, + false, + null, + null, + null); + entries.Add(new SearchResultEntry(wrapper)); + } + } + + _searchService.SearchAsync( + Arg.Is(x => string.Equals(x, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any>>(), + Arg.Any()) + .Returns( + x => + { + var parameters = (IReadOnlyList>)x[1]; + Assert.Contains( + parameters, + x => string.Equals(x.Item1, "_tag", StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Item2, $"{ClinicalReferenceDuplicator.TagDuplicateOf}|{resource.Id}", StringComparison.OrdinalIgnoreCase)); + + var searchResult = new SearchResult( + entries, + null, + null, + new List>()); + return Task.FromResult(searchResult); + }); + + // Set up a validation on a request for creating a duplicate resource. + Resource duplicateResource = null; + _mediator.Send( + Arg.Is(x => string.Equals(x.Resource.InstanceType, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any()) + .Returns( + x => + { + var re = ((CreateResourceRequest)x[0]).Resource; + Assert.NotNull(re); + Assert.Equal(duplicateResourceType, re.InstanceType, true); + + var r = re.ToPoco(); + Assert.NotNull(r.Meta?.Tag); + Assert.Contains( + r.Meta.Tag, + x => string.Equals(x.System, ClinicalReferenceDuplicator.TagDuplicateOf, StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Code, resource.Id, StringComparison.OrdinalIgnoreCase)); + Assert.Contains( + r.Meta.Tag, + x => string.Equals(x.System, ClinicalReferenceDuplicator.TagIsDuplicate, StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Code, bool.TrueString, StringComparison.OrdinalIgnoreCase)); + + if (string.Equals(duplicateResourceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase)) + { + var original = (DocumentReference)resource; + var duplicate = (DiagnosticReport)r; + + Assert.Equal(original.Subject?.Reference, duplicate.Subject?.Reference); + Assert.NotNull(duplicate.PresentedForm); + foreach (var a in original.Content.Select(x => x.Attachment)) + { + Assert.Contains( + duplicate.PresentedForm, + x => string.Equals(x.Url, a.Url, StringComparison.OrdinalIgnoreCase)); + } + + duplicateResource = duplicate; + } + else + { + var original = (DiagnosticReport)resource; + var duplicate = (DocumentReference)r; + + Assert.Equal(original.Subject?.Reference, duplicate.Subject?.Reference); + Assert.NotNull(duplicate.Content); + foreach (var a in original.PresentedForm) + { + Assert.Contains( + duplicate.Content, + x => string.Equals(x.Attachment?.Url, a.Url, StringComparison.OrdinalIgnoreCase)); + } + + duplicateResource = duplicate; + } + + if (string.IsNullOrEmpty(r.Id)) + { + r.Id = Guid.NewGuid().ToString(); + } + + return Task.FromResult( + new UpsertResourceResponse( + new SaveOutcome(new RawResourceElement(CreateResourceWrapper(r.ToResourceElement())), SaveOutcomeType.Created))); + }); + + // Set up a validation on a request for updating the original resource with the id of the duplicate resource. + _mediator.Send( + Arg.Is(x => string.Equals(x.Resource.InstanceType, resourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any()) + .Returns( + x => + { + var re = ((UpsertResourceRequest)x[0]).Resource; + Assert.NotNull(re); + Assert.Equal(resourceType, re.InstanceType, true); + + var r = re.ToPoco(); + Assert.NotNull(r.Meta?.Tag); + Assert.Contains( + r.Meta.Tag, + t => string.Equals(t.System, ClinicalReferenceDuplicator.TagDuplicateOf, StringComparison.OrdinalIgnoreCase) + && string.Equals(t.Code, duplicateResource.Id, StringComparison.OrdinalIgnoreCase)); + + return Task.FromResult( + new UpsertResourceResponse( + new SaveOutcome(new RawResourceElement(CreateResourceWrapper(r.ToResourceElement())), SaveOutcomeType.Updated))); + }); + + // Set up a validation on a request for updating the duplicate resource. + _mediator.Send( + Arg.Is(x => string.Equals(x.Resource.InstanceType, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any()) + .Returns( + x => + { + var re = ((UpsertResourceRequest)x[0]).Resource; + Assert.NotNull(re); + Assert.Equal(duplicateResourceType, re.InstanceType, true); + + var r = re.ToPoco(); + Assert.NotNull(r.Meta?.Tag); + Assert.Contains( + r.Meta.Tag, + t => string.Equals(t.System, ClinicalReferenceDuplicator.TagDuplicateOf, StringComparison.OrdinalIgnoreCase) + && string.Equals(t.Code, resource.Id, StringComparison.OrdinalIgnoreCase)); + + if (string.Equals(duplicateResourceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase)) + { + var original = (DocumentReference)resource; + var duplicate = (DiagnosticReport)r; + + Assert.Equal(original.Subject?.Reference, duplicate.Subject?.Reference); + Assert.NotNull(duplicate.PresentedForm); + + if (original.Content?.Any(x => !string.IsNullOrEmpty(x.Attachment?.Url)) ?? false) + { + foreach (var a in original.Content.Select(x => x.Attachment)) + { + Assert.Contains( + duplicate.PresentedForm, + x => string.Equals(x.Url, a.Url, StringComparison.OrdinalIgnoreCase)); + } + } + else + { + Assert.Equal(0, duplicate.PresentedForm.Count(x => !string.IsNullOrEmpty(x.Url))); + } + + duplicateResource = duplicate; + } + else + { + var original = (DiagnosticReport)resource; + var duplicate = (DocumentReference)r; + + Assert.Equal(original.Subject?.Reference, duplicate.Subject?.Reference); + Assert.NotNull(duplicate.Content); + + if (original.PresentedForm?.Any(x => !string.IsNullOrEmpty(x.Url)) ?? false) + { + foreach (var a in original.PresentedForm) + { + Assert.Contains( + duplicate.Content, + x => string.Equals(x.Attachment?.Url, a.Url, StringComparison.OrdinalIgnoreCase)); + } + } + else + { + Assert.Equal(0, duplicate.Content.Count(x => !string.IsNullOrEmpty(x.Attachment?.Url))); + } + + duplicateResource = duplicate; + } + + return Task.FromResult( + new UpsertResourceResponse( + new SaveOutcome(new RawResourceElement(CreateResourceWrapper(r.ToResourceElement())), SaveOutcomeType.Updated))); + }); + + await _duplicator.UpdateResourceAsync( + resource, + CancellationToken.None); + + // Check how many times create/update was invoked. + await _searchService.Received(1).SearchAsync( + Arg.Is(x => string.Equals(x, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any>>(), + Arg.Any()); + await _mediator.Received(duplicateResources.Any() ? 0 : 1).Send( + Arg.Any(), + Arg.Any()); + await _mediator.Received(duplicateResources.Any() ? 0 : 1).Send( + Arg.Is(x => string.Equals(x.Resource.InstanceType, resourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any()); + await _mediator.Received(duplicateResources.Any() ? duplicateResources.Count : 0).Send( + Arg.Is(x => string.Equals(x.Resource.InstanceType, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any()); + } + + private ResourceWrapper CreateResourceWrapper(ResourceElement resource, bool isDeleted = false) + { + return new ResourceWrapper( + resource, + _rawResourceFactory.Create(resource, keepMeta: true), + new ResourceRequest(HttpMethod.Post, "http://fhir"), + isDeleted, + null, + null, + null, + null, + 0); + } + + public static IEnumerable GetCreateResourceData() + { + var data = new[] + { + new object[] + { + // Create a new DiagnosticReport resource with one attachment. + new DiagnosticReport + { + Id = Guid.NewGuid().ToString(), + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, + }, + }, + }, + new object[] + { + // Create a new DiagnosticReport resource with multiple attachments. + new DiagnosticReport + { + Id = Guid.NewGuid().ToString(), + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment1", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/Binary/attachment2", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment3", + }, + }, + }, + }, + new object[] + { + // Create a new DiagnosticReport resource without any attachment. + new DiagnosticReport + { + Id = Guid.NewGuid().ToString(), + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + }, + }, + new object[] + { + // Create a new DocumentReference resource with one attachment. + new DocumentReference + { + Id = Guid.NewGuid().ToString(), + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + #if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, + #else + Status = DocumentReference.DocumentReferenceStatus.Current, + #endif + }, + }, + new object[] + { + // Create a new DocumentReference resource with multiple attachments. + new DocumentReference + { + Id = Guid.NewGuid().ToString(), + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment1", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/Binary/attachment2", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment3", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + #if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, + #else + Status = DocumentReference.DocumentReferenceStatus.Current, + #endif + }, + }, + new object[] + { + // Create a new DocumentReference resource without any attachment. + new DocumentReference + { + Id = Guid.NewGuid().ToString(), + Subject = new ResourceReference(Guid.NewGuid().ToString()), + #if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, + #else + Status = DocumentReference.DocumentReferenceStatus.Current, + #endif + }, + }, + }; + + foreach (var d in data) + { + yield return d; + } + } + + public static IEnumerable GetUpdateResourceData() + { + var data = new[] + { + new object[] + { + // Update a DiagnosticReport resource with one attachment. + new DiagnosticReport + { + Id = "original", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + }, + }, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-original", + }, + }, + }, + new List + { + new DocumentReference + { + Id = "duplicate", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + }, + }, + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + #if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, + #else + Status = DocumentReference.DocumentReferenceStatus.Current, + #endif + }, + }, + }, + new object[] + { + // Update a DiagnosticReport resource with multiple attachments. + new DiagnosticReport + { + Id = "original", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + }, + }, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-original", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment-original1", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment-original2", + }, + }, + }, + new List + { + new DocumentReference + { + Id = "duplicate", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + }, + }, + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + #if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, + #else + Status = DocumentReference.DocumentReferenceStatus.Current, + #endif + }, + }, + }, + new object[] + { + // Update a DiagnosticReport resource without any attachment. + new DiagnosticReport + { + Id = "original", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + }, + }, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List(), + }, + new List + { + new DocumentReference + { + Id = "duplicate", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + }, + }, + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + #if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, + #else + Status = DocumentReference.DocumentReferenceStatus.Current, + #endif + }, + }, + }, + new object[] + { + // Update a DiagnosticReport resource with attachments when a duplicate resource doesn't exist. + new DiagnosticReport + { + Id = "original", + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-original", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment-original1", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/Binary/attachment-original2", + }, + }, + }, + new List(), + }, + new object[] + { + // Update a DocumentReference resource with one attachment. + new DocumentReference + { + Id = "original", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + }, + }, + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-original", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + new List + { + new DiagnosticReport + { + Id = "duplicate", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + }, + }, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate", + }, + }, + }, + }, + }, + new object[] + { + // Update a DocumentReference resource with multiple attachment. + new DocumentReference + { + Id = "original", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + }, + }, + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-original", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment-original1", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/Binary/attachment-original2", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + new List + { + new DiagnosticReport + { + Id = "duplicate", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + }, + }, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate", + }, + }, + }, + }, + }, + new object[] + { + // Update a DocumentReference resource without any attachment. + new DocumentReference + { + Id = "original", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + }, + }, + Content = new List(), + Subject = new ResourceReference(Guid.NewGuid().ToString()), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + new List + { + new DiagnosticReport + { + Id = "duplicate", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + }, + }, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate", + }, + }, + }, + }, + }, + new object[] + { + // Update a DocumentReference resource with attachments when a duplicate resource doesn't exist. + new DocumentReference + { + Id = "original", + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-original", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment-original1", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/Binary/attachment-original2", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + new List(), + }, + }; + + foreach (var d in data) + { + yield return d; + } + } + } +} diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/DuplicateClinicalReferenceBehaviorTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/DuplicateClinicalReferenceBehaviorTests.cs index 06d573e937..4d416da05c 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/DuplicateClinicalReferenceBehaviorTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/DuplicateClinicalReferenceBehaviorTests.cs @@ -5,24 +5,22 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net.Http; using System.Threading; using Hl7.Fhir.Model; using Hl7.Fhir.Serialization; -using MediatR; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Health.Fhir.Core.Configs; using Microsoft.Health.Fhir.Core.Extensions; +using Microsoft.Health.Fhir.Core.Features.Guidance; using Microsoft.Health.Fhir.Core.Features.Persistence; -using Microsoft.Health.Fhir.Core.Features.Search; using Microsoft.Health.Fhir.Core.Messages.Create; +using Microsoft.Health.Fhir.Core.Messages.Delete; using Microsoft.Health.Fhir.Core.Messages.Upsert; using Microsoft.Health.Fhir.Core.Models; using Microsoft.Health.Fhir.Shared.Core.Features.Guidance; using NSubstitute; -using NSubstitute.ExceptionExtensions; using Xunit; using Task = System.Threading.Tasks.Task; @@ -30,30 +28,24 @@ namespace Microsoft.Health.Fhir.Shared.Core.UnitTests.Features.Guidance { public class DuplicateClinicalReferenceBehaviorTests { - private readonly DuplicateClinicalReferenceBehavior _behavior; - private readonly IMediator _mediator; - private readonly ISearchService _searchService; private readonly CoreFeatureConfiguration _coreFeatureConfiguration; - private readonly ILogger _logger; + private readonly IClinicalReferenceDuplicator _duplicator; private readonly IRawResourceFactory _rawResourceFactory; private readonly IResourceWrapperFactory _resourceWrapperFactory; + private readonly ILogger _logger; public DuplicateClinicalReferenceBehaviorTests() { - _mediator = Substitute.For(); - _searchService = Substitute.For(); + _duplicator = Substitute.For(); + _duplicator.CheckDuplicate(Arg.Any()).Returns(true); + _duplicator.ShouldDuplicate(Arg.Any()).Returns(true); + _logger = Substitute.For>(); _coreFeatureConfiguration = new CoreFeatureConfiguration { EnableClinicalReferenceDuplication = true, }; - _behavior = new DuplicateClinicalReferenceBehavior( - _mediator, - _searchService, - Options.Create(_coreFeatureConfiguration), - _logger); - _rawResourceFactory = Substitute.For(new FhirJsonSerializer()); _resourceWrapperFactory = Substitute.For(); _resourceWrapperFactory @@ -62,112 +54,131 @@ public DuplicateClinicalReferenceBehaviorTests() } [Theory] - [MemberData(nameof(GetCreateResourceData))] - public async Task GivenCreateRequest_WhenResourceIsCreated_ThenDuplicateResourceShouldBeCreated( - Resource resource, - bool shouldDuplicate) + [InlineData(true)] + [InlineData(false)] + public async Task GivenCreateRequest_WhenDuplicateClinicalReferenceIsEnabled_ThenDuplicateResourceShouldBeCreated( + bool enabled) { - var resourceType = resource.TypeName; - var duplicateResourceType = string.Equals(resource, KnownResourceTypes.DiagnosticReport) - ? KnownResourceTypes.DocumentReference : KnownResourceTypes.DiagnosticReport; + var resource = new DiagnosticReport() + { + Id = Guid.NewGuid().ToString(), + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + }; - // Set up a validation on a request for creating the original resource. var resourceElement = resource.ToResourceElement(); var resourceWrapper = CreateResourceWrapper(resourceElement); var request = new CreateResourceRequest(resourceElement); var response = new UpsertResourceResponse( new SaveOutcome(new RawResourceElement(resourceWrapper), SaveOutcomeType.Created)); - _mediator.Send( - Arg.Is(x => string.Equals(x.Resource.InstanceType, resourceType, StringComparison.OrdinalIgnoreCase)), - Arg.Any()) - .Throws(new Exception($"Shouldn't be called to create a {resourceType} resource.")); - - // Set up a validation on a request for creating a duplicate resource. - Resource duplicateResource = null; - _mediator.Send( - Arg.Is(x => string.Equals(x.Resource.InstanceType, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), - Arg.Any()) - .Returns( - x => - { - Assert.True(shouldDuplicate, "Duplicating a resource is unnecessary."); - var re = ((CreateResourceRequest)x[0]).Resource; - Assert.NotNull(re); - Assert.Equal(duplicateResourceType, re.InstanceType, true); - - var r = re.ToPoco(); - Assert.NotNull(r.Meta?.Tag); - Assert.Contains( - r.Meta.Tag, - x => string.Equals(x.System, DuplicateClinicalReferenceBehavior.TagDuplicateOf, StringComparison.OrdinalIgnoreCase) - && string.Equals(x.Code, resource.Id, StringComparison.OrdinalIgnoreCase)); + _coreFeatureConfiguration.EnableClinicalReferenceDuplication = enabled; + var behavior = new DuplicateClinicalReferenceBehavior( + Options.Create(_coreFeatureConfiguration), + _duplicator, + _logger); - if (string.Equals(duplicateResourceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase)) - { - var original = (DocumentReference)resource; - var duplicate = (DiagnosticReport)r; - Assert.NotNull(duplicate.PresentedForm); - foreach (var a in original.Content.Select(x => x.Attachment)) - { - Assert.Contains( - duplicate.PresentedForm, - x => string.Equals(x.Url, a.Url, StringComparison.OrdinalIgnoreCase)); - } + await behavior.Handle( + request, + async (ct) => await Task.Run(() => response), + CancellationToken.None); - duplicateResource = duplicate; - } - else - { - var original = (DiagnosticReport)resource; - var duplicate = (DocumentReference)r; - Assert.NotNull(duplicate.Content); - foreach (var a in original.PresentedForm) - { - Assert.Contains( - duplicate.Content, - x => string.Equals(x.Attachment?.Url, a.Url, StringComparison.OrdinalIgnoreCase)); - } + _duplicator.Received(enabled ? 1 : 0).ShouldDuplicate(Arg.Any()); + await _duplicator.Received(enabled ? 1 : 0).CreateResourceAsync( + Arg.Any(), + Arg.Any()); + } - duplicateResource = duplicate; - } + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, false)] + public async Task GivenUpsertRequest_WhenDuplicateClinicalReferenceIsEnabled_ThenThenDuplicateResourceShouldBeUpdated( + bool enabled, + bool duplicateFound) + { + var resource = new DiagnosticReport() + { + Id = Guid.NewGuid().ToString(), + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + }; - if (string.IsNullOrEmpty(r.Id)) - { - r.Id = Guid.NewGuid().ToString(); - } + var resourceElement = resource.ToResourceElement(); + var resourceWrapper = CreateResourceWrapper(resourceElement); + var request = new UpsertResourceRequest(resourceElement); + var response = new UpsertResourceResponse( + new SaveOutcome(new RawResourceElement(resourceWrapper), SaveOutcomeType.Created)); - return Task.FromResult( - new UpsertResourceResponse( - new SaveOutcome(new RawResourceElement(CreateResourceWrapper(r.ToResourceElement())), SaveOutcomeType.Created))); - }); + var duplicateResources = new List(); + if (duplicateFound) + { + duplicateResources.Add(resource); + } - // Set up a validation on a request for updating the initial resource with the id of the duplicate resource. - _mediator.Send( - Arg.Is(x => string.Equals(x.Resource.InstanceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase)), + _duplicator.UpdateResourceAsync( + Arg.Any(), Arg.Any()) .Returns( x => { - Assert.True(shouldDuplicate, "Duplicating a resource is unnecessary."); + return Task.FromResult>(duplicateResources); + }); - var re = ((UpsertResourceRequest)x[0]).Resource; - Assert.NotNull(re); - Assert.Equal(KnownResourceTypes.DiagnosticReport, re.InstanceType, true); + _coreFeatureConfiguration.EnableClinicalReferenceDuplication = enabled; + var behavior = new DuplicateClinicalReferenceBehavior( + Options.Create(_coreFeatureConfiguration), + _duplicator, + _logger); - var r = re.ToPoco(); - Assert.NotNull(r.Meta?.Tag); - Assert.Contains( - r.Meta.Tag, - t => string.Equals(t.System, DuplicateClinicalReferenceBehavior.TagDuplicateOf, StringComparison.OrdinalIgnoreCase) - && string.Equals(t.Code, duplicateResource.Id, StringComparison.OrdinalIgnoreCase)); + await behavior.Handle( + request, + async (ct) => await Task.Run(() => response), + CancellationToken.None); + + _duplicator.Received(enabled ? 1 : 0).ShouldDuplicate(Arg.Any()); + await _duplicator.Received(enabled ? 1 : 0).UpdateResourceAsync( + Arg.Any(), + Arg.Any()); + await _duplicator.Received(enabled && !duplicateFound ? 1 : 0).CreateResourceAsync( + Arg.Any(), + Arg.Any()); + } - return Task.FromResult( - new UpsertResourceResponse( - new SaveOutcome(new RawResourceElement(CreateResourceWrapper(r.ToResourceElement())), SaveOutcomeType.Updated))); - }); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task GivenDeleteRequest_WhenDuplicateClinicalReferenceIsEnabled_ThenThenDuplicateResourceShouldBeDeleted( + bool enabled) + { + var resource = new DiagnosticReport() + { + Id = Guid.NewGuid().ToString(), + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + }; - await _behavior.Handle(request, async (ct) => await Task.Run(() => response), CancellationToken.None); + var resourceElement = resource.ToResourceElement(); + var resourceWrapper = CreateResourceWrapper(resourceElement); + var request = new DeleteResourceRequest( + new ResourceKey(resource.TypeName, resource.Id), + DeleteOperation.SoftDelete); + var response = new DeleteResourceResponse( + new ResourceKey(resource.TypeName, resource.Id)); + + _coreFeatureConfiguration.EnableClinicalReferenceDuplication = enabled; + var behavior = new DuplicateClinicalReferenceBehavior( + Options.Create(_coreFeatureConfiguration), + _duplicator, + _logger); + + await behavior.Handle( + request, + async (ct) => await Task.Run(() => response), + CancellationToken.None); + + _duplicator.Received(enabled ? 1 : 0).CheckDuplicate(Arg.Any()); + await _duplicator.Received(enabled ? 1 : 0).DeleteResourceAsync( + Arg.Any(), + Arg.Any(), + Arg.Any()); } private ResourceWrapper CreateResourceWrapper(ResourceElement resource, bool isDeleted = false) @@ -183,116 +194,5 @@ private ResourceWrapper CreateResourceWrapper(ResourceElement resource, bool isD null, 0); } - - public static IEnumerable GetCreateResourceData() - { - var data = new[] - { - new object[] - { - // Create a new DiagnosticReport resource with one attachment. - new DiagnosticReport - { - Id = Guid.NewGuid().ToString(), - Status = DiagnosticReport.DiagnosticReportStatus.Registered, - Code = new CodeableConcept() - { - Coding = new List() - { - new Coding() - { - Code = "12345", - }, - }, - }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), - PresentedForm = new List - { - new Attachment() - { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment", - }, - }, - }, - true, - }, - new object[] - { - // Create a new DiagnosticReport resource with multiple attachments. - new DiagnosticReport - { - Id = Guid.NewGuid().ToString(), - Status = DiagnosticReport.DiagnosticReportStatus.Registered, - Code = new CodeableConcept() - { - Coding = new List() - { - new Coding() - { - Code = "12345", - }, - }, - }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), - PresentedForm = new List - { - new Attachment() - { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment", - }, - new Attachment() - { - ContentType = "application/xhtml", - Creation = "2006-12-24", - Url = "http://example.org/fhir/Binary/attachment1", - }, - new Attachment() - { - ContentType = "application/xhtml", - Creation = "2007-12-24", - Url = "http://example.org/fhir/Binary/attachment2", - }, - new Attachment() - { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment3", - }, - }, - }, - true, - }, - new object[] - { - // Create a new DiagnosticReport resource with one attachment. - new DiagnosticReport - { - Id = Guid.NewGuid().ToString(), - Status = DiagnosticReport.DiagnosticReportStatus.Registered, - Code = new CodeableConcept() - { - Coding = new List() - { - new Coding() - { - Code = "12345", - }, - }, - }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), - }, - false, - }, - }; - - foreach (var d in data) - { - yield return d; - } - } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Microsoft.Health.Fhir.Shared.Core.UnitTests.projitems b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Microsoft.Health.Fhir.Shared.Core.UnitTests.projitems index 2aaf61340a..2d35d022f3 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Microsoft.Health.Fhir.Shared.Core.UnitTests.projitems +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Microsoft.Health.Fhir.Shared.Core.UnitTests.projitems @@ -16,6 +16,7 @@ + diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/ClinicalReferenceDuplicator.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/ClinicalReferenceDuplicator.cs new file mode 100644 index 0000000000..baa61c42b3 --- /dev/null +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/ClinicalReferenceDuplicator.cs @@ -0,0 +1,543 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EnsureThat; +using Hl7.Fhir.Model; +using MediatR; +using Microsoft.Extensions.Logging; +using Microsoft.Health.Fhir.Core.Extensions; +using Microsoft.Health.Fhir.Core.Features.Persistence; +using Microsoft.Health.Fhir.Core.Features.Search; +using Microsoft.Health.Fhir.Core.Messages.Create; +using Microsoft.Health.Fhir.Core.Messages.Delete; +using Microsoft.Health.Fhir.Core.Messages.Upsert; +using Microsoft.Health.Fhir.Core.Models; + +namespace Microsoft.Health.Fhir.Core.Features.Guidance +{ + public class ClinicalReferenceDuplicator : IClinicalReferenceDuplicator + { + internal const string TagDuplicateOf = "duplicateOf"; + internal const string TagIsDuplicate = "isDuplicate"; + + private readonly IMediator _mediator; + private readonly ISearchService _searchService; + private readonly ILogger _logger; + + public ClinicalReferenceDuplicator( + IMediator mediator, + ISearchService searchService, + ILogger logger) + { + EnsureArg.IsNotNull(mediator, nameof(mediator)); + EnsureArg.IsNotNull(searchService, nameof(searchService)); + EnsureArg.IsNotNull(logger, nameof(logger)); + + _mediator = mediator; + _searchService = searchService; + _logger = logger; + } + + public async Task CreateResourceAsync( + Resource resource, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(resource, nameof(resource)); + + try + { + var createResponse = await CreateDuplicateResourceInternalAsync( + resource, + cancellationToken); + var duplicateResource = createResponse?.Outcome?.RawResourceElement?.RawResource? + .ToITypedElement(ModelInfoProvider.Instance)? + .ToResourceElement()? + .ToPoco(); + if (duplicateResource == null) + { + // TODO: throw an exception here. + _logger.LogError("A create response contains a null resource."); + } + + _logger.LogInformation( + "A '{DuplicateResourceType}' resource {Outcome}.", + duplicateResource.TypeName, + createResponse?.Outcome?.Outcome.ToString().ToLowerInvariant() ?? "not created"); + + var updateResponse = await UpdateResourceInternalAsync( + resource, + duplicateResource, + cancellationToken); + _logger.LogInformation( + "A '{ResourceType}' resource {Outcome}.", + resource.TypeName, + updateResponse?.Outcome?.Outcome.ToString().ToLowerInvariant() ?? "not updated."); + return duplicateResource; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create a duplicate resource and update a resource with its id."); + throw; + } + } + + public async Task> DeleteResourceAsync( + ResourceKey resourceKey, + DeleteOperation deleteOperation, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(resourceKey, nameof(resourceKey)); + + try + { + var duplicateResources = await SearchResourceAsync( + GetDuplicateResourceType(resourceKey.ResourceType), + resourceKey.Id, + cancellationToken); + _logger.LogInformation("Deleting {Count} duplicate resources...", duplicateResources.Count); + if (duplicateResources.Count > 1) + { + _logger.LogWarning("More than one duplicate resource found."); + } + + var duplicateResourceKeysDeleted = new List(); + foreach (var duplicate in duplicateResources) + { + try + { + _logger.LogInformation("Deleting a duplicate resource: {Id}...", duplicate.ResourceId); + var response = await _mediator.Send( + new DeleteResourceRequest( + new ResourceKey(duplicate.ResourceTypeName, duplicate.ResourceId), + deleteOperation), + cancellationToken); + duplicateResourceKeysDeleted.Add(response.ResourceKey); + } + catch (Exception ex) + { + // Ignore an exception and continue deleting the rest. + _logger.LogError(ex, "Failed to delete a duplicate resource: {Id}...", duplicate.ResourceId); + } + } + + return duplicateResourceKeysDeleted; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to delete a duplicate resource."); + throw; + } + } + + public async Task> SearchResourceAsync( + string duplicateResourceType, + string resourceId, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(duplicateResourceType, nameof(duplicateResourceType)); + EnsureArg.IsNotNull(resourceId, nameof(resourceId)); + + try + { + var queryParameters = new List> + { + Tuple.Create("_tag", $"{TagDuplicateOf}|{resourceId}"), + }; + + _logger.LogInformation( + "Searching a duplicate resource '{DuplicateResourceType}' of a resource '{Id}'...", + duplicateResourceType, + resourceId); + + var resources = new List(); + string continuationToken = null; + do + { + var searchResult = await _searchService.SearchAsync( + duplicateResourceType, + queryParameters, + cancellationToken); + if (searchResult.Results.Any()) + { + resources.AddRange( + searchResult.Results + .Where(x => x.Resource?.RawResource != null) + .Select(x => x.Resource)); + } + + continuationToken = searchResult.ContinuationToken; + } + while (!string.IsNullOrEmpty(continuationToken)); + + return resources; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to search a duplicate resource."); + throw; + } + } + + public async Task> UpdateResourceAsync( + Resource resource, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(resource, nameof(resource)); + + try + { + var duplicateResourceWrappers = await SearchResourceAsync( + GetDuplicateResourceType(resource.TypeName), + resource.Id, + cancellationToken); + var duplicateResources = duplicateResourceWrappers + .Where(x => x.RawResource != null) + .Select(x => x.RawResource.ToITypedElement(ModelInfoProvider.Instance).ToResourceElement().ToPoco()).ToList(); + _logger.LogInformation("Updating {Count} duplicate resources...", duplicateResources.Count); + if (duplicateResources.Any()) + { + if (duplicateResources.Count > 1) + { + _logger.LogWarning("More than one duplicate resource found."); + } + + var duplicateResourcesUpdated = new List(); + foreach (var duplicate in duplicateResources) + { + try + { + _logger.LogInformation("Updating a duplicate resource: {Id}...", duplicate.Id); + var response = await UpdateDuplicateResourceInternalAsync( + resource, + duplicate, + cancellationToken); + var duplicateUpdated = response.Outcome.RawResourceElement?.RawResource? + .ToITypedElement(ModelInfoProvider.Instance)? + .ToResourceElement()? + .ToPoco(); + if (duplicateUpdated == null) + { + _logger.LogError("A update response contains a null resource."); + continue; + } + + duplicateResourcesUpdated.Add(duplicateUpdated); + } + catch (Exception ex) + { + // Ignore an exception and continue updating the rest. + _logger.LogError(ex, "Failed to update a duplicate resource: {Id}...", duplicate.Id); + } + } + + return duplicateResourcesUpdated; + } + + _logger.LogWarning("No duplicate resource found."); + var duplicateResourceCreated = await CreateResourceAsync( + resource, + cancellationToken); + + return new List { duplicateResourceCreated }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to delete a duplicate resource."); + throw; + } + } + + public bool CheckDuplicate(ResourceKey resourceKey) + { + return string.Equals(resourceKey?.ResourceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase) + || string.Equals(resourceKey?.ResourceType, KnownResourceTypes.DocumentReference, StringComparison.OrdinalIgnoreCase); + } + + public bool ShouldDuplicate(Resource resource) + { + if (resource == null) + { + return false; + } + + if (resource is DiagnosticReport) + { + // TODO: need to be more selective about whether the resource should be duplicated based on urls. + // (https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#clinical-notes-1) + return ((DiagnosticReport)resource).PresentedForm?.Any(x => x.Url != null) ?? false; + } + else if (resource is DocumentReference) + { + // TODO: need to be more selective about whether the resource should be duplicated based on urls. + // (https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#clinical-notes-1) + return ((DocumentReference)resource).Content?.Any(x => x.Attachment?.Url != null) ?? false; + } + + return false; + } + + private Task CreateDuplicateResourceInternalAsync( + Resource resource, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(resource, nameof(resource)); + + try + { + var duplicateResource = CreateDuplicateResource(resource); + var request = new CreateResourceRequest(duplicateResource.ToResourceElement()); + + _logger.LogInformation( + "Creating a duplicate resource '{DuplicateResourceType}' of a '{ResourceType}' resource '{Id}'...", + duplicateResource.TypeName, + resource.TypeName, + resource.Id); + return _mediator.Send( + request, + cancellationToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create a duplicate resource."); + throw; + } + } + + private Task UpdateResourceInternalAsync( + Resource resource, + Resource duplicateResource, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(resource, nameof(resource)); + EnsureArg.IsNotNull(duplicateResource, nameof(duplicateResource)); + + try + { + if (resource.Meta == null) + { + resource.Meta = new Meta(); + } + + var tags = new List(); + if (resource.Meta.Tag != null) + { + tags.AddRange( + resource.Meta.Tag.Where(x => !string.Equals(x.System, TagDuplicateOf, StringComparison.OrdinalIgnoreCase))); + } + + tags.Add(new Coding(TagDuplicateOf, duplicateResource.Id)); + resource.Meta.Tag = tags; + + _logger.LogInformation( + "Updating a '{ResourceType}' resource with a duplicate resource '{Id}'...", + resource.TypeName, + duplicateResource.Id); + return _mediator.Send( + new UpsertResourceRequest(resource.ToResourceElement()), + cancellationToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update a resource with a duplicate resource id."); + throw; + } + } + + private Task UpdateDuplicateResourceInternalAsync( + Resource resource, + Resource duplicateResource, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(resource, nameof(resource)); + EnsureArg.IsNotNull(duplicateResource, nameof(duplicateResource)); + + try + { + UpdateDuplicateResource(resource, duplicateResource); + _logger.LogInformation( + "Updating a '{DuplicateResourceType}' resource with a duplicate resource '{Id}'...", + resource.TypeName, + duplicateResource.Id); + return _mediator.Send( + new UpsertResourceRequest(duplicateResource.ToResourceElement()), + cancellationToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update a resource with a duplicate resource id."); + throw; + } + } + + private static Resource CreateDuplicateResource(Resource resource) + { + EnsureArg.IsNotNull(resource, nameof(resource)); + + if (!(resource is DiagnosticReport || resource is DocumentReference)) + { + throw new ArgumentException( + $"A resource to be duplicated must be of type '{nameof(DiagnosticReport)}' or '{nameof(DocumentReference)}'."); + } + + if (resource is DiagnosticReport) + { + var diagnosticReportToDuplicate = (DiagnosticReport)resource; + + // TODO: more fields need to be populated? + var documentReference = new DocumentReference + { + Meta = new Meta + { + Tag = new List + { + new Coding(TagDuplicateOf, diagnosticReportToDuplicate.Id), + new Coding(TagIsDuplicate, bool.TrueString.ToLowerInvariant()), + }, + }, + Content = new List(), + Subject = diagnosticReportToDuplicate.Subject, +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }; + + if (diagnosticReportToDuplicate.PresentedForm?.Any(x => x.Url != null) ?? false) + { + // TODO: need to be more selective about whether the resource should be duplicated based on urls. + // (https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#clinical-notes-1) + foreach (var attachment in diagnosticReportToDuplicate.PresentedForm.Where(x => x.Url != null)) + { + documentReference.Content.Add( + new DocumentReference.ContentComponent + { + Attachment = attachment, + }); + } + } + + return documentReference; + } + + var documentReferenceToDuplicate = (DocumentReference)resource; + + // TODO: more fields need to be populated? + var diagnosticReport = new DiagnosticReport + { + Meta = new Meta + { + Tag = new List + { + new Coding(TagDuplicateOf, resource.Id), + new Coding(TagIsDuplicate, bool.TrueString.ToLowerInvariant()), + }, + }, + PresentedForm = new List(), + Subject = documentReferenceToDuplicate.Subject, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + }; + + if (documentReferenceToDuplicate.Content?.Any(x => x.Attachment?.Url != null) ?? false) + { + // TODO: need to be more selective about whether the resource should be duplicated based on urls. + // (https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#clinical-notes-1) + foreach (var attachment in documentReferenceToDuplicate.Content?.Where(x => x.Attachment?.Url != null).Select(x => x.Attachment)) + { + diagnosticReport.PresentedForm.Add(attachment); + } + } + + return diagnosticReport; + } + + private static string GetDuplicateResourceId(Resource resource) + { + EnsureArg.IsNotNull(resource, nameof(resource)); + + return resource?.Meta?.Tag?.SingleOrDefault(x => x.System == TagDuplicateOf)?.Code; + } + + private static string GetDuplicateResourceType(string resourceType) + { + EnsureArg.IsNotNullOrEmpty(resourceType, nameof(resourceType)); + + return string.Equals(resourceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase) + ? KnownResourceTypes.DocumentReference : KnownResourceTypes.DiagnosticReport; + } + + private static void UpdateDuplicateResource( + Resource resource, + Resource duplicateResource) + { + EnsureArg.IsNotNull(resource, nameof(resource)); + EnsureArg.IsNotNull(duplicateResource, nameof(duplicateResource)); + + if (!(resource is DiagnosticReport || resource is DocumentReference) + || !(duplicateResource is DiagnosticReport || duplicateResource is DocumentReference) + || string.Equals(resource?.TypeName, duplicateResource?.TypeName, StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException( + $"A source/target resource to be updated must be of type '{nameof(DiagnosticReport)}' and '{nameof(DocumentReference)}'."); + } + + if (resource is DiagnosticReport) + { + var diagnosticReport = (DiagnosticReport)resource; + var documentReference = (DocumentReference)duplicateResource; + documentReference.Subject = diagnosticReport.Subject; + + var contents = new List(); + if (documentReference.Content != null) + { + // TODO: save attachments without a url. is this right? + contents.AddRange(documentReference.Content.Where(x => string.IsNullOrEmpty(x.Attachment?.Url))); + } + + if (diagnosticReport.PresentedForm?.Any(x => !string.IsNullOrEmpty(x.Url)) ?? false) + { + foreach (var attachment in diagnosticReport.PresentedForm.Where(x => x.Url != null)) + { + contents.Add( + new DocumentReference.ContentComponent + { + Attachment = attachment, + }); + } + } + + documentReference.Content = contents; + } + else + { + var documentReference = (DocumentReference)resource; + var diagnosticReport = (DiagnosticReport)duplicateResource; + diagnosticReport.Subject = documentReference.Subject; + + var attachments = new List(); + if (diagnosticReport.PresentedForm != null) + { + // TODO: save attachments without a url. is this right? + attachments.AddRange(diagnosticReport.PresentedForm.Where(x => string.IsNullOrEmpty(x.Url))); + } + + if (documentReference.Content?.Any(x => !string.IsNullOrEmpty(x.Attachment?.Url)) ?? false) + { + foreach (var attachment in documentReference.Content?.Where(x => x.Attachment?.Url != null).Select(x => x.Attachment)) + { + attachments.Add(attachment); + } + } + + diagnosticReport.PresentedForm = attachments; + } + } + } +} diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/DuplicateClinicalReferenceBehavior.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/DuplicateClinicalReferenceBehavior.cs index 6a93ab0fd8..1bbbb02b6c 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/DuplicateClinicalReferenceBehavior.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/DuplicateClinicalReferenceBehavior.cs @@ -3,20 +3,16 @@ // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // ------------------------------------------------------------------------------------------------- -using System; -using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using EnsureThat; -using Hl7.Fhir.Model; using MediatR; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Health.Fhir.Core.Configs; using Microsoft.Health.Fhir.Core.Extensions; -using Microsoft.Health.Fhir.Core.Features.Persistence; -using Microsoft.Health.Fhir.Core.Features.Search; +using Microsoft.Health.Fhir.Core.Features.Guidance; using Microsoft.Health.Fhir.Core.Messages.Create; using Microsoft.Health.Fhir.Core.Messages.Delete; using Microsoft.Health.Fhir.Core.Messages.Upsert; @@ -28,27 +24,21 @@ public class DuplicateClinicalReferenceBehavior : IPipelineBehavior, IPipelineBehavior { - internal const string TagDuplicateOf = "duplicateOf"; - - private readonly IMediator _mediator; - private readonly ISearchService _searchService; private readonly CoreFeatureConfiguration _coreFeatureConfiguration; + private readonly IClinicalReferenceDuplicator _clinicalReferenceDuplicator; private readonly ILogger _logger; public DuplicateClinicalReferenceBehavior( - IMediator mediator, - ISearchService searchService, IOptions coreFeatureConfiguration, + IClinicalReferenceDuplicator clinicalReferenceDuplicator, ILogger logger) { - EnsureArg.IsNotNull(mediator, nameof(mediator)); - EnsureArg.IsNotNull(searchService, nameof(searchService)); EnsureArg.IsNotNull(coreFeatureConfiguration?.Value, nameof(coreFeatureConfiguration)); + EnsureArg.IsNotNull(clinicalReferenceDuplicator, nameof(clinicalReferenceDuplicator)); EnsureArg.IsNotNull(logger, nameof(logger)); - _mediator = mediator; - _searchService = searchService; _coreFeatureConfiguration = coreFeatureConfiguration.Value; + _clinicalReferenceDuplicator = clinicalReferenceDuplicator; _logger = logger; } @@ -65,51 +55,11 @@ public async Task Handle( .ToITypedElement(ModelInfoProvider.Instance)? .ToResourceElement()? .ToPoco(); - if (_coreFeatureConfiguration.EnableClinicalReferenceDuplication && ShouldDuplicate(resource)) + if (_coreFeatureConfiguration.EnableClinicalReferenceDuplication && _clinicalReferenceDuplicator.ShouldDuplicate(resource)) { - try - { - var resourceTypeToCreate = (resource is DiagnosticReport) ? KnownResourceTypes.DocumentReference : KnownResourceTypes.DiagnosticReport; - _logger.LogInformation( - "Creating a '{DuplicateResourceType}' resource to duplicate a '{ResourceType}' resource...", - resourceTypeToCreate, - resource.TypeName); - var createResponse = await CreateResourceAsync( - resource, - cancellationToken); - _logger.LogInformation( - "A '{DuplicateResourceType}' resource {Outcome}.", - resourceTypeToCreate, - createResponse?.Outcome?.Outcome.ToString().ToLowerInvariant() ?? "not created"); - - var duplicateResource = createResponse?.Outcome?.RawResourceElement?.RawResource? - .ToITypedElement(ModelInfoProvider.Instance)? - .ToResourceElement()? - .ToPoco(); - if (duplicateResource != null) - { - _logger.LogInformation( - "Updating a '{ResourceType}' resource with the id of a '{DuplicateResourceType}' resource...", - resource.TypeName, - resourceTypeToCreate); - await UpdateResourceAsync( - resource, - duplicateResource, - cancellationToken); - } - else - { - _logger.LogWarning("A response for a create request contains an outcome with a null resource."); - } - } - catch (Exception ex) - { - _logger.LogError( - ex, - "Failed to create a duplicate of a '{ResourceType}' resource.", - resource.TypeName); - throw; - } + await _clinicalReferenceDuplicator.CreateResourceAsync( + resource, + cancellationToken); } return response; @@ -128,66 +78,20 @@ public async Task Handle( .ToITypedElement(ModelInfoProvider.Instance)? .ToResourceElement()? .ToPoco(); - var duplicateResourceId = GetDuplicateResourceId(resource); - if (_coreFeatureConfiguration.EnableClinicalReferenceDuplication) + if (_coreFeatureConfiguration.EnableClinicalReferenceDuplication && _clinicalReferenceDuplicator.ShouldDuplicate(resource)) { - try - { - if (!string.IsNullOrEmpty(duplicateResourceId)) - { - _logger.LogInformation("Searching a duplicate resource for {Id}...", duplicateResourceId); - var searchResult = await SearchResourceAsync( - (resource is DiagnosticReport) ? KnownResourceTypes.DocumentReference : KnownResourceTypes.DiagnosticReport, - duplicateResourceId, - cancellationToken); - - var duplicateResources = searchResult?.Results?.Where(x => x.Resource != null).Select(x => x.Resource).ToList(); - _logger.LogInformation("Updating {Count} duplicate resources...", duplicateResources.Count); - if (duplicateResources.Count > 1) - { - _logger.LogWarning("More than one duplicate resource found."); - } - - foreach (var duplicate in duplicateResources) - { - var duplicateResource = duplicate.RawResource? - .ToITypedElement(ModelInfoProvider.Instance)? - .ToResourceElement()? - .ToPoco(); - if (duplicateResource == null) - { - _logger.LogWarning("Failed to convert a resource element to a poco: {Id}", duplicate.ResourceId); - continue; - } - - UpdateDuplicateResource( - resource, - duplicateResource); - await _mediator.Send( - new UpsertResourceRequest(duplicateResource.ToResourceElement()), - cancellationToken); - } - - return response; - } - - var resourceTypeToCreate = (resource is DiagnosticReport) ? KnownResourceTypes.DocumentReference : KnownResourceTypes.DiagnosticReport; - _logger.LogInformation( - "Creating a '{DuplicateResourceType}' resource to duplicate a '{ResourceType}' resource...", - resourceTypeToCreate, - resource.TypeName); - await CreateResourceAsync( - resource, - cancellationToken); - } - catch (Exception ex) + var duplicateResources = await _clinicalReferenceDuplicator.UpdateResourceAsync( + resource, + cancellationToken); + if (duplicateResources.Any()) { - _logger.LogError( - ex, - "Failed to update a duplicate of a '{ResourceType}' resource.", - resource.TypeName); - throw; + return response; } + + _logger.LogWarning("No duplicate resource found."); + await _clinicalReferenceDuplicator.CreateResourceAsync( + resource, + cancellationToken); } return response; @@ -203,275 +107,15 @@ public async Task Handle( var response = await next(cancellationToken); var resourceKey = request?.ResourceKey; - if (_coreFeatureConfiguration.EnableClinicalReferenceDuplication - && (string.Equals(resourceKey?.ResourceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase) - || string.Equals(resourceKey?.ResourceType, KnownResourceTypes.DocumentReference, StringComparison.OrdinalIgnoreCase))) + if (_coreFeatureConfiguration.EnableClinicalReferenceDuplication && _clinicalReferenceDuplicator.CheckDuplicate(resourceKey)) { - try - { - _logger.LogInformation("Searching a duplicate resource for {Id}...", resourceKey.Id); - var searchResult = await SearchResourceAsync( - string.Equals(resourceKey.ResourceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase) - ? KnownResourceTypes.DocumentReference : KnownResourceTypes.DiagnosticReport, - resourceKey.Id, - cancellationToken); - - var duplicateResources = searchResult.Results.Where(x => x.Resource != null).Select(x => x.Resource).ToList(); - _logger.LogInformation("Deleting {Count} duplicate resources...", duplicateResources.Count); - if (duplicateResources.Count > 1) - { - _logger.LogWarning("More than one duplicate resource found."); - } - - foreach (var duplicate in duplicateResources) - { - _logger.LogInformation("Deleting a duplicate resource: {Id}...", duplicate.ResourceId); - await _mediator.Send( - new DeleteResourceRequest( - new ResourceKey(duplicate.ResourceTypeName, duplicate.ResourceId), - request.DeleteOperation), - cancellationToken); - } - } - catch (Exception ex) - { - _logger.LogError( - ex, - "Failed to delete a duplicate of a '{ResourceType}' resource.", - resourceKey.ResourceType); - throw; - } + await _clinicalReferenceDuplicator.DeleteResourceAsync( + resourceKey, + request.DeleteOperation, + cancellationToken); } return response; } - - private Task CreateResourceAsync( - Resource resource, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(resource, nameof(resource)); - - var duplicateResource = CreateDuplicateResource(resource); - var request = new CreateResourceRequest(duplicateResource.ToResourceElement()); - - return _mediator.Send( - request, - cancellationToken); - } - - private Task UpdateResourceAsync( - Resource resource, - Resource duplicateResource, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(resource, nameof(resource)); - EnsureArg.IsNotNull(duplicateResource, nameof(duplicateResource)); - - if (resource.Meta == null) - { - resource.Meta = new Meta(); - } - - if (resource.Meta.Tag == null) - { - resource.Meta.Tag = new List(); - } - - resource.Meta.Tag.Add(new Coding(TagDuplicateOf, duplicateResource.Id)); - return _mediator.Send( - new UpsertResourceRequest(resource.ToResourceElement()), - cancellationToken); - } - - private Task SearchResourceAsync( - string resourceType, - string resourceId, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(resourceType, nameof(resourceType)); - EnsureArg.IsNotNull(resourceId, nameof(resourceId)); - - var queryParameters = new List> - { - Tuple.Create("_tag", $"{TagDuplicateOf}|{resourceId}"), - }; - - return _searchService.SearchAsync( - resourceType, - queryParameters, - cancellationToken); - } - - private static Resource CreateDuplicateResource(Resource resource) - { - EnsureArg.IsNotNull(resource, nameof(resource)); - - if (!(resource is DiagnosticReport || resource is DocumentReference)) - { - throw new ArgumentException( - $"A resource to be duplicated must be of type '{nameof(DiagnosticReport)}' or '{nameof(DocumentReference)}'."); - } - - if (resource is DiagnosticReport) - { - var diagnosticReportToDuplicate = (DiagnosticReport)resource; - - // TODO: more fields need to be populated? - var documentReference = new DocumentReference - { - Meta = new Meta - { - Tag = new List - { - new Coding(TagDuplicateOf, diagnosticReportToDuplicate.Id), - }, - }, - Content = new List(), - Subject = diagnosticReportToDuplicate.Subject, -#if R4 || R4B || Stu3 - Status = DocumentReferenceStatus.Current, -#else - Status = DocumentReference.DocumentReferenceStatus.Current, -#endif - }; - - if (diagnosticReportToDuplicate.PresentedForm?.Any(x => x.Url != null) ?? false) - { - // TODO: need to be more selective about whether the resource should be duplicated based on urls. - // (https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#clinical-notes-1) - foreach (var attachment in diagnosticReportToDuplicate.PresentedForm.Where(x => x.Url != null)) - { - documentReference.Content.Add( - new DocumentReference.ContentComponent - { - Attachment = attachment, - }); - } - } - - return documentReference; - } - - var documentReferenceToDuplicate = (DocumentReference)resource; - - // TODO: more fields need to be populated? - var diagnosticReport = new DiagnosticReport - { - Meta = new Meta - { - Tag = new List - { - new Coding(TagDuplicateOf, resource.Id), - }, - }, - PresentedForm = new List(), - Subject = documentReferenceToDuplicate.Subject, - Status = DiagnosticReport.DiagnosticReportStatus.Registered, - }; - - if (documentReferenceToDuplicate.Content?.Any(x => x.Attachment?.Url != null) ?? false) - { - // TODO: need to be more selective about whether the resource should be duplicated based on urls. - // (https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#clinical-notes-1) - foreach (var attachment in documentReferenceToDuplicate.Content?.Where(x => x.Attachment?.Url != null).Select(x => x.Attachment)) - { - diagnosticReport.PresentedForm.Add(attachment); - } - } - - return diagnosticReport; - } - - private static string GetDuplicateResourceId(Resource resource) - { - return resource?.Meta?.Tag?.SingleOrDefault(x => x.System == TagDuplicateOf)?.Code; - } - - private static bool ShouldDuplicate(Resource resource) - { - if (resource == null) - { - return false; - } - - if (resource is DiagnosticReport) - { - // TODO: need to be more selective about whether the resource should be duplicated based on urls. - // (https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#clinical-notes-1) - return ((DiagnosticReport)resource).PresentedForm?.Any(x => x.Url != null) ?? false; - } - else if (resource is DocumentReference) - { - // TODO: need to be more selective about whether the resource should be duplicated based on urls. - // (https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#clinical-notes-1) - return ((DocumentReference)resource).Content?.Any(x => x.Attachment?.Url != null) ?? false; - } - - return false; - } - - private static void UpdateDuplicateResource( - Resource resource, - Resource duplicateResource) - { - EnsureArg.IsNotNull(resource, nameof(resource)); - EnsureArg.IsNotNull(duplicateResource, nameof(duplicateResource)); - - if (!(resource is DiagnosticReport || resource is DocumentReference) - || !(duplicateResource is DiagnosticReport || duplicateResource is DocumentReference) - || string.Equals(resource?.TypeName, duplicateResource?.TypeName, StringComparison.OrdinalIgnoreCase)) - { - throw new ArgumentException( - $"A source/target resource to be updated must be of type '{nameof(DiagnosticReport)}' and '{nameof(DocumentReference)}'."); - } - - if (resource is DiagnosticReport) - { - var diagnosticReport = (DiagnosticReport)resource; - var documentReference = (DocumentReference)duplicateResource; - - documentReference.Subject = diagnosticReport.Subject; - if (documentReference.Content == null) - { - documentReference.Content = new List(); - } - - // TODO: is clearing the entire content necessary and right? - documentReference.Content.Clear(); - if (diagnosticReport.PresentedForm?.Any(x => x.Url != null) ?? false) - { - foreach (var attachment in diagnosticReport.PresentedForm.Where(x => x.Url != null)) - { - documentReference.Content.Add( - new DocumentReference.ContentComponent - { - Attachment = attachment, - }); - } - } - } - else - { - var documentReference = (DocumentReference)resource; - var diagnosticReport = (DiagnosticReport)duplicateResource; - - diagnosticReport.Subject = documentReference.Subject; - if (diagnosticReport.PresentedForm == null) - { - diagnosticReport.PresentedForm = new List(); - } - - // TODO: is clearing the entire presented-form necessary and right? - diagnosticReport.PresentedForm.Clear(); - if (documentReference.Content?.Any(x => x.Attachment?.Url != null) ?? false) - { - foreach (var attachment in documentReference.Content?.Where(x => x.Attachment?.Url != null).Select(x => x.Attachment)) - { - diagnosticReport.PresentedForm.Add(attachment); - } - } - } - } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/IClinicalReferenceDuplicator.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/IClinicalReferenceDuplicator.cs new file mode 100644 index 0000000000..f8bd2306dd --- /dev/null +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/IClinicalReferenceDuplicator.cs @@ -0,0 +1,40 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Hl7.Fhir.Model; +using Microsoft.Health.Fhir.Core.Features.Persistence; +using Microsoft.Health.Fhir.Core.Features.Search; +using Microsoft.Health.Fhir.Core.Messages.Delete; + +namespace Microsoft.Health.Fhir.Core.Features.Guidance +{ + public interface IClinicalReferenceDuplicator + { + Task CreateResourceAsync( + Resource resource, + CancellationToken cancellationToken); + + Task> DeleteResourceAsync( + ResourceKey resourceKey, + DeleteOperation deleteOperation, + CancellationToken cancellationToken); + + Task> SearchResourceAsync( + string duplicateResourceType, + string resourceId, + CancellationToken cancellationToken); + + Task> UpdateResourceAsync( + Resource resource, + CancellationToken cancellationToken); + + bool CheckDuplicate(ResourceKey resourceKey); + + bool ShouldDuplicate(Resource resource); + } +} diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Microsoft.Health.Fhir.Shared.Core.projitems b/src/Microsoft.Health.Fhir.Shared.Core/Microsoft.Health.Fhir.Shared.Core.projitems index f649719e70..4d4198730e 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Microsoft.Health.Fhir.Shared.Core.projitems +++ b/src/Microsoft.Health.Fhir.Shared.Core/Microsoft.Health.Fhir.Shared.Core.projitems @@ -16,7 +16,9 @@ + + From 6f5bdf1badfddd2fe427298ded04bfb2d73d1fda Mon Sep 17 00:00:00 2001 From: Isao Yamauchi Date: Mon, 11 Aug 2025 07:26:04 -0700 Subject: [PATCH 4/7] Adding the owning team and category to the UTs --- .../Features/Guidance/ClinicalReferenceDuplicatorTests.cs | 4 ++++ .../Guidance/DuplicateClinicalReferenceBehaviorTests.cs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/ClinicalReferenceDuplicatorTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/ClinicalReferenceDuplicatorTests.cs index e095c4ecb6..47cbe6c469 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/ClinicalReferenceDuplicatorTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/ClinicalReferenceDuplicatorTests.cs @@ -19,6 +19,8 @@ using Microsoft.Health.Fhir.Core.Messages.Create; using Microsoft.Health.Fhir.Core.Messages.Upsert; using Microsoft.Health.Fhir.Core.Models; +using Microsoft.Health.Fhir.Tests.Common; +using Microsoft.Health.Test.Utilities; using NSubstitute; using NSubstitute.ExceptionExtensions; using Xunit; @@ -26,6 +28,8 @@ namespace Microsoft.Health.Fhir.Shared.Core.UnitTests.Features.Guidance { + [Trait(Traits.OwningTeam, OwningTeam.Fhir)] + [Trait(Traits.Category, Categories.Operations)] public class ClinicalReferenceDuplicatorTests { private readonly ClinicalReferenceDuplicator _duplicator; diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/DuplicateClinicalReferenceBehaviorTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/DuplicateClinicalReferenceBehaviorTests.cs index 4d416da05c..47e61f1346 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/DuplicateClinicalReferenceBehaviorTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/DuplicateClinicalReferenceBehaviorTests.cs @@ -20,12 +20,16 @@ using Microsoft.Health.Fhir.Core.Messages.Upsert; using Microsoft.Health.Fhir.Core.Models; using Microsoft.Health.Fhir.Shared.Core.Features.Guidance; +using Microsoft.Health.Fhir.Tests.Common; +using Microsoft.Health.Test.Utilities; using NSubstitute; using Xunit; using Task = System.Threading.Tasks.Task; namespace Microsoft.Health.Fhir.Shared.Core.UnitTests.Features.Guidance { + [Trait(Traits.OwningTeam, OwningTeam.Fhir)] + [Trait(Traits.Category, Categories.Operations)] public class DuplicateClinicalReferenceBehaviorTests { private readonly CoreFeatureConfiguration _coreFeatureConfiguration; From 8fd7b66af4f8d205681d51188b676b68173ac3f7 Mon Sep 17 00:00:00 2001 From: Isao Yamauchi Date: Tue, 12 Aug 2025 07:29:17 -0700 Subject: [PATCH 5/7] Fixing some UT failures. --- .../Operations/OperationsConstants.cs | 2 + .../OperationsCapabilityProviderTests.cs | 15 + .../OperationsCapabilityProvider.cs | 10 + .../Modules/FhirModule.cs | 2 +- .../ClinicalReferenceDuplicatorTests.cs | 1335 +++++++++++++++-- .../Guidance/ClinicalReferenceDuplicator.cs | 287 +++- ...oft.Health.Fhir.Shared.Tests.E2E.projitems | 1 + .../ClinicalReferenceDuplicatorTests.cs | 337 +++++ 8 files changed, 1797 insertions(+), 192 deletions(-) create mode 100644 test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/Guidance/ClinicalReferenceDuplicatorTests.cs diff --git a/src/Microsoft.Health.Fhir.Core/Features/Operations/OperationsConstants.cs b/src/Microsoft.Health.Fhir.Core/Features/Operations/OperationsConstants.cs index 983bcd5f15..c06239c38d 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/Operations/OperationsConstants.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Operations/OperationsConstants.cs @@ -56,6 +56,8 @@ public static class OperationsConstants public const string ResourceTypeBulkUpdate = "resource-type-bulk-update"; + public const string ClinicalReferenceDuplicate = "clinical-reference-duplicate"; + public static readonly ReadOnlyCollection ExcludedResourceTypesForBulkUpdate = new ReadOnlyCollection(new[] { "SearchParameter", "StructureDefinition" }); } } diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Operations/OperationsCapabilityProviderTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Operations/OperationsCapabilityProviderTests.cs index 595fd667c5..0feeeb58c7 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Operations/OperationsCapabilityProviderTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Operations/OperationsCapabilityProviderTests.cs @@ -78,5 +78,20 @@ public void GivenAConformanceBuilder_WhenCallingOperationsCapabilityForIncludes_ builder.Received(support && dataStore == KnownDataStores.SqlServer ? 1 : 0) .Apply(Arg.Is>(x => x.Method.Name == nameof(OperationsCapabilityProvider.AddIncludesDetails))); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void GivenAConformanceBuilder_WhenCallingOperationsCapabilityForClinicalReferenceDuplicate_ThenClinicalReferenceDuplicateIsAddedWhenEnabled(bool added) + { + _coreFeatureConfiguration.EnableClinicalReferenceDuplication = added; + + var provider = new OperationsCapabilityProvider(_operationsOptions, _featureOptions, _coreFeatureOptions, _urlResolver, _fhirRuntimeConfiguration); + ICapabilityStatementBuilder builder = Substitute.For(); + provider.Build(builder); + + builder.Received(added ? 1 : 0) + .Apply(Arg.Is>(x => x.Method.Name == nameof(OperationsCapabilityProvider.AddClinicalReferenceDuplicateDetails))); + } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Operations/OperationsCapabilityProvider.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Operations/OperationsCapabilityProvider.cs index 69f59e358b..1639d95fc8 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Features/Operations/OperationsCapabilityProvider.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Operations/OperationsCapabilityProvider.cs @@ -96,6 +96,11 @@ public void Build(ICapabilityStatementBuilder builder) { builder.Apply(AddIncludesDetails); } + + if (_coreFeatureConfiguration.EnableClinicalReferenceDuplication) + { + builder.Apply(AddClinicalReferenceDuplicateDetails); + } } private void AddExportDetailsHelper(ICapabilityStatementBuilder builder) @@ -175,5 +180,10 @@ public void AddIncludesDetails(ListedCapabilityStatement capabilityStatement) { GetAndAddOperationDefinitionUriToCapabilityStatement(capabilityStatement, OperationsConstants.Includes); } + + public void AddClinicalReferenceDuplicateDetails(ListedCapabilityStatement capabilityStatement) + { + GetAndAddOperationDefinitionUriToCapabilityStatement(capabilityStatement, OperationsConstants.ClinicalReferenceDuplicate); + } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs b/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs index 6cf4a1de68..cf5083dedf 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Modules/FhirModule.cs @@ -221,7 +221,7 @@ ResourceElement SetMetadata(Resource resource, string versionId, DateTimeOffset services.AddTransient, DuplicateClinicalReferenceBehavior>(); services.AddTransient, DuplicateClinicalReferenceBehavior>(); services.AddTransient, DuplicateClinicalReferenceBehavior>(); - services.AddSingleton(); + services.AddTransient(); } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/ClinicalReferenceDuplicatorTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/ClinicalReferenceDuplicatorTests.cs index 47cbe6c469..c3b5e00e72 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/ClinicalReferenceDuplicatorTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/ClinicalReferenceDuplicatorTests.cs @@ -17,6 +17,7 @@ using Microsoft.Health.Fhir.Core.Features.Persistence; using Microsoft.Health.Fhir.Core.Features.Search; using Microsoft.Health.Fhir.Core.Messages.Create; +using Microsoft.Health.Fhir.Core.Messages.Delete; using Microsoft.Health.Fhir.Core.Messages.Upsert; using Microsoft.Health.Fhir.Core.Models; using Microsoft.Health.Fhir.Tests.Common; @@ -33,7 +34,7 @@ namespace Microsoft.Health.Fhir.Shared.Core.UnitTests.Features.Guidance public class ClinicalReferenceDuplicatorTests { private readonly ClinicalReferenceDuplicator _duplicator; - private readonly IMediator _mediator; + private readonly IFhirDataStore _dataStore; private readonly ISearchService _searchService; private readonly IRawResourceFactory _rawResourceFactory; private readonly IResourceWrapperFactory _resourceWrapperFactory; @@ -41,20 +42,21 @@ public class ClinicalReferenceDuplicatorTests public ClinicalReferenceDuplicatorTests() { - _mediator = Substitute.For(); + _dataStore = Substitute.For(); _searchService = Substitute.For(); _logger = Substitute.For>(); - _duplicator = new ClinicalReferenceDuplicator( - _mediator, - _searchService, - _logger); - _rawResourceFactory = Substitute.For(new FhirJsonSerializer()); _resourceWrapperFactory = Substitute.For(); _resourceWrapperFactory .Create(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(x => CreateResourceWrapper(x.ArgAt(0), x.ArgAt(1))); + + _duplicator = new ClinicalReferenceDuplicator( + _dataStore, + _searchService, + _resourceWrapperFactory, + _logger); } [Theory] @@ -68,20 +70,22 @@ public async Task GivenResource_WhenCreating_ThenDuplicateResourceShouldBeCreate // Set up a validation on a request for creating the original resource. var resourceElement = resource.ToResourceElement(); - _mediator.Send( - Arg.Is(x => string.Equals(x.Resource.InstanceType, resourceType, StringComparison.OrdinalIgnoreCase)), + _dataStore.UpsertAsync( + Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, resourceType, StringComparison.OrdinalIgnoreCase)), Arg.Any()) .Throws(new Exception($"Shouldn't be called to create a {resourceType} resource.")); // Set up a validation on a request for creating a duplicate resource. Resource duplicateResource = null; - _mediator.Send( - Arg.Is(x => string.Equals(x.Resource.InstanceType, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), + _dataStore.UpsertAsync( + Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), Arg.Any()) .Returns( x => { - var re = ((CreateResourceRequest)x[0]).Resource; + var re = ((ResourceWrapperOperation)x[0])?.Wrapper?.RawResource? + .ToITypedElement(ModelInfoProvider.Instance)? + .ToResourceElement(); Assert.NotNull(re); Assert.Equal(duplicateResourceType, re.InstanceType, true); @@ -135,18 +139,21 @@ public async Task GivenResource_WhenCreating_ThenDuplicateResourceShouldBeCreate } return Task.FromResult( - new UpsertResourceResponse( - new SaveOutcome(new RawResourceElement(CreateResourceWrapper(r.ToResourceElement())), SaveOutcomeType.Created))); + new UpsertOutcome( + CreateResourceWrapper(r.ToResourceElement()), + SaveOutcomeType.Created)); }); // Set up a validation on a request for updating the original resource with the id of the duplicate resource. - _mediator.Send( - Arg.Is(x => string.Equals(x.Resource.InstanceType, resourceType, StringComparison.OrdinalIgnoreCase)), + _dataStore.UpsertAsync( + Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, resourceType, StringComparison.OrdinalIgnoreCase)), Arg.Any()) .Returns( x => { - var re = ((UpsertResourceRequest)x[0]).Resource; + var re = ((ResourceWrapperOperation)x[0])?.Wrapper?.RawResource? + .ToITypedElement(ModelInfoProvider.Instance)? + .ToResourceElement(); Assert.NotNull(re); Assert.Equal(resourceType, re.InstanceType, true); @@ -158,8 +165,9 @@ public async Task GivenResource_WhenCreating_ThenDuplicateResourceShouldBeCreate && string.Equals(t.Code, duplicateResource.Id, StringComparison.OrdinalIgnoreCase)); return Task.FromResult( - new UpsertResourceResponse( - new SaveOutcome(new RawResourceElement(CreateResourceWrapper(r.ToResourceElement())), SaveOutcomeType.Updated))); + new UpsertOutcome( + CreateResourceWrapper(r.ToResourceElement()), + SaveOutcomeType.Updated)); }); await _duplicator.CreateResourceAsync( @@ -167,11 +175,11 @@ await _duplicator.CreateResourceAsync( CancellationToken.None); // Check how many times create/update was invoked. - await _mediator.Received(1).Send( - Arg.Any(), + await _dataStore.Received(1).UpsertAsync( + Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), Arg.Any()); - await _mediator.Received(1).Send( - Arg.Any(), + await _dataStore.Received(1).UpsertAsync( + Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, resourceType, StringComparison.OrdinalIgnoreCase)), Arg.Any()); } @@ -224,15 +232,17 @@ public async Task GivenResource_WhenUpdating_ThenDuplicateResourceShouldBeUpdate return Task.FromResult(searchResult); }); - // Set up a validation on a request for creating a duplicate resource. + // Set up a validation on a request for creating/updating a duplicate resource. Resource duplicateResource = null; - _mediator.Send( - Arg.Is(x => string.Equals(x.Resource.InstanceType, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), + _dataStore.UpsertAsync( + Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), Arg.Any()) .Returns( x => { - var re = ((CreateResourceRequest)x[0]).Resource; + var re = ((ResourceWrapperOperation)x[0])?.Wrapper?.RawResource? + .ToITypedElement(ModelInfoProvider.Instance)? + .ToResourceElement(); Assert.NotNull(re); Assert.Equal(duplicateResourceType, re.InstanceType, true); @@ -286,18 +296,21 @@ public async Task GivenResource_WhenUpdating_ThenDuplicateResourceShouldBeUpdate } return Task.FromResult( - new UpsertResourceResponse( - new SaveOutcome(new RawResourceElement(CreateResourceWrapper(r.ToResourceElement())), SaveOutcomeType.Created))); + new UpsertOutcome( + CreateResourceWrapper(r.ToResourceElement()), + SaveOutcomeType.Created)); }); // Set up a validation on a request for updating the original resource with the id of the duplicate resource. - _mediator.Send( - Arg.Is(x => string.Equals(x.Resource.InstanceType, resourceType, StringComparison.OrdinalIgnoreCase)), + _dataStore.UpsertAsync( + Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, resourceType, StringComparison.OrdinalIgnoreCase)), Arg.Any()) .Returns( x => { - var re = ((UpsertResourceRequest)x[0]).Resource; + var re = ((ResourceWrapperOperation)x[0])?.Wrapper?.RawResource? + .ToITypedElement(ModelInfoProvider.Instance)? + .ToResourceElement(); Assert.NotNull(re); Assert.Equal(resourceType, re.InstanceType, true); @@ -309,84 +322,115 @@ public async Task GivenResource_WhenUpdating_ThenDuplicateResourceShouldBeUpdate && string.Equals(t.Code, duplicateResource.Id, StringComparison.OrdinalIgnoreCase)); return Task.FromResult( - new UpsertResourceResponse( - new SaveOutcome(new RawResourceElement(CreateResourceWrapper(r.ToResourceElement())), SaveOutcomeType.Updated))); + new UpsertOutcome( + CreateResourceWrapper(r.ToResourceElement()), + SaveOutcomeType.Updated)); }); - // Set up a validation on a request for updating the duplicate resource. - _mediator.Send( - Arg.Is(x => string.Equals(x.Resource.InstanceType, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), + await _duplicator.UpdateResourceAsync( + resource, + CancellationToken.None); + + // Check how many times create/update was invoked. + await _searchService.Received(1).SearchAsync( + Arg.Is(x => string.Equals(x, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any>>(), + Arg.Any()); + await _dataStore.Received(duplicateResources.Any() ? 0 : 1).UpsertAsync( + Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, resourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any()); + await _dataStore.Received(duplicateResources.Any() ? duplicateResources.Count : 1).UpsertAsync( + Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any()); + } + + [Theory] + [MemberData(nameof(GetDeleteResourceData))] + public async Task GivenResource_WhenDeleting_ThenDuplicateResourceShouldBeDeleted( + Resource resource, + DeleteOperation deleteOperation, + List duplicateResources) + { + var resourceType = resource.TypeName; + var duplicateResourceType = string.Equals(resourceType, KnownResourceTypes.DiagnosticReport) + ? KnownResourceTypes.DocumentReference : KnownResourceTypes.DiagnosticReport; + var duplicateResourceIds = duplicateResources.Select(x => x.Id).ToHashSet(StringComparer.OrdinalIgnoreCase); + + // Set up a validation on a request for searching duplicate resources. + var entries = new List(); + if (duplicateResources?.Any() ?? false) + { + foreach (var r in duplicateResources) + { + var wrapper = new ResourceWrapper( + r.ToResourceElement(), + new RawResource(r.ToJson(), FhirResourceFormat.Json, false), + null, + false, + null, + null, + null); + entries.Add(new SearchResultEntry(wrapper)); + } + } + + _searchService.SearchAsync( + Arg.Is(x => string.Equals(x, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any>>(), Arg.Any()) .Returns( x => { - var re = ((UpsertResourceRequest)x[0]).Resource; - Assert.NotNull(re); - Assert.Equal(duplicateResourceType, re.InstanceType, true); - - var r = re.ToPoco(); - Assert.NotNull(r.Meta?.Tag); + var parameters = (IReadOnlyList>)x[1]; Assert.Contains( - r.Meta.Tag, - t => string.Equals(t.System, ClinicalReferenceDuplicator.TagDuplicateOf, StringComparison.OrdinalIgnoreCase) - && string.Equals(t.Code, resource.Id, StringComparison.OrdinalIgnoreCase)); - - if (string.Equals(duplicateResourceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase)) - { - var original = (DocumentReference)resource; - var duplicate = (DiagnosticReport)r; - - Assert.Equal(original.Subject?.Reference, duplicate.Subject?.Reference); - Assert.NotNull(duplicate.PresentedForm); - - if (original.Content?.Any(x => !string.IsNullOrEmpty(x.Attachment?.Url)) ?? false) - { - foreach (var a in original.Content.Select(x => x.Attachment)) - { - Assert.Contains( - duplicate.PresentedForm, - x => string.Equals(x.Url, a.Url, StringComparison.OrdinalIgnoreCase)); - } - } - else - { - Assert.Equal(0, duplicate.PresentedForm.Count(x => !string.IsNullOrEmpty(x.Url))); - } + parameters, + x => string.Equals(x.Item1, "_tag", StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Item2, $"{ClinicalReferenceDuplicator.TagDuplicateOf}|{resource.Id}", StringComparison.OrdinalIgnoreCase)); - duplicateResource = duplicate; - } - else - { - var original = (DiagnosticReport)resource; - var duplicate = (DocumentReference)r; + var searchResult = new SearchResult( + entries, + null, + null, + new List>()); + return Task.FromResult(searchResult); + }); - Assert.Equal(original.Subject?.Reference, duplicate.Subject?.Reference); - Assert.NotNull(duplicate.Content); + // Set up a validation on a request for soft-deleting the duplicate resource. + _dataStore.UpsertAsync( + Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any()) + .Returns( + x => + { + var r = ((ResourceWrapperOperation)x[0])?.Wrapper; + Assert.NotNull(r); + Assert.Equal(duplicateResourceType, r.ResourceTypeName, true); + Assert.Contains(r.ResourceId, duplicateResourceIds); - if (original.PresentedForm?.Any(x => !string.IsNullOrEmpty(x.Url)) ?? false) - { - foreach (var a in original.PresentedForm) - { - Assert.Contains( - duplicate.Content, - x => string.Equals(x.Attachment?.Url, a.Url, StringComparison.OrdinalIgnoreCase)); - } - } - else - { - Assert.Equal(0, duplicate.Content.Count(x => !string.IsNullOrEmpty(x.Attachment?.Url))); - } + return Task.FromResult( + new UpsertOutcome(r, SaveOutcomeType.Updated)); + }); - duplicateResource = duplicate; - } + // Set up a validation on a request for hard-deleting the duplicate resource. + _dataStore.HardDeleteAsync( + Arg.Is(x => string.Equals(x.ResourceType, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any(), + Arg.Any(), + Arg.Any()) + .Returns( + x => + { + var k = (ResourceKey)x[0]; + Assert.NotNull(k); + Assert.Equal(duplicateResourceType, k.ResourceType, true); + Assert.Contains(k.Id, duplicateResourceIds); - return Task.FromResult( - new UpsertResourceResponse( - new SaveOutcome(new RawResourceElement(CreateResourceWrapper(r.ToResourceElement())), SaveOutcomeType.Updated))); + return Task.CompletedTask; }); - await _duplicator.UpdateResourceAsync( - resource, + await _duplicator.DeleteResourceAsync( + new ResourceKey(resource.TypeName, resource.Id), + deleteOperation, CancellationToken.None); // Check how many times create/update was invoked. @@ -394,14 +438,13 @@ await _searchService.Received(1).SearchAsync( Arg.Is(x => string.Equals(x, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), Arg.Any>>(), Arg.Any()); - await _mediator.Received(duplicateResources.Any() ? 0 : 1).Send( - Arg.Any(), - Arg.Any()); - await _mediator.Received(duplicateResources.Any() ? 0 : 1).Send( - Arg.Is(x => string.Equals(x.Resource.InstanceType, resourceType, StringComparison.OrdinalIgnoreCase)), + await _dataStore.Received(deleteOperation == DeleteOperation.SoftDelete && duplicateResources.Any() ? duplicateResources.Count : 0).UpsertAsync( + Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), Arg.Any()); - await _mediator.Received(duplicateResources.Any() ? duplicateResources.Count : 0).Send( - Arg.Is(x => string.Equals(x.Resource.InstanceType, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), + await _dataStore.Received(deleteOperation == DeleteOperation.HardDelete && duplicateResources.Any() ? duplicateResources.Count : 0).HardDeleteAsync( + Arg.Is(x => string.Equals(x.ResourceType, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any(), + Arg.Any(), Arg.Any()); } @@ -669,6 +712,7 @@ public static IEnumerable GetUpdateResourceData() Tag = new List { new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, Content = new List @@ -684,11 +728,11 @@ public static IEnumerable GetUpdateResourceData() }, }, Subject = new ResourceReference(Guid.NewGuid().ToString()), - #if R4 || R4B || Stu3 +#if R4 || R4B || Stu3 Status = DocumentReferenceStatus.Current, - #else +#else Status = DocumentReference.DocumentReferenceStatus.Current, - #endif +#endif }, }, }, @@ -749,6 +793,7 @@ public static IEnumerable GetUpdateResourceData() Tag = new List { new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, Content = new List @@ -764,11 +809,11 @@ public static IEnumerable GetUpdateResourceData() }, }, Subject = new ResourceReference(Guid.NewGuid().ToString()), - #if R4 || R4B || Stu3 +#if R4 || R4B || Stu3 Status = DocumentReferenceStatus.Current, - #else +#else Status = DocumentReference.DocumentReferenceStatus.Current, - #endif +#endif }, }, }, @@ -809,6 +854,7 @@ public static IEnumerable GetUpdateResourceData() Tag = new List { new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, Content = new List @@ -824,11 +870,11 @@ public static IEnumerable GetUpdateResourceData() }, }, Subject = new ResourceReference(Guid.NewGuid().ToString()), - #if R4 || R4B || Stu3 +#if R4 || R4B || Stu3 Status = DocumentReferenceStatus.Current, - #else +#else Status = DocumentReference.DocumentReferenceStatus.Current, - #endif +#endif }, }, }, @@ -875,6 +921,135 @@ public static IEnumerable GetUpdateResourceData() new List(), }, new object[] + { + // Update a DiagnosticReport resource with multiple duplicates. + new DiagnosticReport + { + Id = "original", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + }, + }, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-original", + }, + }, + }, + new List + { + new DocumentReference + { + Id = "duplicate", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + }, + }, + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + new DocumentReference + { + Id = "duplicate1", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + }, + }, + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate1", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + new DocumentReference + { + Id = "duplicate2", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + }, + }, + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate2", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + }, + }, + new object[] { // Update a DocumentReference resource with one attachment. new DocumentReference @@ -916,6 +1091,7 @@ public static IEnumerable GetUpdateResourceData() Tag = new List { new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, Status = DiagnosticReport.DiagnosticReportStatus.Registered, @@ -1002,6 +1178,7 @@ public static IEnumerable GetUpdateResourceData() Tag = new List { new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, Status = DiagnosticReport.DiagnosticReportStatus.Registered, @@ -1059,6 +1236,7 @@ public static IEnumerable GetUpdateResourceData() Tag = new List { new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, Status = DiagnosticReport.DiagnosticReportStatus.Registered, @@ -1130,6 +1308,939 @@ public static IEnumerable GetUpdateResourceData() }, new List(), }, + new object[] + { + // Update a DocumentReference resource with multiple duplicates. + new DocumentReference + { + Id = "original", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + }, + }, + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-original", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + new List + { + new DiagnosticReport + { + Id = "duplicate", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + }, + }, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate", + }, + }, + }, + new DiagnosticReport + { + Id = "duplicate1", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + }, + }, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate1", + }, + }, + }, + new DiagnosticReport + { + Id = "duplicate2", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + }, + }, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate2", + }, + }, + }, + }, + }, + }; + + foreach (var d in data) + { + yield return d; + } + } + + public static IEnumerable GetDeleteResourceData() + { + var data = new[] + { + new object[] + { + // Delete a DiagnosticReport resource with one attachment. + new DiagnosticReport + { + Id = "original", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + }, + }, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-original", + }, + }, + }, + DeleteOperation.SoftDelete, + new List + { + new DocumentReference + { + Id = "duplicate", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + }, + }, + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + }, + }, + new object[] + { + // Delete a DiagnosticReport resource with multiple attachments. + new DiagnosticReport + { + Id = "original", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + }, + }, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-original", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment-original1", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment-original2", + }, + }, + }, + DeleteOperation.SoftDelete, + new List + { + new DocumentReference + { + Id = "duplicate", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + }, + }, + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + }, + }, + new object[] + { + // Delete a DiagnosticReport resource without any attachment. + new DiagnosticReport + { + Id = "original", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + }, + }, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List(), + }, + DeleteOperation.SoftDelete, + new List + { + new DocumentReference + { + Id = "duplicate", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + }, + }, + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + }, + }, + new object[] + { + // Delete a DiagnosticReport resource with attachments when no duplicates. + new DiagnosticReport + { + Id = "original", + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-original", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment-original1", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/Binary/attachment-original2", + }, + }, + }, + DeleteOperation.SoftDelete, + new List(), + }, + new object[] + { + // Delete a DiagnosticReport resource with multiple duplicates. + new DiagnosticReport + { + Id = "original", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + }, + }, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-original", + }, + }, + }, + DeleteOperation.SoftDelete, + new List + { + new DocumentReference + { + Id = "duplicate", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + }, + }, + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + new DocumentReference + { + Id = "duplicate1", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + }, + }, + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + new DocumentReference + { + Id = "duplicate2", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + }, + }, + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + }, + }, + new object[] + { + // Delete a DocumentReference resource with one attachment. + new DocumentReference + { + Id = "original", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + }, + }, + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-original", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + DeleteOperation.HardDelete, + new List + { + new DiagnosticReport + { + Id = "duplicate", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + }, + }, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate", + }, + }, + }, + }, + }, + new object[] + { + // Delete a DocumentReference resource with multiple attachment. + new DocumentReference + { + Id = "original", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + }, + }, + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-original", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment-original1", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/Binary/attachment-original2", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + DeleteOperation.HardDelete, + new List + { + new DiagnosticReport + { + Id = "duplicate", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + }, + }, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate", + }, + }, + }, + }, + }, + new object[] + { + // Delete a DocumentReference resource without any attachment. + new DocumentReference + { + Id = "original", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + }, + }, + Content = new List(), + Subject = new ResourceReference(Guid.NewGuid().ToString()), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + DeleteOperation.HardDelete, + new List + { + new DiagnosticReport + { + Id = "duplicate", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + }, + }, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate", + }, + }, + }, + }, + }, + new object[] + { + // Delete a DocumentReference resource with attachments when no duplicates. + new DocumentReference + { + Id = "original", + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-original", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment-original1", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/Binary/attachment-original2", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + DeleteOperation.HardDelete, + new List(), + }, + new object[] + { + // Delete a DocumentReference resource with multiple duplicates. + new DocumentReference + { + Id = "original", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + }, + }, + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-original", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + DeleteOperation.HardDelete, + new List + { + new DiagnosticReport + { + Id = "duplicate", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + }, + }, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate", + }, + }, + }, + new DiagnosticReport + { + Id = "duplicate1", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + }, + }, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate", + }, + }, + }, + new DiagnosticReport + { + Id = "duplicate2", + Meta = new Meta() + { + Tag = new List + { + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + }, + }, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate", + }, + }, + }, + }, + }, }; foreach (var d in data) diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/ClinicalReferenceDuplicator.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/ClinicalReferenceDuplicator.cs index baa61c42b3..2921c46edc 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/ClinicalReferenceDuplicator.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/ClinicalReferenceDuplicator.cs @@ -10,14 +10,11 @@ using System.Threading.Tasks; using EnsureThat; using Hl7.Fhir.Model; -using MediatR; using Microsoft.Extensions.Logging; using Microsoft.Health.Fhir.Core.Extensions; using Microsoft.Health.Fhir.Core.Features.Persistence; using Microsoft.Health.Fhir.Core.Features.Search; -using Microsoft.Health.Fhir.Core.Messages.Create; using Microsoft.Health.Fhir.Core.Messages.Delete; -using Microsoft.Health.Fhir.Core.Messages.Upsert; using Microsoft.Health.Fhir.Core.Models; namespace Microsoft.Health.Fhir.Core.Features.Guidance @@ -27,21 +24,27 @@ public class ClinicalReferenceDuplicator : IClinicalReferenceDuplicator internal const string TagDuplicateOf = "duplicateOf"; internal const string TagIsDuplicate = "isDuplicate"; - private readonly IMediator _mediator; + private readonly IFhirDataStore _dataStore; private readonly ISearchService _searchService; + private readonly IResourceWrapperFactory _resourceWrapperFactory; + private readonly ResourceIdProvider _resourceIdProvider; private readonly ILogger _logger; public ClinicalReferenceDuplicator( - IMediator mediator, + IFhirDataStore dataStore, ISearchService searchService, + IResourceWrapperFactory resourceWrapperFactory, ILogger logger) { - EnsureArg.IsNotNull(mediator, nameof(mediator)); + EnsureArg.IsNotNull(dataStore, nameof(dataStore)); EnsureArg.IsNotNull(searchService, nameof(searchService)); + EnsureArg.IsNotNull(resourceWrapperFactory, nameof(resourceWrapperFactory)); EnsureArg.IsNotNull(logger, nameof(logger)); - _mediator = mediator; + _dataStore = dataStore; _searchService = searchService; + _resourceWrapperFactory = resourceWrapperFactory; + _resourceIdProvider = new ResourceIdProvider(); _logger = logger; } @@ -53,32 +56,13 @@ public async Task CreateResourceAsync( try { - var createResponse = await CreateDuplicateResourceInternalAsync( + var duplicateResource = await CreateDuplicateResourceInternalAsync( resource, cancellationToken); - var duplicateResource = createResponse?.Outcome?.RawResourceElement?.RawResource? - .ToITypedElement(ModelInfoProvider.Instance)? - .ToResourceElement()? - .ToPoco(); - if (duplicateResource == null) - { - // TODO: throw an exception here. - _logger.LogError("A create response contains a null resource."); - } - - _logger.LogInformation( - "A '{DuplicateResourceType}' resource {Outcome}.", - duplicateResource.TypeName, - createResponse?.Outcome?.Outcome.ToString().ToLowerInvariant() ?? "not created"); - - var updateResponse = await UpdateResourceInternalAsync( + await UpdateResourceInternalAsync( resource, duplicateResource, cancellationToken); - _logger.LogInformation( - "A '{ResourceType}' resource {Outcome}.", - resource.TypeName, - updateResponse?.Outcome?.Outcome.ToString().ToLowerInvariant() ?? "not updated."); return duplicateResource; } catch (Exception ex) @@ -97,10 +81,14 @@ public async Task> DeleteResourceAsync( try { - var duplicateResources = await SearchResourceAsync( + var duplicateResourceWrappers = await SearchResourceAsync( GetDuplicateResourceType(resourceKey.ResourceType), resourceKey.Id, cancellationToken); + var duplicateResources = duplicateResourceWrappers + .Select(x => x.RawResource?.ToITypedElement(ModelInfoProvider.Instance)?.ToResourceElement()?.ToPoco()) + .Where(x => x != null) + .ToList(); _logger.LogInformation("Deleting {Count} duplicate resources...", duplicateResources.Count); if (duplicateResources.Count > 1) { @@ -112,18 +100,16 @@ public async Task> DeleteResourceAsync( { try { - _logger.LogInformation("Deleting a duplicate resource: {Id}...", duplicate.ResourceId); - var response = await _mediator.Send( - new DeleteResourceRequest( - new ResourceKey(duplicate.ResourceTypeName, duplicate.ResourceId), - deleteOperation), + var key = await DeleteDuplicateResourceInternalAsync( + duplicate, + deleteOperation, cancellationToken); - duplicateResourceKeysDeleted.Add(response.ResourceKey); + duplicateResourceKeysDeleted.Add(key); } catch (Exception ex) { - // Ignore an exception and continue deleting the rest. - _logger.LogError(ex, "Failed to delete a duplicate resource: {Id}...", duplicate.ResourceId); + _logger.LogError(ex, "Failed to delete a duplicate resource: {Id}...", duplicate.Id); + throw; } } @@ -198,8 +184,9 @@ public async Task> UpdateResourceAsync( resource.Id, cancellationToken); var duplicateResources = duplicateResourceWrappers - .Where(x => x.RawResource != null) - .Select(x => x.RawResource.ToITypedElement(ModelInfoProvider.Instance).ToResourceElement().ToPoco()).ToList(); + .Select(x => x.RawResource?.ToITypedElement(ModelInfoProvider.Instance)?.ToResourceElement()?.ToPoco()) + .Where(x => x != null) + .ToList(); _logger.LogInformation("Updating {Count} duplicate resources...", duplicateResources.Count); if (duplicateResources.Any()) { @@ -213,27 +200,16 @@ public async Task> UpdateResourceAsync( { try { - _logger.LogInformation("Updating a duplicate resource: {Id}...", duplicate.Id); - var response = await UpdateDuplicateResourceInternalAsync( + var duplicateUpdated = await UpdateDuplicateResourceInternalAsync( resource, duplicate, cancellationToken); - var duplicateUpdated = response.Outcome.RawResourceElement?.RawResource? - .ToITypedElement(ModelInfoProvider.Instance)? - .ToResourceElement()? - .ToPoco(); - if (duplicateUpdated == null) - { - _logger.LogError("A update response contains a null resource."); - continue; - } - duplicateResourcesUpdated.Add(duplicateUpdated); } catch (Exception ex) { - // Ignore an exception and continue updating the rest. _logger.LogError(ex, "Failed to update a duplicate resource: {Id}...", duplicate.Id); + throw; } } @@ -283,7 +259,7 @@ public bool ShouldDuplicate(Resource resource) return false; } - private Task CreateDuplicateResourceInternalAsync( + private async Task CreateDuplicateResourceInternalAsync( Resource resource, CancellationToken cancellationToken) { @@ -292,16 +268,49 @@ private Task CreateDuplicateResourceInternalAsync( try { var duplicateResource = CreateDuplicateResource(resource); - var request = new CreateResourceRequest(duplicateResource.ToResourceElement()); + var duplicateResourceWrapper = _resourceWrapperFactory.CreateResourceWrapper( + duplicateResource, + _resourceIdProvider, + false, + true); _logger.LogInformation( "Creating a duplicate resource '{DuplicateResourceType}' of a '{ResourceType}' resource '{Id}'...", duplicateResource.TypeName, resource.TypeName, resource.Id); - return _mediator.Send( - request, + var outcome = await _dataStore.UpsertAsync( + new ResourceWrapperOperation( + duplicateResourceWrapper, + true, + true, + null, + false, + false, + null), cancellationToken); + Resource resourceCreated = null; + if (outcome?.Wrapper?.RawResource != null) + { + _logger.LogInformation( + "A '{DuplicateResourceType}' resource {Outcome}.", + duplicateResource.TypeName, + outcome.OutcomeType.ToString().ToLowerInvariant()); + + resourceCreated = outcome.Wrapper?.RawResource? + .ToITypedElement(ModelInfoProvider.Instance)? + .ToResourceElement()? + .ToPoco(); + } + else + { + // TODO: throws an exception here. + _logger.LogError( + "Failed to create a '{DuplicateResourceType}' resource.", + duplicateResource.TypeName); + } + + return resourceCreated; } catch (Exception ex) { @@ -310,47 +319,118 @@ private Task CreateDuplicateResourceInternalAsync( } } - private Task UpdateResourceInternalAsync( - Resource resource, + private async Task DeleteDuplicateResourceInternalAsync( Resource duplicateResource, + DeleteOperation deleteOperation, CancellationToken cancellationToken) { - EnsureArg.IsNotNull(resource, nameof(resource)); EnsureArg.IsNotNull(duplicateResource, nameof(duplicateResource)); try { - if (resource.Meta == null) + _logger.LogInformation( + "Deleting a duplicate resource '{DuplicateResourceType}': {Id}...", + duplicateResource.TypeName, + duplicateResource.Id); + if (deleteOperation == DeleteOperation.HardDelete) { - resource.Meta = new Meta(); + await _dataStore.HardDeleteAsync( + new ResourceKey(duplicateResource.TypeName, duplicateResource.Id), + false, + false, + cancellationToken); } - - var tags = new List(); - if (resource.Meta.Tag != null) + else { - tags.AddRange( - resource.Meta.Tag.Where(x => !string.Equals(x.System, TagDuplicateOf, StringComparison.OrdinalIgnoreCase))); + var duplicateResourceWrapper = _resourceWrapperFactory.CreateResourceWrapper( + duplicateResource, + _resourceIdProvider, + true, + false); + await _dataStore.UpsertAsync( + new ResourceWrapperOperation( + duplicateResourceWrapper, + false, + false, + null, + false, + false, + null), + cancellationToken); } - tags.Add(new Coding(TagDuplicateOf, duplicateResource.Id)); - resource.Meta.Tag = tags; + return new ResourceKey(duplicateResource.TypeName, duplicateResource.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to delete a duplicate resource."); + throw; + } + } + + private async Task UpdateDuplicateResourceInternalAsync( + Resource resource, + Resource duplicateResource, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(resource, nameof(resource)); + EnsureArg.IsNotNull(duplicateResource, nameof(duplicateResource)); + + try + { + UpdateDuplicateResource(resource, duplicateResource); + + var duplicateResourceWrapper = _resourceWrapperFactory.Create( + duplicateResource.ToResourceElement(), + false, + true); _logger.LogInformation( - "Updating a '{ResourceType}' resource with a duplicate resource '{Id}'...", - resource.TypeName, - duplicateResource.Id); - return _mediator.Send( - new UpsertResourceRequest(resource.ToResourceElement()), + "Updating a duplicate resource '{DuplicateResourceType}': {Id}...", + duplicateResource.TypeName, + resource.Id); + var outcome = await _dataStore.UpsertAsync( + new ResourceWrapperOperation( + duplicateResourceWrapper, + true, + true, + null, + false, + false, + null), cancellationToken); + Resource resourceUpdated = null; + if (outcome?.Wrapper?.RawResource != null) + { + _logger.LogInformation( + "A '{DuplicateResourceType}' resource {Outcome}.", + duplicateResource.TypeName, + outcome.OutcomeType.ToString().ToLowerInvariant()); + + resourceUpdated = outcome.Wrapper?.RawResource? + .ToITypedElement(ModelInfoProvider.Instance)? + .ToResourceElement()? + .ToPoco(); + } + else + { + // TODO: throws an exception here. + _logger.LogError( + "Failed to update a '{DuplicateResourceType}' resource: {Id}.", + duplicateResource.TypeName, + duplicateResource.Id); + } + + return resourceUpdated; } catch (Exception ex) { - _logger.LogError(ex, "Failed to update a resource with a duplicate resource id."); + _logger.LogError(ex, "Failed to update a duplicate resource."); throw; } } - private Task UpdateDuplicateResourceInternalAsync( + private async Task UpdateResourceInternalAsync( Resource resource, Resource duplicateResource, CancellationToken cancellationToken) @@ -360,14 +440,63 @@ private Task UpdateDuplicateResourceInternalAsync( try { - UpdateDuplicateResource(resource, duplicateResource); + if (resource.Meta == null) + { + resource.Meta = new Meta(); + } + + var tags = new List(); + if (resource.Meta.Tag != null) + { + tags.AddRange( + resource.Meta.Tag.Where(x => !string.Equals(x.System, TagDuplicateOf, StringComparison.OrdinalIgnoreCase))); + } + + tags.Add(new Coding(TagDuplicateOf, duplicateResource.Id)); + resource.Meta.Tag = tags; + + var resourceWrapper = _resourceWrapperFactory.Create( + resource.ToResourceElement(), + false, + true); + _logger.LogInformation( - "Updating a '{DuplicateResourceType}' resource with a duplicate resource '{Id}'...", + "Updating a '{ResourceType}' resource with a duplicate resource '{Id}'...", resource.TypeName, duplicateResource.Id); - return _mediator.Send( - new UpsertResourceRequest(duplicateResource.ToResourceElement()), + var outcome = await _dataStore.UpsertAsync( + new ResourceWrapperOperation( + resourceWrapper, + true, + true, + null, + false, + false, + null), cancellationToken); + Resource resourceUpdated = null; + if (outcome?.Wrapper?.RawResource != null) + { + _logger.LogInformation( + "A '{ResourceType}' resource {Outcome}.", + resource.TypeName, + outcome.OutcomeType.ToString().ToLowerInvariant()); + + resourceUpdated = outcome.Wrapper?.RawResource? + .ToITypedElement(ModelInfoProvider.Instance)? + .ToResourceElement()? + .ToPoco(); + } + else + { + // TODO: throws an exception here. + _logger.LogError( + "Failed to update a '{ResourceType}' resource: {Id}.", + resource.TypeName, + resource.Id); + } + + return resourceUpdated; } catch (Exception ex) { diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Microsoft.Health.Fhir.Shared.Tests.E2E.projitems b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Microsoft.Health.Fhir.Shared.Tests.E2E.projitems index e98c0ad945..34adef9b5b 100644 --- a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Microsoft.Health.Fhir.Shared.Tests.E2E.projitems +++ b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Microsoft.Health.Fhir.Shared.Tests.E2E.projitems @@ -40,6 +40,7 @@ + diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/Guidance/ClinicalReferenceDuplicatorTests.cs b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/Guidance/ClinicalReferenceDuplicatorTests.cs new file mode 100644 index 0000000000..dd3a0c4e01 --- /dev/null +++ b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/Guidance/ClinicalReferenceDuplicatorTests.cs @@ -0,0 +1,337 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using DotLiquid.Util; +using EnsureThat; +using Hl7.Fhir.Model; +using Microsoft.Health.Fhir.Core.Features.Guidance; +using Microsoft.Health.Fhir.Core.Features.Operations; +using Microsoft.Health.Fhir.Core.Models; +using Microsoft.Health.Fhir.Tests.Common; +using Microsoft.Health.Fhir.Tests.Common.Extensions; +using Microsoft.Health.Fhir.Tests.Common.FixtureParameters; +using Microsoft.Health.Fhir.Tests.E2E.Common; +using Microsoft.Health.Fhir.Tests.E2E.Rest.Import; +using Microsoft.Health.Test.Utilities; +using Xunit; +using Task = System.Threading.Tasks.Task; + +namespace Microsoft.Health.Fhir.Tests.E2E.Rest.Guidance +{ + [Trait(Traits.OwningTeam, OwningTeam.Fhir)] + [Trait(Traits.Category, Categories.Operations)] + [HttpIntegrationFixtureArgumentSets(DataStore.All, Format.Json)] + public class ClinicalReferenceDuplicatorTests : IClassFixture + { + private readonly HttpIntegrationTestFixture _fixture; + + public ClinicalReferenceDuplicatorTests(HttpIntegrationTestFixture fixture) + { + _fixture = fixture; + } + + private TestFhirClient Client => _fixture.TestFhirClient; + + [Theory] + [MemberData(nameof(GetCreateResourceData))] + public async Task GivenResource_WhenCreating_ThenDuplicateResourceShouldBeCreated( + Resource resource) + { + var supportsClinicalReferenceDuplicate = _fixture.TestFhirServer.Metadata.SupportsOperation( + OperationsConstants.ClinicalReferenceDuplicate); + await CreateResourceAsync(resource); + } + + private async Task CreateResourceAsync(Resource resource) + { + var resourceType = resource.TypeName; + var duplicateResourceType = string.Equals(resourceType, KnownResourceTypes.DiagnosticReport) + ? KnownResourceTypes.DocumentReference : KnownResourceTypes.DiagnosticReport; + + // Create a resource. + TagResource(resource); + var response = await Client.CreateAsync( + resourceType, + resource); + Assert.Equal(HttpStatusCode.Created, response.Response.StatusCode); + Assert.NotNull(response.Resource); + + // Look for a duplicate resource. + var searchResponse = await Client.SearchAsync( + $"{duplicateResourceType}?_tag={ClinicalReferenceDuplicator.TagDuplicateOf}|{response.Resource?.Id}"); + Assert.NotNull(searchResponse.Resource?.Entry); + Assert.Single(searchResponse.Resource.Entry); + + // Check if a duplicate resource has the subject and attachments that match ones from a resource created. + if (string.Equals(duplicateResourceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase)) + { + var original = (DocumentReference)resource; + var duplicate = (DiagnosticReport)searchResponse.Resource.Entry[0].Resource; + + Assert.Equal(original.Subject?.Reference, duplicate.Subject?.Reference); + Assert.NotNull(duplicate.PresentedForm); + + if (original.Content?.Any(x => !string.IsNullOrEmpty(x.Attachment?.Url)) ?? false) + { + foreach (var a in original.Content.Select(x => x.Attachment)) + { + Assert.Contains( + duplicate.PresentedForm, + x => string.Equals(x.Url, a.Url, StringComparison.OrdinalIgnoreCase)); + } + } + else + { + Assert.Equal(0, duplicate.PresentedForm.Count(x => !string.IsNullOrEmpty(x.Url))); + } + } + else + { + var original = (DiagnosticReport)resource; + var duplicate = (DocumentReference)searchResponse.Resource.Entry[0].Resource; + + Assert.Equal(original.Subject?.Reference, duplicate.Subject?.Reference); + Assert.NotNull(duplicate.Content); + + if (original.PresentedForm?.Any(x => !string.IsNullOrEmpty(x.Url)) ?? false) + { + foreach (var a in original.PresentedForm) + { + Assert.Contains( + duplicate.Content, + x => string.Equals(x.Attachment?.Url, a.Url, StringComparison.OrdinalIgnoreCase)); + } + } + else + { + Assert.Equal(0, duplicate.Content.Count(x => !string.IsNullOrEmpty(x.Attachment?.Url))); + } + } + } + + private static void TagResource(Resource resource) + { + EnsureArg.IsNotNull(resource, nameof(resource)); + + if (resource.Meta == null) + { + resource.Meta = new Meta(); + } + + if (resource.Meta.Tag != null) + { + resource.Meta.Tag = new List(); + } + + var tag = $"clinicalreftest-{DateTime.UtcNow.Ticks}"; + resource.Meta.Tag.Add(new Coding("testTag", tag)); + } + + public static IEnumerable GetCreateResourceData() + { + var data = new[] + { + new object[] + { + // Create a new DiagnosticReport resource with one attachment. + new DiagnosticReport + { + Id = Guid.NewGuid().ToString(), + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, + }, + }, + }, + new object[] + { + // Create a new DiagnosticReport resource with multiple attachments. + new DiagnosticReport + { + Id = Guid.NewGuid().ToString(), + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment1", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/Binary/attachment2", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment3", + }, + }, + }, + }, + new object[] + { + // Create a new DiagnosticReport resource without any attachment. + new DiagnosticReport + { + Id = Guid.NewGuid().ToString(), + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + }, + }, + new object[] + { + // Create a new DocumentReference resource with one attachment. + new DocumentReference + { + Id = Guid.NewGuid().ToString(), + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + #if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, + #else + Status = DocumentReference.DocumentReferenceStatus.Current, + #endif + }, + }, + new object[] + { + // Create a new DocumentReference resource with multiple attachments. + new DocumentReference + { + Id = Guid.NewGuid().ToString(), + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment1", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/Binary/attachment2", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment3", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + #if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, + #else + Status = DocumentReference.DocumentReferenceStatus.Current, + #endif + }, + }, + new object[] + { + // Create a new DocumentReference resource without any attachment. + new DocumentReference + { + Id = Guid.NewGuid().ToString(), + Subject = new ResourceReference(Guid.NewGuid().ToString()), + #if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, + #else + Status = DocumentReference.DocumentReferenceStatus.Current, + #endif + }, + }, + }; + + foreach (var d in data) + { + yield return d; + } + } + } +} From 565e9e2fd24e52f8067be9a5a1a7922d2d111eed Mon Sep 17 00:00:00 2001 From: Isao Yamauchi Date: Wed, 13 Aug 2025 14:59:10 -0700 Subject: [PATCH 6/7] More refactoring and more tests added. --- .../Operations/OperationsConstants.cs | 2 - .../OperationsCapabilityProviderTests.cs | 15 - .../OperationsCapabilityProvider.cs | 10 - .../ClinicalReferenceDuplicatorTests.cs | 186 +++--- ...DuplicateClinicalReferenceBehaviorTests.cs | 43 +- .../Guidance/ClinicalReferenceDuplicator.cs | 328 +++++------ .../DuplicateClinicalReferenceBehavior.cs | 58 +- .../Guidance/IClinicalReferenceDuplicator.cs | 15 +- .../appsettings.json | 1 + .../ClinicalReferenceDuplicatorTests.cs | 534 ++++++++++++++++-- 10 files changed, 788 insertions(+), 404 deletions(-) diff --git a/src/Microsoft.Health.Fhir.Core/Features/Operations/OperationsConstants.cs b/src/Microsoft.Health.Fhir.Core/Features/Operations/OperationsConstants.cs index c06239c38d..983bcd5f15 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/Operations/OperationsConstants.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Operations/OperationsConstants.cs @@ -56,8 +56,6 @@ public static class OperationsConstants public const string ResourceTypeBulkUpdate = "resource-type-bulk-update"; - public const string ClinicalReferenceDuplicate = "clinical-reference-duplicate"; - public static readonly ReadOnlyCollection ExcludedResourceTypesForBulkUpdate = new ReadOnlyCollection(new[] { "SearchParameter", "StructureDefinition" }); } } diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Operations/OperationsCapabilityProviderTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Operations/OperationsCapabilityProviderTests.cs index 0feeeb58c7..595fd667c5 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Operations/OperationsCapabilityProviderTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Operations/OperationsCapabilityProviderTests.cs @@ -78,20 +78,5 @@ public void GivenAConformanceBuilder_WhenCallingOperationsCapabilityForIncludes_ builder.Received(support && dataStore == KnownDataStores.SqlServer ? 1 : 0) .Apply(Arg.Is>(x => x.Method.Name == nameof(OperationsCapabilityProvider.AddIncludesDetails))); } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void GivenAConformanceBuilder_WhenCallingOperationsCapabilityForClinicalReferenceDuplicate_ThenClinicalReferenceDuplicateIsAddedWhenEnabled(bool added) - { - _coreFeatureConfiguration.EnableClinicalReferenceDuplication = added; - - var provider = new OperationsCapabilityProvider(_operationsOptions, _featureOptions, _coreFeatureOptions, _urlResolver, _fhirRuntimeConfiguration); - ICapabilityStatementBuilder builder = Substitute.For(); - provider.Build(builder); - - builder.Received(added ? 1 : 0) - .Apply(Arg.Is>(x => x.Method.Name == nameof(OperationsCapabilityProvider.AddClinicalReferenceDuplicateDetails))); - } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Operations/OperationsCapabilityProvider.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Operations/OperationsCapabilityProvider.cs index 1639d95fc8..69f59e358b 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Features/Operations/OperationsCapabilityProvider.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Operations/OperationsCapabilityProvider.cs @@ -96,11 +96,6 @@ public void Build(ICapabilityStatementBuilder builder) { builder.Apply(AddIncludesDetails); } - - if (_coreFeatureConfiguration.EnableClinicalReferenceDuplication) - { - builder.Apply(AddClinicalReferenceDuplicateDetails); - } } private void AddExportDetailsHelper(ICapabilityStatementBuilder builder) @@ -180,10 +175,5 @@ public void AddIncludesDetails(ListedCapabilityStatement capabilityStatement) { GetAndAddOperationDefinitionUriToCapabilityStatement(capabilityStatement, OperationsConstants.Includes); } - - public void AddClinicalReferenceDuplicateDetails(ListedCapabilityStatement capabilityStatement) - { - GetAndAddOperationDefinitionUriToCapabilityStatement(capabilityStatement, OperationsConstants.ClinicalReferenceDuplicate); - } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/ClinicalReferenceDuplicatorTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/ClinicalReferenceDuplicatorTests.cs index c3b5e00e72..7c62a0ba46 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/ClinicalReferenceDuplicatorTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/ClinicalReferenceDuplicatorTests.cs @@ -68,7 +68,7 @@ public async Task GivenResource_WhenCreating_ThenDuplicateResourceShouldBeCreate var duplicateResourceType = string.Equals(resourceType, KnownResourceTypes.DiagnosticReport) ? KnownResourceTypes.DocumentReference : KnownResourceTypes.DiagnosticReport; - // Set up a validation on a request for creating the original resource. + // Set up a validation on a request for creating the source resource. var resourceElement = resource.ToResourceElement(); _dataStore.UpsertAsync( Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, resourceType, StringComparison.OrdinalIgnoreCase)), @@ -102,12 +102,12 @@ public async Task GivenResource_WhenCreating_ThenDuplicateResourceShouldBeCreate if (string.Equals(duplicateResourceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase)) { - var original = (DocumentReference)resource; + var source = (DocumentReference)resource; var duplicate = (DiagnosticReport)r; - Assert.Equal(original.Subject?.Reference, duplicate.Subject?.Reference); + Assert.Equal(source.Subject?.Reference, duplicate.Subject?.Reference); Assert.NotNull(duplicate.PresentedForm); - foreach (var a in original.Content.Select(x => x.Attachment)) + foreach (var a in source.Content.Select(x => x.Attachment)) { Assert.Contains( duplicate.PresentedForm, @@ -118,12 +118,12 @@ public async Task GivenResource_WhenCreating_ThenDuplicateResourceShouldBeCreate } else { - var original = (DiagnosticReport)resource; + var source = (DiagnosticReport)resource; var duplicate = (DocumentReference)r; - Assert.Equal(original.Subject?.Reference, duplicate.Subject?.Reference); + Assert.Equal(source.Subject?.Reference, duplicate.Subject?.Reference); Assert.NotNull(duplicate.Content); - foreach (var a in original.PresentedForm) + foreach (var a in source.PresentedForm) { Assert.Contains( duplicate.Content, @@ -144,7 +144,7 @@ public async Task GivenResource_WhenCreating_ThenDuplicateResourceShouldBeCreate SaveOutcomeType.Created)); }); - // Set up a validation on a request for updating the original resource with the id of the duplicate resource. + // Set up a validation on a request for updating the source resource with the id of the duplicate resource. _dataStore.UpsertAsync( Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, resourceType, StringComparison.OrdinalIgnoreCase)), Arg.Any()) @@ -171,7 +171,7 @@ public async Task GivenResource_WhenCreating_ThenDuplicateResourceShouldBeCreate }); await _duplicator.CreateResourceAsync( - resource, + new RawResourceElement(CreateResourceWrapper(resourceElement)), CancellationToken.None); // Check how many times create/update was invoked. @@ -259,12 +259,12 @@ public async Task GivenResource_WhenUpdating_ThenDuplicateResourceShouldBeUpdate if (string.Equals(duplicateResourceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase)) { - var original = (DocumentReference)resource; + var source = (DocumentReference)resource; var duplicate = (DiagnosticReport)r; - Assert.Equal(original.Subject?.Reference, duplicate.Subject?.Reference); + Assert.Equal(source.Subject?.Reference, duplicate.Subject?.Reference); Assert.NotNull(duplicate.PresentedForm); - foreach (var a in original.Content.Select(x => x.Attachment)) + foreach (var a in source.Content.Select(x => x.Attachment)) { Assert.Contains( duplicate.PresentedForm, @@ -275,12 +275,12 @@ public async Task GivenResource_WhenUpdating_ThenDuplicateResourceShouldBeUpdate } else { - var original = (DiagnosticReport)resource; + var source = (DiagnosticReport)resource; var duplicate = (DocumentReference)r; - Assert.Equal(original.Subject?.Reference, duplicate.Subject?.Reference); + Assert.Equal(source.Subject?.Reference, duplicate.Subject?.Reference); Assert.NotNull(duplicate.Content); - foreach (var a in original.PresentedForm) + foreach (var a in source.PresentedForm) { Assert.Contains( duplicate.Content, @@ -301,7 +301,7 @@ public async Task GivenResource_WhenUpdating_ThenDuplicateResourceShouldBeUpdate SaveOutcomeType.Created)); }); - // Set up a validation on a request for updating the original resource with the id of the duplicate resource. + // Set up a validation on a request for updating the source resource with the id of the duplicate resource. _dataStore.UpsertAsync( Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, resourceType, StringComparison.OrdinalIgnoreCase)), Arg.Any()) @@ -328,7 +328,7 @@ public async Task GivenResource_WhenUpdating_ThenDuplicateResourceShouldBeUpdate }); await _duplicator.UpdateResourceAsync( - resource, + new RawResourceElement(CreateResourceWrapper(resource.ToResourceElement())), CancellationToken.None); // Check how many times create/update was invoked. @@ -672,7 +672,7 @@ public static IEnumerable GetUpdateResourceData() // Update a DiagnosticReport resource with one attachment. new DiagnosticReport { - Id = "original", + Id = "source", Meta = new Meta() { Tag = new List @@ -698,7 +698,7 @@ public static IEnumerable GetUpdateResourceData() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-original", + Url = "http://example.org/fhir/Binary/attachment-source", }, }, }, @@ -711,7 +711,7 @@ public static IEnumerable GetUpdateResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, @@ -741,7 +741,7 @@ public static IEnumerable GetUpdateResourceData() // Update a DiagnosticReport resource with multiple attachments. new DiagnosticReport { - Id = "original", + Id = "source", Meta = new Meta() { Tag = new List @@ -767,19 +767,19 @@ public static IEnumerable GetUpdateResourceData() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-original", + Url = "http://example.org/fhir/Binary/attachment-source", }, new Attachment() { ContentType = "application/xhtml", Creation = "2006-12-24", - Url = "http://example.org/fhir/Binary/attachment-original1", + Url = "http://example.org/fhir/Binary/attachment-source1", }, new Attachment() { ContentType = "application/xhtml", Creation = "2006-12-24", - Url = "http://example.org/fhir/Binary/attachment-original2", + Url = "http://example.org/fhir/Binary/attachment-source2", }, }, }, @@ -792,7 +792,7 @@ public static IEnumerable GetUpdateResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, @@ -822,7 +822,7 @@ public static IEnumerable GetUpdateResourceData() // Update a DiagnosticReport resource without any attachment. new DiagnosticReport { - Id = "original", + Id = "source", Meta = new Meta() { Tag = new List @@ -853,7 +853,7 @@ public static IEnumerable GetUpdateResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, @@ -883,7 +883,7 @@ public static IEnumerable GetUpdateResourceData() // Update a DiagnosticReport resource with attachments when a duplicate resource doesn't exist. new DiagnosticReport { - Id = "original", + Id = "source", Status = DiagnosticReport.DiagnosticReportStatus.Registered, Code = new CodeableConcept() { @@ -902,19 +902,19 @@ public static IEnumerable GetUpdateResourceData() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-original", + Url = "http://example.org/fhir/Binary/attachment-source", }, new Attachment() { ContentType = "application/xhtml", Creation = "2006-12-24", - Url = "http://example.org/fhir/Binary/attachment-original1", + Url = "http://example.org/fhir/Binary/attachment-source1", }, new Attachment() { ContentType = "application/xhtml", Creation = "2007-12-24", - Url = "http://example.org/fhir/Binary/attachment-original2", + Url = "http://example.org/fhir/Binary/attachment-source2", }, }, }, @@ -925,7 +925,7 @@ public static IEnumerable GetUpdateResourceData() // Update a DiagnosticReport resource with multiple duplicates. new DiagnosticReport { - Id = "original", + Id = "source", Meta = new Meta() { Tag = new List @@ -951,7 +951,7 @@ public static IEnumerable GetUpdateResourceData() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-original", + Url = "http://example.org/fhir/Binary/attachment-source", }, }, }, @@ -964,7 +964,7 @@ public static IEnumerable GetUpdateResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, @@ -994,7 +994,7 @@ public static IEnumerable GetUpdateResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, @@ -1024,7 +1024,7 @@ public static IEnumerable GetUpdateResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, @@ -1054,7 +1054,7 @@ public static IEnumerable GetUpdateResourceData() // Update a DocumentReference resource with one attachment. new DocumentReference { - Id = "original", + Id = "source", Meta = new Meta() { Tag = new List @@ -1070,7 +1070,7 @@ public static IEnumerable GetUpdateResourceData() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-original", + Url = "http://example.org/fhir/Binary/attachment-source", }, }, }, @@ -1090,7 +1090,7 @@ public static IEnumerable GetUpdateResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, @@ -1123,7 +1123,7 @@ public static IEnumerable GetUpdateResourceData() // Update a DocumentReference resource with multiple attachment. new DocumentReference { - Id = "original", + Id = "source", Meta = new Meta() { Tag = new List @@ -1139,7 +1139,7 @@ public static IEnumerable GetUpdateResourceData() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-original", + Url = "http://example.org/fhir/Binary/attachment-source", }, }, new DocumentReference.ContentComponent() @@ -1148,7 +1148,7 @@ public static IEnumerable GetUpdateResourceData() { ContentType = "application/xhtml", Creation = "2006-12-24", - Url = "http://example.org/fhir/Binary/attachment-original1", + Url = "http://example.org/fhir/Binary/attachment-source1", }, }, new DocumentReference.ContentComponent() @@ -1157,7 +1157,7 @@ public static IEnumerable GetUpdateResourceData() { ContentType = "application/xhtml", Creation = "2007-12-24", - Url = "http://example.org/fhir/Binary/attachment-original2", + Url = "http://example.org/fhir/Binary/attachment-source2", }, }, }, @@ -1177,7 +1177,7 @@ public static IEnumerable GetUpdateResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, @@ -1210,7 +1210,7 @@ public static IEnumerable GetUpdateResourceData() // Update a DocumentReference resource without any attachment. new DocumentReference { - Id = "original", + Id = "source", Meta = new Meta() { Tag = new List @@ -1235,7 +1235,7 @@ public static IEnumerable GetUpdateResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, @@ -1268,7 +1268,7 @@ public static IEnumerable GetUpdateResourceData() // Update a DocumentReference resource with attachments when a duplicate resource doesn't exist. new DocumentReference { - Id = "original", + Id = "source", Content = new List { new DocumentReference.ContentComponent() @@ -1277,7 +1277,7 @@ public static IEnumerable GetUpdateResourceData() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-original", + Url = "http://example.org/fhir/Binary/attachment-source", }, }, new DocumentReference.ContentComponent() @@ -1286,7 +1286,7 @@ public static IEnumerable GetUpdateResourceData() { ContentType = "application/xhtml", Creation = "2006-12-24", - Url = "http://example.org/fhir/Binary/attachment-original1", + Url = "http://example.org/fhir/Binary/attachment-source1", }, }, new DocumentReference.ContentComponent() @@ -1295,7 +1295,7 @@ public static IEnumerable GetUpdateResourceData() { ContentType = "application/xhtml", Creation = "2007-12-24", - Url = "http://example.org/fhir/Binary/attachment-original2", + Url = "http://example.org/fhir/Binary/attachment-source2", }, }, }, @@ -1313,7 +1313,7 @@ public static IEnumerable GetUpdateResourceData() // Update a DocumentReference resource with multiple duplicates. new DocumentReference { - Id = "original", + Id = "source", Meta = new Meta() { Tag = new List @@ -1329,7 +1329,7 @@ public static IEnumerable GetUpdateResourceData() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-original", + Url = "http://example.org/fhir/Binary/attachment-source", }, }, }, @@ -1349,7 +1349,7 @@ public static IEnumerable GetUpdateResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, @@ -1382,7 +1382,7 @@ public static IEnumerable GetUpdateResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, @@ -1415,7 +1415,7 @@ public static IEnumerable GetUpdateResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, @@ -1460,7 +1460,7 @@ public static IEnumerable GetDeleteResourceData() // Delete a DiagnosticReport resource with one attachment. new DiagnosticReport { - Id = "original", + Id = "source", Meta = new Meta() { Tag = new List @@ -1486,7 +1486,7 @@ public static IEnumerable GetDeleteResourceData() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-original", + Url = "http://example.org/fhir/Binary/attachment-source", }, }, }, @@ -1500,7 +1500,7 @@ public static IEnumerable GetDeleteResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, @@ -1530,7 +1530,7 @@ public static IEnumerable GetDeleteResourceData() // Delete a DiagnosticReport resource with multiple attachments. new DiagnosticReport { - Id = "original", + Id = "source", Meta = new Meta() { Tag = new List @@ -1556,19 +1556,19 @@ public static IEnumerable GetDeleteResourceData() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-original", + Url = "http://example.org/fhir/Binary/attachment-source", }, new Attachment() { ContentType = "application/xhtml", Creation = "2006-12-24", - Url = "http://example.org/fhir/Binary/attachment-original1", + Url = "http://example.org/fhir/Binary/attachment-source1", }, new Attachment() { ContentType = "application/xhtml", Creation = "2006-12-24", - Url = "http://example.org/fhir/Binary/attachment-original2", + Url = "http://example.org/fhir/Binary/attachment-source2", }, }, }, @@ -1583,7 +1583,7 @@ public static IEnumerable GetDeleteResourceData() Tag = new List { new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), }, }, Content = new List @@ -1612,7 +1612,7 @@ public static IEnumerable GetDeleteResourceData() // Delete a DiagnosticReport resource without any attachment. new DiagnosticReport { - Id = "original", + Id = "source", Meta = new Meta() { Tag = new List @@ -1644,7 +1644,7 @@ public static IEnumerable GetDeleteResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, @@ -1674,7 +1674,7 @@ public static IEnumerable GetDeleteResourceData() // Delete a DiagnosticReport resource with attachments when no duplicates. new DiagnosticReport { - Id = "original", + Id = "source", Status = DiagnosticReport.DiagnosticReportStatus.Registered, Code = new CodeableConcept() { @@ -1693,19 +1693,19 @@ public static IEnumerable GetDeleteResourceData() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-original", + Url = "http://example.org/fhir/Binary/attachment-source", }, new Attachment() { ContentType = "application/xhtml", Creation = "2006-12-24", - Url = "http://example.org/fhir/Binary/attachment-original1", + Url = "http://example.org/fhir/Binary/attachment-source1", }, new Attachment() { ContentType = "application/xhtml", Creation = "2007-12-24", - Url = "http://example.org/fhir/Binary/attachment-original2", + Url = "http://example.org/fhir/Binary/attachment-source2", }, }, }, @@ -1717,7 +1717,7 @@ public static IEnumerable GetDeleteResourceData() // Delete a DiagnosticReport resource with multiple duplicates. new DiagnosticReport { - Id = "original", + Id = "source", Meta = new Meta() { Tag = new List @@ -1743,7 +1743,7 @@ public static IEnumerable GetDeleteResourceData() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-original", + Url = "http://example.org/fhir/Binary/attachment-source", }, }, }, @@ -1757,7 +1757,7 @@ public static IEnumerable GetDeleteResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, @@ -1787,7 +1787,7 @@ public static IEnumerable GetDeleteResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, @@ -1817,7 +1817,7 @@ public static IEnumerable GetDeleteResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, @@ -1847,7 +1847,7 @@ public static IEnumerable GetDeleteResourceData() // Delete a DocumentReference resource with one attachment. new DocumentReference { - Id = "original", + Id = "source", Meta = new Meta() { Tag = new List @@ -1863,7 +1863,7 @@ public static IEnumerable GetDeleteResourceData() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-original", + Url = "http://example.org/fhir/Binary/attachment-source", }, }, }, @@ -1884,7 +1884,7 @@ public static IEnumerable GetDeleteResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, @@ -1917,7 +1917,7 @@ public static IEnumerable GetDeleteResourceData() // Delete a DocumentReference resource with multiple attachment. new DocumentReference { - Id = "original", + Id = "source", Meta = new Meta() { Tag = new List @@ -1933,7 +1933,7 @@ public static IEnumerable GetDeleteResourceData() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-original", + Url = "http://example.org/fhir/Binary/attachment-source", }, }, new DocumentReference.ContentComponent() @@ -1942,7 +1942,7 @@ public static IEnumerable GetDeleteResourceData() { ContentType = "application/xhtml", Creation = "2006-12-24", - Url = "http://example.org/fhir/Binary/attachment-original1", + Url = "http://example.org/fhir/Binary/attachment-source1", }, }, new DocumentReference.ContentComponent() @@ -1951,7 +1951,7 @@ public static IEnumerable GetDeleteResourceData() { ContentType = "application/xhtml", Creation = "2007-12-24", - Url = "http://example.org/fhir/Binary/attachment-original2", + Url = "http://example.org/fhir/Binary/attachment-source2", }, }, }, @@ -1972,7 +1972,7 @@ public static IEnumerable GetDeleteResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, @@ -2005,7 +2005,7 @@ public static IEnumerable GetDeleteResourceData() // Delete a DocumentReference resource without any attachment. new DocumentReference { - Id = "original", + Id = "source", Meta = new Meta() { Tag = new List @@ -2031,7 +2031,7 @@ public static IEnumerable GetDeleteResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, @@ -2064,7 +2064,7 @@ public static IEnumerable GetDeleteResourceData() // Delete a DocumentReference resource with attachments when no duplicates. new DocumentReference { - Id = "original", + Id = "source", Content = new List { new DocumentReference.ContentComponent() @@ -2073,7 +2073,7 @@ public static IEnumerable GetDeleteResourceData() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-original", + Url = "http://example.org/fhir/Binary/attachment-source", }, }, new DocumentReference.ContentComponent() @@ -2082,7 +2082,7 @@ public static IEnumerable GetDeleteResourceData() { ContentType = "application/xhtml", Creation = "2006-12-24", - Url = "http://example.org/fhir/Binary/attachment-original1", + Url = "http://example.org/fhir/Binary/attachment-source1", }, }, new DocumentReference.ContentComponent() @@ -2091,7 +2091,7 @@ public static IEnumerable GetDeleteResourceData() { ContentType = "application/xhtml", Creation = "2007-12-24", - Url = "http://example.org/fhir/Binary/attachment-original2", + Url = "http://example.org/fhir/Binary/attachment-source2", }, }, }, @@ -2110,7 +2110,7 @@ public static IEnumerable GetDeleteResourceData() // Delete a DocumentReference resource with multiple duplicates. new DocumentReference { - Id = "original", + Id = "source", Meta = new Meta() { Tag = new List @@ -2126,7 +2126,7 @@ public static IEnumerable GetDeleteResourceData() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-original", + Url = "http://example.org/fhir/Binary/attachment-source", }, }, }, @@ -2147,7 +2147,7 @@ public static IEnumerable GetDeleteResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, @@ -2180,7 +2180,7 @@ public static IEnumerable GetDeleteResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, @@ -2213,7 +2213,7 @@ public static IEnumerable GetDeleteResourceData() { Tag = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "original"), + new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/DuplicateClinicalReferenceBehaviorTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/DuplicateClinicalReferenceBehaviorTests.cs index 47e61f1346..97fb7fed20 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/DuplicateClinicalReferenceBehaviorTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/DuplicateClinicalReferenceBehaviorTests.cs @@ -41,8 +41,7 @@ public class DuplicateClinicalReferenceBehaviorTests public DuplicateClinicalReferenceBehaviorTests() { _duplicator = Substitute.For(); - _duplicator.CheckDuplicate(Arg.Any()).Returns(true); - _duplicator.ShouldDuplicate(Arg.Any()).Returns(true); + _duplicator.IsDuplicatableResourceType(Arg.Any()).Returns(true); _logger = Substitute.For>(); _coreFeatureConfiguration = new CoreFeatureConfiguration @@ -78,27 +77,24 @@ public async Task GivenCreateRequest_WhenDuplicateClinicalReferenceIsEnabled_The _coreFeatureConfiguration.EnableClinicalReferenceDuplication = enabled; var behavior = new DuplicateClinicalReferenceBehavior( Options.Create(_coreFeatureConfiguration), - _duplicator, - _logger); + _duplicator); await behavior.Handle( request, async (ct) => await Task.Run(() => response), CancellationToken.None); - _duplicator.Received(enabled ? 1 : 0).ShouldDuplicate(Arg.Any()); + _duplicator.Received(enabled ? 1 : 0).IsDuplicatableResourceType(Arg.Any()); await _duplicator.Received(enabled ? 1 : 0).CreateResourceAsync( - Arg.Any(), + Arg.Any(), Arg.Any()); } [Theory] - [InlineData(true, true)] - [InlineData(true, false)] - [InlineData(false, false)] + [InlineData(true)] + [InlineData(false)] public async Task GivenUpsertRequest_WhenDuplicateClinicalReferenceIsEnabled_ThenThenDuplicateResourceShouldBeUpdated( - bool enabled, - bool duplicateFound) + bool enabled) { var resource = new DiagnosticReport() { @@ -112,38 +108,28 @@ public async Task GivenUpsertRequest_WhenDuplicateClinicalReferenceIsEnabled_The var response = new UpsertResourceResponse( new SaveOutcome(new RawResourceElement(resourceWrapper), SaveOutcomeType.Created)); - var duplicateResources = new List(); - if (duplicateFound) - { - duplicateResources.Add(resource); - } - _duplicator.UpdateResourceAsync( - Arg.Any(), + Arg.Any(), Arg.Any()) .Returns( x => { - return Task.FromResult>(duplicateResources); + return Task.FromResult>(new List { resourceWrapper }); }); _coreFeatureConfiguration.EnableClinicalReferenceDuplication = enabled; var behavior = new DuplicateClinicalReferenceBehavior( Options.Create(_coreFeatureConfiguration), - _duplicator, - _logger); + _duplicator); await behavior.Handle( request, async (ct) => await Task.Run(() => response), CancellationToken.None); - _duplicator.Received(enabled ? 1 : 0).ShouldDuplicate(Arg.Any()); + _duplicator.Received(enabled ? 1 : 0).IsDuplicatableResourceType(Arg.Any()); await _duplicator.Received(enabled ? 1 : 0).UpdateResourceAsync( - Arg.Any(), - Arg.Any()); - await _duplicator.Received(enabled && !duplicateFound ? 1 : 0).CreateResourceAsync( - Arg.Any(), + Arg.Any(), Arg.Any()); } @@ -170,15 +156,14 @@ public async Task GivenDeleteRequest_WhenDuplicateClinicalReferenceIsEnabled_The _coreFeatureConfiguration.EnableClinicalReferenceDuplication = enabled; var behavior = new DuplicateClinicalReferenceBehavior( Options.Create(_coreFeatureConfiguration), - _duplicator, - _logger); + _duplicator); await behavior.Handle( request, async (ct) => await Task.Run(() => response), CancellationToken.None); - _duplicator.Received(enabled ? 1 : 0).CheckDuplicate(Arg.Any()); + _duplicator.Received(enabled ? 1 : 0).IsDuplicatableResourceType(Arg.Any()); await _duplicator.Received(enabled ? 1 : 0).DeleteResourceAsync( Arg.Any(), Arg.Any(), diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/ClinicalReferenceDuplicator.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/ClinicalReferenceDuplicator.cs index 2921c46edc..a2a8d1f265 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/ClinicalReferenceDuplicator.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/ClinicalReferenceDuplicator.cs @@ -48,22 +48,30 @@ public ClinicalReferenceDuplicator( _logger = logger; } - public async Task CreateResourceAsync( - Resource resource, + public async Task<(ResourceWrapper source, ResourceWrapper duplicate)> CreateResourceAsync( + RawResourceElement rawResourceElement, CancellationToken cancellationToken) { - EnsureArg.IsNotNull(resource, nameof(resource)); + EnsureArg.IsNotNull(rawResourceElement, nameof(rawResourceElement)); + EnsureArg.IsNotNull(rawResourceElement.RawResource, nameof(rawResourceElement.RawResource)); try { - var duplicateResource = await CreateDuplicateResourceInternalAsync( + var resource = ConvertToResource(rawResourceElement); + if (!ShouldDuplicate(resource)) + { + _logger.LogWarning("A resource doesn't have any attachment with clinical reference."); + return (default, default); + } + + var duplicateResourceWrapper = await CreateDuplicateResourceInternalAsync( resource, cancellationToken); - await UpdateResourceInternalAsync( + var sourceResourceWrapper = await UpdateResourceInternalAsync( resource, - duplicateResource, + duplicateResourceWrapper, cancellationToken); - return duplicateResource; + return (sourceResourceWrapper, duplicateResourceWrapper); } catch (Exception ex) { @@ -85,31 +93,31 @@ public async Task> DeleteResourceAsync( GetDuplicateResourceType(resourceKey.ResourceType), resourceKey.Id, cancellationToken); - var duplicateResources = duplicateResourceWrappers - .Select(x => x.RawResource?.ToITypedElement(ModelInfoProvider.Instance)?.ToResourceElement()?.ToPoco()) - .Where(x => x != null) - .ToList(); - _logger.LogInformation("Deleting {Count} duplicate resources...", duplicateResources.Count); - if (duplicateResources.Count > 1) - { - _logger.LogWarning("More than one duplicate resource found."); - } + _logger.LogInformation("Deleting {Count} duplicate resources...", duplicateResourceWrappers.Count); var duplicateResourceKeysDeleted = new List(); - foreach (var duplicate in duplicateResources) + if (duplicateResourceWrappers.Any()) { - try + if (duplicateResourceWrappers.Count > 1) { - var key = await DeleteDuplicateResourceInternalAsync( - duplicate, - deleteOperation, - cancellationToken); - duplicateResourceKeysDeleted.Add(key); + _logger.LogWarning("More than one duplicate resource found."); } - catch (Exception ex) + + foreach (var wrapper in duplicateResourceWrappers) { - _logger.LogError(ex, "Failed to delete a duplicate resource: {Id}...", duplicate.Id); - throw; + try + { + var key = await DeleteDuplicateResourceInternalAsync( + wrapper, + deleteOperation, + cancellationToken); + duplicateResourceKeysDeleted.Add(key); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to delete a duplicate resource: {Id}...", wrapper.ResourceId); + throw; + } } } @@ -171,57 +179,68 @@ public async Task> SearchResourceAsync( } } - public async Task> UpdateResourceAsync( - Resource resource, + public async Task> UpdateResourceAsync( + RawResourceElement rawResourceElement, CancellationToken cancellationToken) { - EnsureArg.IsNotNull(resource, nameof(resource)); + EnsureArg.IsNotNull(rawResourceElement, nameof(rawResourceElement)); + EnsureArg.IsNotNull(rawResourceElement.RawResource, nameof(rawResourceElement.RawResource)); try { - var duplicateResourceWrappers = await SearchResourceAsync( - GetDuplicateResourceType(resource.TypeName), - resource.Id, - cancellationToken); - var duplicateResources = duplicateResourceWrappers - .Select(x => x.RawResource?.ToITypedElement(ModelInfoProvider.Instance)?.ToResourceElement()?.ToPoco()) - .Where(x => x != null) - .ToList(); - _logger.LogInformation("Updating {Count} duplicate resources...", duplicateResources.Count); - if (duplicateResources.Any()) + var resource = ConvertToResource(rawResourceElement); + if (ShouldDuplicate(resource)) { - if (duplicateResources.Count > 1) - { - _logger.LogWarning("More than one duplicate resource found."); - } + var duplicateResourceWrappers = await SearchResourceAsync( + GetDuplicateResourceType(rawResourceElement.InstanceType), + rawResourceElement.Id, + cancellationToken); + _logger.LogInformation("Updating {Count} duplicate resources...", duplicateResourceWrappers.Count); - var duplicateResourcesUpdated = new List(); - foreach (var duplicate in duplicateResources) + if (duplicateResourceWrappers.Any()) { - try + if (duplicateResourceWrappers.Count > 1) { - var duplicateUpdated = await UpdateDuplicateResourceInternalAsync( - resource, - duplicate, - cancellationToken); - duplicateResourcesUpdated.Add(duplicateUpdated); + _logger.LogWarning("More than one duplicate resource found."); } - catch (Exception ex) + + var duplicateResourcesUpdated = new List(); + foreach (var wrapper in duplicateResourceWrappers) { - _logger.LogError(ex, "Failed to update a duplicate resource: {Id}...", duplicate.Id); - throw; + try + { + var duplicate = ConvertToResource(wrapper.RawResource); + var duplicateUpdated = await UpdateDuplicateResourceInternalAsync( + resource, + duplicate, + cancellationToken); + duplicateResourcesUpdated.Add(duplicateUpdated); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update a duplicate resource: {Id}...", wrapper.ResourceId); + throw; + } } + + return duplicateResourcesUpdated; } - return duplicateResourcesUpdated; + _logger.LogWarning("No duplicate resources found."); + (var sourceWrapper, var duplicateWrapper) = await CreateResourceAsync( + rawResourceElement, + cancellationToken); + return new List { duplicateWrapper }; + } + else + { + _logger.LogWarning("A resource doesn't have any attachment with clinical reference."); + await DeleteResourceAsync( + new ResourceKey(resource.TypeName, resource.Id), + DeleteOperation.HardDelete, + cancellationToken); + return new List(); } - - _logger.LogWarning("No duplicate resource found."); - var duplicateResourceCreated = await CreateResourceAsync( - resource, - cancellationToken); - - return new List { duplicateResourceCreated }; } catch (Exception ex) { @@ -230,36 +249,13 @@ public async Task> UpdateResourceAsync( } } - public bool CheckDuplicate(ResourceKey resourceKey) - { - return string.Equals(resourceKey?.ResourceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase) - || string.Equals(resourceKey?.ResourceType, KnownResourceTypes.DocumentReference, StringComparison.OrdinalIgnoreCase); - } - - public bool ShouldDuplicate(Resource resource) + public bool IsDuplicatableResourceType(string resourceType) { - if (resource == null) - { - return false; - } - - if (resource is DiagnosticReport) - { - // TODO: need to be more selective about whether the resource should be duplicated based on urls. - // (https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#clinical-notes-1) - return ((DiagnosticReport)resource).PresentedForm?.Any(x => x.Url != null) ?? false; - } - else if (resource is DocumentReference) - { - // TODO: need to be more selective about whether the resource should be duplicated based on urls. - // (https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#clinical-notes-1) - return ((DocumentReference)resource).Content?.Any(x => x.Attachment?.Url != null) ?? false; - } - - return false; + return string.Equals(resourceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase) + || string.Equals(resourceType, KnownResourceTypes.DocumentReference, StringComparison.OrdinalIgnoreCase); } - private async Task CreateDuplicateResourceInternalAsync( + private async Task CreateDuplicateResourceInternalAsync( Resource resource, CancellationToken cancellationToken) { @@ -289,28 +285,12 @@ private async Task CreateDuplicateResourceInternalAsync( false, null), cancellationToken); - Resource resourceCreated = null; - if (outcome?.Wrapper?.RawResource != null) - { - _logger.LogInformation( - "A '{DuplicateResourceType}' resource {Outcome}.", - duplicateResource.TypeName, - outcome.OutcomeType.ToString().ToLowerInvariant()); - - resourceCreated = outcome.Wrapper?.RawResource? - .ToITypedElement(ModelInfoProvider.Instance)? - .ToResourceElement()? - .ToPoco(); - } - else - { - // TODO: throws an exception here. - _logger.LogError( - "Failed to create a '{DuplicateResourceType}' resource.", - duplicateResource.TypeName); - } - return resourceCreated; + _logger.LogInformation( + "A '{DuplicateResourceType}' resource {Outcome}.", + duplicateResource.TypeName, + outcome.OutcomeType.ToString().ToLowerInvariant()); + return outcome.Wrapper; } catch (Exception ex) { @@ -320,33 +300,28 @@ private async Task CreateDuplicateResourceInternalAsync( } private async Task DeleteDuplicateResourceInternalAsync( - Resource duplicateResource, + ResourceWrapper duplicateResourceWrapper, DeleteOperation deleteOperation, CancellationToken cancellationToken) { - EnsureArg.IsNotNull(duplicateResource, nameof(duplicateResource)); + EnsureArg.IsNotNull(duplicateResourceWrapper, nameof(duplicateResourceWrapper)); try { _logger.LogInformation( "Deleting a duplicate resource '{DuplicateResourceType}': {Id}...", - duplicateResource.TypeName, - duplicateResource.Id); + duplicateResourceWrapper.ResourceTypeName, + duplicateResourceWrapper.ResourceId); if (deleteOperation == DeleteOperation.HardDelete) { await _dataStore.HardDeleteAsync( - new ResourceKey(duplicateResource.TypeName, duplicateResource.Id), + new ResourceKey(duplicateResourceWrapper.ResourceTypeName, duplicateResourceWrapper.ResourceId), false, false, cancellationToken); } else { - var duplicateResourceWrapper = _resourceWrapperFactory.CreateResourceWrapper( - duplicateResource, - _resourceIdProvider, - true, - false); await _dataStore.UpsertAsync( new ResourceWrapperOperation( duplicateResourceWrapper, @@ -359,7 +334,7 @@ await _dataStore.UpsertAsync( cancellationToken); } - return new ResourceKey(duplicateResource.TypeName, duplicateResource.Id); + return new ResourceKey(duplicateResourceWrapper.ResourceTypeName, duplicateResourceWrapper.ResourceId); } catch (Exception ex) { @@ -368,7 +343,7 @@ await _dataStore.UpsertAsync( } } - private async Task UpdateDuplicateResourceInternalAsync( + private async Task UpdateDuplicateResourceInternalAsync( Resource resource, Resource duplicateResource, CancellationToken cancellationToken) @@ -399,29 +374,12 @@ private async Task UpdateDuplicateResourceInternalAsync( false, null), cancellationToken); - Resource resourceUpdated = null; - if (outcome?.Wrapper?.RawResource != null) - { - _logger.LogInformation( - "A '{DuplicateResourceType}' resource {Outcome}.", - duplicateResource.TypeName, - outcome.OutcomeType.ToString().ToLowerInvariant()); - - resourceUpdated = outcome.Wrapper?.RawResource? - .ToITypedElement(ModelInfoProvider.Instance)? - .ToResourceElement()? - .ToPoco(); - } - else - { - // TODO: throws an exception here. - _logger.LogError( - "Failed to update a '{DuplicateResourceType}' resource: {Id}.", - duplicateResource.TypeName, - duplicateResource.Id); - } - return resourceUpdated; + _logger.LogInformation( + "A '{DuplicateResourceType}' resource {Outcome}.", + duplicateResource.TypeName, + outcome.OutcomeType.ToString().ToLowerInvariant()); + return outcome.Wrapper; } catch (Exception ex) { @@ -430,13 +388,13 @@ private async Task UpdateDuplicateResourceInternalAsync( } } - private async Task UpdateResourceInternalAsync( + private async Task UpdateResourceInternalAsync( Resource resource, - Resource duplicateResource, + ResourceWrapper duplicateResourceWrapper, CancellationToken cancellationToken) { EnsureArg.IsNotNull(resource, nameof(resource)); - EnsureArg.IsNotNull(duplicateResource, nameof(duplicateResource)); + EnsureArg.IsNotNull(duplicateResourceWrapper, nameof(duplicateResourceWrapper)); try { @@ -452,7 +410,7 @@ private async Task UpdateResourceInternalAsync( resource.Meta.Tag.Where(x => !string.Equals(x.System, TagDuplicateOf, StringComparison.OrdinalIgnoreCase))); } - tags.Add(new Coding(TagDuplicateOf, duplicateResource.Id)); + tags.Add(new Coding(TagDuplicateOf, duplicateResourceWrapper.ResourceId)); resource.Meta.Tag = tags; var resourceWrapper = _resourceWrapperFactory.Create( @@ -463,7 +421,7 @@ private async Task UpdateResourceInternalAsync( _logger.LogInformation( "Updating a '{ResourceType}' resource with a duplicate resource '{Id}'...", resource.TypeName, - duplicateResource.Id); + duplicateResourceWrapper.ResourceId); var outcome = await _dataStore.UpsertAsync( new ResourceWrapperOperation( resourceWrapper, @@ -474,29 +432,12 @@ private async Task UpdateResourceInternalAsync( false, null), cancellationToken); - Resource resourceUpdated = null; - if (outcome?.Wrapper?.RawResource != null) - { - _logger.LogInformation( - "A '{ResourceType}' resource {Outcome}.", - resource.TypeName, - outcome.OutcomeType.ToString().ToLowerInvariant()); - - resourceUpdated = outcome.Wrapper?.RawResource? - .ToITypedElement(ModelInfoProvider.Instance)? - .ToResourceElement()? - .ToPoco(); - } - else - { - // TODO: throws an exception here. - _logger.LogError( - "Failed to update a '{ResourceType}' resource: {Id}.", - resource.TypeName, - resource.Id); - } - return resourceUpdated; + _logger.LogInformation( + "A '{ResourceType}' resource {Outcome}.", + resource.TypeName, + outcome.OutcomeType.ToString().ToLowerInvariant()); + return outcome.Wrapper; } catch (Exception ex) { @@ -587,6 +528,23 @@ private static Resource CreateDuplicateResource(Resource resource) return diagnosticReport; } + private static Resource ConvertToResource(RawResource rawResource) + { + EnsureArg.IsNotNull(rawResource, nameof(rawResource)); + + return rawResource + .ToITypedElement(ModelInfoProvider.Instance) + .ToResourceElement() + .ToPoco(); + } + + private static Resource ConvertToResource(RawResourceElement rawResourceElement) + { + EnsureArg.IsNotNull(rawResourceElement, nameof(rawResourceElement)); + + return ConvertToResource(rawResourceElement.RawResource); + } + private static string GetDuplicateResourceId(Resource resource) { EnsureArg.IsNotNull(resource, nameof(resource)); @@ -602,6 +560,34 @@ private static string GetDuplicateResourceType(string resourceType) ? KnownResourceTypes.DocumentReference : KnownResourceTypes.DiagnosticReport; } + private static bool ShouldDuplicate(Resource resource) + { + if (resource == null) + { + return false; + } + + if (resource.Meta?.Tag?.Any(x => string.Equals(x.System, TagDuplicateOf, StringComparison.OrdinalIgnoreCase)) ?? false) + { + return true; + } + + if (resource is DiagnosticReport) + { + // TODO: need to be more selective about whether the resource should be duplicated based on urls. + // (https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#clinical-notes-1) + return ((DiagnosticReport)resource).PresentedForm?.Any(x => x.Url != null) ?? false; + } + else if (resource is DocumentReference) + { + // TODO: need to be more selective about whether the resource should be duplicated based on urls. + // (https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#clinical-notes-1) + return ((DocumentReference)resource).Content?.Any(x => x.Attachment?.Url != null) ?? false; + } + + return false; + } + private static void UpdateDuplicateResource( Resource resource, Resource duplicateResource) diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/DuplicateClinicalReferenceBehavior.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/DuplicateClinicalReferenceBehavior.cs index 1bbbb02b6c..b19b20c564 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/DuplicateClinicalReferenceBehavior.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/DuplicateClinicalReferenceBehavior.cs @@ -3,16 +3,14 @@ // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // ------------------------------------------------------------------------------------------------- -using System.Linq; using System.Threading; using System.Threading.Tasks; using EnsureThat; using MediatR; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Health.Fhir.Core.Configs; -using Microsoft.Health.Fhir.Core.Extensions; using Microsoft.Health.Fhir.Core.Features.Guidance; +using Microsoft.Health.Fhir.Core.Features.Persistence; using Microsoft.Health.Fhir.Core.Messages.Create; using Microsoft.Health.Fhir.Core.Messages.Delete; using Microsoft.Health.Fhir.Core.Messages.Upsert; @@ -26,20 +24,16 @@ public class DuplicateClinicalReferenceBehavior : IPipelineBehavior _logger; public DuplicateClinicalReferenceBehavior( IOptions coreFeatureConfiguration, - IClinicalReferenceDuplicator clinicalReferenceDuplicator, - ILogger logger) + IClinicalReferenceDuplicator clinicalReferenceDuplicator) { EnsureArg.IsNotNull(coreFeatureConfiguration?.Value, nameof(coreFeatureConfiguration)); EnsureArg.IsNotNull(clinicalReferenceDuplicator, nameof(clinicalReferenceDuplicator)); - EnsureArg.IsNotNull(logger, nameof(logger)); _coreFeatureConfiguration = coreFeatureConfiguration.Value; _clinicalReferenceDuplicator = clinicalReferenceDuplicator; - _logger = logger; } public async Task Handle( @@ -51,15 +45,19 @@ public async Task Handle( EnsureArg.IsNotNull(next, nameof(next)); var response = await next(cancellationToken); - var resource = response?.Outcome?.RawResourceElement?.RawResource? - .ToITypedElement(ModelInfoProvider.Instance)? - .ToResourceElement()? - .ToPoco(); - if (_coreFeatureConfiguration.EnableClinicalReferenceDuplication && _clinicalReferenceDuplicator.ShouldDuplicate(resource)) + if (_coreFeatureConfiguration.EnableClinicalReferenceDuplication + && response?.Outcome?.RawResourceElement?.InstanceType != null + && response?.Outcome?.RawResourceElement?.RawResource != null + && _clinicalReferenceDuplicator.IsDuplicatableResourceType(response.Outcome.RawResourceElement.InstanceType)) { - await _clinicalReferenceDuplicator.CreateResourceAsync( - resource, + (var source, var duplicate) = await _clinicalReferenceDuplicator.CreateResourceAsync( + response.Outcome.RawResourceElement, cancellationToken); + if (source != null) + { + return new UpsertResourceResponse( + new SaveOutcome(new RawResourceElement(source), response.Outcome.Outcome)); + } } return response; @@ -74,23 +72,13 @@ public async Task Handle( EnsureArg.IsNotNull(next, nameof(next)); var response = await next(cancellationToken); - var resource = response?.Outcome?.RawResourceElement?.RawResource? - .ToITypedElement(ModelInfoProvider.Instance)? - .ToResourceElement()? - .ToPoco(); - if (_coreFeatureConfiguration.EnableClinicalReferenceDuplication && _clinicalReferenceDuplicator.ShouldDuplicate(resource)) + if (_coreFeatureConfiguration.EnableClinicalReferenceDuplication + && response?.Outcome?.RawResourceElement?.InstanceType != null + && response?.Outcome?.RawResourceElement?.RawResource != null + && _clinicalReferenceDuplicator.IsDuplicatableResourceType(response.Outcome.RawResourceElement.InstanceType)) { - var duplicateResources = await _clinicalReferenceDuplicator.UpdateResourceAsync( - resource, - cancellationToken); - if (duplicateResources.Any()) - { - return response; - } - - _logger.LogWarning("No duplicate resource found."); - await _clinicalReferenceDuplicator.CreateResourceAsync( - resource, + await _clinicalReferenceDuplicator.UpdateResourceAsync( + response.Outcome.RawResourceElement, cancellationToken); } @@ -106,11 +94,13 @@ public async Task Handle( EnsureArg.IsNotNull(next, nameof(next)); var response = await next(cancellationToken); - var resourceKey = request?.ResourceKey; - if (_coreFeatureConfiguration.EnableClinicalReferenceDuplication && _clinicalReferenceDuplicator.CheckDuplicate(resourceKey)) + if (_coreFeatureConfiguration.EnableClinicalReferenceDuplication + && response?.ResourceKey?.Id != null + && response?.ResourceKey?.ResourceType != null + && _clinicalReferenceDuplicator.IsDuplicatableResourceType(response.ResourceKey.ResourceType)) { await _clinicalReferenceDuplicator.DeleteResourceAsync( - resourceKey, + response.ResourceKey, request.DeleteOperation, cancellationToken); } diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/IClinicalReferenceDuplicator.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/IClinicalReferenceDuplicator.cs index f8bd2306dd..6c2cdf6881 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/IClinicalReferenceDuplicator.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/IClinicalReferenceDuplicator.cs @@ -6,17 +6,16 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Hl7.Fhir.Model; using Microsoft.Health.Fhir.Core.Features.Persistence; -using Microsoft.Health.Fhir.Core.Features.Search; using Microsoft.Health.Fhir.Core.Messages.Delete; +using Microsoft.Health.Fhir.Core.Models; namespace Microsoft.Health.Fhir.Core.Features.Guidance { public interface IClinicalReferenceDuplicator { - Task CreateResourceAsync( - Resource resource, + Task<(ResourceWrapper source, ResourceWrapper duplicate)> CreateResourceAsync( + RawResourceElement rawResourceElement, CancellationToken cancellationToken); Task> DeleteResourceAsync( @@ -29,12 +28,10 @@ Task> SearchResourceAsync( string resourceId, CancellationToken cancellationToken); - Task> UpdateResourceAsync( - Resource resource, + Task> UpdateResourceAsync( + RawResourceElement rawResourceElement, CancellationToken cancellationToken); - bool CheckDuplicate(ResourceKey resourceKey); - - bool ShouldDuplicate(Resource resource); + bool IsDuplicatableResourceType(string resourceType); } } diff --git a/src/Microsoft.Health.Fhir.Shared.Web/appsettings.json b/src/Microsoft.Health.Fhir.Shared.Web/appsettings.json index 36f3258875..3ad7d43f89 100644 --- a/src/Microsoft.Health.Fhir.Shared.Web/appsettings.json +++ b/src/Microsoft.Health.Fhir.Shared.Web/appsettings.json @@ -32,6 +32,7 @@ "SupportsSqlReplicas": false, "SupportsIncludes": true, "EnableGeoRedundancy": false, + "EnableClinicalReferenceDuplication": true, "Versioning": { "Default": "versioned", "ResourceTypeOverrides": null diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/Guidance/ClinicalReferenceDuplicatorTests.cs b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/Guidance/ClinicalReferenceDuplicatorTests.cs index dd3a0c4e01..13bbbb42ae 100644 --- a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/Guidance/ClinicalReferenceDuplicatorTests.cs +++ b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/Guidance/ClinicalReferenceDuplicatorTests.cs @@ -7,6 +7,8 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Text; +using System.Threading.Tasks; using DotLiquid.Util; using EnsureThat; using Hl7.Fhir.Model; @@ -43,21 +45,39 @@ public ClinicalReferenceDuplicatorTests(HttpIntegrationTestFixture fixture) public async Task GivenResource_WhenCreating_ThenDuplicateResourceShouldBeCreated( Resource resource) { - var supportsClinicalReferenceDuplicate = _fixture.TestFhirServer.Metadata.SupportsOperation( - OperationsConstants.ClinicalReferenceDuplicate); await CreateResourceAsync(resource); } - private async Task CreateResourceAsync(Resource resource) + [Theory] + [MemberData(nameof(GetCreateResourceData))] + public async Task GivenResource_WhenDeleting_ThenDuplicateResourceShouldBeDeleted( + Resource resource) + { + (var source, var duplicate) = await CreateResourceAsync(resource); + await DeleteResourceAsync(source); + } + + [Theory] + [MemberData(nameof(GetUpdateResourceData))] + public async Task GivenResource_WhenUpdating_ThenDuplicateResourceShouldBeUpdated( + Resource resource, + string subject, + List attachments) + { + (var source, var duplicate) = await CreateResourceAsync(resource); + await UpdateResourceAsync(source, subject, attachments); + } + + private async Task<(Resource source, Resource duplicate)> CreateResourceAsync(Resource resource) { var resourceType = resource.TypeName; - var duplicateResourceType = string.Equals(resourceType, KnownResourceTypes.DiagnosticReport) + var duplicateResourceType = string.Equals(resourceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase) ? KnownResourceTypes.DocumentReference : KnownResourceTypes.DiagnosticReport; // Create a resource. TagResource(resource); var response = await Client.CreateAsync( - resourceType, + resource.TypeName, resource); Assert.Equal(HttpStatusCode.Created, response.Response.StatusCode); Assert.NotNull(response.Resource); @@ -66,51 +86,125 @@ private async Task CreateResourceAsync(Resource resource) var searchResponse = await Client.SearchAsync( $"{duplicateResourceType}?_tag={ClinicalReferenceDuplicator.TagDuplicateOf}|{response.Resource?.Id}"); Assert.NotNull(searchResponse.Resource?.Entry); - Assert.Single(searchResponse.Resource.Entry); // Check if a duplicate resource has the subject and attachments that match ones from a resource created. - if (string.Equals(duplicateResourceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase)) + var source = response.Resource; + var duplicate = searchResponse.Resource?.Entry?.FirstOrDefault()?.Resource; + if (duplicate != null) { - var original = (DocumentReference)resource; - var duplicate = (DiagnosticReport)searchResponse.Resource.Entry[0].Resource; + ValidateResourceProperties(source, duplicate); + } - Assert.Equal(original.Subject?.Reference, duplicate.Subject?.Reference); - Assert.NotNull(duplicate.PresentedForm); + return (source, duplicate); + } - if (original.Content?.Any(x => !string.IsNullOrEmpty(x.Attachment?.Url)) ?? false) - { - foreach (var a in original.Content.Select(x => x.Attachment)) - { - Assert.Contains( - duplicate.PresentedForm, - x => string.Equals(x.Url, a.Url, StringComparison.OrdinalIgnoreCase)); - } - } - else + private async Task DeleteResourceAsync(Resource resource) + { + // Delete a resource. + var response = await Client.DeleteAsync(resource); + Assert.Equal(HttpStatusCode.NoContent, response.Response.StatusCode); + + // Look for a duplicate resource. + var searchResponse = await Client.SearchAsync( + $"{resource.TypeName}?_tag={ClinicalReferenceDuplicator.TagDuplicateOf}|{resource.Id}"); + Assert.Equal(0, searchResponse.Resource?.Entry?.Count ?? 0); + } + + private async Task<(Resource source, Resource duplicate)> UpdateResourceAsync( + Resource resource, + string subject, + List attachments) + { + var resourceType = resource.TypeName; + var duplicateResourceType = KnownResourceTypes.DocumentReference; + + if (string.Equals(resourceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase)) + { + var diagnosticReport = (DiagnosticReport)resource; + diagnosticReport.Subject = new ResourceReference(subject); + diagnosticReport.PresentedForm = attachments; + } + else + { + var documentReference = (DocumentReference)resource; + documentReference.Subject = new ResourceReference(subject); + + var contents = new List(); + foreach (var attachment in attachments) { - Assert.Equal(0, duplicate.PresentedForm.Count(x => !string.IsNullOrEmpty(x.Url))); + contents.Add( + new DocumentReference.ContentComponent() + { + Attachment = attachment, + }); } + + documentReference.Content = contents; + duplicateResourceType = KnownResourceTypes.DiagnosticReport; } - else + + // Update a resource. + var response = await Client.UpdateAsync(resource); + Assert.Equal(HttpStatusCode.OK, response.Response.StatusCode); + Assert.NotNull(response.Resource); + + // Look for a duplicate resource. + var searchResponse = await Client.SearchAsync( + $"{duplicateResourceType}?_tag={ClinicalReferenceDuplicator.TagDuplicateOf}|{response.Resource?.Id}"); + Assert.NotNull(searchResponse.Resource?.Entry); + + // Check if a duplicate resource has the subject and attachments that match ones from a resource created. + var source = response.Resource; + var duplicate = searchResponse.Resource?.Entry?.FirstOrDefault()?.Resource; + if (duplicate != null) { - var original = (DiagnosticReport)resource; - var duplicate = (DocumentReference)searchResponse.Resource.Entry[0].Resource; + ValidateResourceProperties(source, duplicate); + } - Assert.Equal(original.Subject?.Reference, duplicate.Subject?.Reference); - Assert.NotNull(duplicate.Content); + return (source, duplicate); + } + + private static void ValidateResourceProperties(Resource source, Resource duplicate) + { + EnsureArg.IsNotNull(source, nameof(source)); + EnsureArg.IsNotNull(duplicate, nameof(duplicate)); - if (original.PresentedForm?.Any(x => !string.IsNullOrEmpty(x.Url)) ?? false) + if (string.Equals(source.TypeName, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase)) + { + var diagnosticReport = (DiagnosticReport)source; + var documentReference = (DocumentReference)duplicate; + Assert.Equal(diagnosticReport.Subject?.Reference, documentReference.Subject?.Reference); + Assert.Equal( + diagnosticReport.PresentedForm?.Count(x => !string.IsNullOrEmpty(x.Url)), + documentReference.Content?.Count(x => !string.IsNullOrEmpty(x.Attachment?.Url))); + + if (diagnosticReport.PresentedForm?.Any(x => !string.IsNullOrEmpty(x.Url)) ?? false) { - foreach (var a in original.PresentedForm) + foreach (var a in diagnosticReport.PresentedForm.Where(x => !string.IsNullOrEmpty(x.Url))) { Assert.Contains( - duplicate.Content, + documentReference.Content, x => string.Equals(x.Attachment?.Url, a.Url, StringComparison.OrdinalIgnoreCase)); } } - else + } + else + { + var documentReference = (DocumentReference)source; + var diagnosticReport = (DiagnosticReport)duplicate; + Assert.Equal(documentReference.Subject?.Reference, diagnosticReport.Subject?.Reference); + Assert.Equal( + documentReference.Content?.Count(x => !string.IsNullOrEmpty(x.Attachment?.Url)), + diagnosticReport.PresentedForm?.Count(x => !string.IsNullOrEmpty(x.Url))); + + if (documentReference.Content?.Any(x => !string.IsNullOrEmpty(x.Attachment?.Url)) ?? false) { - Assert.Equal(0, duplicate.Content.Count(x => !string.IsNullOrEmpty(x.Attachment?.Url))); + foreach (var a in documentReference.Content.Where(x => !string.IsNullOrEmpty(x?.Attachment?.Url)).Select(x => x.Attachment)) + { + Assert.Contains( + diagnosticReport.PresentedForm, + x => string.Equals(x?.Url, a.Url, StringComparison.OrdinalIgnoreCase)); + } } } } @@ -252,11 +346,11 @@ public static IEnumerable GetCreateResourceData() }, }, Subject = new ResourceReference(Guid.NewGuid().ToString()), - #if R4 || R4B || Stu3 +#if R4 || R4B || Stu3 Status = DocumentReferenceStatus.Current, - #else +#else Status = DocumentReference.DocumentReferenceStatus.Current, - #endif +#endif }, }, new object[] @@ -305,11 +399,11 @@ public static IEnumerable GetCreateResourceData() }, }, Subject = new ResourceReference(Guid.NewGuid().ToString()), - #if R4 || R4B || Stu3 +#if R4 || R4B || Stu3 Status = DocumentReferenceStatus.Current, - #else +#else Status = DocumentReference.DocumentReferenceStatus.Current, - #endif +#endif }, }, new object[] @@ -318,12 +412,370 @@ public static IEnumerable GetCreateResourceData() new DocumentReference { Id = Guid.NewGuid().ToString(), + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + Data = Encoding.UTF8.GetBytes(Convert.ToBase64String(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()))), + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + }, + }; + + foreach (var d in data) + { + yield return d; + } + } + + public static IEnumerable GetUpdateResourceData() + { + var data = new[] + { + new object[] + { + // Create a new DiagnosticReport resource with one attachment. Update with multiple attachments. + new DiagnosticReport + { + Id = Guid.NewGuid().ToString(), + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, + }, + }, + Guid.NewGuid().ToString(), + new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment1", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/Binary/attachment2", + }, + }, + }, + new object[] + { + // Create a new DiagnosticReport resource with multiple attachments. Update with one attachment. + new DiagnosticReport + { + Id = Guid.NewGuid().ToString(), + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment1", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/Binary/attachment2", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2008-12-24", + Url = "http://example.org/fhir/Binary/attachment3", + }, + }, + }, + Guid.NewGuid().ToString(), + new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2009-12-24", + Url = "http://example.org/fhir/Binary/attachment4", + }, + }, + }, + new object[] + { + // Create a new DiagnosticReport resource with multiple attachments. Update without any attachment. + new DiagnosticReport + { + Id = Guid.NewGuid().ToString(), + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment1", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/Binary/attachment2", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment3", + }, + }, + }, + Guid.NewGuid().ToString(), + new List(), + }, + new object[] + { + // Create a new DocumentReference resource with one attachment. Update with multiple attachments. + new DocumentReference + { + Id = Guid.NewGuid().ToString(), + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + Guid.NewGuid().ToString(), + new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment1", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/Binary/attachment2", + }, + }, + }, + new object[] + { + // Create a new DocumentReference resource with multiple attachments. Update with one attachment. + new DocumentReference + { + Id = Guid.NewGuid().ToString(), + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment1", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/Binary/attachment2", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2008-12-24", + Url = "http://example.org/fhir/Binary/attachment3", + }, + }, + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + Guid.NewGuid().ToString(), + new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2009-12-24", + Url = "http://example.org/fhir/Binary/attachment4", + }, + }, + }, + new object[] + { + // Create a new DocumentReference resource with multiple attachments. Update without any attachment. + new DocumentReference + { + Id = Guid.NewGuid().ToString(), + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment1", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/Binary/attachment2", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment3", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + Data = Encoding.UTF8.GetBytes(Convert.ToBase64String(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()))), + }, + }, + }, Subject = new ResourceReference(Guid.NewGuid().ToString()), - #if R4 || R4B || Stu3 +#if R4 || R4B || Stu3 Status = DocumentReferenceStatus.Current, - #else +#else Status = DocumentReference.DocumentReferenceStatus.Current, - #endif +#endif + }, + Guid.NewGuid().ToString(), + new List() + { + new Attachment() + { + Data = Encoding.UTF8.GetBytes(Convert.ToBase64String(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()))), + }, }, }, }; From 9b514ac8832bc8d08ccd79e748025d3004c692bc Mon Sep 17 00:00:00 2001 From: Isao Yamauchi Date: Mon, 25 Aug 2025 09:46:13 -0700 Subject: [PATCH 7/7] Updating the implementation and tests. --- .../ClinicalReferenceDuplicatorTests.cs | 3925 ++++++++++++----- .../Guidance/ClinicalReferenceDuplicator.cs | 705 +-- .../ClinicalReferenceDuplicatorHelper.cs | 111 + .../DuplicateClinicalReferenceBehavior.cs | 9 +- .../Guidance/IClinicalReferenceDuplicator.cs | 7 +- ...icrosoft.Health.Fhir.Shared.Core.projitems | 1 + .../ClinicalReferenceDuplicatorTests.cs | 6 +- 7 files changed, 3348 insertions(+), 1416 deletions(-) create mode 100644 src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/ClinicalReferenceDuplicatorHelper.cs diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/ClinicalReferenceDuplicatorTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/ClinicalReferenceDuplicatorTests.cs index 7c62a0ba46..738d4eb9dc 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/ClinicalReferenceDuplicatorTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Guidance/ClinicalReferenceDuplicatorTests.cs @@ -8,17 +8,15 @@ using System.Linq; using System.Net.Http; using System.Threading; +using EnsureThat; using Hl7.Fhir.Model; using Hl7.Fhir.Serialization; -using MediatR; using Microsoft.Extensions.Logging; using Microsoft.Health.Fhir.Core.Extensions; using Microsoft.Health.Fhir.Core.Features.Guidance; using Microsoft.Health.Fhir.Core.Features.Persistence; using Microsoft.Health.Fhir.Core.Features.Search; -using Microsoft.Health.Fhir.Core.Messages.Create; using Microsoft.Health.Fhir.Core.Messages.Delete; -using Microsoft.Health.Fhir.Core.Messages.Upsert; using Microsoft.Health.Fhir.Core.Models; using Microsoft.Health.Fhir.Tests.Common; using Microsoft.Health.Test.Utilities; @@ -62,11 +60,42 @@ public ClinicalReferenceDuplicatorTests() [Theory] [MemberData(nameof(GetCreateResourceData))] public async Task GivenResource_WhenCreating_ThenDuplicateResourceShouldBeCreated( - Resource resource) + Resource resource, + Resource duplicateResource, + int searchCalls, + int upsertCalls) { var resourceType = resource.TypeName; var duplicateResourceType = string.Equals(resourceType, KnownResourceTypes.DiagnosticReport) ? KnownResourceTypes.DocumentReference : KnownResourceTypes.DiagnosticReport; + var codes = new List(); + var subject = string.Empty; + if (string.Equals(resourceType, KnownResourceTypes.DiagnosticReport)) + { + var diagnosticReport = (DiagnosticReport)resource; + subject = diagnosticReport.Subject?.Reference; + codes.AddRange(diagnosticReport.Code.Coding + .Where(x => ClinicalReferenceDuplicator.ClinicalReferenceSystems.Contains(x.System) + && ClinicalReferenceDuplicator.ClinicalReferenceCodes.Contains(x.Code))); + } + else + { + var documentReference = (DocumentReference)resource; + subject = documentReference.Subject?.Reference; +#if R4 || R4B || Stu3 + codes.AddRange(documentReference.Content + .Where(x => ClinicalReferenceDuplicator.ClinicalReferenceSystems.Contains(x.Format?.System) + && ClinicalReferenceDuplicator.ClinicalReferenceCodes.Contains(x.Format?.Code)) + .Select(x => x.Format)); +#else + codes.AddRange(documentReference.Content + .SelectMany(x => x?.Profile? + .Where(y => y?.Value?.GetType() == typeof(Coding) + && ClinicalReferenceDuplicator.ClinicalReferenceSystems.Contains(((Coding)y.Value)?.System) + && ClinicalReferenceDuplicator.ClinicalReferenceCodes.Contains(((Coding)y.Value)?.Code)) + .Select(y => (Coding)y.Value))); +#endif + } // Set up a validation on a request for creating the source resource. var resourceElement = resource.ToResourceElement(); @@ -75,110 +104,107 @@ public async Task GivenResource_WhenCreating_ThenDuplicateResourceShouldBeCreate Arg.Any()) .Throws(new Exception($"Shouldn't be called to create a {resourceType} resource.")); - // Set up a validation on a request for creating a duplicate resource. - Resource duplicateResource = null; - _dataStore.UpsertAsync( - Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), + // Set up a search result (no results for create). + _searchService.SearchAsync( + Arg.Is(x => string.Equals(x, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any>>(), Arg.Any()) .Returns( x => { - var re = ((ResourceWrapperOperation)x[0])?.Wrapper?.RawResource? - .ToITypedElement(ModelInfoProvider.Instance)? - .ToResourceElement(); - Assert.NotNull(re); - Assert.Equal(duplicateResourceType, re.InstanceType, true); + Assert.True(searchCalls > 0, "SearchAsync shouldn't be called."); - var r = re.ToPoco(); - Assert.NotNull(r.Meta?.Tag); - Assert.Contains( - r.Meta.Tag, - x => string.Equals(x.System, ClinicalReferenceDuplicator.TagDuplicateOf, StringComparison.OrdinalIgnoreCase) - && string.Equals(x.Code, resource.Id, StringComparison.OrdinalIgnoreCase)); + var parameters = (IReadOnlyList>)x[1]; Assert.Contains( - r.Meta.Tag, - x => string.Equals(x.System, ClinicalReferenceDuplicator.TagIsDuplicate, StringComparison.OrdinalIgnoreCase) - && string.Equals(x.Code, bool.TrueString, StringComparison.OrdinalIgnoreCase)); + parameters, + x => string.Equals(x.Item1, "subject", StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Item2, subject, StringComparison.OrdinalIgnoreCase)); if (string.Equals(duplicateResourceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase)) { - var source = (DocumentReference)resource; - var duplicate = (DiagnosticReport)r; - - Assert.Equal(source.Subject?.Reference, duplicate.Subject?.Reference); - Assert.NotNull(duplicate.PresentedForm); - foreach (var a in source.Content.Select(x => x.Attachment)) - { - Assert.Contains( - duplicate.PresentedForm, - x => string.Equals(x.Url, a.Url, StringComparison.OrdinalIgnoreCase)); - } - - duplicateResource = duplicate; + Assert.Contains( + parameters, + x => string.Equals(x.Item1, "code", StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Item2, string.Join(",", codes.Select(x => ClinicalReferenceDuplicatorHelper.ConvertToString(x))), StringComparison.OrdinalIgnoreCase)); } else { - var source = (DiagnosticReport)resource; - var duplicate = (DocumentReference)r; - - Assert.Equal(source.Subject?.Reference, duplicate.Subject?.Reference); - Assert.NotNull(duplicate.Content); - foreach (var a in source.PresentedForm) - { - Assert.Contains( - duplicate.Content, - x => string.Equals(x.Attachment?.Url, a.Url, StringComparison.OrdinalIgnoreCase)); - } - - duplicateResource = duplicate; - } - - if (string.IsNullOrEmpty(r.Id)) - { - r.Id = Guid.NewGuid().ToString(); +#if R4 || R4B || Stu3 + Assert.Contains( + parameters, + x => string.Equals(x.Item1, "format", StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Item2, string.Join(",", codes.Select(x => ClinicalReferenceDuplicatorHelper.ConvertToString(x))), StringComparison.OrdinalIgnoreCase)); +#else + Assert.Contains( + parameters, + x => string.Equals(x.Item1, "format-code", StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Item2, string.Join(",", codes.Select(x => ClinicalReferenceDuplicatorHelper.ConvertToString(x))), StringComparison.OrdinalIgnoreCase)); +#endif } - return Task.FromResult( - new UpsertOutcome( - CreateResourceWrapper(r.ToResourceElement()), - SaveOutcomeType.Created)); + var searchResult = new SearchResult( + new List(), + null, + null, + new List>()); + return Task.FromResult(searchResult); }); - // Set up a validation on a request for updating the source resource with the id of the duplicate resource. + // Set up a validation on a request for creating a duplicate resource. _dataStore.UpsertAsync( - Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, resourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), Arg.Any()) .Returns( x => { + Assert.True(upsertCalls > 0, "UpsertAsync shouldn't be called."); + var re = ((ResourceWrapperOperation)x[0])?.Wrapper?.RawResource? .ToITypedElement(ModelInfoProvider.Instance)? .ToResourceElement(); Assert.NotNull(re); - Assert.Equal(resourceType, re.InstanceType, true); + Assert.Equal(duplicateResourceType, re.InstanceType, true); var r = re.ToPoco(); Assert.NotNull(r.Meta?.Tag); Assert.Contains( r.Meta.Tag, - t => string.Equals(t.System, ClinicalReferenceDuplicator.TagDuplicateOf, StringComparison.OrdinalIgnoreCase) - && string.Equals(t.Code, duplicateResource.Id, StringComparison.OrdinalIgnoreCase)); + x => string.Equals(x.System, ClinicalReferenceDuplicator.TagDuplicateOf, StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Code, resource.Id, StringComparison.OrdinalIgnoreCase)); + Assert.Contains( + r.Meta.Tag, + x => string.Equals(x.System, ClinicalReferenceDuplicator.TagDuplicateCreatedOn, StringComparison.OrdinalIgnoreCase) + && DateTime.TryParse(x.Code, out _)); + + ValidateDuplicateResource(duplicateResource, r, true); + if (string.IsNullOrEmpty(r.Id)) + { + r.Id = Guid.NewGuid().ToString(); + } return Task.FromResult( new UpsertOutcome( CreateResourceWrapper(r.ToResourceElement()), - SaveOutcomeType.Updated)); + SaveOutcomeType.Created)); }); await _duplicator.CreateResourceAsync( new RawResourceElement(CreateResourceWrapper(resourceElement)), CancellationToken.None); - // Check how many times create/update was invoked. - await _dataStore.Received(1).UpsertAsync( + // Check how many times these methods were called. + await _searchService.Received(searchCalls).SearchAsync( + Arg.Is(x => string.Equals(x, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any>>(), + Arg.Any()); + await _searchService.Received(0).SearchAsync( + Arg.Is(x => string.Equals(x, resourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any>>(), + Arg.Any()); + await _dataStore.Received(upsertCalls).UpsertAsync( Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), Arg.Any()); - await _dataStore.Received(1).UpsertAsync( + await _dataStore.Received(0).UpsertAsync( Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, resourceType, StringComparison.OrdinalIgnoreCase)), Arg.Any()); } @@ -187,28 +213,56 @@ await _dataStore.Received(1).UpsertAsync( [MemberData(nameof(GetUpdateResourceData))] public async Task GivenResource_WhenUpdating_ThenDuplicateResourceShouldBeUpdated( Resource resource, - List duplicateResources) + List duplicateResources, + List searchResults, + int searchCalls, + int upsertCalls) { var resourceType = resource.TypeName; var duplicateResourceType = string.Equals(resourceType, KnownResourceTypes.DiagnosticReport) ? KnownResourceTypes.DocumentReference : KnownResourceTypes.DiagnosticReport; + var codes = new List(); + var subject = string.Empty; + if (string.Equals(resourceType, KnownResourceTypes.DiagnosticReport)) + { + var diagnosticReport = (DiagnosticReport)resource; + subject = diagnosticReport.Subject?.Reference; + codes.AddRange(diagnosticReport.Code.Coding + .Where(x => ClinicalReferenceDuplicator.ClinicalReferenceSystems.Contains(x.System) + && ClinicalReferenceDuplicator.ClinicalReferenceCodes.Contains(x.Code))); + } + else + { + var documentReference = (DocumentReference)resource; + subject = documentReference.Subject?.Reference; +#if R4 || R4B || Stu3 + codes.AddRange(documentReference.Content + .Where(x => ClinicalReferenceDuplicator.ClinicalReferenceSystems.Contains(x.Format?.System) + && ClinicalReferenceDuplicator.ClinicalReferenceCodes.Contains(x.Format?.Code)) + .Select(x => x.Format)); +#else + codes.AddRange(documentReference.Content + .SelectMany(x => x?.Profile? + .Where(y => y?.Value?.GetType() == typeof(Coding) + && ClinicalReferenceDuplicator.ClinicalReferenceSystems.Contains(((Coding)y.Value)?.System) + && ClinicalReferenceDuplicator.ClinicalReferenceCodes.Contains(((Coding)y.Value)?.Code)) + .Select(y => (Coding)y.Value))); +#endif + } // Set up a validation on a request for searching duplicate resources. var entries = new List(); - if (duplicateResources?.Any() ?? false) + foreach (var r in searchResults) { - foreach (var r in duplicateResources) - { - var wrapper = new ResourceWrapper( - r.ToResourceElement(), - new RawResource(r.ToJson(), FhirResourceFormat.Json, false), - null, - false, - null, - null, - null); - entries.Add(new SearchResultEntry(wrapper)); - } + var wrapper = new ResourceWrapper( + r.ToResourceElement(), + new RawResource(r.ToJson(), FhirResourceFormat.Json, false), + null, + false, + null, + null, + null); + entries.Add(new SearchResultEntry(wrapper)); } _searchService.SearchAsync( @@ -218,138 +272,105 @@ public async Task GivenResource_WhenUpdating_ThenDuplicateResourceShouldBeUpdate .Returns( x => { + Assert.True(searchCalls > 0, "SearchAsync shouldn't be called."); + var parameters = (IReadOnlyList>)x[1]; Assert.Contains( parameters, - x => string.Equals(x.Item1, "_tag", StringComparison.OrdinalIgnoreCase) - && string.Equals(x.Item2, $"{ClinicalReferenceDuplicator.TagDuplicateOf}|{resource.Id}", StringComparison.OrdinalIgnoreCase)); - - var searchResult = new SearchResult( - entries, - null, - null, - new List>()); - return Task.FromResult(searchResult); - }); - - // Set up a validation on a request for creating/updating a duplicate resource. - Resource duplicateResource = null; - _dataStore.UpsertAsync( - Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), - Arg.Any()) - .Returns( - x => - { - var re = ((ResourceWrapperOperation)x[0])?.Wrapper?.RawResource? - .ToITypedElement(ModelInfoProvider.Instance)? - .ToResourceElement(); - Assert.NotNull(re); - Assert.Equal(duplicateResourceType, re.InstanceType, true); - - var r = re.ToPoco(); - Assert.NotNull(r.Meta?.Tag); - Assert.Contains( - r.Meta.Tag, - x => string.Equals(x.System, ClinicalReferenceDuplicator.TagDuplicateOf, StringComparison.OrdinalIgnoreCase) - && string.Equals(x.Code, resource.Id, StringComparison.OrdinalIgnoreCase)); - Assert.Contains( - r.Meta.Tag, - x => string.Equals(x.System, ClinicalReferenceDuplicator.TagIsDuplicate, StringComparison.OrdinalIgnoreCase) - && string.Equals(x.Code, bool.TrueString, StringComparison.OrdinalIgnoreCase)); + x => string.Equals(x.Item1, "subject", StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Item2, subject, StringComparison.OrdinalIgnoreCase)); if (string.Equals(duplicateResourceType, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase)) { - var source = (DocumentReference)resource; - var duplicate = (DiagnosticReport)r; - - Assert.Equal(source.Subject?.Reference, duplicate.Subject?.Reference); - Assert.NotNull(duplicate.PresentedForm); - foreach (var a in source.Content.Select(x => x.Attachment)) - { - Assert.Contains( - duplicate.PresentedForm, - x => string.Equals(x.Url, a.Url, StringComparison.OrdinalIgnoreCase)); - } - - duplicateResource = duplicate; + Assert.Contains( + parameters, + x => string.Equals(x.Item1, "code", StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Item2, string.Join(",", codes.Select(x => ClinicalReferenceDuplicatorHelper.ConvertToString(x))), StringComparison.OrdinalIgnoreCase)); } else { - var source = (DiagnosticReport)resource; - var duplicate = (DocumentReference)r; - - Assert.Equal(source.Subject?.Reference, duplicate.Subject?.Reference); - Assert.NotNull(duplicate.Content); - foreach (var a in source.PresentedForm) - { - Assert.Contains( - duplicate.Content, - x => string.Equals(x.Attachment?.Url, a.Url, StringComparison.OrdinalIgnoreCase)); - } - - duplicateResource = duplicate; - } - - if (string.IsNullOrEmpty(r.Id)) - { - r.Id = Guid.NewGuid().ToString(); +#if R4 || R4B || Stu3 + Assert.Contains( + parameters, + x => string.Equals(x.Item1, "format", StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Item2, string.Join(",", codes.Select(x => ClinicalReferenceDuplicatorHelper.ConvertToString(x))), StringComparison.OrdinalIgnoreCase)); +#else + Assert.Contains( + parameters, + x => string.Equals(x.Item1, "format-code", StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Item2, string.Join(",", codes.Select(x => ClinicalReferenceDuplicatorHelper.ConvertToString(x))), StringComparison.OrdinalIgnoreCase)); +#endif } return Task.FromResult( - new UpsertOutcome( - CreateResourceWrapper(r.ToResourceElement()), - SaveOutcomeType.Created)); + new SearchResult( + entries, + null, + null, + new List>())); }); - // Set up a validation on a request for updating the source resource with the id of the duplicate resource. + // Set up a validation on a request for creating a duplicate resource. _dataStore.UpsertAsync( - Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, resourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), Arg.Any()) .Returns( x => { + Assert.True(upsertCalls > 0, "UpsertAsync shouldn't be called."); + var re = ((ResourceWrapperOperation)x[0])?.Wrapper?.RawResource? .ToITypedElement(ModelInfoProvider.Instance)? .ToResourceElement(); Assert.NotNull(re); - Assert.Equal(resourceType, re.InstanceType, true); + Assert.Equal(duplicateResourceType, re.InstanceType, true); var r = re.ToPoco(); - Assert.NotNull(r.Meta?.Tag); - Assert.Contains( - r.Meta.Tag, - t => string.Equals(t.System, ClinicalReferenceDuplicator.TagDuplicateOf, StringComparison.OrdinalIgnoreCase) - && string.Equals(t.Code, duplicateResource.Id, StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(r); + + ValidateDuplicateResource( + duplicateResources.Where(x => string.Equals(x.Id, r.Id, StringComparison.OrdinalIgnoreCase)).First(), + r); + if (string.IsNullOrEmpty(r.Id)) + { + r.Id = Guid.NewGuid().ToString(); + } return Task.FromResult( new UpsertOutcome( CreateResourceWrapper(r.ToResourceElement()), - SaveOutcomeType.Updated)); + SaveOutcomeType.Created)); }); await _duplicator.UpdateResourceAsync( new RawResourceElement(CreateResourceWrapper(resource.ToResourceElement())), CancellationToken.None); - // Check how many times create/update was invoked. - await _searchService.Received(1).SearchAsync( + // Check how many times these methods were called. + await _searchService.Received(searchCalls).SearchAsync( Arg.Is(x => string.Equals(x, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), Arg.Any>>(), Arg.Any()); - await _dataStore.Received(duplicateResources.Any() ? 0 : 1).UpsertAsync( - Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, resourceType, StringComparison.OrdinalIgnoreCase)), + await _searchService.Received(0).SearchAsync( + Arg.Is(x => string.Equals(x, resourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any>>(), Arg.Any()); - await _dataStore.Received(duplicateResources.Any() ? duplicateResources.Count : 1).UpsertAsync( + await _dataStore.Received(upsertCalls).UpsertAsync( Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), Arg.Any()); + await _dataStore.Received(0).UpsertAsync( + Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, resourceType, StringComparison.OrdinalIgnoreCase)), + Arg.Any()); } [Theory] [MemberData(nameof(GetDeleteResourceData))] public async Task GivenResource_WhenDeleting_ThenDuplicateResourceShouldBeDeleted( Resource resource, + List duplicateResources, DeleteOperation deleteOperation, - List duplicateResources) + int searchCalls, + int deleteCalls) { var resourceType = resource.TypeName; var duplicateResourceType = string.Equals(resourceType, KnownResourceTypes.DiagnosticReport) @@ -381,6 +402,8 @@ public async Task GivenResource_WhenDeleting_ThenDuplicateResourceShouldBeDelete .Returns( x => { + Assert.True(searchCalls > 0, "SearchAsync shouldn't be called."); + var parameters = (IReadOnlyList>)x[1]; Assert.Contains( parameters, @@ -402,6 +425,8 @@ public async Task GivenResource_WhenDeleting_ThenDuplicateResourceShouldBeDelete .Returns( x => { + Assert.True(deleteCalls > 0, "Upsert shouldn't be called."); + var r = ((ResourceWrapperOperation)x[0])?.Wrapper; Assert.NotNull(r); Assert.Equal(duplicateResourceType, r.ResourceTypeName, true); @@ -420,6 +445,8 @@ public async Task GivenResource_WhenDeleting_ThenDuplicateResourceShouldBeDelete .Returns( x => { + Assert.True(deleteCalls > 0, "HardDelete shouldn't be called."); + var k = (ResourceKey)x[0]; Assert.NotNull(k); Assert.Equal(duplicateResourceType, k.ResourceType, true); @@ -434,14 +461,14 @@ await _duplicator.DeleteResourceAsync( CancellationToken.None); // Check how many times create/update was invoked. - await _searchService.Received(1).SearchAsync( + await _searchService.Received(searchCalls).SearchAsync( Arg.Is(x => string.Equals(x, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), Arg.Any>>(), Arg.Any()); - await _dataStore.Received(deleteOperation == DeleteOperation.SoftDelete && duplicateResources.Any() ? duplicateResources.Count : 0).UpsertAsync( + await _dataStore.Received(deleteOperation == DeleteOperation.SoftDelete ? deleteCalls : 0).UpsertAsync( Arg.Is(x => string.Equals(x.Wrapper.ResourceTypeName, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), Arg.Any()); - await _dataStore.Received(deleteOperation == DeleteOperation.HardDelete && duplicateResources.Any() ? duplicateResources.Count : 0).HardDeleteAsync( + await _dataStore.Received(deleteOperation == DeleteOperation.HardDelete ? deleteCalls : 0).HardDeleteAsync( Arg.Is(x => string.Equals(x.ResourceType, duplicateResourceType, StringComparison.OrdinalIgnoreCase)), Arg.Any(), Arg.Any(), @@ -462,6 +489,36 @@ private ResourceWrapper CreateResourceWrapper(ResourceElement resource, bool isD 0); } + private static void ValidateDuplicateResource(Resource expected, Resource actual, bool ignoreId = false) + { + EnsureArg.IsNotNull(expected, nameof(expected)); + EnsureArg.IsNotNull(actual, nameof(actual)); + + if (!ignoreId) + { + Assert.Equal(expected.Id, actual.Id); + } + + Assert.Equal(expected.TypeName, actual.TypeName); + if (string.Equals(expected.TypeName, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase)) + { + var expectedResource = (DiagnosticReport)expected; + var actualResource = (DiagnosticReport)actual; + + Assert.Equal(expectedResource.Subject?.Reference, actualResource.Subject?.Reference); + Assert.True(ClinicalReferenceDuplicatorHelper.CompareCodings(expectedResource.Code?.Coding, actualResource.Code?.Coding)); + Assert.True(ClinicalReferenceDuplicatorHelper.CompareAttachments(expectedResource.PresentedForm, actualResource.PresentedForm)); + } + else + { + var expectedResource = (DocumentReference)expected; + var actualResource = (DocumentReference)actual; + + Assert.Equal(expectedResource.Subject?.Reference, actualResource.Subject?.Reference); + Assert.True(ClinicalReferenceDuplicatorHelper.CompareContents(expectedResource.Content, actualResource.Content)); + } + } + public static IEnumerable GetCreateResourceData() { var data = new[] @@ -479,11 +536,12 @@ public static IEnumerable GetCreateResourceData() { new Coding() { - Code = "12345", + Code = "11488-4", + System = "https://loinc.org", }, }, }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), + Subject = new ResourceReference("patient"), PresentedForm = new List { new Attachment() @@ -494,10 +552,53 @@ public static IEnumerable GetCreateResourceData() }, }, }, + new DocumentReference + { + Id = Guid.NewGuid().ToString(), + Content = new List + { + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, +#if R4 || R4B || Stu3 + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + }, +#endif + }, + }, + Subject = new ResourceReference("patient"), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + 1, + 1, }, new object[] { - // Create a new DiagnosticReport resource with multiple attachments. + // Create a new DiagnosticReport resource with multiple codes and attachments. new DiagnosticReport { Id = Guid.NewGuid().ToString(), @@ -508,11 +609,27 @@ public static IEnumerable GetCreateResourceData() { new Coding() { - Code = "12345", + Code = "12345-1", + System = "https://loinc.org", + }, + new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + new Coding() + { + Code = "12345-2", + System = "https://loinc.org", + }, + new Coding() + { + Code = "11502-2", + System = "https://loinc.org", }, }, }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), + Subject = new ResourceReference("patient"), PresentedForm = new List { new Attachment() @@ -536,40 +653,17 @@ public static IEnumerable GetCreateResourceData() new Attachment() { ContentType = "application/xhtml", - Creation = "2005-12-24", + Creation = "2008-12-24", Url = "http://example.org/fhir/Binary/attachment3", }, }, }, - }, - new object[] - { - // Create a new DiagnosticReport resource without any attachment. - new DiagnosticReport - { - Id = Guid.NewGuid().ToString(), - Status = DiagnosticReport.DiagnosticReportStatus.Registered, - Code = new CodeableConcept() - { - Coding = new List() - { - new Coding() - { - Code = "12345", - }, - }, - }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), - }, - }, - new object[] - { - // Create a new DocumentReference resource with one attachment. new DocumentReference { Id = Guid.NewGuid().ToString(), Content = new List { +#if R4 || R4B || Stu3 new DocumentReference.ContentComponent() { Attachment = new Attachment() @@ -578,24 +672,54 @@ public static IEnumerable GetCreateResourceData() Creation = "2005-12-24", Url = "http://example.org/fhir/Binary/attachment", }, + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment1", + }, + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/Binary/attachment2", + }, + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2008-12-24", + Url = "http://example.org/fhir/Binary/attachment3", + }, + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, }, - }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), - #if R4 || R4B || Stu3 - Status = DocumentReferenceStatus.Current, - #else - Status = DocumentReference.DocumentReferenceStatus.Current, - #endif - }, - }, - new object[] - { - // Create a new DocumentReference resource with multiple attachments. - new DocumentReference - { - Id = Guid.NewGuid().ToString(), - Content = new List - { new DocumentReference.ContentComponent() { Attachment = new Attachment() @@ -604,6 +728,11 @@ public static IEnumerable GetCreateResourceData() Creation = "2005-12-24", Url = "http://example.org/fhir/Binary/attachment", }, + Format = new Coding() + { + Code = "11502-2", + System = "https://loinc.org", + }, }, new DocumentReference.ContentComponent() { @@ -613,6 +742,11 @@ public static IEnumerable GetCreateResourceData() Creation = "2006-12-24", Url = "http://example.org/fhir/Binary/attachment1", }, + Format = new Coding() + { + Code = "11502-2", + System = "https://loinc.org", + }, }, new DocumentReference.ContentComponent() { @@ -622,133 +756,181 @@ public static IEnumerable GetCreateResourceData() Creation = "2007-12-24", Url = "http://example.org/fhir/Binary/attachment2", }, + Format = new Coding() + { + Code = "11502-2", + System = "https://loinc.org", + }, }, new DocumentReference.ContentComponent() { Attachment = new Attachment() { ContentType = "application/xhtml", - Creation = "2005-12-24", + Creation = "2008-12-24", Url = "http://example.org/fhir/Binary/attachment3", }, + Format = new Coding() + { + Code = "11502-2", + System = "https://loinc.org", + }, }, - }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), - #if R4 || R4B || Stu3 - Status = DocumentReferenceStatus.Current, - #else - Status = DocumentReference.DocumentReferenceStatus.Current, - #endif - }, - }, - new object[] - { - // Create a new DocumentReference resource without any attachment. - new DocumentReference - { - Id = Guid.NewGuid().ToString(), - Subject = new ResourceReference(Guid.NewGuid().ToString()), - #if R4 || R4B || Stu3 - Status = DocumentReferenceStatus.Current, - #else - Status = DocumentReference.DocumentReferenceStatus.Current, - #endif - }, - }, - }; - - foreach (var d in data) - { - yield return d; - } - } - - public static IEnumerable GetUpdateResourceData() - { - var data = new[] - { - new object[] - { - // Update a DiagnosticReport resource with one attachment. - new DiagnosticReport - { - Id = "source", - Meta = new Meta() - { - Tag = new List - { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), - }, - }, - Status = DiagnosticReport.DiagnosticReportStatus.Registered, - Code = new CodeableConcept() - { - Coding = new List() +#else + new DocumentReference.ContentComponent() { - new Coding() + Attachment = new Attachment() { - Code = "12345", + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11502-2", + System = "https://loinc.org", + }, + }, }, }, - }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), - PresentedForm = new List - { - new Attachment() + new DocumentReference.ContentComponent() { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-source", + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment1", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11502-2", + System = "https://loinc.org", + }, + }, + }, }, - }, - }, - new List - { - new DocumentReference - { - Id = "duplicate", - Meta = new Meta() + new DocumentReference.ContentComponent() { - Tag = new List + Attachment = new Attachment() { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/Binary/attachment2", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11502-2", + System = "https://loinc.org", + }, + }, }, }, - Content = new List + new DocumentReference.ContentComponent() { - new DocumentReference.ContentComponent() + Attachment = new Attachment() { - Attachment = new Attachment() + ContentType = "application/xhtml", + Creation = "2008-12-24", + Url = "http://example.org/fhir/Binary/attachment3", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate", + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11502-2", + System = "https://loinc.org", + }, }, }, }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), +#endif + }, + Subject = new ResourceReference("patient"), #if R4 || R4B || Stu3 - Status = DocumentReferenceStatus.Current, + Status = DocumentReferenceStatus.Current, #else - Status = DocumentReference.DocumentReferenceStatus.Current, + Status = DocumentReference.DocumentReferenceStatus.Current, #endif - }, }, + 1, + 1, }, new object[] { - // Update a DiagnosticReport resource with multiple attachments. + // Create a new DiagnosticReport resource without any attachment. new DiagnosticReport { - Id = "source", - Meta = new Meta() + Id = Guid.NewGuid().ToString(), + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() { - Tag = new List + Coding = new List() { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, }, }, + Subject = new ResourceReference("patient"), + }, + null, + 0, + 0, + }, + new object[] + { + // Create a new DiagnosticReport resource without clinical reference codes. + new DiagnosticReport + { + Id = Guid.NewGuid().ToString(), Status = DiagnosticReport.DiagnosticReportStatus.Registered, Code = new CodeableConcept() { @@ -756,80 +938,91 @@ public static IEnumerable GetUpdateResourceData() { new Coding() { - Code = "12345", + Code = "12345-1", + System = "https://loinc.org", }, }, }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), + Subject = new ResourceReference("patient"), PresentedForm = new List { new Attachment() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-source", + Url = "http://example.org/fhir/Binary/attachment", }, new Attachment() { ContentType = "application/xhtml", Creation = "2006-12-24", - Url = "http://example.org/fhir/Binary/attachment-source1", + Url = "http://example.org/fhir/Binary/attachment1", }, new Attachment() { ContentType = "application/xhtml", - Creation = "2006-12-24", - Url = "http://example.org/fhir/Binary/attachment-source2", + Creation = "2007-12-24", + Url = "http://example.org/fhir/Binary/attachment2", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment3", }, }, }, - new List + null, + 0, + 0, + }, + new object[] + { + // Create a new DocumentReference resource with one attachment. + new DocumentReference { - new DocumentReference + Id = Guid.NewGuid().ToString(), + Content = new List { - Id = "duplicate", - Meta = new Meta() + new DocumentReference.ContentComponent() { - Tag = new List + Attachment = new Attachment() { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", }, - }, - Content = new List - { - new DocumentReference.ContentComponent() +#if R4 || R4B || Stu3 + Format = new Coding() { - Attachment = new Attachment() + Code = "11488-4", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate", + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, }, }, +#endif }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), + }, + Subject = new ResourceReference("patient"), #if R4 || R4B || Stu3 - Status = DocumentReferenceStatus.Current, + Status = DocumentReferenceStatus.Current, #else - Status = DocumentReference.DocumentReferenceStatus.Current, + Status = DocumentReference.DocumentReferenceStatus.Current, #endif - }, }, - }, - new object[] - { - // Update a DiagnosticReport resource without any attachment. new DiagnosticReport { - Id = "source", - Meta = new Meta() - { - Tag = new List - { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), - }, - }, + Id = Guid.NewGuid().ToString(), Status = DiagnosticReport.DiagnosticReportStatus.Registered, Code = new CodeableConcept() { @@ -837,53 +1030,156 @@ public static IEnumerable GetUpdateResourceData() { new Coding() { - Code = "12345", + Code = "11488-4", + System = "https://loinc.org", }, }, }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), - PresentedForm = new List(), - }, - new List - { - new DocumentReference + Subject = new ResourceReference("patient"), + PresentedForm = new List { - Id = "duplicate", - Meta = new Meta() + new Attachment() { - Tag = new List - { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), - }, - }, - Content = new List + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, + }, + }, + 1, + 1, + }, + new object[] + { + // Create a new DocumentReference resource with multiple attachments. + new DocumentReference + { + Id = Guid.NewGuid().ToString(), + Content = new List + { + new DocumentReference.ContentComponent() { - new DocumentReference.ContentComponent() + Attachment = new Attachment() { - Attachment = new Attachment() + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, +#if R4 || R4B || Stu3 + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate", + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, }, }, +#endif }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment1", + }, #if R4 || R4B || Stu3 - Status = DocumentReferenceStatus.Current, + Format = new Coding() + { + Code = "12345-1", + System = "https://loinc.org", + }, #else - Status = DocumentReference.DocumentReferenceStatus.Current, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "12345-1", + System = "https://loinc.org", + }, + }, + }, +#endif + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/Binary/attachment2", + }, +#if R4 || R4B || Stu3 + Format = new Coding() + { + Code = "18748-4", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "18748-4", + System = "https://loinc.org", + }, + }, + }, +#endif + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment3", + }, +#if R4 || R4B || Stu3 + Format = new Coding() + { + Code = "12345-2", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "12345-2", + System = "https://loinc.org", + }, + }, + }, #endif + }, }, + Subject = new ResourceReference("patient"), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif }, - }, - new object[] - { - // Update a DiagnosticReport resource with attachments when a duplicate resource doesn't exist. new DiagnosticReport { - Id = "source", + Id = Guid.NewGuid().ToString(), Status = DiagnosticReport.DiagnosticReportStatus.Registered, Code = new CodeableConcept() { @@ -891,48 +1187,158 @@ public static IEnumerable GetUpdateResourceData() { new Coding() { - Code = "12345", + Code = "11488-4", + System = "https://loinc.org", + }, + new Coding() + { + Code = "18748-4", + System = "https://loinc.org", }, }, }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), + Subject = new ResourceReference("patient"), PresentedForm = new List { new Attachment() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-source", - }, - new Attachment() - { - ContentType = "application/xhtml", - Creation = "2006-12-24", - Url = "http://example.org/fhir/Binary/attachment-source1", + Url = "http://example.org/fhir/Binary/attachment", }, new Attachment() { ContentType = "application/xhtml", Creation = "2007-12-24", - Url = "http://example.org/fhir/Binary/attachment-source2", + Url = "http://example.org/fhir/Binary/attachment2", }, }, }, - new List(), + 1, + 1, }, new object[] { - // Update a DiagnosticReport resource with multiple duplicates. - new DiagnosticReport + // Create a new DocumentReference resource without any attachment with clinical reference. + new DocumentReference { - Id = "source", - Meta = new Meta() + Id = Guid.NewGuid().ToString(), + Content = new List { - Tag = new List + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment", + }, +#if R4 || R4B || Stu3 + Format = new Coding() + { + Code = "12345-1", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "12345-1", + System = "https://loinc.org", + }, + }, + }, +#endif + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment1", + }, +#if R4 || R4B || Stu3 + Format = new Coding() + { + Code = "12345-2", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "12345-2", + System = "https://loinc.org", + }, + }, + }, +#endif + }, + new DocumentReference.ContentComponent() { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment2", + }, +#if R4 || R4B || Stu3 + Format = new Coding() + { + Code = "12345-3", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "12345-3", + System = "https://loinc.org", + }, + }, + }, +#endif }, }, + Subject = new ResourceReference("patient"), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + null, + 0, + 0, + }, + }; + + foreach (var d in data) + { + yield return d; + } + } + + public static IEnumerable GetUpdateResourceData() + { + var data = new[] + { + new object[] + { + // Update a new DiagnosticReport resource with one attachment. + new DiagnosticReport + { + Id = Guid.NewGuid().ToString(), Status = DiagnosticReport.DiagnosticReportStatus.Registered, Code = new CodeableConcept() { @@ -940,18 +1346,19 @@ public static IEnumerable GetUpdateResourceData() { new Coding() { - Code = "12345", + Code = "11488-4", + System = "https://loinc.org", }, }, }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), + Subject = new ResourceReference("patient"), PresentedForm = new List { new Attachment() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-source", + Url = "http://example.org/fhir/source/attachment", }, }, }, @@ -960,14 +1367,6 @@ public static IEnumerable GetUpdateResourceData() new DocumentReference { Id = "duplicate", - Meta = new Meta() - { - Tag = new List - { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), - }, - }, Content = new List { new DocumentReference.ContentComponent() @@ -976,58 +1375,70 @@ public static IEnumerable GetUpdateResourceData() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate", + Url = "http://example.org/fhir/duplicate/attachment", }, - }, - }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), #if R4 || R4B || Stu3 - Status = DocumentReferenceStatus.Current, + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, #else - Status = DocumentReference.DocumentReferenceStatus.Current, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + }, #endif - }, - new DocumentReference - { - Id = "duplicate1", - Meta = new Meta() - { - Tag = new List - { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, - }, - Content = new List - { new DocumentReference.ContentComponent() { Attachment = new Attachment() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate1", + Url = "http://example.org/fhir/source/attachment", + }, +#if R4 || R4B || Stu3 + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, }, +#endif }, }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), + Subject = new ResourceReference("patient"), #if R4 || R4B || Stu3 Status = DocumentReferenceStatus.Current, #else Status = DocumentReference.DocumentReferenceStatus.Current, #endif }, + }, + new List + { new DocumentReference { - Id = "duplicate2", - Meta = new Meta() - { - Tag = new List - { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), - }, - }, + Id = "duplicate", Content = new List { new DocumentReference.ContentComponent() @@ -1036,11 +1447,30 @@ public static IEnumerable GetUpdateResourceData() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate2", + Url = "http://example.org/fhir/duplicate/attachment", }, +#if R4 || R4B || Stu3 + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + }, +#endif }, }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), + Subject = new ResourceReference("patient"), #if R4 || R4B || Stu3 Status = DocumentReferenceStatus.Current, #else @@ -1048,426 +1478,639 @@ public static IEnumerable GetUpdateResourceData() #endif }, }, + 1, + 1, }, new object[] { - // Update a DocumentReference resource with one attachment. - new DocumentReference + // Update a new DiagnosticReport resource with multiple codes and attachments. + new DiagnosticReport { - Id = "source", - Meta = new Meta() + Id = Guid.NewGuid().ToString(), + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() { - Tag = new List + Coding = new List() { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + new Coding() + { + Code = "12345-3", + System = "https://loinc.org", + }, + new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, }, }, - Content = new List + Subject = new ResourceReference("patient"), + PresentedForm = new List { - new DocumentReference.ContentComponent() + new Attachment() { - Attachment = new Attachment() - { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-source", - }, + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/source/attachment1", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", }, }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), -#if R4 || R4B || Stu3 - Status = DocumentReferenceStatus.Current, -#else - Status = DocumentReference.DocumentReferenceStatus.Current, -#endif }, new List { - new DiagnosticReport + new DocumentReference { Id = "duplicate", - Meta = new Meta() + Content = new List { - Tag = new List + new DocumentReference.ContentComponent() { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/duplicate/attachment", + }, +#if R4 || R4B || Stu3 + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + }, +#endif }, - }, - Status = DiagnosticReport.DiagnosticReportStatus.Registered, - Code = new CodeableConcept() - { - Coding = new List() +#if R4 || R4B || Stu3 + new DocumentReference.ContentComponent() { - new Coding() + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", + }, + Format = new Coding() { - Code = "12345", + Code = "11488-4", + System = "https://loinc.org", }, }, - }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), - PresentedForm = new List - { - new Attachment() + new DocumentReference.ContentComponent() { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate", + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/source/attachment1", + }, + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, }, - }, - }, - }, - }, - new object[] - { - // Update a DocumentReference resource with multiple attachment. - new DocumentReference - { - Id = "source", - Meta = new Meta() - { - Tag = new List - { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), - }, - }, - Content = new List - { - new DocumentReference.ContentComponent() - { - Attachment = new Attachment() + new DocumentReference.ContentComponent() { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-source", + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", + }, + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, }, - }, - new DocumentReference.ContentComponent() - { - Attachment = new Attachment() + new DocumentReference.ContentComponent() { - ContentType = "application/xhtml", - Creation = "2006-12-24", - Url = "http://example.org/fhir/Binary/attachment-source1", + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", + }, + Format = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, }, - }, - new DocumentReference.ContentComponent() - { - Attachment = new Attachment() + new DocumentReference.ContentComponent() { - ContentType = "application/xhtml", - Creation = "2007-12-24", - Url = "http://example.org/fhir/Binary/attachment-source2", + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/source/attachment1", + }, + Format = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", + }, + Format = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, +#else + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/source/attachment1", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, + }, }, +#endif }, - }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), + Subject = new ResourceReference("patient"), #if R4 || R4B || Stu3 - Status = DocumentReferenceStatus.Current, + Status = DocumentReferenceStatus.Current, #else - Status = DocumentReference.DocumentReferenceStatus.Current, + Status = DocumentReference.DocumentReferenceStatus.Current, #endif + }, }, new List { - new DiagnosticReport + new DocumentReference { Id = "duplicate", - Meta = new Meta() - { - Tag = new List - { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), - }, - }, - Status = DiagnosticReport.DiagnosticReportStatus.Registered, - Code = new CodeableConcept() + Content = new List { - Coding = new List() + new DocumentReference.ContentComponent() { - new Coding() + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/duplicate/attachment", + }, +#if R4 || R4B || Stu3 + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, +#else + Profile = new List { - Code = "12345", + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, }, +#endif }, }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), - PresentedForm = new List + Subject = new ResourceReference("patient"), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + }, + 1, + 1, + }, + new object[] + { + // Update a new DiagnosticReport resource without any attachment. + new DiagnosticReport + { + Id = Guid.NewGuid().ToString(), + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() { - new Attachment() + new Coding() { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate", + Code = "11488-4", + System = "https://loinc.org", }, }, }, + Subject = new ResourceReference("patient"), }, + new List(), + new List(), + 0, + 0, }, new object[] { - // Update a DocumentReference resource without any attachment. - new DocumentReference + // Update a new DiagnosticReport resource without any clinical reference code. + new DiagnosticReport { - Id = "source", - Meta = new Meta() + Id = Guid.NewGuid().ToString(), + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() { - Tag = new List + Coding = new List() { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + new Coding() + { + Code = "12345-1", + System = "https://loinc.org", + }, + new Coding() + { + Code = "12345-2", + System = "https://loinc.org", + }, + new Coding() + { + Code = "12345-3", + System = "https://loinc.org", + }, }, }, - Content = new List(), - Subject = new ResourceReference(Guid.NewGuid().ToString()), -#if R4 || R4B || Stu3 - Status = DocumentReferenceStatus.Current, -#else - Status = DocumentReference.DocumentReferenceStatus.Current, -#endif - }, - new List - { - new DiagnosticReport + Subject = new ResourceReference("patient"), + PresentedForm = new List { - Id = "duplicate", - Meta = new Meta() - { - Tag = new List - { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), - }, - }, - Status = DiagnosticReport.DiagnosticReportStatus.Registered, - Code = new CodeableConcept() - { - Coding = new List() - { - new Coding() - { - Code = "12345", - }, - }, - }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), - PresentedForm = new List + new Attachment() { - new Attachment() - { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate", - }, + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", }, }, }, + new List(), + new List(), + 0, + 0, }, new object[] { - // Update a DocumentReference resource with attachments when a duplicate resource doesn't exist. - new DocumentReference + // Update a new DiagnosticReport resource with attachments already existing. + new DiagnosticReport { - Id = "source", - Content = new List + Id = Guid.NewGuid().ToString(), + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() { - new DocumentReference.ContentComponent() + Coding = new List() { - Attachment = new Attachment() + new Coding() { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-source", + Code = "11488-4", + System = "https://loinc.org", }, - }, - new DocumentReference.ContentComponent() - { - Attachment = new Attachment() + new Coding() { - ContentType = "application/xhtml", - Creation = "2006-12-24", - Url = "http://example.org/fhir/Binary/attachment-source1", + Code = "12345-3", + System = "https://loinc.org", }, - }, - new DocumentReference.ContentComponent() - { - Attachment = new Attachment() + new Coding() { - ContentType = "application/xhtml", - Creation = "2007-12-24", - Url = "http://example.org/fhir/Binary/attachment-source2", + Code = "34117-2", + System = "https://loinc.org", }, }, }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), -#if R4 || R4B || Stu3 - Status = DocumentReferenceStatus.Current, -#else - Status = DocumentReference.DocumentReferenceStatus.Current, -#endif - }, - new List(), - }, - new object[] - { - // Update a DocumentReference resource with multiple duplicates. - new DocumentReference - { - Id = "source", - Meta = new Meta() + Subject = new ResourceReference("patient"), + PresentedForm = new List { - Tag = new List + new Attachment() { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", }, - }, - Content = new List - { - new DocumentReference.ContentComponent() + new Attachment() { - Attachment = new Attachment() - { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-source", - }, + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/source/attachment1", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", }, }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), -#if R4 || R4B || Stu3 - Status = DocumentReferenceStatus.Current, -#else - Status = DocumentReference.DocumentReferenceStatus.Current, -#endif }, + new List(), new List { - new DiagnosticReport + new DocumentReference { - Id = "duplicate", - Meta = new Meta() + Id = Guid.NewGuid().ToString(), + Content = new List { - Tag = new List +#if R4 || R4B || Stu3 + new DocumentReference.ContentComponent() { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", + }, + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, }, - }, - Status = DiagnosticReport.DiagnosticReportStatus.Registered, - Code = new CodeableConcept() - { - Coding = new List() + new DocumentReference.ContentComponent() { - new Coding() + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/source/attachment1", + }, + Format = new Coding() { - Code = "12345", + Code = "11488-4", + System = "https://loinc.org", }, }, - }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), - PresentedForm = new List - { - new Attachment() + new DocumentReference.ContentComponent() { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate", + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", + }, + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, }, - }, - }, - new DiagnosticReport - { - Id = "duplicate1", - Meta = new Meta() - { - Tag = new List + new DocumentReference.ContentComponent() { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", + }, + Format = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, }, - }, - Status = DiagnosticReport.DiagnosticReportStatus.Registered, - Code = new CodeableConcept() - { - Coding = new List() + new DocumentReference.ContentComponent() { - new Coding() + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/source/attachment1", + }, + Format = new Coding() { - Code = "12345", + Code = "34117-2", + System = "https://loinc.org", }, }, - }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), - PresentedForm = new List - { - new Attachment() + new DocumentReference.ContentComponent() { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate1", + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", + }, + Format = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, }, - }, - }, - new DiagnosticReport - { - Id = "duplicate2", - Meta = new Meta() - { - Tag = new List +#else + new DocumentReference.ContentComponent() { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, + }, }, - }, - Status = DiagnosticReport.DiagnosticReportStatus.Registered, - Code = new CodeableConcept() - { - Coding = new List() + new DocumentReference.ContentComponent() { - new Coding() + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/source/attachment1", + }, + Profile = new List { - Code = "12345", + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, }, }, - }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), - PresentedForm = new List - { - new Attachment() + new DocumentReference.ContentComponent() { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate2", + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, + }, }, +#endif }, + Subject = new ResourceReference("patient"), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif }, }, + 1, + 0, }, - }; - - foreach (var d in data) - { - yield return d; - } - } - - public static IEnumerable GetDeleteResourceData() - { - var data = new[] - { new object[] { - // Delete a DiagnosticReport resource with one attachment. + // Update a new DiagnosticReport resource with multiple duplicate resources found. new DiagnosticReport { - Id = "source", - Meta = new Meta() - { - Tag = new List - { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), - }, - }, + Id = Guid.NewGuid().ToString(), Status = DiagnosticReport.DiagnosticReportStatus.Registered, Code = new CodeableConcept() { @@ -1475,48 +2118,476 @@ public static IEnumerable GetDeleteResourceData() { new Coding() { - Code = "12345", + Code = "11488-4", + System = "https://loinc.org", + }, + new Coding() + { + Code = "12345-3", + System = "https://loinc.org", + }, + new Coding() + { + Code = "34117-2", + System = "https://loinc.org", }, }, }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), + Subject = new ResourceReference("patient"), PresentedForm = new List { new Attachment() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-source", + Url = "http://example.org/fhir/source/attachment", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/source/attachment1", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", }, }, }, - DeleteOperation.SoftDelete, new List { new DocumentReference { Id = "duplicate", - Meta = new Meta() + Content = new List { - Tag = new List +#if R4 || R4B || Stu3 + new DocumentReference.ContentComponent() { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", + }, + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/source/attachment1", + }, + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", + }, + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2008-12-24", + Url = "http://example.org/fhir/duplicate/attachment", + }, + Format = new Coding() + { + Code = "54321-0", + System = "https://loinc.org", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", + }, + Format = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/source/attachment1", + }, + Format = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", + }, + Format = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, +#else + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2008-12-24", + Url = "http://example.org/fhir/dupliate/attachment", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "54321-0", + System = "https://loinc.org", + }, + }, + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, + }, }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/source/attachment1", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, + }, + }, +#endif }, + Subject = new ResourceReference("patient"), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + new DocumentReference + { + Id = "duplicate1", Content = new List { +#if R4 || R4B || Stu3 new DocumentReference.ContentComponent() { Attachment = new Attachment() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate", + Url = "http://example.org/fhir/source/attachment", + }, + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/source/attachment1", + }, + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", + }, + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2008-12-24", + Url = "http://example.org/fhir/duplicate1/attachment", + }, + Format = new Coding() + { + Code = "54321-1", + System = "https://loinc.org", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", + }, + Format = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/source/attachment1", + }, + Format = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", + }, + Format = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, +#else + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2008-12-24", + Url = "http://example.org/fhir/dupliate1/attachment", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "54321-1", + System = "https://loinc.org", + }, + }, + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/source/attachment1", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, }, }, +#endif }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), + Subject = new ResourceReference("patient"), #if R4 || R4B || Stu3 Status = DocumentReferenceStatus.Current, #else @@ -1524,347 +2595,748 @@ public static IEnumerable GetDeleteResourceData() #endif }, }, - }, - new object[] - { - // Delete a DiagnosticReport resource with multiple attachments. - new DiagnosticReport + new List { - Id = "source", - Meta = new Meta() + new DocumentReference { - Tag = new List + Id = "duplicate", + Content = new List { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), +#if R4 || R4B || Stu3 + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2008-12-24", + Url = "http://example.org/fhir/duplicate/attachment", + }, + Format = new Coding() + { + Code = "54321-0", + System = "https://loinc.org", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", + }, + Format = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/source/attachment1", + }, + Format = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", + }, + Format = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, +#else + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2008-12-24", + Url = "http://example.org/fhir/dupliate/attachment", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "54321-0", + System = "https://loinc.org", + }, + }, + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/source/attachment1", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "34117-2", + System = "https://loinc.org", + }, + }, + }, + }, +#endif }, + Subject = new ResourceReference("patient"), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif }, - Status = DiagnosticReport.DiagnosticReportStatus.Registered, - Code = new CodeableConcept() + new DocumentReference { - Coding = new List() + Id = "duplicate1", + Content = new List { - new Coding() +#if R4 || R4B || Stu3 + new DocumentReference.ContentComponent() { - Code = "12345", + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", + }, + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2008-12-24", + Url = "http://example.org/fhir/duplicate1/attachment", + }, + Format = new Coding() + { + Code = "54321-1", + System = "https://loinc.org", + }, + }, +#else + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + }, + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2008-12-24", + Url = "http://example.org/fhir/dupliate1/attachment", + }, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "54321-1", + System = "https://loinc.org", + }, + }, + }, }, +#endif }, + Subject = new ResourceReference("patient"), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), - PresentedForm = new List + }, + 1, + 2, + }, + new object[] + { + // Update a new DocumentReference resource with one attachment. + new DocumentReference + { + Id = Guid.NewGuid().ToString(), + Content = new List { - new Attachment() + new DocumentReference.ContentComponent() { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-source", + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", + }, +#if R4 || R4B || Stu3 + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + }, +#endif }, - new Attachment() + }, + Subject = new ResourceReference("patient"), +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + new List + { + new DiagnosticReport + { + Id = "duplicate", + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() { - ContentType = "application/xhtml", - Creation = "2006-12-24", - Url = "http://example.org/fhir/Binary/attachment-source1", + Coding = new List() + { + new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, }, - new Attachment() + Subject = new ResourceReference("patient"), + PresentedForm = new List { - ContentType = "application/xhtml", - Creation = "2006-12-24", - Url = "http://example.org/fhir/Binary/attachment-source2", + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/duplicate/attachment", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", + }, }, }, }, - DeleteOperation.SoftDelete, new List { - new DocumentReference + new DiagnosticReport { Id = "duplicate", - Meta = new Meta() + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() { - Tag = new List + Coding = new List() { - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), + new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, }, }, - Content = new List + Subject = new ResourceReference("patient"), + PresentedForm = new List { - new DocumentReference.ContentComponent() + new Attachment() { - Attachment = new Attachment() - { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate", - }, + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/duplicate/attachment", }, }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), -#if R4 || R4B || Stu3 - Status = DocumentReferenceStatus.Current, -#else - Status = DocumentReference.DocumentReferenceStatus.Current, -#endif }, }, + 1, + 1, }, new object[] { - // Delete a DiagnosticReport resource without any attachment. - new DiagnosticReport + // Update a new DocumentReference resource with multiple codes and attachments. + new DocumentReference { - Id = "source", - Meta = new Meta() + Id = Guid.NewGuid().ToString(), + Content = new List { - Tag = new List + new DocumentReference.ContentComponent() { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", + }, +#if R4 || R4B || Stu3 + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + }, +#endif }, - }, - Status = DiagnosticReport.DiagnosticReportStatus.Registered, - Code = new CodeableConcept() - { - Coding = new List() + new DocumentReference.ContentComponent() { - new Coding() + Attachment = new Attachment() { - Code = "12345", + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/source/attachment1", + }, +#if R4 || R4B || Stu3 + Format = new Coding() + { + Code = "12345-1", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "12345-1", + System = "https://loinc.org", + }, + }, }, +#endif }, - }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), - PresentedForm = new List(), - }, - DeleteOperation.SoftDelete, - new List - { - new DocumentReference - { - Id = "duplicate", - Meta = new Meta() + new DocumentReference.ContentComponent() { - Tag = new List + Attachment = new Attachment() { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", + }, +#if R4 || R4B || Stu3 + Format = new Coding() + { + Code = "18748-4", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "18748-4", + System = "https://loinc.org", + }, + }, }, +#endif }, - Content = new List + new DocumentReference.ContentComponent() { - new DocumentReference.ContentComponent() + Attachment = new Attachment() { - Attachment = new Attachment() + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment3", + }, +#if R4 || R4B || Stu3 + Format = new Coding() + { + Code = "12345-2", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate", + Value = new Coding() + { + Code = "12345-2", + System = "https://loinc.org", + }, }, }, +#endif }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), + }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), #if R4 || R4B || Stu3 - Status = DocumentReferenceStatus.Current, + Status = DocumentReferenceStatus.Current, #else - Status = DocumentReference.DocumentReferenceStatus.Current, + Status = DocumentReference.DocumentReferenceStatus.Current, #endif - }, }, - }, - new object[] - { - // Delete a DiagnosticReport resource with attachments when no duplicates. - new DiagnosticReport + new List { - Id = "source", - Status = DiagnosticReport.DiagnosticReportStatus.Registered, - Code = new CodeableConcept() + new DiagnosticReport { - Coding = new List() + Id = "duplicate", + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() { - new Coding() + Coding = new List() { - Code = "12345", + new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + new Coding() + { + Code = "12345-3", + System = "https://loinc.org", + }, + new Coding() + { + Code = "18748-4", + System = "https://loinc.org", + }, }, }, - }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), - PresentedForm = new List - { - new Attachment() - { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-source", - }, - new Attachment() - { - ContentType = "application/xhtml", - Creation = "2006-12-24", - Url = "http://example.org/fhir/Binary/attachment-source1", - }, - new Attachment() + Subject = new ResourceReference("patient"), + PresentedForm = new List { - ContentType = "application/xhtml", - Creation = "2007-12-24", - Url = "http://example.org/fhir/Binary/attachment-source2", + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/duplicate/attachment1", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", + }, }, }, }, - DeleteOperation.SoftDelete, - new List(), - }, - new object[] - { - // Delete a DiagnosticReport resource with multiple duplicates. - new DiagnosticReport + new List { - Id = "source", - Meta = new Meta() - { - Tag = new List - { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), - }, - }, - Status = DiagnosticReport.DiagnosticReportStatus.Registered, - Code = new CodeableConcept() + new DiagnosticReport { - Coding = new List() + Id = "duplicate", + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() { - new Coding() + Coding = new List() { - Code = "12345", + new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + new Coding() + { + Code = "12345-3", + System = "https://loinc.org", + }, + new Coding() + { + Code = "18748-4", + System = "https://loinc.org", + }, }, }, - }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), - PresentedForm = new List - { - new Attachment() + Subject = new ResourceReference("patient"), + PresentedForm = new List { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-source", + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/duplicate/attachment1", + }, }, }, }, - DeleteOperation.SoftDelete, - new List + 1, + 1, + }, + new object[] + { + // Update a new DocumentReference resource without any clinical reference code. + new DocumentReference { - new DocumentReference + Id = Guid.NewGuid().ToString(), + Content = new List { - Id = "duplicate", - Meta = new Meta() + new DocumentReference.ContentComponent() { - Tag = new List + Attachment = new Attachment() { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", }, - }, - Content = new List - { - new DocumentReference.ContentComponent() +#if R4 || R4B || Stu3 + Format = new Coding() + { + Code = "54321-0", + System = "https://loinc.org", + }, +#else + Profile = new List { - Attachment = new Attachment() + new DocumentReference.ProfileComponent() { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate", + Value = new Coding() + { + Code = "54321-0", + System = "https://loinc.org", + }, }, }, +#endif }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), + }, + Subject = new ResourceReference("patient"), #if R4 || R4B || Stu3 - Status = DocumentReferenceStatus.Current, + Status = DocumentReferenceStatus.Current, #else - Status = DocumentReference.DocumentReferenceStatus.Current, + Status = DocumentReference.DocumentReferenceStatus.Current, #endif - }, - new DocumentReference + }, + new List(), + new List(), + 0, + 0, + }, + new object[] + { + // Update a new DocumentReference resource with codes and attachments already existing. + new DocumentReference + { + Id = Guid.NewGuid().ToString(), + Content = new List { - Id = "duplicate1", - Meta = new Meta() + new DocumentReference.ContentComponent() { - Tag = new List + Attachment = new Attachment() { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", }, - }, - Content = new List - { - new DocumentReference.ContentComponent() +#if R4 || R4B || Stu3 + Format = new Coding() { - Attachment = new Attachment() + Code = "11488-4", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate", + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, }, }, - }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), -#if R4 || R4B || Stu3 - Status = DocumentReferenceStatus.Current, -#else - Status = DocumentReference.DocumentReferenceStatus.Current, #endif - }, - new DocumentReference - { - Id = "duplicate2", - Meta = new Meta() + }, + new DocumentReference.ContentComponent() { - Tag = new List + Attachment = new Attachment() { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/source/attachment1", }, - }, - Content = new List - { - new DocumentReference.ContentComponent() +#if R4 || R4B || Stu3 + Format = new Coding() { - Attachment = new Attachment() + Code = "12345-1", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate", + Value = new Coding() + { + Code = "12345-1", + System = "https://loinc.org", + }, }, }, +#endif }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", + }, #if R4 || R4B || Stu3 - Status = DocumentReferenceStatus.Current, + Format = new Coding() + { + Code = "18748-4", + System = "https://loinc.org", + }, #else - Status = DocumentReference.DocumentReferenceStatus.Current, + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "18748-4", + System = "https://loinc.org", + }, + }, + }, #endif - }, - }, - }, - new object[] - { - // Delete a DocumentReference resource with one attachment. - new DocumentReference - { - Id = "source", - Meta = new Meta() - { - Tag = new List - { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), }, - }, - Content = new List - { new DocumentReference.ContentComponent() { Attachment = new Attachment() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-source", + Url = "http://example.org/fhir/source/attachment3", + }, +#if R4 || R4B || Stu3 + Format = new Coding() + { + Code = "12345-2", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "12345-2", + System = "https://loinc.org", + }, + }, }, +#endif }, }, Subject = new ResourceReference(Guid.NewGuid().ToString()), @@ -1874,20 +3346,12 @@ public static IEnumerable GetDeleteResourceData() Status = DocumentReference.DocumentReferenceStatus.Current, #endif }, - DeleteOperation.HardDelete, + new List(), new List { new DiagnosticReport { Id = "duplicate", - Meta = new Meta() - { - Tag = new List - { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), - }, - }, Status = DiagnosticReport.DiagnosticReportStatus.Registered, Code = new CodeableConcept() { @@ -1895,36 +3359,54 @@ public static IEnumerable GetDeleteResourceData() { new Coding() { - Code = "12345", + Code = "11488-4", + System = "https://loinc.org", + }, + new Coding() + { + Code = "12345-3", + System = "https://loinc.org", + }, + new Coding() + { + Code = "18748-4", + System = "https://loinc.org", }, }, }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), + Subject = new ResourceReference("patient"), PresentedForm = new List { new Attachment() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate", + Url = "http://example.org/fhir/source/attachment", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/duplicate/attachment1", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", }, }, }, }, + 1, + 0, }, new object[] { - // Delete a DocumentReference resource with multiple attachment. + // Update a new DocumentReference resource with multiple duplicate resources found. new DocumentReference { - Id = "source", - Meta = new Meta() - { - Tag = new List - { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), - }, - }, + Id = Guid.NewGuid().ToString(), Content = new List { new DocumentReference.ContentComponent() @@ -1933,8 +3415,27 @@ public static IEnumerable GetDeleteResourceData() { ContentType = "application/xhtml", Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-source", + Url = "http://example.org/fhir/source/attachment", }, +#if R4 || R4B || Stu3 + Format = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + }, + }, +#endif }, new DocumentReference.ContentComponent() { @@ -1942,8 +3443,27 @@ public static IEnumerable GetDeleteResourceData() { ContentType = "application/xhtml", Creation = "2006-12-24", - Url = "http://example.org/fhir/Binary/attachment-source1", + Url = "http://example.org/fhir/source/attachment1", + }, +#if R4 || R4B || Stu3 + Format = new Coding() + { + Code = "12345-1", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "12345-1", + System = "https://loinc.org", + }, + }, }, +#endif }, new DocumentReference.ContentComponent() { @@ -1951,8 +3471,55 @@ public static IEnumerable GetDeleteResourceData() { ContentType = "application/xhtml", Creation = "2007-12-24", - Url = "http://example.org/fhir/Binary/attachment-source2", + Url = "http://example.org/fhir/source/attachment2", + }, +#if R4 || R4B || Stu3 + Format = new Coding() + { + Code = "18748-4", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "18748-4", + System = "https://loinc.org", + }, + }, + }, +#endif + }, + new DocumentReference.ContentComponent() + { + Attachment = new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment3", + }, +#if R4 || R4B || Stu3 + Format = new Coding() + { + Code = "12345-2", + System = "https://loinc.org", + }, +#else + Profile = new List + { + new DocumentReference.ProfileComponent() + { + Value = new Coding() + { + Code = "12345-2", + System = "https://loinc.org", + }, + }, }, +#endif }, }, Subject = new ResourceReference(Guid.NewGuid().ToString()), @@ -1962,20 +3529,116 @@ public static IEnumerable GetDeleteResourceData() Status = DocumentReference.DocumentReferenceStatus.Current, #endif }, - DeleteOperation.HardDelete, new List { new DiagnosticReport { Id = "duplicate", - Meta = new Meta() + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "12345-3", + System = "https://loinc.org", + }, + new Coding() + { + Code = "18748-4", + System = "https://loinc.org", + }, + }, + }, + Subject = new ResourceReference("patient"), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", + }, + }, + }, + new DiagnosticReport + { + Id = "duplicate1", + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() + { + new Coding() + { + Code = "11488-4", + System = "https://loinc.org", + }, + new Coding() + { + Code = "12345-3", + System = "https://loinc.org", + }, + new Coding() + { + Code = "18748-4", + System = "https://loinc.org", + }, + }, + }, + Subject = new ResourceReference("patient"), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/source/attachment", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/duplicate/attachment1", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2007-12-24", + Url = "http://example.org/fhir/source/attachment2", + }, + }, + }, + }, + new List + { + new DiagnosticReport + { + Id = "duplicate", + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() { - Tag = new List + Coding = new List() { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), + new Coding() + { + Code = "18748-4", + System = "https://loinc.org", + }, + new Coding() + { + Code = "12345-3", + System = "https://loinc.org", + }, }, }, + Subject = new ResourceReference("patient"), + PresentedForm = new List(), + }, + new DiagnosticReport + { + Id = "duplicate1", Status = DiagnosticReport.DiagnosticReportStatus.Registered, Code = new CodeableConcept() { @@ -1983,164 +3646,156 @@ public static IEnumerable GetDeleteResourceData() { new Coding() { - Code = "12345", + Code = "11488-4", + System = "https://loinc.org", + }, + new Coding() + { + Code = "12345-3", + System = "https://loinc.org", + }, + new Coding() + { + Code = "18748-4", + System = "https://loinc.org", }, }, }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), + Subject = new ResourceReference("patient"), PresentedForm = new List { new Attachment() { ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate", + Creation = "2006-12-24", + Url = "http://example.org/fhir/duplicate/attachment1", }, }, }, }, + 1, + 2, }, + }; + + foreach (var d in data) + { + yield return d; + } + } + + public static IEnumerable GetDeleteResourceData() + { + var data = new[] + { new object[] { - // Delete a DocumentReference resource without any attachment. - new DocumentReference + // Delete a DiagnosticReport resource with one duplicate resource. + new DiagnosticReport { Id = "source", - Meta = new Meta() + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() { - Tag = new List + Coding = new List() { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + new Coding() + { + Code = "12345", + }, }, }, - Content = new List(), Subject = new ResourceReference(Guid.NewGuid().ToString()), -#if R4 || R4B || Stu3 - Status = DocumentReferenceStatus.Current, -#else - Status = DocumentReference.DocumentReferenceStatus.Current, -#endif + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-source", + }, + }, }, - DeleteOperation.HardDelete, new List { - new DiagnosticReport + new DocumentReference { Id = "duplicate", Meta = new Meta() { Tag = new List { + new Coding(ClinicalReferenceDuplicator.TagDuplicateCreatedOn, DateTime.UtcNow.ToString("o")), new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, - Status = DiagnosticReport.DiagnosticReportStatus.Registered, - Code = new CodeableConcept() + Content = new List { - Coding = new List() + new DocumentReference.ContentComponent() { - new Coding() + Attachment = new Attachment() { - Code = "12345", + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-source", }, }, }, Subject = new ResourceReference(Guid.NewGuid().ToString()), - PresentedForm = new List - { - new Attachment() - { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate", - }, - }, +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif }, }, + DeleteOperation.SoftDelete, + 1, + 1, }, new object[] { - // Delete a DocumentReference resource with attachments when no duplicates. - new DocumentReference + // Delete a DiagnosticReport resource with multiple duplicate resources. + new DiagnosticReport { Id = "source", - Content = new List + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() { - new DocumentReference.ContentComponent() - { - Attachment = new Attachment() - { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-source", - }, - }, - new DocumentReference.ContentComponent() - { - Attachment = new Attachment() - { - ContentType = "application/xhtml", - Creation = "2006-12-24", - Url = "http://example.org/fhir/Binary/attachment-source1", - }, - }, - new DocumentReference.ContentComponent() + Coding = new List() { - Attachment = new Attachment() + new Coding() { - ContentType = "application/xhtml", - Creation = "2007-12-24", - Url = "http://example.org/fhir/Binary/attachment-source2", + Code = "12345", }, }, }, Subject = new ResourceReference(Guid.NewGuid().ToString()), -#if R4 || R4B || Stu3 - Status = DocumentReferenceStatus.Current, -#else - Status = DocumentReference.DocumentReferenceStatus.Current, -#endif - }, - DeleteOperation.HardDelete, - new List(), - }, - new object[] - { - // Delete a DocumentReference resource with multiple duplicates. - new DocumentReference - { - Id = "source", - Meta = new Meta() + PresentedForm = new List { - Tag = new List + new Attachment() { - new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "duplicate"), + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-source", }, - }, - Content = new List - { - new DocumentReference.ContentComponent() + new Attachment() { - Attachment = new Attachment() - { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-source", - }, + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment-source1", + }, + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2006-12-24", + Url = "http://example.org/fhir/Binary/attachment-source2", }, }, - Subject = new ResourceReference(Guid.NewGuid().ToString()), -#if R4 || R4B || Stu3 - Status = DocumentReferenceStatus.Current, -#else - Status = DocumentReference.DocumentReferenceStatus.Current, -#endif }, - DeleteOperation.HardDelete, new List { - new DiagnosticReport + new DocumentReference { Id = "duplicate", Meta = new Meta() @@ -2148,32 +3803,28 @@ public static IEnumerable GetDeleteResourceData() Tag = new List { new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, - Status = DiagnosticReport.DiagnosticReportStatus.Registered, - Code = new CodeableConcept() + Content = new List { - Coding = new List() + new DocumentReference.ContentComponent() { - new Coding() + Attachment = new Attachment() { - Code = "12345", + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate", }, }, }, Subject = new ResourceReference(Guid.NewGuid().ToString()), - PresentedForm = new List - { - new Attachment() - { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate", - }, - }, +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif }, - new DiagnosticReport + new DocumentReference { Id = "duplicate1", Meta = new Meta() @@ -2181,32 +3832,28 @@ public static IEnumerable GetDeleteResourceData() Tag = new List { new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, - Status = DiagnosticReport.DiagnosticReportStatus.Registered, - Code = new CodeableConcept() + Content = new List { - Coding = new List() + new DocumentReference.ContentComponent() { - new Coding() + Attachment = new Attachment() { - Code = "12345", + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate1", }, }, }, Subject = new ResourceReference(Guid.NewGuid().ToString()), - PresentedForm = new List - { - new Attachment() - { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate", - }, - }, +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif }, - new DiagnosticReport + new DocumentReference { Id = "duplicate2", Meta = new Meta() @@ -2214,32 +3861,64 @@ public static IEnumerable GetDeleteResourceData() Tag = new List { new Coding(ClinicalReferenceDuplicator.TagDuplicateOf, "source"), - new Coding(ClinicalReferenceDuplicator.TagIsDuplicate, "true"), }, }, - Status = DiagnosticReport.DiagnosticReportStatus.Registered, - Code = new CodeableConcept() + Content = new List { - Coding = new List() + new DocumentReference.ContentComponent() { - new Coding() + Attachment = new Attachment() { - Code = "12345", + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-duplicate2", }, }, }, Subject = new ResourceReference(Guid.NewGuid().ToString()), - PresentedForm = new List +#if R4 || R4B || Stu3 + Status = DocumentReferenceStatus.Current, +#else + Status = DocumentReference.DocumentReferenceStatus.Current, +#endif + }, + }, + DeleteOperation.HardDelete, + 1, + 3, + }, + new object[] + { + // Delete a DiagnosticReport resource without duplicate resource. + new DiagnosticReport + { + Id = "source", + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + Code = new CodeableConcept() + { + Coding = new List() { - new Attachment() + new Coding() { - ContentType = "application/xhtml", - Creation = "2005-12-24", - Url = "http://example.org/fhir/Binary/attachment-duplicate", + Code = "12345", }, }, }, + Subject = new ResourceReference(Guid.NewGuid().ToString()), + PresentedForm = new List + { + new Attachment() + { + ContentType = "application/xhtml", + Creation = "2005-12-24", + Url = "http://example.org/fhir/Binary/attachment-source", + }, + }, }, + new List(), + DeleteOperation.SoftDelete, + 1, + 0, }, }; diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/ClinicalReferenceDuplicator.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/ClinicalReferenceDuplicator.cs index a2a8d1f265..86a85954d7 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/ClinicalReferenceDuplicator.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/ClinicalReferenceDuplicator.cs @@ -21,8 +21,27 @@ namespace Microsoft.Health.Fhir.Core.Features.Guidance { public class ClinicalReferenceDuplicator : IClinicalReferenceDuplicator { + internal const string TagDuplicateCreatedOn = "duplicateCreatedOn"; internal const string TagDuplicateOf = "duplicateOf"; - internal const string TagIsDuplicate = "isDuplicate"; + + // See the link for more info, https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#clinical-notes + internal static readonly HashSet ClinicalReferenceSystems = new HashSet(StringComparer.CurrentCultureIgnoreCase) + { + "https://loinc.org", + }; + + // See the link for more info, https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#clinical-notes + internal static readonly HashSet ClinicalReferenceCodes = new HashSet(StringComparer.CurrentCultureIgnoreCase) + { + "11488-4", + "18842-5", + "34117-2", + "28570-0", + "11506-3", + "18748-4", + "11502-2", + "11526-1", + }; private readonly IFhirDataStore _dataStore; private readonly ISearchService _searchService; @@ -48,36 +67,16 @@ public ClinicalReferenceDuplicator( _logger = logger; } - public async Task<(ResourceWrapper source, ResourceWrapper duplicate)> CreateResourceAsync( + public Task> CreateResourceAsync( RawResourceElement rawResourceElement, CancellationToken cancellationToken) { EnsureArg.IsNotNull(rawResourceElement, nameof(rawResourceElement)); EnsureArg.IsNotNull(rawResourceElement.RawResource, nameof(rawResourceElement.RawResource)); - try - { - var resource = ConvertToResource(rawResourceElement); - if (!ShouldDuplicate(resource)) - { - _logger.LogWarning("A resource doesn't have any attachment with clinical reference."); - return (default, default); - } - - var duplicateResourceWrapper = await CreateDuplicateResourceInternalAsync( - resource, - cancellationToken); - var sourceResourceWrapper = await UpdateResourceInternalAsync( - resource, - duplicateResourceWrapper, - cancellationToken); - return (sourceResourceWrapper, duplicateResourceWrapper); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to create a duplicate resource and update a resource with its id."); - throw; - } + return UpsertDuplicateResourceInternalAsync( + rawResourceElement, + cancellationToken); } public async Task> DeleteResourceAsync( @@ -89,9 +88,13 @@ public async Task> DeleteResourceAsync( try { - var duplicateResourceWrappers = await SearchResourceAsync( - GetDuplicateResourceType(resourceKey.ResourceType), - resourceKey.Id, + // Steps: + // 1. Search a duplicate resource(s), tagged with the id of a deleted (source) resource. + // 1.1. If the search result has a resource(s), delete it. + // 1.2. If the search result has no resources, no actions. + // 3. Return the duplicate resource(s) deleted. + var duplicateResourceWrappers = await SearchDuplicateResourceAsync( + resourceKey, cancellationToken); _logger.LogInformation("Deleting {Count} duplicate resources...", duplicateResourceWrappers.Count); @@ -130,123 +133,16 @@ public async Task> DeleteResourceAsync( } } - public async Task> SearchResourceAsync( - string duplicateResourceType, - string resourceId, - CancellationToken cancellationToken) - { - EnsureArg.IsNotNull(duplicateResourceType, nameof(duplicateResourceType)); - EnsureArg.IsNotNull(resourceId, nameof(resourceId)); - - try - { - var queryParameters = new List> - { - Tuple.Create("_tag", $"{TagDuplicateOf}|{resourceId}"), - }; - - _logger.LogInformation( - "Searching a duplicate resource '{DuplicateResourceType}' of a resource '{Id}'...", - duplicateResourceType, - resourceId); - - var resources = new List(); - string continuationToken = null; - do - { - var searchResult = await _searchService.SearchAsync( - duplicateResourceType, - queryParameters, - cancellationToken); - if (searchResult.Results.Any()) - { - resources.AddRange( - searchResult.Results - .Where(x => x.Resource?.RawResource != null) - .Select(x => x.Resource)); - } - - continuationToken = searchResult.ContinuationToken; - } - while (!string.IsNullOrEmpty(continuationToken)); - - return resources; - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to search a duplicate resource."); - throw; - } - } - - public async Task> UpdateResourceAsync( + public Task> UpdateResourceAsync( RawResourceElement rawResourceElement, CancellationToken cancellationToken) { EnsureArg.IsNotNull(rawResourceElement, nameof(rawResourceElement)); EnsureArg.IsNotNull(rawResourceElement.RawResource, nameof(rawResourceElement.RawResource)); - try - { - var resource = ConvertToResource(rawResourceElement); - if (ShouldDuplicate(resource)) - { - var duplicateResourceWrappers = await SearchResourceAsync( - GetDuplicateResourceType(rawResourceElement.InstanceType), - rawResourceElement.Id, - cancellationToken); - _logger.LogInformation("Updating {Count} duplicate resources...", duplicateResourceWrappers.Count); - - if (duplicateResourceWrappers.Any()) - { - if (duplicateResourceWrappers.Count > 1) - { - _logger.LogWarning("More than one duplicate resource found."); - } - - var duplicateResourcesUpdated = new List(); - foreach (var wrapper in duplicateResourceWrappers) - { - try - { - var duplicate = ConvertToResource(wrapper.RawResource); - var duplicateUpdated = await UpdateDuplicateResourceInternalAsync( - resource, - duplicate, - cancellationToken); - duplicateResourcesUpdated.Add(duplicateUpdated); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to update a duplicate resource: {Id}...", wrapper.ResourceId); - throw; - } - } - - return duplicateResourcesUpdated; - } - - _logger.LogWarning("No duplicate resources found."); - (var sourceWrapper, var duplicateWrapper) = await CreateResourceAsync( - rawResourceElement, - cancellationToken); - return new List { duplicateWrapper }; - } - else - { - _logger.LogWarning("A resource doesn't have any attachment with clinical reference."); - await DeleteResourceAsync( - new ResourceKey(resource.TypeName, resource.Id), - DeleteOperation.HardDelete, - cancellationToken); - return new List(); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to delete a duplicate resource."); - throw; - } + return UpsertDuplicateResourceInternalAsync( + rawResourceElement, + cancellationToken); } public bool IsDuplicatableResourceType(string resourceType) @@ -343,19 +239,151 @@ await _dataStore.UpsertAsync( } } + private async Task> SearchDuplicateResourceAsync( + Resource resource, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(resource, nameof(resource)); + + try + { + var duplicateResourceType = GetDuplicateResourceType(resource.TypeName); + var queryParameters = new List>(); + if (string.Equals(resource.TypeName, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase)) + { + var diagnosticReport = (DiagnosticReport)resource; + var codes = GetClinicalReferenceCodes(diagnosticReport) + .Select(x => ClinicalReferenceDuplicatorHelper.ConvertToString(x)) + .ToList(); +#if R4 || R4B || Stu3 + queryParameters.Add(Tuple.Create("format", string.Join(",", codes))); +#else + queryParameters.Add(Tuple.Create("format-code", string.Join(",", codes))); +#endif + if (!string.IsNullOrEmpty(diagnosticReport.Subject?.Reference)) + { + queryParameters.Add(Tuple.Create("subject", diagnosticReport.Subject.Reference)); + } + } + else + { + var documentReference = (DocumentReference)resource; + var codes = GetClinicalReferenceCodes(documentReference) + .Select(x => ClinicalReferenceDuplicatorHelper.ConvertToString(x)) + .ToList(); + queryParameters.Add(Tuple.Create("code", string.Join(",", codes))); + if (!string.IsNullOrEmpty(documentReference.Subject?.Reference)) + { + queryParameters.Add(Tuple.Create("subject", documentReference.Subject.Reference)); + } + } + + _logger.LogInformation( + "Searching a duplicate resource '{DuplicateResourceType}' of a resource '{Id}'...", + duplicateResourceType, + resource.Id); + + var resources = new List(); + string continuationToken = null; + do + { + var searchResult = await _searchService.SearchAsync( + duplicateResourceType, + queryParameters, + cancellationToken); + if (searchResult.Results.Any()) + { + resources.AddRange( + searchResult.Results + .Where(x => x.Resource?.RawResource != null) + .Select(x => x.Resource)); + } + + continuationToken = searchResult.ContinuationToken; + } + while (!string.IsNullOrEmpty(continuationToken)); + + return resources; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to search a duplicate resource."); + throw; + } + } + + private async Task> SearchDuplicateResourceAsync( + ResourceKey resourceKey, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(resourceKey, nameof(resourceKey)); + + try + { + var duplicateResourceType = GetDuplicateResourceType(resourceKey.ResourceType); + var queryParameters = new List> + { + Tuple.Create("_tag", $"{TagDuplicateOf}|{resourceKey.Id}"), + }; + + _logger.LogInformation( + "Searching a duplicate resource '{DuplicateResourceType}' of a resource '{Id}'...", + duplicateResourceType, + resourceKey.Id); + + var resources = new List(); + string continuationToken = null; + do + { + var searchResult = await _searchService.SearchAsync( + duplicateResourceType, + queryParameters, + cancellationToken); + if (searchResult.Results.Any()) + { + resources.AddRange( + searchResult.Results + .Where(x => x.Resource?.RawResource != null) + .Select(x => x.Resource)); + } + + continuationToken = searchResult.ContinuationToken; + } + while (!string.IsNullOrEmpty(continuationToken)); + + return resources; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to search a duplicate resource."); + throw; + } + } + private async Task UpdateDuplicateResourceInternalAsync( Resource resource, - Resource duplicateResource, + ResourceWrapper duplicateResourceWrapper, CancellationToken cancellationToken) { EnsureArg.IsNotNull(resource, nameof(resource)); - EnsureArg.IsNotNull(duplicateResource, nameof(duplicateResource)); + EnsureArg.IsNotNull(duplicateResourceWrapper, nameof(duplicateResourceWrapper)); try { - UpdateDuplicateResource(resource, duplicateResource); + var duplicateResource = ConvertToResource(duplicateResourceWrapper.RawResource); + var duplicateResourceUpdated = UpdateDuplicateResource( + resource, + duplicateResource); + if (!duplicateResourceUpdated) + { + _logger.LogInformation( + "Updating a duplicate resource '{DuplicateResourceType}' unnecessary: {Id}...", + duplicateResource.TypeName, + resource.Id); + return duplicateResourceWrapper; + } - var duplicateResourceWrapper = _resourceWrapperFactory.Create( + var duplicateResourceWrapperToUpdate = _resourceWrapperFactory.Create( duplicateResource.ToResourceElement(), false, true); @@ -366,7 +394,7 @@ private async Task UpdateDuplicateResourceInternalAsync( resource.Id); var outcome = await _dataStore.UpsertAsync( new ResourceWrapperOperation( - duplicateResourceWrapper, + duplicateResourceWrapperToUpdate, true, true, null, @@ -388,64 +416,83 @@ private async Task UpdateDuplicateResourceInternalAsync( } } - private async Task UpdateResourceInternalAsync( - Resource resource, - ResourceWrapper duplicateResourceWrapper, + private async Task> UpsertDuplicateResourceInternalAsync( + RawResourceElement rawResourceElement, CancellationToken cancellationToken) { - EnsureArg.IsNotNull(resource, nameof(resource)); - EnsureArg.IsNotNull(duplicateResourceWrapper, nameof(duplicateResourceWrapper)); + EnsureArg.IsNotNull(rawResourceElement, nameof(rawResourceElement)); + EnsureArg.IsNotNull(rawResourceElement.RawResource, nameof(rawResourceElement.RawResource)); try { - if (resource.Meta == null) + // Steps: + // 1. Check if a source resource has one of the clinical reference code. + // (e.g. DiagnosticReport?code=[system|code]&subject=Patient/[id], DocumentReference?format=[system|code]&subject=Patient/[id]) + // 1.1. If it has, move to 2. + // 1.2. If it hasn't, no actions. + // 2. Search a duplicate resource(s) with the clinical reference code and subject (if specified). + // (e.g. DiagnosticReport?code=[system|code]) + // 2.1. If the search result has a resource(s), add missing attachemts in the source resport to it. + // 2.2. If the search result has no resources, create a duplicate resource by copying the code, subject, and attachments. + // (Also, tag a duplicate resource so that we can identify a resource we created as a duplicate.) + // 3. Return the duplicate resource(s) updated/created. + var resource = ConvertToResource(rawResourceElement); + var duplicateResourceWrappers = new List(); + if (ShouldDuplicate(resource)) { - resource.Meta = new Meta(); + var resourceWrappersFound = await SearchDuplicateResourceAsync( + resource, + cancellationToken); + if (!resourceWrappersFound.Any()) + { + var duplicateResourceWrapper = await CreateDuplicateResourceInternalAsync( + resource, + cancellationToken); + duplicateResourceWrappers.Add(duplicateResourceWrapper); + } + else + { + foreach (var wrapper in resourceWrappersFound) + { + var wrapperUpdated = await UpdateDuplicateResourceInternalAsync( + resource, + wrapper, + cancellationToken); + duplicateResourceWrappers.Add(wrapperUpdated); + } + } } - - var tags = new List(); - if (resource.Meta.Tag != null) + else { - tags.AddRange( - resource.Meta.Tag.Where(x => !string.Equals(x.System, TagDuplicateOf, StringComparison.OrdinalIgnoreCase))); + _logger.LogInformation("A resource doesn't have any attachment with clinical reference."); } - tags.Add(new Coding(TagDuplicateOf, duplicateResourceWrapper.ResourceId)); - resource.Meta.Tag = tags; - - var resourceWrapper = _resourceWrapperFactory.Create( - resource.ToResourceElement(), - false, - true); - - _logger.LogInformation( - "Updating a '{ResourceType}' resource with a duplicate resource '{Id}'...", - resource.TypeName, - duplicateResourceWrapper.ResourceId); - var outcome = await _dataStore.UpsertAsync( - new ResourceWrapperOperation( - resourceWrapper, - true, - true, - null, - false, - false, - null), - cancellationToken); - - _logger.LogInformation( - "A '{ResourceType}' resource {Outcome}.", - resource.TypeName, - outcome.OutcomeType.ToString().ToLowerInvariant()); - return outcome.Wrapper; + return duplicateResourceWrappers; } catch (Exception ex) { - _logger.LogError(ex, "Failed to update a resource with a duplicate resource id."); + _logger.LogError(ex, "Failed to upsert a duplicate resource."); throw; } } + private static Resource ConvertToResource(RawResource rawResource) + { + EnsureArg.IsNotNull(rawResource, nameof(rawResource)); + + return rawResource + .ToITypedElement(ModelInfoProvider.Instance) + .ToResourceElement() + .ToPoco(); + } + + private static Resource ConvertToResource(RawResourceElement rawResourceElement) + { + EnsureArg.IsNotNull(rawResourceElement, nameof(rawResourceElement)); + + return ConvertToResource(rawResourceElement.RawResource); + } + private static Resource CreateDuplicateResource(Resource resource) { EnsureArg.IsNotNull(resource, nameof(resource)); @@ -458,7 +505,7 @@ private static Resource CreateDuplicateResource(Resource resource) if (resource is DiagnosticReport) { - var diagnosticReportToDuplicate = (DiagnosticReport)resource; + var diagnosticReport = (DiagnosticReport)resource; // TODO: more fields need to be populated? var documentReference = new DocumentReference @@ -467,12 +514,12 @@ private static Resource CreateDuplicateResource(Resource resource) { Tag = new List { - new Coding(TagDuplicateOf, diagnosticReportToDuplicate.Id), - new Coding(TagIsDuplicate, bool.TrueString.ToLowerInvariant()), + new Coding(TagDuplicateCreatedOn, DateTime.UtcNow.ToString("o")), + new Coding(TagDuplicateOf, diagnosticReport.Id), }, }, Content = new List(), - Subject = diagnosticReportToDuplicate.Subject, + Subject = diagnosticReport.Subject, #if R4 || R4B || Stu3 Status = DocumentReferenceStatus.Current, #else @@ -480,76 +527,162 @@ private static Resource CreateDuplicateResource(Resource resource) #endif }; - if (diagnosticReportToDuplicate.PresentedForm?.Any(x => x.Url != null) ?? false) + var crefs = GetClinicalReferences(diagnosticReport); +#if R4 || R4B || Stu3 + foreach (var cref in crefs) { - // TODO: need to be more selective about whether the resource should be duplicated based on urls. - // (https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#clinical-notes-1) - foreach (var attachment in diagnosticReportToDuplicate.PresentedForm.Where(x => x.Url != null)) + foreach (var code in cref.codes) + { + foreach (var attachment in cref.attachments) + { + documentReference.Content.Add( + new DocumentReference.ContentComponent + { + Attachment = attachment, + Format = code, + }); + } + } + } +#else + foreach (var cref in crefs) + { + var profiles = cref.codes + .Select( + x => + { + return new DocumentReference.ProfileComponent() + { + Value = x, + }; + }) + .ToList(); + foreach (var attachment in cref.attachments) { documentReference.Content.Add( new DocumentReference.ContentComponent { Attachment = attachment, + Profile = profiles, }); } } +#endif return documentReference; } - - var documentReferenceToDuplicate = (DocumentReference)resource; - - // TODO: more fields need to be populated? - var diagnosticReport = new DiagnosticReport + else { - Meta = new Meta + var documentReference = (DocumentReference)resource; + + // TODO: more fields need to be populated? + var diagnosticReport = new DiagnosticReport { - Tag = new List + Meta = new Meta { - new Coding(TagDuplicateOf, resource.Id), - new Coding(TagIsDuplicate, bool.TrueString.ToLowerInvariant()), + Tag = new List + { + new Coding(TagDuplicateCreatedOn, DateTime.UtcNow.ToString("o")), + new Coding(TagDuplicateOf, resource.Id), + }, }, - }, - PresentedForm = new List(), - Subject = documentReferenceToDuplicate.Subject, - Status = DiagnosticReport.DiagnosticReportStatus.Registered, - }; + PresentedForm = new List(), + Subject = documentReference.Subject, + Status = DiagnosticReport.DiagnosticReportStatus.Registered, + }; - if (documentReferenceToDuplicate.Content?.Any(x => x.Attachment?.Url != null) ?? false) - { - // TODO: need to be more selective about whether the resource should be duplicated based on urls. - // (https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#clinical-notes-1) - foreach (var attachment in documentReferenceToDuplicate.Content?.Where(x => x.Attachment?.Url != null).Select(x => x.Attachment)) + var crefs = GetClinicalReferences(documentReference); + var codes = crefs.SelectMany(x => x.codes); + diagnosticReport.Code = new CodeableConcept(); + foreach (var code in crefs.SelectMany(x => x.codes)) { - diagnosticReport.PresentedForm.Add(attachment); + diagnosticReport.Code.Add(code.System, code.Code, code.Display); } - } - return diagnosticReport; - } - - private static Resource ConvertToResource(RawResource rawResource) - { - EnsureArg.IsNotNull(rawResource, nameof(rawResource)); - - return rawResource - .ToITypedElement(ModelInfoProvider.Instance) - .ToResourceElement() - .ToPoco(); + diagnosticReport.PresentedForm.AddRange(crefs.SelectMany(x => x.attachments)); + return diagnosticReport; + } } - private static Resource ConvertToResource(RawResourceElement rawResourceElement) + private static List GetClinicalReferenceCodes(Resource resource) { - EnsureArg.IsNotNull(rawResourceElement, nameof(rawResourceElement)); + EnsureArg.IsNotNull(resource, nameof(resource)); - return ConvertToResource(rawResourceElement.RawResource); + if (string.Equals(resource.TypeName, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase)) + { + var diagnosticReport = (DiagnosticReport)resource; + return diagnosticReport.Code?.Coding? + .Where(x => ClinicalReferenceSystems.Contains(x?.System) && ClinicalReferenceCodes.Contains(x?.Code)) + .DistinctBy(x => ClinicalReferenceDuplicatorHelper.ConvertToString(x)) + .ToList(); + } + else + { + var documentReference = (DocumentReference)resource; +#if R4 || R4B || Stu3 + return documentReference.Content + .Where(x => ClinicalReferenceSystems.Contains(x?.Format?.System) && ClinicalReferenceCodes.Contains(x?.Format?.Code)) + .Select(x => x.Format) + .DistinctBy(x => ClinicalReferenceDuplicatorHelper.ConvertToString(x)) + .ToList(); +#else + return documentReference.Content + .SelectMany(x => x?.Profile? + .Where(y => y?.Value?.GetType() == typeof(Coding) + && ClinicalReferenceSystems.Contains(((Coding)y.Value)?.System) + && ClinicalReferenceCodes.Contains(((Coding)y.Value)?.Code)) + .Select(y => (Coding)y.Value)) + .DistinctBy(x => ClinicalReferenceDuplicatorHelper.ConvertToString(x)) + .ToList(); +#endif + } } - private static string GetDuplicateResourceId(Resource resource) + private static List<(IList codes, IList attachments)> GetClinicalReferences(Resource resource) { EnsureArg.IsNotNull(resource, nameof(resource)); - return resource?.Meta?.Tag?.SingleOrDefault(x => x.System == TagDuplicateOf)?.Code; + var clinicalReferences = new List<(IList codes, IList attachments)>(); + if (string.Equals(resource.TypeName, KnownResourceTypes.DiagnosticReport, StringComparison.OrdinalIgnoreCase)) + { + var diagnosticReport = (DiagnosticReport)resource; + var codes = diagnosticReport.Code?.Coding? + .Where(x => ClinicalReferenceSystems.Contains(x?.System) && ClinicalReferenceCodes.Contains(x?.Code)) + .DistinctBy(x => ClinicalReferenceDuplicatorHelper.ConvertToString(x)) + .ToList(); + clinicalReferences.Add(new(codes, diagnosticReport.PresentedForm)); + } + else + { + var documentReference = (DocumentReference)resource; +#if R4 || R4B || Stu3 + var contents = documentReference.Content + .Where(x => ClinicalReferenceSystems.Contains(x?.Format?.System) && ClinicalReferenceCodes.Contains(x?.Format?.Code)) + .DistinctBy(x => ClinicalReferenceDuplicatorHelper.ConvertToString(x.Format)) + .ToList(); + foreach (var content in contents) + { + clinicalReferences.Add(new(new List { content.Format }, new List { content.Attachment })); + } +#else + foreach (var content in documentReference.Content) + { + var codes = content.Profile + .Where(x => (x.Value is Coding) + && ClinicalReferenceSystems.Contains(((Coding)x.Value).System) + && ClinicalReferenceCodes.Contains(((Coding)x.Value).Code)) + .Select(x => (Coding)x.Value) + .DistinctBy(x => ClinicalReferenceDuplicatorHelper.ConvertToString(x)) + .ToList(); + if (codes.Any()) + { + clinicalReferences.Add(new(codes, new List { content.Attachment })); + } + } +#endif + } + + return clinicalReferences; } private static string GetDuplicateResourceType(string resourceType) @@ -567,28 +700,11 @@ private static bool ShouldDuplicate(Resource resource) return false; } - if (resource.Meta?.Tag?.Any(x => string.Equals(x.System, TagDuplicateOf, StringComparison.OrdinalIgnoreCase)) ?? false) - { - return true; - } - - if (resource is DiagnosticReport) - { - // TODO: need to be more selective about whether the resource should be duplicated based on urls. - // (https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#clinical-notes-1) - return ((DiagnosticReport)resource).PresentedForm?.Any(x => x.Url != null) ?? false; - } - else if (resource is DocumentReference) - { - // TODO: need to be more selective about whether the resource should be duplicated based on urls. - // (https://hl7.org/fhir/us/core/STU6.1/clinical-notes.html#clinical-notes-1) - return ((DocumentReference)resource).Content?.Any(x => x.Attachment?.Url != null) ?? false; - } - - return false; + var crefs = GetClinicalReferences(resource); + return crefs.SelectMany(x => x.codes).Any() && crefs.SelectMany(x => x.attachments).Any(); } - private static void UpdateDuplicateResource( + private static bool UpdateDuplicateResource( Resource resource, Resource duplicateResource) { @@ -603,56 +719,93 @@ private static void UpdateDuplicateResource( $"A source/target resource to be updated must be of type '{nameof(DiagnosticReport)}' and '{nameof(DocumentReference)}'."); } + var resourceUpdated = false; if (resource is DiagnosticReport) { var diagnosticReport = (DiagnosticReport)resource; var documentReference = (DocumentReference)duplicateResource; - documentReference.Subject = diagnosticReport.Subject; - var contents = new List(); - if (documentReference.Content != null) +#if R4 || R4B || Stu3 + foreach (var code in GetClinicalReferenceCodes(diagnosticReport)) { - // TODO: save attachments without a url. is this right? - contents.AddRange(documentReference.Content.Where(x => string.IsNullOrEmpty(x.Attachment?.Url))); + foreach (var attachment in diagnosticReport.PresentedForm) + { + if (!documentReference.Content.Any( + x => ClinicalReferenceDuplicatorHelper.CompareCoding(x.Format, code) + && ClinicalReferenceDuplicatorHelper.CompareAttachment(x.Attachment, attachment))) + { + documentReference.Content.Add( + new DocumentReference.ContentComponent() + { + Attachment = attachment, + Format = code, + }); + resourceUpdated = true; + } + } } - - if (diagnosticReport.PresentedForm?.Any(x => !string.IsNullOrEmpty(x.Url)) ?? false) +#else + var profiles = GetClinicalReferenceCodes(diagnosticReport) + .Select( + x => new DocumentReference.ProfileComponent() + { + Value = new Coding(x.System, x.Code, x.Display), + }) + .ToList(); + foreach (var attachment in diagnosticReport.PresentedForm) { - foreach (var attachment in diagnosticReport.PresentedForm.Where(x => x.Url != null)) + var contents = documentReference.Content + .Where(x => ClinicalReferenceDuplicatorHelper.CompareAttachment(x.Attachment, attachment)) + .ToList(); + if (!contents.Any()) { - contents.Add( - new DocumentReference.ContentComponent + documentReference.Content.Add( + new DocumentReference.ContentComponent() { Attachment = attachment, + Profile = profiles, }); + resourceUpdated = true; + continue; } - } - documentReference.Content = contents; + foreach (var content in contents) + { + if (!ClinicalReferenceDuplicatorHelper.CompareCodings(content.Profile, profiles)) + { + content.Profile = content.Profile + .UnionBy( + profiles, + x => (x.Value is Coding) ? (((Coding)x.Value).System, ((Coding)x.Value).Code) : (string.Empty, string.Empty)) + .ToList(); + resourceUpdated = true; + } + } + } +#endif } else { var documentReference = (DocumentReference)resource; var diagnosticReport = (DiagnosticReport)duplicateResource; - diagnosticReport.Subject = documentReference.Subject; - - var attachments = new List(); - if (diagnosticReport.PresentedForm != null) + var codes = GetClinicalReferenceCodes(diagnosticReport) + .Select(x => ClinicalReferenceDuplicatorHelper.ConvertToString(x)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + var attachments = GetClinicalReferences(documentReference) + .Where(x => x.codes.Any(y => codes.Contains(ClinicalReferenceDuplicatorHelper.ConvertToString(y)))) + .SelectMany(x => x.attachments) + .ToList(); + foreach (var attachment in attachments) { - // TODO: save attachments without a url. is this right? - attachments.AddRange(diagnosticReport.PresentedForm.Where(x => string.IsNullOrEmpty(x.Url))); - } - - if (documentReference.Content?.Any(x => !string.IsNullOrEmpty(x.Attachment?.Url)) ?? false) - { - foreach (var attachment in documentReference.Content?.Where(x => x.Attachment?.Url != null).Select(x => x.Attachment)) + if (!diagnosticReport.PresentedForm.Any(x => ClinicalReferenceDuplicatorHelper.CompareAttachment(x, attachment))) { - attachments.Add(attachment); + diagnosticReport.PresentedForm.Add(attachment); + resourceUpdated = true; } } - - diagnosticReport.PresentedForm = attachments; } + + return resourceUpdated; } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/ClinicalReferenceDuplicatorHelper.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/ClinicalReferenceDuplicatorHelper.cs new file mode 100644 index 0000000000..6b6ab464b1 --- /dev/null +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/ClinicalReferenceDuplicatorHelper.cs @@ -0,0 +1,111 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using EnsureThat; +using Hl7.Fhir.Model; + +namespace Microsoft.Health.Fhir.Core.Features.Guidance +{ + public static class ClinicalReferenceDuplicatorHelper + { + public static bool CompareAttachment(Attachment x, Attachment y) + { + if (x == null || y == null) + { + return x == null && y == null; + } + + return string.Equals(x.ContentType, y.ContentType, StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Language, y.Language, StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Url, y.Url, StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Title, y.Title, StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Creation, y.Creation, StringComparison.OrdinalIgnoreCase) + && ((x.Data != null && y.Data != null) ? x.Data.SequenceEqual(y.Data) : x.Data == null && y.Data == null) + && ((x.Size != null && y.Size != null) ? x.Size == y.Size : x.Size == null && y.Size == null); + } + + public static bool CompareAttachments(IReadOnlyList x, IReadOnlyList y) + { + if ((x?.Count ?? 0) != (y?.Count ?? 0)) + { + return false; + } + + return x.All(xx => y.Any(yy => CompareAttachment(xx, yy))); + } + + public static bool CompareCoding(Coding x, Coding y) + { + if (x == null || y == null) + { + return x == null && y == null; + } + + return string.Equals(x.System, y.System, StringComparison.OrdinalIgnoreCase) + && string.Equals(x.Code, y.Code, StringComparison.OrdinalIgnoreCase); + + // Note: disabling comparing display since some serializer sets its value to System for some resource types but not others. + // && string.Equals(x.Display, y.Display, StringComparison.OrdinalIgnoreCase); + } + + public static bool CompareCodings(IReadOnlyList x, IReadOnlyList y) + { + if ((x?.Count ?? 0) != (y?.Count ?? 0)) + { + return false; + } + + return x.All(xx => y.Any(yy => CompareCoding(xx, yy))); + } + +#if !R4 && !R4B && !Stu3 + public static bool CompareCodings(IReadOnlyList x, IReadOnlyList y) + { + if ((x?.Count ?? 0) != (y?.Count ?? 0)) + { + return false; + } + + return CompareCodings( + x.Where(xx => xx.Value?.GetType() == typeof(Coding)).Select(xx => (Coding)xx.Value).ToList(), + y.Where(yy => yy.Value?.GetType() == typeof(Coding)).Select(yy => (Coding)yy.Value).ToList()); + } +#endif + + public static bool CompareContent(DocumentReference.ContentComponent x, DocumentReference.ContentComponent y) + { + if (x == null || y == null) + { + return x == null && y == null; + } + +#if R4 || R4B || Stu3 + return CompareCoding(x.Format, y.Format) && CompareAttachment(x.Attachment, y.Attachment); +#else + return CompareCodings(x.Profile, y.Profile) && CompareAttachment(x.Attachment, y.Attachment); +#endif + } + + public static bool CompareContents(IReadOnlyList x, IReadOnlyList y) + { + if ((x?.Count ?? 0) != (y?.Count ?? 0)) + { + return false; + } + + return x.All(xx => y.Any(yy => CompareContent(xx, yy))); + } + + public static string ConvertToString(Coding coding) + { + EnsureArg.IsNotNull(coding, nameof(coding)); + + return $"{coding.System}|{coding.Code}"; + } + } +} diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/DuplicateClinicalReferenceBehavior.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/DuplicateClinicalReferenceBehavior.cs index b19b20c564..9973921ea0 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/DuplicateClinicalReferenceBehavior.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/DuplicateClinicalReferenceBehavior.cs @@ -10,11 +10,9 @@ using Microsoft.Extensions.Options; using Microsoft.Health.Fhir.Core.Configs; using Microsoft.Health.Fhir.Core.Features.Guidance; -using Microsoft.Health.Fhir.Core.Features.Persistence; using Microsoft.Health.Fhir.Core.Messages.Create; using Microsoft.Health.Fhir.Core.Messages.Delete; using Microsoft.Health.Fhir.Core.Messages.Upsert; -using Microsoft.Health.Fhir.Core.Models; namespace Microsoft.Health.Fhir.Shared.Core.Features.Guidance { @@ -50,14 +48,9 @@ public async Task Handle( && response?.Outcome?.RawResourceElement?.RawResource != null && _clinicalReferenceDuplicator.IsDuplicatableResourceType(response.Outcome.RawResourceElement.InstanceType)) { - (var source, var duplicate) = await _clinicalReferenceDuplicator.CreateResourceAsync( + await _clinicalReferenceDuplicator.CreateResourceAsync( response.Outcome.RawResourceElement, cancellationToken); - if (source != null) - { - return new UpsertResourceResponse( - new SaveOutcome(new RawResourceElement(source), response.Outcome.Outcome)); - } } return response; diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/IClinicalReferenceDuplicator.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/IClinicalReferenceDuplicator.cs index 6c2cdf6881..87072e0b69 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/IClinicalReferenceDuplicator.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Guidance/IClinicalReferenceDuplicator.cs @@ -14,7 +14,7 @@ namespace Microsoft.Health.Fhir.Core.Features.Guidance { public interface IClinicalReferenceDuplicator { - Task<(ResourceWrapper source, ResourceWrapper duplicate)> CreateResourceAsync( + Task> CreateResourceAsync( RawResourceElement rawResourceElement, CancellationToken cancellationToken); @@ -23,11 +23,6 @@ Task> DeleteResourceAsync( DeleteOperation deleteOperation, CancellationToken cancellationToken); - Task> SearchResourceAsync( - string duplicateResourceType, - string resourceId, - CancellationToken cancellationToken); - Task> UpdateResourceAsync( RawResourceElement rawResourceElement, CancellationToken cancellationToken); diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Microsoft.Health.Fhir.Shared.Core.projitems b/src/Microsoft.Health.Fhir.Shared.Core/Microsoft.Health.Fhir.Shared.Core.projitems index 6bf1384f7d..6b1e1faf58 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Microsoft.Health.Fhir.Shared.Core.projitems +++ b/src/Microsoft.Health.Fhir.Shared.Core/Microsoft.Health.Fhir.Shared.Core.projitems @@ -17,6 +17,7 @@ + diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/Guidance/ClinicalReferenceDuplicatorTests.cs b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/Guidance/ClinicalReferenceDuplicatorTests.cs index 13bbbb42ae..823258e291 100644 --- a/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/Guidance/ClinicalReferenceDuplicatorTests.cs +++ b/test/Microsoft.Health.Fhir.Shared.Tests.E2E/Rest/Guidance/ClinicalReferenceDuplicatorTests.cs @@ -40,7 +40,7 @@ public ClinicalReferenceDuplicatorTests(HttpIntegrationTestFixture fixture) private TestFhirClient Client => _fixture.TestFhirClient; - [Theory] + [Theory(Skip = "Disabling the test since we haven't finalized the design.")] [MemberData(nameof(GetCreateResourceData))] public async Task GivenResource_WhenCreating_ThenDuplicateResourceShouldBeCreated( Resource resource) @@ -48,7 +48,7 @@ public async Task GivenResource_WhenCreating_ThenDuplicateResourceShouldBeCreate await CreateResourceAsync(resource); } - [Theory] + [Theory(Skip = "Disabling the test since we haven't finalized the design.")] [MemberData(nameof(GetCreateResourceData))] public async Task GivenResource_WhenDeleting_ThenDuplicateResourceShouldBeDeleted( Resource resource) @@ -57,7 +57,7 @@ public async Task GivenResource_WhenDeleting_ThenDuplicateResourceShouldBeDelete await DeleteResourceAsync(source); } - [Theory] + [Theory(Skip = "Disabling the test since we haven't finalized the design.")] [MemberData(nameof(GetUpdateResourceData))] public async Task GivenResource_WhenUpdating_ThenDuplicateResourceShouldBeUpdated( Resource resource,