From 2322fc10824d1d8b6914ec7a32bc87ae6ce4e4e6 Mon Sep 17 00:00:00 2001 From: "Tarun Mathew (Centific Technologies Inc)" Date: Mon, 3 Feb 2025 14:40:44 -0800 Subject: [PATCH 1/2] Implementing $include operation with paging. --- .../Extensions/HttpRequestExtensions.cs | 2 +- .../Features/Audit/AuditHelper.cs | 3 +- ...stContextBeforeAuthenticationMiddleware.cs | 2 +- ...CompartmentResourceTypesRouteConstraint.cs | 1 + .../CompartmentTypesRouteConstraint.cs | 1 + .../Routing/ResourceTypesRouteConstraint.cs | 1 + .../Routing/SearchPostReroutingMiddleware.cs | 1 + .../Features/Routing/UrlResolver.cs | 53 ++- .../FhirServerApplicationBuilderExtensions.cs | 4 +- .../Operations/Export/ExportJobTaskTests.cs | 14 +- ...ts.cs => ContinuationTokenEncoderTests.cs} | 12 +- .../Configs/CoreFeatureConfiguration.cs | 2 +- .../Extensions/SearchServiceExtensions.cs | 2 +- .../Features/KnownQueryParameterNames.cs | 5 + .../Operations/Export/ExportJobTask.cs | 2 +- .../Operations/Reindex/ReindexJobTask.cs | 2 +- .../Features/Routing/IUrlResolver.cs | 5 +- .../Routing/KnownActionParameterNames.cs | 2 +- .../Features/Routing/KnownRoutes.cs | 5 +- .../Features/Routing/RouteNames.cs | 4 +- ...nverter.cs => ContinuationTokenEncoder.cs} | 6 +- .../Features/Search/ISearchOptionsFactory.cs | 6 +- .../Features/Search/ISearchService.cs | 4 +- .../Features/Search/SearchOptions.cs | 11 + .../Features/Search/SearchResult.cs | 10 +- .../Features/Search/SearchService.cs | 7 +- .../Messages/Search/SearchResourceRequest.cs | 5 +- .../Resources.Designer.cs | 9 + src/Microsoft.Health.Fhir.Core/Resources.resx | 5 +- .../Search/FhirCosmosSearchService.cs | 17 + .../ApiNotificationMiddlewareTests.cs | 2 +- ...RouteDataPopulatingFilterAttributeTests.cs | 2 +- .../SearchParameterFilterAttributeTests.cs | 2 +- .../ValidateIdSegmentAttributeTests.cs | 4 +- .../Filters/ValidateResourceIdFilterTests.cs | 2 +- .../ValidateResourceTypeFilterTests.cs | 2 +- .../Formatters/FhirJsonInputFormatterTests.cs | 2 +- .../Formatters/FhirXmlInputFormatterTests.cs | 2 +- .../CompartmentTypesRouteConstraintTests.cs | 2 +- .../ResourceTypesRouteConstraintTests.cs | 2 +- .../Features/Routing/UrlResolverTests.cs | 2 +- .../Controllers/ConvertDataController.cs | 3 +- .../Controllers/EverythingController.cs | 2 +- .../Controllers/IncludesController.cs | 53 +++ .../Controllers/MemberMatchController.cs | 2 +- .../OperationDefinitionController.cs | 2 +- .../Controllers/SearchParameterController.cs | 2 +- .../Controllers/ValidateController.cs | 2 +- ...ntextRouteDataPopulatingFilterAttribute.cs | 2 +- .../Filters/SearchParameterFilterAttribute.cs | 2 +- .../Filters/ValidateIdSegmentAttribute.cs | 3 +- .../ValidateResourceIdFilterAttribute.cs | 3 +- .../ValidateResourceTypeFilterAttribute.cs | 2 +- .../Resources/Bundle/BundleHandler.cs | 2 +- .../Bundle/TransactionBundleValidator.cs | 3 +- ...Microsoft.Health.Fhir.Shared.Api.projitems | 1 + .../FhirServerServiceCollectionExtensions.cs | 1 + .../Operations/Reindex/ReindexJobTaskTests.cs | 2 +- .../Features/Search/BundleFactoryTests.cs | 2 +- .../Extensions/FhirMediatorExtensions.cs | 9 + .../Everything/PatientEverythingService.cs | 6 +- .../Features/Search/BundleFactory.cs | 73 +++- .../Features/Search/SearchOptionsFactory.cs | 34 +- .../Features/Search/SearchResourceHandler.cs | 6 +- .../ServerProvideProfileValidation.cs | 2 +- .../Features/Search/ContinuationToken.cs | 13 - .../Search/ContinuationTokenConverter.cs | 22 ++ .../Visitors/IncludesOperationRewriter.cs | 60 ++++ .../QueryGenerators/SqlQueryGenerator.cs | 82 ++++- .../Search/IncludesContinuationToken.cs | 119 +++++++ .../Features/Search/SqlServerSearchService.cs | 333 +++++++++++++++++- .../Resources.Designer.cs | 9 + .../Resources.resx | 5 +- 73 files changed, 943 insertions(+), 146 deletions(-) rename src/Microsoft.Health.Fhir.Core.UnitTests/Features/Search/{ContinuationTokenConverterTests.cs => ContinuationTokenEncoderTests.cs} (83%) rename src/{Microsoft.Health.Fhir.Api => Microsoft.Health.Fhir.Core}/Features/Routing/KnownActionParameterNames.cs (93%) rename src/{Microsoft.Health.Fhir.Api => Microsoft.Health.Fhir.Core}/Features/Routing/KnownRoutes.cs (97%) rename src/{Microsoft.Health.Fhir.Api => Microsoft.Health.Fhir.Core}/Features/Routing/RouteNames.cs (97%) rename src/Microsoft.Health.Fhir.Core/Features/Search/{ContinuationTokenConverter.cs => ContinuationTokenEncoder.cs} (92%) create mode 100644 src/Microsoft.Health.Fhir.Shared.Api/Controllers/IncludesController.cs create mode 100644 src/Microsoft.Health.Fhir.SqlServer/Features/Search/ContinuationTokenConverter.cs create mode 100644 src/Microsoft.Health.Fhir.SqlServer/Features/Search/Expressions/Visitors/IncludesOperationRewriter.cs create mode 100644 src/Microsoft.Health.Fhir.SqlServer/Features/Search/IncludesContinuationToken.cs diff --git a/src/Microsoft.Health.Fhir.Api/Extensions/HttpRequestExtensions.cs b/src/Microsoft.Health.Fhir.Api/Extensions/HttpRequestExtensions.cs index 62db30fc52..52586789f0 100644 --- a/src/Microsoft.Health.Fhir.Api/Extensions/HttpRequestExtensions.cs +++ b/src/Microsoft.Health.Fhir.Api/Extensions/HttpRequestExtensions.cs @@ -9,7 +9,7 @@ using System.Web; using EnsureThat; using Microsoft.AspNetCore.Http; -using Microsoft.Health.Fhir.Api.Features.Routing; +using Microsoft.Health.Fhir.Core.Features.Routing; namespace Microsoft.Health.Fhir.Api.Extensions { diff --git a/src/Microsoft.Health.Fhir.Api/Features/Audit/AuditHelper.cs b/src/Microsoft.Health.Fhir.Api/Features/Audit/AuditHelper.cs index a2e37db190..763ee737fa 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/Audit/AuditHelper.cs +++ b/src/Microsoft.Health.Fhir.Api/Features/Audit/AuditHelper.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Net; using System.Reflection; @@ -17,9 +16,9 @@ using Microsoft.Health.Core.Features.Context; using Microsoft.Health.Core.Features.Security; using Microsoft.Health.Fhir.Api.Features.AnonymousOperations; -using Microsoft.Health.Fhir.Api.Features.Routing; using Microsoft.Health.Fhir.Core.Features.Audit; using Microsoft.Health.Fhir.Core.Features.Context; +using Microsoft.Health.Fhir.Core.Features.Routing; namespace Microsoft.Health.Fhir.Api.Features.Audit { diff --git a/src/Microsoft.Health.Fhir.Api/Features/Context/FhirRequestContextBeforeAuthenticationMiddleware.cs b/src/Microsoft.Health.Fhir.Api/Features/Context/FhirRequestContextBeforeAuthenticationMiddleware.cs index 7f3496a6eb..a7d96968be 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/Context/FhirRequestContextBeforeAuthenticationMiddleware.cs +++ b/src/Microsoft.Health.Fhir.Api/Features/Context/FhirRequestContextBeforeAuthenticationMiddleware.cs @@ -10,8 +10,8 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Health.Api.Features.Audit; using Microsoft.Health.Core.Features.Context; -using Microsoft.Health.Fhir.Api.Features.Routing; using Microsoft.Health.Fhir.Core.Features.Context; +using Microsoft.Health.Fhir.Core.Features.Routing; namespace Microsoft.Health.Fhir.Api.Features.Context { diff --git a/src/Microsoft.Health.Fhir.Api/Features/Routing/CompartmentResourceTypesRouteConstraint.cs b/src/Microsoft.Health.Fhir.Api/Features/Routing/CompartmentResourceTypesRouteConstraint.cs index 0fe4f74025..ea0da50dc1 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/Routing/CompartmentResourceTypesRouteConstraint.cs +++ b/src/Microsoft.Health.Fhir.Api/Features/Routing/CompartmentResourceTypesRouteConstraint.cs @@ -6,6 +6,7 @@ using EnsureThat; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using Microsoft.Health.Fhir.Core.Features.Routing; namespace Microsoft.Health.Fhir.Api.Features.Routing { diff --git a/src/Microsoft.Health.Fhir.Api/Features/Routing/CompartmentTypesRouteConstraint.cs b/src/Microsoft.Health.Fhir.Api/Features/Routing/CompartmentTypesRouteConstraint.cs index ba2644aa63..94c3b11c63 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/Routing/CompartmentTypesRouteConstraint.cs +++ b/src/Microsoft.Health.Fhir.Api/Features/Routing/CompartmentTypesRouteConstraint.cs @@ -6,6 +6,7 @@ using EnsureThat; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Core.Models; namespace Microsoft.Health.Fhir.Api.Features.Routing diff --git a/src/Microsoft.Health.Fhir.Api/Features/Routing/ResourceTypesRouteConstraint.cs b/src/Microsoft.Health.Fhir.Api/Features/Routing/ResourceTypesRouteConstraint.cs index 3a8577b050..33556b8ff7 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/Routing/ResourceTypesRouteConstraint.cs +++ b/src/Microsoft.Health.Fhir.Api/Features/Routing/ResourceTypesRouteConstraint.cs @@ -6,6 +6,7 @@ using EnsureThat; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Core.Models; namespace Microsoft.Health.Fhir.Api.Features.Routing diff --git a/src/Microsoft.Health.Fhir.Api/Features/Routing/SearchPostReroutingMiddleware.cs b/src/Microsoft.Health.Fhir.Api/Features/Routing/SearchPostReroutingMiddleware.cs index 2e11c8bb50..e639fa1ee4 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/Routing/SearchPostReroutingMiddleware.cs +++ b/src/Microsoft.Health.Fhir.Api/Features/Routing/SearchPostReroutingMiddleware.cs @@ -12,6 +12,7 @@ using EnsureThat; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; +using Microsoft.Health.Fhir.Core.Features.Routing; namespace Microsoft.Health.Fhir.Api.Features.Routing { diff --git a/src/Microsoft.Health.Fhir.Api/Features/Routing/UrlResolver.cs b/src/Microsoft.Health.Fhir.Api/Features/Routing/UrlResolver.cs index 3c0fb0b6c4..7b776f814d 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/Routing/UrlResolver.cs +++ b/src/Microsoft.Health.Fhir.Api/Features/Routing/UrlResolver.cs @@ -145,9 +145,9 @@ private Uri ResolveResourceUrl(string resourceId, string resourceTypeName, strin Request.Host.Value); } - public Uri ResolveRouteUrl(IReadOnlyCollection> unsupportedSearchParams = null, IReadOnlyList<(SearchParameterInfo searchParameterInfo, SortOrder sortOrder)> resultSortOrder = null, string continuationToken = null, bool removeTotalParameter = false) + public Uri ResolveRouteUrl(IReadOnlyCollection> unsupportedSearchParams = null, IReadOnlyList<(SearchParameterInfo searchParameterInfo, SortOrder sortOrder)> resultSortOrder = null, string continuationToken = null, bool removeTotalParameter = false, string includesContinuationToken = null, string routeNameOverride = null, IDictionary ambientRouteValuesOverride = null) { - string routeName = _fhirRequestContextAccessor.RequestContext.RouteName; + string routeName = routeNameOverride ?? _fhirRequestContextAccessor.RequestContext.RouteName; Debug.Assert(!string.IsNullOrWhiteSpace(routeName), "The routeName should not be null or empty."); @@ -215,12 +215,29 @@ public Uri ResolveRouteUrl(IReadOnlyCollection> unsupporte routeValues[KnownQueryParameterNames.ContinuationToken] = continuationToken; } + if (includesContinuationToken != null) + { + routeValues[KnownQueryParameterNames.IncludesContinuationToken] = includesContinuationToken; + } + + RouteValuesAddress routeValuesAddress = null; + if (ambientRouteValuesOverride != null) + { + routeValuesAddress = new RouteValuesAddress() + { + AmbientValues = new RouteValueDictionary(ambientRouteValuesOverride), + ExplicitValues = routeValues ?? new(), + RouteName = routeName, + }; + } + return GetRouteUri( ActionContext.HttpContext, routeName, routeValues, Request.Scheme, - Request.Host.Value); + Request.Host.Value, + routeValuesAddress); } public Uri ResolveRouteNameUrl(string routeName, IDictionary routeValues) @@ -328,7 +345,7 @@ public Uri ResolveOperationDefinitionUrl(string operationName) Request?.Host.Value); } - private Uri GetRouteUri(HttpContext httpContext, string routeName, RouteValueDictionary routeValues, string scheme, string host) + private Uri GetRouteUri(HttpContext httpContext, string routeName, RouteValueDictionary routeValues, string scheme, string host, RouteValuesAddress address = null) { var uriString = string.Empty; @@ -351,13 +368,27 @@ private Uri GetRouteUri(HttpContext httpContext, string routeName, RouteValueDic pathBase += "/"; } - uriString = _linkGenerator.GetUriByRouteValues( - httpContext, - routeName, - routeValues, - scheme, - new HostString(host), - pathBase); + if (address == null) + { + uriString = _linkGenerator.GetUriByRouteValues( + httpContext, + routeName, + routeValues, + scheme, + new HostString(host), + pathBase); + } + else + { + uriString = _linkGenerator.GetUriByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues, + scheme, + new HostString(host), + pathBase); + } } return new Uri(uriString); diff --git a/src/Microsoft.Health.Fhir.Api/Registration/FhirServerApplicationBuilderExtensions.cs b/src/Microsoft.Health.Fhir.Api/Registration/FhirServerApplicationBuilderExtensions.cs index eabd16d4a1..5577b0e1fe 100644 --- a/src/Microsoft.Health.Fhir.Api/Registration/FhirServerApplicationBuilderExtensions.cs +++ b/src/Microsoft.Health.Fhir.Api/Registration/FhirServerApplicationBuilderExtensions.cs @@ -14,10 +14,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Options; -using Microsoft.Health.Api.Registration; using Microsoft.Health.Fhir.Api.Configs; -using Microsoft.Health.Fhir.Api.Features.Routing; -using Microsoft.Health.Fhir.Core.Features.Cors; +using Microsoft.Health.Fhir.Core.Features.Routing; using Newtonsoft.Json; namespace Microsoft.AspNetCore.Builder diff --git a/src/Microsoft.Health.Fhir.Core.UnitTests/Features/Operations/Export/ExportJobTaskTests.cs b/src/Microsoft.Health.Fhir.Core.UnitTests/Features/Operations/Export/ExportJobTaskTests.cs index 87beb0f173..4f490dc2fa 100644 --- a/src/Microsoft.Health.Fhir.Core.UnitTests/Features/Operations/Export/ExportJobTaskTests.cs +++ b/src/Microsoft.Health.Fhir.Core.UnitTests/Features/Operations/Export/ExportJobTaskTests.cs @@ -159,7 +159,7 @@ public async Task GivenThereAreTwoPagesOfSearchResults_WhenExecuted_ThenCorrectS // Second search returns a search result without continuation token. _searchService.SearchAsync( null, - Arg.Is(CreateQueryParametersExpressionWithContinuationToken(ContinuationTokenConverter.Encode(continuationToken), KnownResourceTypes.Patient)), + Arg.Is(CreateQueryParametersExpressionWithContinuationToken(ContinuationTokenEncoder.Encode(continuationToken), KnownResourceTypes.Patient)), _cancellationToken, true, ResourceVersionType.Latest) @@ -199,7 +199,7 @@ public async Task GivenThereAreTwoPagesOfSearchResultsWithSinceParameter_WhenExe // Second search returns a search result without continuation token. _searchService.SearchAsync( null, - Arg.Is(CreateQueryParametersExpressionWithContinuationToken(ContinuationTokenConverter.Encode(continuationToken), _exportJobRecord.Since, KnownResourceTypes.Patient)), + Arg.Is(CreateQueryParametersExpressionWithContinuationToken(ContinuationTokenEncoder.Encode(continuationToken), _exportJobRecord.Since, KnownResourceTypes.Patient)), _cancellationToken, true) .Returns(x => @@ -239,7 +239,7 @@ public async Task GivenThereAreMultiplePagesOfSearchResults_WhenExecuted_ThenCor // Second search returns a search result with continuation token. _searchService.SearchAsync( null, - Arg.Is(CreateQueryParametersExpressionWithContinuationToken(ContinuationTokenConverter.Encode(continuationToken), KnownResourceTypes.Patient)), + Arg.Is(CreateQueryParametersExpressionWithContinuationToken(ContinuationTokenEncoder.Encode(continuationToken), KnownResourceTypes.Patient)), _cancellationToken, true) .Returns(x => @@ -254,7 +254,7 @@ public async Task GivenThereAreMultiplePagesOfSearchResults_WhenExecuted_ThenCor // Third search returns a search result without continuation token. _searchService.SearchAsync( null, - Arg.Is(CreateQueryParametersExpressionWithContinuationToken(ContinuationTokenConverter.Encode(newContinuationToken), KnownResourceTypes.Patient)), + Arg.Is(CreateQueryParametersExpressionWithContinuationToken(ContinuationTokenEncoder.Encode(newContinuationToken), KnownResourceTypes.Patient)), _cancellationToken, true) .Returns(x => @@ -295,7 +295,7 @@ public async Task GivenThereAreMultiplePagesOfSearchResultsWithSinceParameter_Wh // Second search returns a search result with continuation token. _searchService.SearchAsync( null, - Arg.Is(CreateQueryParametersExpressionWithContinuationToken(ContinuationTokenConverter.Encode(continuationToken), _exportJobRecord.Since, KnownResourceTypes.Patient)), + Arg.Is(CreateQueryParametersExpressionWithContinuationToken(ContinuationTokenEncoder.Encode(continuationToken), _exportJobRecord.Since, KnownResourceTypes.Patient)), _cancellationToken, true) .Returns(x => @@ -310,7 +310,7 @@ public async Task GivenThereAreMultiplePagesOfSearchResultsWithSinceParameter_Wh // Third search returns a search result without continuation token. _searchService.SearchAsync( null, - Arg.Is(CreateQueryParametersExpressionWithContinuationToken(ContinuationTokenConverter.Encode(newContinuationToken), _exportJobRecord.Since, KnownResourceTypes.Patient)), + Arg.Is(CreateQueryParametersExpressionWithContinuationToken(ContinuationTokenEncoder.Encode(newContinuationToken), _exportJobRecord.Since, KnownResourceTypes.Patient)), _cancellationToken, true) .Returns(x => @@ -1340,7 +1340,7 @@ public async Task GivenAGroupExportJobToResume_WhenExecuted_ThenAllPatientResour { // The ids aren't in the query parameters because of the reset ids = new string[] { "1", "2", "3" }; - continuationTokenIndex = int.Parse(ContinuationTokenConverter.Decode( + continuationTokenIndex = int.Parse(ContinuationTokenEncoder.Decode( x.ArgAt>>(1) .Where(x => x.Item1 == Core.Features.KnownQueryParameterNames.ContinuationToken) .Select(x => x.Item2).First())[2..]); diff --git a/src/Microsoft.Health.Fhir.Core.UnitTests/Features/Search/ContinuationTokenConverterTests.cs b/src/Microsoft.Health.Fhir.Core.UnitTests/Features/Search/ContinuationTokenEncoderTests.cs similarity index 83% rename from src/Microsoft.Health.Fhir.Core.UnitTests/Features/Search/ContinuationTokenConverterTests.cs rename to src/Microsoft.Health.Fhir.Core.UnitTests/Features/Search/ContinuationTokenEncoderTests.cs index 9a389615a0..0fd0d508e9 100644 --- a/src/Microsoft.Health.Fhir.Core.UnitTests/Features/Search/ContinuationTokenConverterTests.cs +++ b/src/Microsoft.Health.Fhir.Core.UnitTests/Features/Search/ContinuationTokenEncoderTests.cs @@ -15,15 +15,15 @@ namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Search { [Trait(Traits.OwningTeam, OwningTeam.Fhir)] [Trait(Traits.Category, Categories.Search)] - public class ContinuationTokenConverterTests + public class ContinuationTokenEncoderTests { [Fact] public void GivenAString_WhenEcodingAndDecoding_ThenOriginalStringIsPreserved() { var data = Guid.NewGuid().ToString(); - var encoded = ContinuationTokenConverter.Encode(data); - var decoded = ContinuationTokenConverter.Decode(encoded); + var encoded = ContinuationTokenEncoder.Encode(data); + var decoded = ContinuationTokenEncoder.Decode(encoded); Assert.Equal(data, decoded); } @@ -35,7 +35,7 @@ public void GivenAnOldStringInBase64_WhenDecoding_ThenOriginalStringIsPreserved( var encodedPrevious = Convert.ToBase64String(Encoding.UTF8.GetBytes(data)); - var decoded = ContinuationTokenConverter.Decode(encodedPrevious); + var decoded = ContinuationTokenEncoder.Decode(encodedPrevious); Assert.Equal(data, decoded); } @@ -47,7 +47,7 @@ public void GivenAnInvalidString_WhenDecoding_ThenAnErrorIsThrown() var encodedPrevious = Convert.ToBase64String(Encoding.UTF8.GetBytes(data)).Insert(5, "aaaafffff"); - Assert.Throws(() => ContinuationTokenConverter.Decode(encodedPrevious)); + Assert.Throws(() => ContinuationTokenEncoder.Decode(encodedPrevious)); } [Fact] @@ -55,7 +55,7 @@ public void GivenShortBase64WhenDecoding_ThenCorrectValueIsReturned() { var data = "YWJj"; - var decoded = ContinuationTokenConverter.Decode(data); + var decoded = ContinuationTokenEncoder.Decode(data); Assert.Equal("abc", decoded); } diff --git a/src/Microsoft.Health.Fhir.Core/Configs/CoreFeatureConfiguration.cs b/src/Microsoft.Health.Fhir.Core/Configs/CoreFeatureConfiguration.cs index fee91964b5..7c8427323f 100644 --- a/src/Microsoft.Health.Fhir.Core/Configs/CoreFeatureConfiguration.cs +++ b/src/Microsoft.Health.Fhir.Core/Configs/CoreFeatureConfiguration.cs @@ -45,7 +45,7 @@ public class CoreFeatureConfiguration /// /// Gets or sets the default value for included search results. /// - public int DefaultIncludeCountPerSearch { get; set; } = 100; + public int DefaultIncludeCountPerSearch { get; set; } = 500; /// /// Gets or sets a value whether we need to run profile validation during resource creation. diff --git a/src/Microsoft.Health.Fhir.Core/Extensions/SearchServiceExtensions.cs b/src/Microsoft.Health.Fhir.Core/Extensions/SearchServiceExtensions.cs index fdd3d0ea33..99a78f8856 100644 --- a/src/Microsoft.Health.Fhir.Core/Extensions/SearchServiceExtensions.cs +++ b/src/Microsoft.Health.Fhir.Core/Extensions/SearchServiceExtensions.cs @@ -80,7 +80,7 @@ public static class SearchServiceExtensions var searchParameters = new List>(filteredParameters); if (!string.IsNullOrEmpty(lastContinuationToken)) { - searchParameters.Add(Tuple.Create(KnownQueryParameterNames.ContinuationToken, ContinuationTokenConverter.Encode(lastContinuationToken))); + searchParameters.Add(Tuple.Create(KnownQueryParameterNames.ContinuationToken, ContinuationTokenEncoder.Encode(lastContinuationToken))); } statistics.Iterate(); diff --git a/src/Microsoft.Health.Fhir.Core/Features/KnownQueryParameterNames.cs b/src/Microsoft.Health.Fhir.Core/Features/KnownQueryParameterNames.cs index abf121dbac..5b568ec5a6 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/KnownQueryParameterNames.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/KnownQueryParameterNames.cs @@ -125,5 +125,10 @@ public static class KnownQueryParameterNames /// Used by export to specify the number of resources to be processed by the search engine. /// public const string MaxCount = "_maxCount"; + + /// + /// The include continuation token parameter. + /// + public const string IncludesContinuationToken = "includesCt"; } } diff --git a/src/Microsoft.Health.Fhir.Core/Features/Operations/Export/ExportJobTask.cs b/src/Microsoft.Health.Fhir.Core/Features/Operations/Export/ExportJobTask.cs index fa4261b0ec..0081a8811f 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/Operations/Export/ExportJobTask.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Operations/Export/ExportJobTask.cs @@ -797,7 +797,7 @@ private async Task ProcessProgressChange( { // Update the continuation token in local cache and queryParams. // We will add or udpate the continuation token in the query parameters list. - progress.UpdateContinuationToken(ContinuationTokenConverter.Encode(continuationToken)); + progress.UpdateContinuationToken(ContinuationTokenEncoder.Encode(continuationToken)); bool replacedContinuationToken = false; for (int index = 0; index < queryParametersList.Count; index++) diff --git a/src/Microsoft.Health.Fhir.Core/Features/Operations/Reindex/ReindexJobTask.cs b/src/Microsoft.Health.Fhir.Core/Features/Operations/Reindex/ReindexJobTask.cs index 88983ff9bc..4501dcd803 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/Operations/Reindex/ReindexJobTask.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Operations/Reindex/ReindexJobTask.cs @@ -474,7 +474,7 @@ private async Task ProcessQueryAsync(ReindexJobQueryStatu // For cases like retry or stale query we don't want to start another chain. if (!string.IsNullOrEmpty(results?.ContinuationToken) && !query.CreatedChild) { - var encodedContinuationToken = ContinuationTokenConverter.Encode(results.ContinuationToken); + var encodedContinuationToken = ContinuationTokenEncoder.Encode(results.ContinuationToken); var nextQuery = new ReindexJobQueryStatus(query.ResourceType, encodedContinuationToken) { LastModified = Clock.UtcNow, diff --git a/src/Microsoft.Health.Fhir.Core/Features/Routing/IUrlResolver.cs b/src/Microsoft.Health.Fhir.Core/Features/Routing/IUrlResolver.cs index c923c50b0c..d54e085cea 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/Routing/IUrlResolver.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Routing/IUrlResolver.cs @@ -46,8 +46,11 @@ public interface IUrlResolver /// The order the results are sorted in /// The continuation token. /// True if the _total parameter should be removed from the url, false otherwise. + /// The continuation token for $includes operation. + /// The route name to override one in the http context. + /// The ambient route values to override the route-values address in the http context. /// The URL. - Uri ResolveRouteUrl(IReadOnlyCollection> unsupportedSearchParams = null, IReadOnlyList<(SearchParameterInfo searchParameterInfo, SortOrder sortOrder)> resultSortOrder = null, string continuationToken = null, bool removeTotalParameter = false); + Uri ResolveRouteUrl(IReadOnlyCollection> unsupportedSearchParams = null, IReadOnlyList<(SearchParameterInfo searchParameterInfo, SortOrder sortOrder)> resultSortOrder = null, string continuationToken = null, bool removeTotalParameter = false, string includesContinuationToken = null, string routeNameOverride = null, IDictionary ambientRouteValuesOverride = null); /// /// Resolves the URL for the specified routeName. diff --git a/src/Microsoft.Health.Fhir.Api/Features/Routing/KnownActionParameterNames.cs b/src/Microsoft.Health.Fhir.Core/Features/Routing/KnownActionParameterNames.cs similarity index 93% rename from src/Microsoft.Health.Fhir.Api/Features/Routing/KnownActionParameterNames.cs rename to src/Microsoft.Health.Fhir.Core/Features/Routing/KnownActionParameterNames.cs index b949615354..cfcc32a4f0 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/Routing/KnownActionParameterNames.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Routing/KnownActionParameterNames.cs @@ -3,7 +3,7 @@ // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // ------------------------------------------------------------------------------------------------- -namespace Microsoft.Health.Fhir.Api.Features.Routing +namespace Microsoft.Health.Fhir.Core.Features.Routing { internal class KnownActionParameterNames { diff --git a/src/Microsoft.Health.Fhir.Api/Features/Routing/KnownRoutes.cs b/src/Microsoft.Health.Fhir.Core/Features/Routing/KnownRoutes.cs similarity index 97% rename from src/Microsoft.Health.Fhir.Api/Features/Routing/KnownRoutes.cs rename to src/Microsoft.Health.Fhir.Core/Features/Routing/KnownRoutes.cs index a2a234f28a..91692df4fa 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/Routing/KnownRoutes.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Routing/KnownRoutes.cs @@ -5,7 +5,7 @@ using OperationsConstants = Microsoft.Health.Fhir.Core.Features.Operations.OperationsConstants; -namespace Microsoft.Health.Fhir.Api.Features.Routing +namespace Microsoft.Health.Fhir.Core.Features.Routing { internal class KnownRoutes { @@ -93,5 +93,8 @@ internal class KnownRoutes public const string BulkDeleteOperationDefinition = OperationDefinition + "/" + OperationsConstants.BulkDelete; public const string ResourceTypeBulkDeleteOperationDefinition = OperationDefinition + "/" + OperationsConstants.ResourceTypeBulkDelete; public const string BulkDeleteSoftDeletedOperationDefinition = OperationDefinition + "/" + OperationsConstants.BulkDeleteSoftDeleted; + + public const string Includes = "$includes"; + public const string IncludesResourceType = ResourceType + "/" + Includes; } } diff --git a/src/Microsoft.Health.Fhir.Api/Features/Routing/RouteNames.cs b/src/Microsoft.Health.Fhir.Core/Features/Routing/RouteNames.cs similarity index 97% rename from src/Microsoft.Health.Fhir.Api/Features/Routing/RouteNames.cs rename to src/Microsoft.Health.Fhir.Core/Features/Routing/RouteNames.cs index 6d2e3d56ac..6298b504c9 100644 --- a/src/Microsoft.Health.Fhir.Api/Features/Routing/RouteNames.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Routing/RouteNames.cs @@ -3,7 +3,7 @@ // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // ------------------------------------------------------------------------------------------------- -namespace Microsoft.Health.Fhir.Api.Features.Routing +namespace Microsoft.Health.Fhir.Core.Features.Routing { internal static class RouteNames { @@ -80,5 +80,7 @@ internal static class RouteNames internal const string BulkDeleteDefinition = nameof(BulkDeleteDefinition); internal const string BulkDeleteSoftDeletedDefinition = nameof(BulkDeleteSoftDeletedDefinition); + + internal const string Includes = nameof(Includes); } } diff --git a/src/Microsoft.Health.Fhir.Core/Features/Search/ContinuationTokenConverter.cs b/src/Microsoft.Health.Fhir.Core/Features/Search/ContinuationTokenEncoder.cs similarity index 92% rename from src/Microsoft.Health.Fhir.Core/Features/Search/ContinuationTokenConverter.cs rename to src/Microsoft.Health.Fhir.Core/Features/Search/ContinuationTokenEncoder.cs index 8fb67e99db..296bc1ab1b 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/Search/ContinuationTokenConverter.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Search/ContinuationTokenEncoder.cs @@ -13,7 +13,7 @@ namespace Microsoft.Health.Fhir.Core.Features.Search { - public sealed class ContinuationTokenConverter + public sealed class ContinuationTokenEncoder { private static readonly RecyclableMemoryStreamManager StreamManager = new(); private const string TokenVersion = "v2|"; @@ -26,7 +26,7 @@ public static string Decode(string encodedContinuationToken) try { - using MemoryStream memoryStream = StreamManager.GetStream(nameof(ContinuationTokenConverter), continuationTokenBytes, 0, continuationTokenBytes.Length); + using MemoryStream memoryStream = StreamManager.GetStream(nameof(ContinuationTokenEncoder), continuationTokenBytes, 0, continuationTokenBytes.Length); using var deflate = new DeflateStream(memoryStream, CompressionMode.Decompress); using var reader = new StreamReader(deflate, Encoding.UTF8); @@ -54,7 +54,7 @@ public static string Encode(string continuationToken) { EnsureArg.IsNotEmptyOrWhiteSpace(continuationToken); - using RecyclableMemoryStream memoryStream = StreamManager.GetStream(tag: nameof(ContinuationTokenConverter)); + using RecyclableMemoryStream memoryStream = StreamManager.GetStream(tag: nameof(ContinuationTokenEncoder)); using var deflate = new DeflateStream(memoryStream, CompressionLevel.Fastest); using var writer = new StreamWriter(deflate, Encoding.UTF8); diff --git a/src/Microsoft.Health.Fhir.Core/Features/Search/ISearchOptionsFactory.cs b/src/Microsoft.Health.Fhir.Core/Features/Search/ISearchOptionsFactory.cs index fc62a2a55f..7d4536968c 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/Search/ISearchOptionsFactory.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Search/ISearchOptionsFactory.cs @@ -15,7 +15,8 @@ SearchOptions Create( IReadOnlyList> queryParameters, bool isAsyncOperation = false, ResourceVersionType resourceVersionTypes = ResourceVersionType.Latest, - bool onlyIds = false); + bool onlyIds = false, + bool isIncludesOperation = false); SearchOptions Create( string compartmentType, @@ -25,6 +26,7 @@ SearchOptions Create( bool isAsyncOperation = false, bool useSmartCompartmentDefinition = false, ResourceVersionType resourceVersionTypes = ResourceVersionType.Latest, - bool onlyIds = false); + bool onlyIds = false, + bool isIncludesOperation = false); } } diff --git a/src/Microsoft.Health.Fhir.Core/Features/Search/ISearchService.cs b/src/Microsoft.Health.Fhir.Core/Features/Search/ISearchService.cs index 1561ddd012..a874e0dc35 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/Search/ISearchService.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Search/ISearchService.cs @@ -25,6 +25,7 @@ public interface ISearchService /// Whether the search is part of an async operation. /// Which version types (latest, soft-deleted, history) to include in search. /// Whether to return only the resource ids, not the full resource + /// Whether the search is to query remaining include resources. /// A representing the result. Task SearchAsync( string resourceType, @@ -32,7 +33,8 @@ Task SearchAsync( CancellationToken cancellationToken, bool isAsyncOperation = false, ResourceVersionType resourceVersionTypes = ResourceVersionType.Latest, - bool onlyIds = false); + bool onlyIds = false, + bool isIncludesOperation = false); /// /// Searches the resources using the . diff --git a/src/Microsoft.Health.Fhir.Core/Features/Search/SearchOptions.cs b/src/Microsoft.Health.Fhir.Core/Features/Search/SearchOptions.cs index 149170fb90..6f35bb8d62 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/Search/SearchOptions.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Search/SearchOptions.cs @@ -51,6 +51,7 @@ internal SearchOptions(SearchOptions other) QueryHints = other.QueryHints; ResourceVersionTypes = other.ResourceVersionTypes; + IncludesContinuationToken = other.IncludesContinuationToken; } /// @@ -150,6 +151,16 @@ internal set /// public bool IsLargeAsyncOperation { get; internal set; } + /// + /// Flag for $includes operation. + /// + public bool IsIncludesOperation => !string.IsNullOrEmpty(IncludesContinuationToken); + + /// + /// Gets the optional continuation token for $includes operation. + /// + public string IncludesContinuationToken { get; internal set; } + /// /// Performs a shallow clone of this instance /// diff --git a/src/Microsoft.Health.Fhir.Core/Features/Search/SearchResult.cs b/src/Microsoft.Health.Fhir.Core/Features/Search/SearchResult.cs index c043389d4c..d6d22c64ad 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/Search/SearchResult.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Search/SearchResult.cs @@ -24,12 +24,14 @@ public class SearchResult /// The parameters the results are sorted by /// The list of unsupported search parameters. /// List of search issues found. + /// The continuation token for $includes operation. public SearchResult( IEnumerable results, string continuationToken, IReadOnlyList<(SearchParameterInfo searchParameterInfo, SortOrder sortOrder)> sortOrder, IReadOnlyList> unsupportedSearchParameters, - IReadOnlyList searchIssues = null) + IReadOnlyList searchIssues = null, + string includesContinuationToken = null) { EnsureArg.IsNotNull(results, nameof(results)); EnsureArg.IsNotNull(unsupportedSearchParameters, nameof(unsupportedSearchParameters)); @@ -39,6 +41,7 @@ public SearchResult( ContinuationToken = continuationToken; SortOrder = sortOrder; SearchIssues = searchIssues ?? Array.Empty(); + IncludesContinuationToken = includesContinuationToken; } public SearchResult(int totalCount, IReadOnlyList> unsupportedSearchParameters) @@ -81,6 +84,11 @@ public SearchResult(int totalCount, IReadOnlyList> unsuppo /// public string ContinuationToken { get; } + /// + /// Gets the continuation token for $includes operation. + /// + public string IncludesContinuationToken { get; } + /// /// A list of issues that will be returned inside a search result. /// diff --git a/src/Microsoft.Health.Fhir.Core/Features/Search/SearchService.cs b/src/Microsoft.Health.Fhir.Core/Features/Search/SearchService.cs index 523e1159ef..85beff452e 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/Search/SearchService.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Search/SearchService.cs @@ -47,15 +47,16 @@ protected SearchService(ISearchOptionsFactory searchOptionsFactory, IFhirDataSto } /// - public async Task SearchAsync( + public virtual async Task SearchAsync( string resourceType, IReadOnlyList> queryParameters, CancellationToken cancellationToken, bool isAsyncOperation = false, ResourceVersionType resourceVersionTypes = ResourceVersionType.Latest, - bool onlyIds = false) + bool onlyIds = false, + bool isIncludesOperation = false) { - SearchOptions searchOptions = _searchOptionsFactory.Create(resourceType, queryParameters, isAsyncOperation, resourceVersionTypes, onlyIds); + SearchOptions searchOptions = _searchOptionsFactory.Create(resourceType, queryParameters, isAsyncOperation, resourceVersionTypes, onlyIds, isIncludesOperation); // Execute the actual search. return await SearchAsync(searchOptions, cancellationToken); diff --git a/src/Microsoft.Health.Fhir.Core/Messages/Search/SearchResourceRequest.cs b/src/Microsoft.Health.Fhir.Core/Messages/Search/SearchResourceRequest.cs index 36e3cce2f9..f505fa952e 100644 --- a/src/Microsoft.Health.Fhir.Core/Messages/Search/SearchResourceRequest.cs +++ b/src/Microsoft.Health.Fhir.Core/Messages/Search/SearchResourceRequest.cs @@ -11,14 +11,17 @@ namespace Microsoft.Health.Fhir.Core.Messages.Search { public class SearchResourceRequest : IRequest { - public SearchResourceRequest(string resourceType, IReadOnlyList> queries) + public SearchResourceRequest(string resourceType, IReadOnlyList> queries, bool isIncludesRequest = false) { ResourceType = resourceType; Queries = queries; + IsIncludesRequest = isIncludesRequest; } public string ResourceType { get; } public IReadOnlyList> Queries { get; set; } + + public bool IsIncludesRequest { get; } = false; } } diff --git a/src/Microsoft.Health.Fhir.Core/Resources.Designer.cs b/src/Microsoft.Health.Fhir.Core/Resources.Designer.cs index a7532e80a9..1b806132e8 100644 --- a/src/Microsoft.Health.Fhir.Core/Resources.Designer.cs +++ b/src/Microsoft.Health.Fhir.Core/Resources.Designer.cs @@ -1690,6 +1690,15 @@ internal static string UnsupportedConfigurationMessage { } } + /// + /// Looks up a localized string similar to The '$includes' operation is not supported for CosmosDB data store.. + /// + internal static string UnsupportedIncludesOperation { + get { + return ResourceManager.GetString("UnsupportedIncludesOperation", resourceCulture); + } + } + /// /// Looks up a localized string similar to Only Code, ResourceType, and Url are supported as query parameters for $status. /// diff --git a/src/Microsoft.Health.Fhir.Core/Resources.resx b/src/Microsoft.Health.Fhir.Core/Resources.resx index 2408c458e3..8801b3bed8 100644 --- a/src/Microsoft.Health.Fhir.Core/Resources.resx +++ b/src/Microsoft.Health.Fhir.Core/Resources.resx @@ -757,4 +757,7 @@ Error occurred during an operation that is dependent on the customer-managed key. Use https://go.microsoft.com/fwlink/?linkid=2300268 to troubleshoot the issue. - \ No newline at end of file + + The '$includes' operation is not supported for CosmosDB data store. + + diff --git a/src/Microsoft.Health.Fhir.CosmosDb/Features/Search/FhirCosmosSearchService.cs b/src/Microsoft.Health.Fhir.CosmosDb/Features/Search/FhirCosmosSearchService.cs index 3717558f6a..95a7052ebd 100644 --- a/src/Microsoft.Health.Fhir.CosmosDb/Features/Search/FhirCosmosSearchService.cs +++ b/src/Microsoft.Health.Fhir.CosmosDb/Features/Search/FhirCosmosSearchService.cs @@ -207,6 +207,23 @@ public override async Task SearchAsync( return searchResult; } + public override Task SearchAsync( + string resourceType, + IReadOnlyList> queryParameters, + CancellationToken cancellationToken, + bool isAsyncOperation = false, + ResourceVersionType resourceVersionTypes = ResourceVersionType.Latest, + bool onlyIds = false, + bool isIncludesOperation = false) + { + if (isIncludesOperation) + { + throw new SearchOperationNotSupportedException(Fhir.Core.Resources.UnsupportedIncludesOperation); + } + + return base.SearchAsync(resourceType, queryParameters, cancellationToken, isAsyncOperation, resourceVersionTypes, onlyIds, isIncludesOperation); + } + private async Task> PerformChainedSearch(SearchOptions searchOptions, IReadOnlyList chainedExpressions, CancellationToken cancellationToken) { var chainedReferences = new List(); diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/ApiNotifications/ApiNotificationMiddlewareTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/ApiNotifications/ApiNotificationMiddlewareTests.cs index adbeca4c6c..92e74e1404 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/ApiNotifications/ApiNotificationMiddlewareTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/ApiNotifications/ApiNotificationMiddlewareTests.cs @@ -10,8 +10,8 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Health.Core.Features.Context; using Microsoft.Health.Fhir.Api.Features.ApiNotifications; -using Microsoft.Health.Fhir.Api.Features.Routing; using Microsoft.Health.Fhir.Core.Features.Context; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Core.UnitTests.Features.Context; using Microsoft.Health.Fhir.Tests.Common; using Microsoft.Health.Test.Utilities; diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/FhirRequestContextRouteDataPopulatingFilterAttributeTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/FhirRequestContextRouteDataPopulatingFilterAttributeTests.cs index 45c5d2c73e..fc89b1bd12 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/FhirRequestContextRouteDataPopulatingFilterAttributeTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/FhirRequestContextRouteDataPopulatingFilterAttributeTests.cs @@ -15,10 +15,10 @@ using Microsoft.Health.Api.Features.Audit; using Microsoft.Health.Core.Features.Context; using Microsoft.Health.Fhir.Api.Features.Filters; -using Microsoft.Health.Fhir.Api.Features.Routing; using Microsoft.Health.Fhir.Core.Extensions; using Microsoft.Health.Fhir.Core.Features; using Microsoft.Health.Fhir.Core.Features.Context; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Core.UnitTests.Features.Context; using Microsoft.Health.Fhir.Tests.Common; using Microsoft.Health.Fhir.ValueSets; diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/SearchParameterFilterAttributeTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/SearchParameterFilterAttributeTests.cs index f86bd8b8d8..bb739f3442 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/SearchParameterFilterAttributeTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/SearchParameterFilterAttributeTests.cs @@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Routing; using Microsoft.Health.Fhir.Api.Features.Filters; -using Microsoft.Health.Fhir.Api.Features.Routing; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Shared.Core.Features.Search.Parameters; using Microsoft.Health.Fhir.Tests.Common; using Microsoft.Health.Test.Utilities; diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/ValidateIdSegmentAttributeTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/ValidateIdSegmentAttributeTests.cs index 466956c932..faa00b468e 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/ValidateIdSegmentAttributeTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/ValidateIdSegmentAttributeTests.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; -using System.Linq; using Hl7.Fhir.Model; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -13,9 +12,8 @@ using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Routing; using Microsoft.Health.Fhir.Api.Features.Filters; -using Microsoft.Health.Fhir.Api.Features.Routing; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Core.Features.Validation; -using Microsoft.Health.Fhir.Core.Models; using Microsoft.Health.Fhir.Tests.Common; using Microsoft.Health.Test.Utilities; using Xunit; diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/ValidateResourceIdFilterTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/ValidateResourceIdFilterTests.cs index f6a96f6341..d93b94800e 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/ValidateResourceIdFilterTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/ValidateResourceIdFilterTests.cs @@ -13,7 +13,7 @@ using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Routing; using Microsoft.Health.Fhir.Api.Features.Filters; -using Microsoft.Health.Fhir.Api.Features.Routing; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Core.Features.Validation; using Microsoft.Health.Fhir.Core.Models; using Microsoft.Health.Fhir.Tests.Common; diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/ValidateResourceTypeFilterTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/ValidateResourceTypeFilterTests.cs index dab14f8035..1b503f7896 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/ValidateResourceTypeFilterTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Filters/ValidateResourceTypeFilterTests.cs @@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Routing; using Microsoft.Health.Fhir.Api.Features.Filters; -using Microsoft.Health.Fhir.Api.Features.Routing; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Core.Features.Validation; using Microsoft.Health.Fhir.Tests.Common; using Microsoft.Health.Test.Utilities; diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/FhirJsonInputFormatterTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/FhirJsonInputFormatterTests.cs index b6cbd86405..224af00d58 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/FhirJsonInputFormatterTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/FhirJsonInputFormatterTests.cs @@ -17,7 +17,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.Health.Fhir.Api.Features.Formatters; -using Microsoft.Health.Fhir.Api.Features.Routing; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Tests.Common; using Microsoft.Health.Test.Utilities; using Newtonsoft.Json.Linq; diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/FhirXmlInputFormatterTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/FhirXmlInputFormatterTests.cs index f55117b395..0014735ccd 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/FhirXmlInputFormatterTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Formatters/FhirXmlInputFormatterTests.cs @@ -16,7 +16,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.Health.Fhir.Api.Features.Formatters; -using Microsoft.Health.Fhir.Api.Features.Routing; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Tests.Common; using Microsoft.Health.Test.Utilities; using NSubstitute; diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Routing/CompartmentTypesRouteConstraintTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Routing/CompartmentTypesRouteConstraintTests.cs index ce7338ab39..b737f9c579 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Routing/CompartmentTypesRouteConstraintTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Routing/CompartmentTypesRouteConstraintTests.cs @@ -8,8 +8,8 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Health.Fhir.Api.Features.Routing; using Microsoft.Health.Fhir.Api.Modules; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Tests.Common; using Microsoft.Health.Test.Utilities; using Xunit; diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Routing/ResourceTypesRouteConstraintTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Routing/ResourceTypesRouteConstraintTests.cs index 6a50065fa3..c962305cde 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Routing/ResourceTypesRouteConstraintTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Routing/ResourceTypesRouteConstraintTests.cs @@ -8,8 +8,8 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Health.Fhir.Api.Features.Routing; using Microsoft.Health.Fhir.Api.Modules; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Tests.Common; using Microsoft.Health.Test.Utilities; using Xunit; diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Routing/UrlResolverTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Routing/UrlResolverTests.cs index c64661bc63..97a6d98757 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Routing/UrlResolverTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Routing/UrlResolverTests.cs @@ -19,13 +19,13 @@ using Microsoft.Health.Fhir.Core.Features; using Microsoft.Health.Fhir.Core.Features.Context; using Microsoft.Health.Fhir.Core.Features.Operations; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Core.Features.Search; using Microsoft.Health.Fhir.Core.Models; using Microsoft.Health.Fhir.Tests.Common; using Microsoft.Health.Test.Utilities; using NSubstitute; using Xunit; -using Endpoint = Microsoft.AspNetCore.Http.Endpoint; namespace Microsoft.Health.Fhir.Api.UnitTests.Features.Routing { diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/ConvertDataController.cs b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/ConvertDataController.cs index 2016575414..8f535c6f0d 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/ConvertDataController.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/ConvertDataController.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; using EnsureThat; using Hl7.Fhir.Model; @@ -16,12 +15,12 @@ using Microsoft.Extensions.Options; using Microsoft.Health.Api.Features.Audit; using Microsoft.Health.Fhir.Api.Features.Filters; -using Microsoft.Health.Fhir.Api.Features.Routing; using Microsoft.Health.Fhir.Core.Configs; using Microsoft.Health.Fhir.Core.Exceptions; using Microsoft.Health.Fhir.Core.Features.Operations; using Microsoft.Health.Fhir.Core.Features.Operations.ConvertData; using Microsoft.Health.Fhir.Core.Features.Operations.ConvertData.Models; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Core.Messages.ConvertData; using Microsoft.Health.Fhir.TemplateManagement.Models; using Microsoft.Health.Fhir.ValueSets; diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/EverythingController.cs b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/EverythingController.cs index 3e8bfcc179..cde08c7e97 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/EverythingController.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/EverythingController.cs @@ -16,10 +16,10 @@ using Microsoft.Health.Core.Features.Context; using Microsoft.Health.Fhir.Api.Features.ActionResults; using Microsoft.Health.Fhir.Api.Features.Filters; -using Microsoft.Health.Fhir.Api.Features.Routing; using Microsoft.Health.Fhir.Core.Features; using Microsoft.Health.Fhir.Core.Features.Context; using Microsoft.Health.Fhir.Core.Features.Operations.Everything; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Core.Messages.Everything; using Microsoft.Health.Fhir.Core.Models; using Microsoft.Health.Fhir.ValueSets; diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/IncludesController.cs b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/IncludesController.cs new file mode 100644 index 0000000000..f3d6227711 --- /dev/null +++ b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/IncludesController.cs @@ -0,0 +1,53 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System.Threading.Tasks; +using EnsureThat; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Microsoft.Health.Api.Features.Audit; +using Microsoft.Health.Fhir.Api.Extensions; +using Microsoft.Health.Fhir.Api.Features.ActionResults; +using Microsoft.Health.Fhir.Api.Features.Filters; +using Microsoft.Health.Fhir.Api.Features.Routing; +using Microsoft.Health.Fhir.Core.Configs; +using Microsoft.Health.Fhir.Core.Extensions; +using Microsoft.Health.Fhir.Core.Features.Routing; +using Microsoft.Health.Fhir.Core.Models; +using Microsoft.Health.Fhir.Core.Registration; +using Microsoft.Health.Fhir.ValueSets; + +namespace Microsoft.Health.Fhir.Api.Controllers +{ + [ServiceFilter(typeof(OperationOutcomeExceptionFilterAttribute))] + public class IncludesController : Controller + { + private readonly IMediator _mediator; + private readonly CoreFeatureConfiguration _coreFeaturesConfig; + + public IncludesController(IMediator mediator, IOptions coreFeatures) + { + EnsureArg.IsNotNull(mediator, nameof(mediator)); + EnsureArg.IsNotNull(coreFeatures?.Value, nameof(coreFeatures)); + + _mediator = mediator; + _coreFeaturesConfig = coreFeatures.Value; + } + + [HttpGet] + [Route(KnownRoutes.IncludesResourceType, Name = RouteNames.Includes)] + [AuditEventType(AuditEventSubType.SearchSystem)] + public async Task Search(string typeParameter) + { + ResourceElement response = await _mediator.SearchIncludeResourceAsync( + typeParameter, + Request.GetQueriesForSearch(), + HttpContext.RequestAborted); + + return FhirResult.Create(response); + } + } +} diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/MemberMatchController.cs b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/MemberMatchController.cs index f73d60d096..9c7f809fdc 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/MemberMatchController.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/MemberMatchController.cs @@ -12,9 +12,9 @@ using Microsoft.Health.Api.Features.Audit; using Microsoft.Health.Fhir.Api.Features.ActionResults; using Microsoft.Health.Fhir.Api.Features.Filters; -using Microsoft.Health.Fhir.Api.Features.Routing; using Microsoft.Health.Fhir.Core.Exceptions; using Microsoft.Health.Fhir.Core.Extensions; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Core.Messages.MemberMatch; using Microsoft.Health.Fhir.Core.Models; using Microsoft.Health.Fhir.ValueSets; diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/OperationDefinitionController.cs b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/OperationDefinitionController.cs index 23ceaf7a9f..3b5e065434 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/OperationDefinitionController.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/OperationDefinitionController.cs @@ -13,10 +13,10 @@ using Microsoft.Health.Fhir.Api.Configs; using Microsoft.Health.Fhir.Api.Features.ActionResults; using Microsoft.Health.Fhir.Api.Features.Filters; -using Microsoft.Health.Fhir.Api.Features.Routing; using Microsoft.Health.Fhir.Core.Configs; using Microsoft.Health.Fhir.Core.Exceptions; using Microsoft.Health.Fhir.Core.Features.Operations; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Core.Messages.Operation; using Microsoft.Health.Fhir.Shared.Core.Extensions; diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/SearchParameterController.cs b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/SearchParameterController.cs index 70653303cd..5ff7f492d7 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/SearchParameterController.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/SearchParameterController.cs @@ -16,11 +16,11 @@ using Microsoft.Health.Fhir.Api.Extensions; using Microsoft.Health.Fhir.Api.Features.ActionResults; using Microsoft.Health.Fhir.Api.Features.Filters; -using Microsoft.Health.Fhir.Api.Features.Routing; using Microsoft.Health.Fhir.Core.Configs; using Microsoft.Health.Fhir.Core.Exceptions; using Microsoft.Health.Fhir.Core.Features.Operations; using Microsoft.Health.Fhir.Core.Features.Operations.SearchParameterState; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Core.Features.Search.Registry; using Microsoft.Health.Fhir.Core.Messages.SearchParameterState; using Microsoft.Health.Fhir.Core.Registration; diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/ValidateController.cs b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/ValidateController.cs index 329aca4b3a..9f262273ce 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/ValidateController.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/ValidateController.cs @@ -13,9 +13,9 @@ using Microsoft.Health.Api.Features.Audit; using Microsoft.Health.Fhir.Api.Features.ActionResults; using Microsoft.Health.Fhir.Api.Features.Filters; -using Microsoft.Health.Fhir.Api.Features.Routing; using Microsoft.Health.Fhir.Core.Extensions; using Microsoft.Health.Fhir.Core.Features.Persistence; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Core.Messages.Operation; using Microsoft.Health.Fhir.Core.Models; using Microsoft.Health.Fhir.ValueSets; diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/FhirRequestContextRouteDataPopulatingFilterAttribute.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/FhirRequestContextRouteDataPopulatingFilterAttribute.cs index 8f01bf6540..399c81fda0 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/FhirRequestContextRouteDataPopulatingFilterAttribute.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/FhirRequestContextRouteDataPopulatingFilterAttribute.cs @@ -10,9 +10,9 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Health.Api.Features.Audit; using Microsoft.Health.Core.Features.Context; -using Microsoft.Health.Fhir.Api.Features.Routing; using Microsoft.Health.Fhir.Core.Features; using Microsoft.Health.Fhir.Core.Features.Context; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.ValueSets; namespace Microsoft.Health.Fhir.Api.Features.Filters diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/SearchParameterFilterAttribute.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/SearchParameterFilterAttribute.cs index b8de1e4e6c..11c1b1af8a 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/SearchParameterFilterAttribute.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/SearchParameterFilterAttribute.cs @@ -7,7 +7,7 @@ using EnsureThat; using Hl7.Fhir.Model; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Health.Fhir.Api.Features.Routing; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Shared.Core.Features.Search.Parameters; using Task = System.Threading.Tasks.Task; diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/ValidateIdSegmentAttribute.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/ValidateIdSegmentAttribute.cs index 1c67bf68c3..c9cccc8093 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/ValidateIdSegmentAttribute.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/ValidateIdSegmentAttribute.cs @@ -7,9 +7,8 @@ using System.Collections.Generic; using EnsureThat; using FluentValidation.Results; -using Hl7.Fhir.Model; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Health.Fhir.Api.Features.Routing; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Core.Features.Validation; namespace Microsoft.Health.Fhir.Api.Features.Filters diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/ValidateResourceIdFilterAttribute.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/ValidateResourceIdFilterAttribute.cs index 117b1a4073..567348694a 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/ValidateResourceIdFilterAttribute.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/ValidateResourceIdFilterAttribute.cs @@ -11,9 +11,8 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Routing; -using Microsoft.Health.Fhir.Api.Features.Routing; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Core.Features.Validation; -using Namotion.Reflection; namespace Microsoft.Health.Fhir.Api.Features.Filters { diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/ValidateResourceTypeFilterAttribute.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/ValidateResourceTypeFilterAttribute.cs index 3798939743..e6b33145eb 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/ValidateResourceTypeFilterAttribute.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Filters/ValidateResourceTypeFilterAttribute.cs @@ -9,7 +9,7 @@ using FluentValidation.Results; using Hl7.Fhir.Model; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Health.Fhir.Api.Features.Routing; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Core.Features.Validation; using Microsoft.Health.Fhir.Core.Models; diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandler.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandler.cs index 243de5dc3a..f9de161593 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandler.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/BundleHandler.cs @@ -38,7 +38,6 @@ #if !STU3 using Microsoft.Health.Fhir.Api.Features.Formatters; #endif -using Microsoft.Health.Fhir.Api.Features.Routing; using Microsoft.Health.Fhir.Core.Configs; using Microsoft.Health.Fhir.Core.Exceptions; using Microsoft.Health.Fhir.Core.Extensions; @@ -48,6 +47,7 @@ using Microsoft.Health.Fhir.Core.Features.Persistence.Orchestration; using Microsoft.Health.Fhir.Core.Features.Resources; using Microsoft.Health.Fhir.Core.Features.Resources.Bundle; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Core.Features.Security; using Microsoft.Health.Fhir.Core.Features.Validation; using Microsoft.Health.Fhir.Core.Messages.Bundle; diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/TransactionBundleValidator.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/TransactionBundleValidator.cs index 4b053b1609..b9dc5bb257 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/TransactionBundleValidator.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/Bundle/TransactionBundleValidator.cs @@ -9,12 +9,11 @@ using System.Threading; using System.Threading.Tasks; using EnsureThat; -using Microsoft.Build.Framework; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; -using Microsoft.Health.Fhir.Api.Features.Routing; using Microsoft.Health.Fhir.Core.Exceptions; using Microsoft.Health.Fhir.Core.Features.Resources; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Core.Features.Search; using Microsoft.Health.Fhir.Core.Models; using static Hl7.Fhir.Model.Bundle; diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Microsoft.Health.Fhir.Shared.Api.projitems b/src/Microsoft.Health.Fhir.Shared.Api/Microsoft.Health.Fhir.Shared.Api.projitems index 4184a19ce9..d0a4186d49 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Microsoft.Health.Fhir.Shared.Api.projitems +++ b/src/Microsoft.Health.Fhir.Shared.Api/Microsoft.Health.Fhir.Shared.Api.projitems @@ -13,6 +13,7 @@ + diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Registration/FhirServerServiceCollectionExtensions.cs b/src/Microsoft.Health.Fhir.Shared.Api/Registration/FhirServerServiceCollectionExtensions.cs index 64abbdcd71..af581c69a9 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Registration/FhirServerServiceCollectionExtensions.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Registration/FhirServerServiceCollectionExtensions.cs @@ -29,6 +29,7 @@ using Microsoft.Health.Fhir.Api.Features.Throttling; using Microsoft.Health.Fhir.Core.Features.Cors; using Microsoft.Health.Fhir.Core.Features.Persistence.Orchestration; +using Microsoft.Health.Fhir.Core.Features.Routing; using Microsoft.Health.Fhir.Core.Logging.Metrics; using Microsoft.Health.Fhir.Core.Registration; using Polly; diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Operations/Reindex/ReindexJobTaskTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Operations/Reindex/ReindexJobTaskTests.cs index fce99b66df..78b2f0f4a4 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Operations/Reindex/ReindexJobTaskTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Operations/Reindex/ReindexJobTaskTests.cs @@ -40,7 +40,7 @@ namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Operations.Reindex [Trait(Traits.Category, Categories.IndexAndReindex)] public class ReindexJobTaskTests : IClassFixture, IAsyncLifetime { - private readonly string _base64EncodedToken = ContinuationTokenConverter.Encode("token"); + private readonly string _base64EncodedToken = ContinuationTokenEncoder.Encode("token"); private const int _mockedSearchCount = 5; private static readonly WeakETag _weakETag = WeakETag.FromVersionId("0"); diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Search/BundleFactoryTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Search/BundleFactoryTests.cs index 3ed215a5dd..aaaf5e6786 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Search/BundleFactoryTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Search/BundleFactoryTests.cs @@ -157,7 +157,7 @@ private ResourceWrapper CreateResourceWrapper(ResourceElement resourceElement, H [Fact] public void GivenASearchResultWithContinuationToken_WhenCreateSearchBundle_ThenCorrectBundleShouldBeReturned() { - string encodedContinuationToken = ContinuationTokenConverter.Encode(_continuationToken); + string encodedContinuationToken = ContinuationTokenEncoder.Encode(_continuationToken); _urlResolver.ResolveRouteUrl(_unsupportedSearchParameters, null, encodedContinuationToken, true).Returns(_nextUrl); _urlResolver.ResolveRouteUrl(_unsupportedSearchParameters).Returns(_selfUrl); diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Extensions/FhirMediatorExtensions.cs b/src/Microsoft.Health.Fhir.Shared.Core/Extensions/FhirMediatorExtensions.cs index 093ceff432..976a87ef5f 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Extensions/FhirMediatorExtensions.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core/Extensions/FhirMediatorExtensions.cs @@ -150,6 +150,15 @@ public static async Task SearchResourceCompartmentAsync(this IM return result.Bundle; } + public static async Task SearchIncludeResourceAsync(this IMediator mediator, string type, IReadOnlyList> queries, CancellationToken cancellationToken = default) + { + EnsureArg.IsNotNull(mediator, nameof(mediator)); + + var result = await mediator.Send(new SearchResourceRequest(type, queries, true), cancellationToken); + + return result.Bundle; + } + public static async Task GetCapabilitiesAsync(this IMediator mediator, CancellationToken cancellationToken = default) { EnsureArg.IsNotNull(mediator, nameof(mediator)); diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Operations/Everything/PatientEverythingService.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Operations/Everything/PatientEverythingService.cs index c93d2afc26..08575cccc6 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Features/Operations/Everything/PatientEverythingService.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Operations/Everything/PatientEverythingService.cs @@ -100,7 +100,7 @@ public async Task SearchAsync( { EverythingOperationContinuationToken token = string.IsNullOrEmpty(continuationToken) ? new EverythingOperationContinuationToken() - : EverythingOperationContinuationToken.FromJson(ContinuationTokenConverter.Decode(continuationToken)); + : EverythingOperationContinuationToken.FromJson(ContinuationTokenEncoder.Decode(continuationToken)); if (token == null || token.Phase < 0 || token.Phase > 3) { @@ -110,7 +110,7 @@ public async Task SearchAsync( SearchResult searchResult; string encodedInternalContinuationToken = string.IsNullOrEmpty(token.InternalContinuationToken) ? null - : ContinuationTokenConverter.Encode(token.InternalContinuationToken); + : ContinuationTokenEncoder.Encode(token.InternalContinuationToken); IReadOnlyList types = string.IsNullOrEmpty(type) ? new List() : type.SplitByOrSeparator(); // We will need to store the id of the patient that links to this one if we are processing a "seealso" link @@ -205,7 +205,7 @@ public async Task SearchAsync( if (!searchResult.Results.Any() && nextContinuationToken != null) { // Run patient $everything on links. - return await SearchAsync(parentPatientId, start, end, since, type, ContinuationTokenConverter.Encode(nextContinuationToken), cancellationToken); + return await SearchAsync(parentPatientId, start, end, since, type, ContinuationTokenEncoder.Encode(nextContinuationToken), cancellationToken); } } diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Search/BundleFactory.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Search/BundleFactory.cs index 67082f50d2..3727e82252 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Features/Search/BundleFactory.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Search/BundleFactory.cs @@ -16,6 +16,7 @@ using Microsoft.Health.Fhir.Core.Features.Context; using Microsoft.Health.Fhir.Core.Features.Persistence; using Microsoft.Health.Fhir.Core.Features.Routing; +using Microsoft.Health.Fhir.Core.Features.Telemetry; using Microsoft.Health.Fhir.Core.Models; using Microsoft.Health.Fhir.Shared.Core.Features.Search; using Microsoft.Health.Fhir.ValueSets; @@ -148,19 +149,73 @@ public Resource CreateDeletedResourcesBundle(string bundleId, DateTimeOffset las private void CreateLinks(SearchResult result, Bundle bundle) { bool problemWithLinks = false; - if (result.ContinuationToken != null) + if (!_fhirRequestContextAccessor.RequestContext?.RouteName?.Equals(RouteNames.Includes, StringComparison.OrdinalIgnoreCase) ?? true) { - try + if (result.ContinuationToken != null) { - bundle.NextLink = _urlResolver.ResolveRouteUrl( - result.UnsupportedSearchParameters, - result.SortOrder, - ContinuationTokenConverter.Encode(result.ContinuationToken), - true); + try + { + bundle.NextLink = _urlResolver.ResolveRouteUrl( + result.UnsupportedSearchParameters, + result.SortOrder, + ContinuationTokenEncoder.Encode(result.ContinuationToken), + true); + } + catch (UriFormatException) + { + problemWithLinks = true; + } } - catch (UriFormatException) + + if (result.IncludesContinuationToken != null) { - problemWithLinks = true; + try + { + var ambientRouteValuesOverride = new Dictionary + { + { KnownHttpRequestProperties.RouteValueAction, "Search" }, + { KnownHttpRequestProperties.RouteValueController, RouteNames.Includes }, + { KnownActionParameterNames.ResourceType, _fhirRequestContextAccessor.RequestContext.ResourceType }, + }; + + Uri url = _urlResolver.ResolveRouteUrl( + result.UnsupportedSearchParameters, + result.SortOrder, + null, + true, + ContinuationTokenEncoder.Encode(result.IncludesContinuationToken), + RouteNames.Includes, + ambientRouteValuesOverride); + bundle.Link.Add( + new Bundle.LinkComponent() + { + Relation = "related", + Url = url.AbsoluteUri, + }); + } + catch (UriFormatException) + { + problemWithLinks = true; + } + } + } + else + { + if (result.IncludesContinuationToken != null) + { + try + { + bundle.NextLink = _urlResolver.ResolveRouteUrl( + result.UnsupportedSearchParameters, + result.SortOrder, + null, + true, + ContinuationTokenEncoder.Encode(result.IncludesContinuationToken)); + } + catch (UriFormatException) + { + problemWithLinks = true; + } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Search/SearchOptionsFactory.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Search/SearchOptionsFactory.cs index 66eafb847d..7e1852d19c 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Features/Search/SearchOptionsFactory.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Search/SearchOptionsFactory.cs @@ -85,9 +85,9 @@ private SearchParameterInfo ResourceTypeSearchParameter } } - public SearchOptions Create(string resourceType, IReadOnlyList> queryParameters, bool isAsyncOperation = false, ResourceVersionType resourceVersionTypes = ResourceVersionType.Latest, bool onlyIds = false) + public SearchOptions Create(string resourceType, IReadOnlyList> queryParameters, bool isAsyncOperation = false, ResourceVersionType resourceVersionTypes = ResourceVersionType.Latest, bool onlyIds = false, bool isIncludesOperation = false) { - return Create(null, null, resourceType, queryParameters, isAsyncOperation, resourceVersionTypes: resourceVersionTypes, onlyIds: onlyIds); + return Create(null, null, resourceType, queryParameters, isAsyncOperation, resourceVersionTypes: resourceVersionTypes, onlyIds: onlyIds, isIncludesOperation: isIncludesOperation); } [SuppressMessage("Design", "CA1308", Justification = "ToLower() is required to format parameter output correctly.")] @@ -99,7 +99,8 @@ public SearchOptions Create( bool isAsyncOperation = false, bool useSmartCompartmentDefinition = false, ResourceVersionType resourceVersionTypes = ResourceVersionType.Latest, - bool onlyIds = false) + bool onlyIds = false, + bool isIncludesOperation = false) { var searchOptions = new SearchOptions(); @@ -118,6 +119,7 @@ public SearchOptions Create( searchOptions.IgnoreSearchParamHash = queryParameters != null && queryParameters.Any(_ => _.Item1 == KnownQueryParameterNames.IgnoreSearchParamHash && _.Item2 != null); string continuationToken = null; + string includesContinuationToken = null; string feedRange = null; var searchParams = new SearchParams(); @@ -137,7 +139,7 @@ public SearchOptions Create( string.Format(Core.Resources.MultipleQueryParametersNotAllowed, KnownQueryParameterNames.ContinuationToken)); } - continuationToken = ContinuationTokenConverter.Decode(query.Item2); + continuationToken = ContinuationTokenEncoder.Decode(query.Item2); setDefaultBundleTotal = false; } else if (string.Equals(query.Item1, KnownQueryParameterNames.FeedRange, StringComparison.OrdinalIgnoreCase)) @@ -218,6 +220,24 @@ public SearchOptions Create( throw new BadRequestException(ex.Message); } } + else if (string.Equals(query.Item1, KnownQueryParameterNames.IncludesContinuationToken, StringComparison.OrdinalIgnoreCase)) + { + // This is an unreachable case. The mapping of the query parameters makes it so only one continuation token can exist. + if (includesContinuationToken != null) + { + throw new InvalidSearchOperationException( + string.Format(Core.Resources.MultipleQueryParametersNotAllowed, KnownQueryParameterNames.IncludesContinuationToken)); + } + + if (!isIncludesOperation) + { + // TODO: should this case be ignored or throw an exception? The Hl7 doc says, we should ignore it in general. (https://www.hl7.org/fhir/R4/search.html#errors) + // throw new BadRequestException(string.Format(Core.Resources.InvalidContinuationToken, query.Item2, SupportedTotalTypes)); + } + + includesContinuationToken = ContinuationTokenEncoder.Decode(query.Item2); + setDefaultBundleTotal = false; + } else { // Parse the search parameters. @@ -233,7 +253,13 @@ public SearchOptions Create( } } + if (isIncludesOperation && string.IsNullOrEmpty(includesContinuationToken)) + { + throw new BadRequestException("'includesCt' parameter must be provided for $includes operation."); + } + searchOptions.ContinuationToken = continuationToken; + searchOptions.IncludesContinuationToken = includesContinuationToken; searchOptions.FeedRange = feedRange; if (setDefaultBundleTotal) diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Search/SearchResourceHandler.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Search/SearchResourceHandler.cs index 17550dff26..5a919a531d 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Features/Search/SearchResourceHandler.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Search/SearchResourceHandler.cs @@ -55,7 +55,11 @@ public async Task Handle(SearchResourceRequest request, throw new UnauthorizedFhirActionException(); } - SearchResult searchResult = await _searchService.SearchAsync(request.ResourceType, request.Queries, cancellationToken); + SearchResult searchResult = await _searchService.SearchAsync( + resourceType: request.ResourceType, + queryParameters: request.Queries, + cancellationToken: cancellationToken, + isIncludesOperation: request.IsIncludesRequest); searchResult = _dataResourceFilter.Filter(searchResult: searchResult); ResourceElement bundle = _bundleFactory.CreateSearchBundle(searchResult); diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Validation/ServerProvideProfileValidation.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Validation/ServerProvideProfileValidation.cs index 64d4111590..d69a2981e8 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Features/Validation/ServerProvideProfileValidation.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Validation/ServerProvideProfileValidation.cs @@ -104,7 +104,7 @@ private async Task> GetSummaries() var queryParameters = new List>(); if (ct != null) { - ct = ContinuationTokenConverter.Encode(ct); + ct = ContinuationTokenEncoder.Encode(ct); queryParameters.Add(new Tuple(KnownQueryParameterNames.ContinuationToken, ct)); } diff --git a/src/Microsoft.Health.Fhir.SqlServer/Features/Search/ContinuationToken.cs b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/ContinuationToken.cs index 7075d80375..511c8bd571 100644 --- a/src/Microsoft.Health.Fhir.SqlServer/Features/Search/ContinuationToken.cs +++ b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/ContinuationToken.cs @@ -3,7 +3,6 @@ // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // ------------------------------------------------------------------------------------------------- -using System; using System.Globalization; using System.Text.Json; @@ -100,17 +99,5 @@ public static ContinuationToken FromString(string json) return null; } } - - private class ContinuationTokenConverter : System.Text.Json.Serialization.JsonConverter - { - public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.TokenType switch - { - JsonTokenType.String => reader.GetString(), - JsonTokenType.Number => reader.GetInt64(), - _ => throw new NotSupportedException(), - }; - - public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) => throw new NotImplementedException(); - } } } diff --git a/src/Microsoft.Health.Fhir.SqlServer/Features/Search/ContinuationTokenConverter.cs b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/ContinuationTokenConverter.cs new file mode 100644 index 0000000000..3f1f280227 --- /dev/null +++ b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/ContinuationTokenConverter.cs @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------------------------------- +// 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.Text.Json; + +namespace Microsoft.Health.Fhir.SqlServer.Features.Search +{ + internal class ContinuationTokenConverter : System.Text.Json.Serialization.JsonConverter + { + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.TokenType switch + { + JsonTokenType.String => reader.GetString(), + JsonTokenType.Number => reader.GetInt64(), + _ => throw new NotSupportedException(), + }; + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) => throw new NotImplementedException(); + } +} diff --git a/src/Microsoft.Health.Fhir.SqlServer/Features/Search/Expressions/Visitors/IncludesOperationRewriter.cs b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/Expressions/Visitors/IncludesOperationRewriter.cs new file mode 100644 index 0000000000..1bd457b927 --- /dev/null +++ b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/Expressions/Visitors/IncludesOperationRewriter.cs @@ -0,0 +1,60 @@ +// ------------------------------------------------------------------------------------------------- +// 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 Microsoft.Health.Fhir.Core.Features.Search.Expressions; + +namespace Microsoft.Health.Fhir.SqlServer.Features.Search.Expressions.Visitors +{ + internal class IncludesOperationRewriter : SqlExpressionRewriterWithInitialContext + { + internal static readonly IncludesOperationRewriter Instance = new IncludesOperationRewriter(); + + private static readonly SearchParamTableExpression IncludeLimitExpression = new SearchParamTableExpression( + null, + null, + SearchParamTableExpressionKind.IncludeLimit); + + private static readonly SearchParamTableExpression IncludeUnionAllExpression = new SearchParamTableExpression( + null, + null, + SearchParamTableExpressionKind.IncludeUnionAll); + + public override Expression VisitSqlRoot(SqlRootExpression expression, object context) + { + if (expression == null + || expression.SearchParamTableExpressions.Count == 1 + || expression.SearchParamTableExpressions.All(e => e.Kind != SearchParamTableExpressionKind.Include)) + { + return expression; + } + + // SearchParamTableExpressions contains at least one Include expression + var nonIncludeExpressions = expression.SearchParamTableExpressions.Where(e => e.Kind != SearchParamTableExpressionKind.Include).ToList(); + var includeExpressions = expression.SearchParamTableExpressions.Where(e => e.Kind == SearchParamTableExpressionKind.Include).ToList(); + + // Sort include expressions if there is an include iterate expression + // Order so that include iterate expression appear after the expressions they select from + IEnumerable sortedIncludeExpressions = includeExpressions; + if (includeExpressions.Any(e => ((IncludeExpression)e.Predicate).Iterate)) + { + IEnumerable nonIncludeIterateExpressions = includeExpressions.Where(e => !((IncludeExpression)e.Predicate).Iterate); + List includeIterateExpressions = includeExpressions.Where(e => ((IncludeExpression)e.Predicate).Iterate).ToList(); + sortedIncludeExpressions = nonIncludeIterateExpressions.Concat(SortIncludeIterateExpressions(includeIterateExpressions)); + } + + // Add sorted include expressions after all other expressions + var reorderedExpressions = nonIncludeExpressions.Concat(sortedIncludeExpressions).Concat(new[] { IncludeUnionAllExpression, IncludeLimitExpression }).ToList(); + return new SqlRootExpression(reorderedExpressions, expression.ResourceTableExpressions); + } + + private IEnumerable SortIncludeIterateExpressions(List includeIterateExpressions) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Microsoft.Health.Fhir.SqlServer/Features/Search/Expressions/Visitors/QueryGenerators/SqlQueryGenerator.cs b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/Expressions/Visitors/QueryGenerators/SqlQueryGenerator.cs index 0d88fd274f..b80d141a23 100644 --- a/src/Microsoft.Health.Fhir.SqlServer/Features/Search/Expressions/Visitors/QueryGenerators/SqlQueryGenerator.cs +++ b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/Expressions/Visitors/QueryGenerators/SqlQueryGenerator.cs @@ -781,7 +781,7 @@ private void HandleTableKindInclude( StringBuilder.Append("SELECT DISTINCT "); - if (includeExpression.Reversed) + if (includeExpression.Reversed || context.IsIncludesOperation) { // In case its revinclude, we limit the number of returned items as the resultset size is potentially // unbounded. we ask for +1 so in the limit expression we know if to mark at truncated... @@ -791,8 +791,15 @@ private void HandleTableKindInclude( var table = !includeExpression.Reversed ? referenceTargetResourceTableAlias : referenceSourceTableAlias; StringBuilder.Append(VLatest.Resource.ResourceTypeId, table).Append(" AS T1, ") - .Append(VLatest.Resource.ResourceSurrogateId, table) - .AppendLine(" AS Sid1, 0 AS IsMatch "); + .Append(VLatest.Resource.ResourceSurrogateId, table); + if (!context.IsIncludesOperation) + { + StringBuilder.AppendLine(" AS Sid1, 0 AS IsMatch "); + } + else + { + StringBuilder.AppendLine(" AS Sid1, 0 AS IsMatch, 0 AS IsPartial "); + } StringBuilder.Append("FROM ").Append(VLatest.ReferenceSearchParam).Append(' ').AppendLine(referenceSourceTableAlias) .Append(_joinShift).Append("JOIN ").Append(context.AddCurrentClause && _allowCurrent ? VLatest.CurrentResource : VLatest.Resource).Append(' ').Append(referenceTargetResourceTableAlias) @@ -891,10 +898,31 @@ private void HandleTableKindInclude( } } - if (includeExpression.Reversed && includeExpression.SourceResourceType != "*") + var includesContinuationToken = IncludesContinuationToken.FromString(context.IncludesContinuationToken); + if (!context.IsIncludesOperation || includesContinuationToken?.IncludeResourceTypeId == null || includesContinuationToken?.IncludeResourceSurrogateId == null) { - delimited.BeginDelimitedElement().Append(VLatest.ReferenceSearchParam.ResourceTypeId, referenceSourceTableAlias) - .Append(" = ").Append(Parameters.AddParameter(VLatest.ReferenceSearchParam.ResourceTypeId, Model.GetResourceTypeId(includeExpression.SourceResourceType), true)); + if (includeExpression.Reversed && includeExpression.SourceResourceType != "*") + { + delimited.BeginDelimitedElement().Append(VLatest.ReferenceSearchParam.ResourceTypeId, referenceSourceTableAlias) + .Append(" = ").Append(Parameters.AddParameter(VLatest.ReferenceSearchParam.ResourceTypeId, Model.GetResourceTypeId(includeExpression.SourceResourceType), true)); + } + } + else + { + var tableAlias = includeExpression.Reversed ? referenceSourceTableAlias : referenceTargetResourceTableAlias; + delimited.BeginDelimitedElement() + .Append(VLatest.Resource.ResourceTypeId, tableAlias) + .Append(" > ") + .Append(includesContinuationToken.IncludeResourceTypeId) + .Append(" OR (") + .Append(VLatest.Resource.ResourceTypeId, tableAlias) + .Append(" = ") + .Append(includesContinuationToken.IncludeResourceTypeId) + .Append(" AND ") + .Append(VLatest.ReferenceSearchParam.ResourceSurrogateId, tableAlias) + .Append(" > ") + .Append(includesContinuationToken.IncludeResourceSurrogateId) + .Append(")"); } var scope = delimited.BeginDelimitedElement(); @@ -906,7 +934,7 @@ private void HandleTableKindInclude( .Append(" WHERE ").Append(VLatest.Resource.ResourceTypeId, table).Append(" = T1 AND ") .Append(VLatest.Resource.ResourceSurrogateId, table).Append(" = Sid1"); - if (!includeExpression.Iterate) + if (!includeExpression.Iterate && !context.IsIncludesOperation) { // Limit the join to the main select CTE. // The main select will have max+1 items in the result set to account for paging, so we only want to join using the max amount. @@ -923,6 +951,12 @@ private void HandleTableKindInclude( scope.Append(")"); } + if (context.IsIncludesOperation) + { + StringBuilder.AppendLine("ORDER BY T1 ASC, Sid1 ASC"); + _includeCteIds.Add(TableExpressionName(_tableExpressionCounter)); + } + if (includeExpression.Reversed) { // mark that this cte is a reverse one, meaning we need to add another items limitation @@ -962,8 +996,15 @@ private void HandleTableKindInclude( private void HandleTableKindIncludeLimit(SearchOptions context) { + var includeCount = context.IncludeCount; + if (context.IsIncludesOperation) + { + // Adding 1 for detecting an IsPartial resource if any. + includeCount++; + } + StringBuilder.Append("SELECT DISTINCT TOP (") - .Append(Parameters.AddParameter(context.IncludeCount, includeInHash: false)) + .Append(Parameters.AddParameter(includeCount, includeInHash: false)) .Append(") T1, Sid1, IsMatch, "); StringBuilder.Append("CASE WHEN count_big(*) over() > ") @@ -971,9 +1012,15 @@ private void HandleTableKindIncludeLimit(SearchOptions context) .AppendLine(" THEN 1 ELSE 0 END AS IsPartial "); StringBuilder.Append("FROM ").AppendLine(TableExpressionName(_tableExpressionCounter - 1)); - - // the 'original' include cte is not in the union, but this new layer is instead - _includeCteIds.Add(TableExpressionName(_tableExpressionCounter)); + if (!context.IsIncludesOperation) + { + // the 'original' include cte is not in the union, but this new layer is instead + _includeCteIds.Add(TableExpressionName(_tableExpressionCounter)); + } + else + { + StringBuilder.AppendLine("ORDER BY T1 ASC, Sid1 ASC"); + } } private void HandleTableKindIncludeUnionAll(SearchOptions context) @@ -990,9 +1037,18 @@ private void HandleTableKindIncludeUnionAll(SearchOptions context) StringBuilder.AppendLine(); } - StringBuilder.Append("FROM ").AppendLine(_cteMainSelect); + // Excluding a cte for matched resources for $includes operation. + var rootCte = _cteMainSelect; + var skip = 0; + if (context.IsIncludesOperation) + { + rootCte = _includeCteIds.FirstOrDefault(); + skip = rootCte == null ? 0 : 1; + } + + StringBuilder.Append("FROM ").AppendLine(rootCte); - foreach (var includeCte in _includeCteIds) + foreach (var includeCte in _includeCteIds.Skip(skip)) { StringBuilder.AppendLine("UNION ALL"); StringBuilder.Append("SELECT T1, Sid1, IsMatch, IsPartial"); diff --git a/src/Microsoft.Health.Fhir.SqlServer/Features/Search/IncludesContinuationToken.cs b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/IncludesContinuationToken.cs new file mode 100644 index 0000000000..cd48e048a6 --- /dev/null +++ b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/IncludesContinuationToken.cs @@ -0,0 +1,119 @@ +// ------------------------------------------------------------------------------------------------- +// 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.Text.Json; +using EnsureThat; + +namespace Microsoft.Health.Fhir.SqlServer.Features.Search +{ + public class IncludesContinuationToken + { + private static readonly JsonSerializerOptions Options = new JsonSerializerOptions() { Converters = { new ContinuationTokenConverter() } }; + + private readonly object[] _tokens; + + public IncludesContinuationToken(object[] tokens) + { + EnsureArg.IsNotNull(tokens, nameof(tokens)); + + _tokens = tokens; + Initialize(); + } + + public long MatchResourceSurrogateIdMax + { + get; + private set; + } + + public long MatchResourceSurrogateIdMin + { + get; + private set; + } + + public short MatchResourceTypeId + { + get; + private set; + } + + public long? IncludeResourceSurrogateId + { + get; + private set; + } + + public short? IncludeResourceTypeId + { + get; + private set; + } + + public string ToJson() + { + return JsonSerializer.Serialize(_tokens); + } + + public override string ToString() + { + return ToJson(); + } + + public static IncludesContinuationToken FromString(string json) + { + if (json == null) + { + return null; + } + + try + { + object[] result = JsonSerializer.Deserialize(json, Options); + return new IncludesContinuationToken(result); + } + catch (JsonException) + { + return null; + } + } + + private void Initialize() + { + var initialized = false; + if (_tokens?.Length >= 3 + && short.TryParse(_tokens[0]?.ToString(), out var tid) + && long.TryParse(_tokens[1]?.ToString(), out var sid0) + && long.TryParse(_tokens[2]?.ToString(), out var sid1)) + { + MatchResourceTypeId = tid; + MatchResourceSurrogateIdMin = sid0; + MatchResourceSurrogateIdMax = sid1; + initialized = true; + + if (_tokens.Length > 3) + { + if (_tokens.Length == 5 + && short.TryParse(_tokens[3]?.ToString(), out tid) + && long.TryParse(_tokens[4]?.ToString(), out sid0)) + { + IncludeResourceTypeId = tid; + IncludeResourceSurrogateId = sid0; + } + else + { + initialized = false; + } + } + } + + if (!initialized) + { + throw new ArgumentException("Initialization failed due to invalid tokens."); + } + } + } +} diff --git a/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs index 97b65c9c7b..aba0722fb0 100644 --- a/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs +++ b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs @@ -269,6 +269,11 @@ private async Task RunSearch(SqlSearchOptions sqlSearchOptions, Ca private async Task SearchImpl(SqlSearchOptions sqlSearchOptions, bool reuseQueryPlans, CancellationToken cancellationToken) { + if (sqlSearchOptions.IsIncludesOperation) + { + return await SearchIncludeImpl(sqlSearchOptions, cancellationToken); + } + Stopwatch stopwatch = Stopwatch.StartNew(); Expression searchExpression = sqlSearchOptions.Expression; @@ -456,11 +461,13 @@ await _sqlRetryService.ExecuteSql( return; } - var resources = new List(sqlSearchOptions.MaxItemCount); + var matchedResources = new List(sqlSearchOptions.MaxItemCount); + var includedResources = new List(sqlSearchOptions.MaxItemCount); short? newContinuationType = null; long? newContinuationId = null; bool moreResults = false; int matchCount = 0; + long? matchedResourceSurrogateIdStart = null; string sortValue = null; var isResultPartial = false; @@ -522,6 +529,10 @@ await _sqlRetryService.ExecuteSql( { newContinuationType = resourceTypeId; newContinuationId = resourceSurrogateId; + if (!matchedResourceSurrogateIdStart.HasValue) + { + matchedResourceSurrogateIdStart = resourceSurrogateId; + } // If sort value needed, that means we have an extra column tracking sort value. // Keep track of sort value if this is the last row. @@ -539,27 +550,44 @@ await _sqlRetryService.ExecuteSql( } matchCount++; + matchedResources.Add(new SearchResultEntry( + new ResourceWrapper( + resourceId, + version.ToString(CultureInfo.InvariantCulture), + _model.GetResourceTypeName(resourceTypeId), + clonedSearchOptions.OnlyIds ? null : new RawResource(rawResource, FhirResourceFormat.Json, isMetaSet: isRawResourceMetaSet), + new ResourceRequest(requestMethod), + resourceSurrogateId.ToLastUpdated(), + isDeleted, + null, + null, + null, + searchParameterHash, + resourceSurrogateId), + SearchEntryMode.Match)); + } + else + { + includedResources.Add(new SearchResultEntry( + new ResourceWrapper( + resourceId, + version.ToString(CultureInfo.InvariantCulture), + _model.GetResourceTypeName(resourceTypeId), + clonedSearchOptions.OnlyIds ? null : new RawResource(rawResource, FhirResourceFormat.Json, isMetaSet: isRawResourceMetaSet), + new ResourceRequest(requestMethod), + resourceSurrogateId.ToLastUpdated(), + isDeleted, + null, + null, + null, + searchParameterHash, + resourceSurrogateId), + SearchEntryMode.Include)); } // as long as at least one entry was marked as partial, this resultset // should be marked as partial isResultPartial = isResultPartial || isPartialEntry; - - resources.Add(new SearchResultEntry( - new ResourceWrapper( - resourceId, - version.ToString(CultureInfo.InvariantCulture), - _model.GetResourceTypeName(resourceTypeId), - clonedSearchOptions.OnlyIds ? null : new RawResource(rawResource, FhirResourceFormat.Json, isMetaSet: isRawResourceMetaSet), - new ResourceRequest(requestMethod), - resourceSurrogateId.ToLastUpdated(), - isDeleted, - null, - null, - null, - searchParameterHash, - resourceSurrogateId), - isMatch ? SearchEntryMode.Match : SearchEntryMode.Include)); } // call NextResultAsync to get the info messages @@ -575,6 +603,22 @@ await _sqlRetryService.ExecuteSql( _ => sortValue, }).ToArray()) : null; + IncludesContinuationToken includeContinuationToken = null; + if (clonedSearchOptions.Expression is MultiaryExpression + && ((MultiaryExpression)clonedSearchOptions.Expression).Expressions.Any(x => x is IncludeExpression) + && newContinuationType.HasValue + && newContinuationId.HasValue + && matchedResourceSurrogateIdStart.HasValue + && includedResources.Count > clonedSearchOptions.IncludeCount) + { + includeContinuationToken = new IncludesContinuationToken( + new object[] + { + newContinuationType.Value, + matchedResourceSurrogateIdStart.Value, + newContinuationId.Value, + }); + } if (isResultPartial) { @@ -607,7 +651,13 @@ await _sqlRetryService.ExecuteSql( sqlSearchOptions.SortHasMissingModifier = true; } - searchResult = new SearchResult(resources, continuationToken?.ToJson(), originalSort, clonedSearchOptions.UnsupportedSearchParams); + var resources = new List(matchedResources); + if (includedResources.Count <= clonedSearchOptions.IncludeCount) + { + resources.AddRange(includedResources); + } + + searchResult = new SearchResult(resources, continuationToken?.ToJson(), originalSort, clonedSearchOptions.UnsupportedSearchParams, null, includeContinuationToken?.ToJson()); } } }, @@ -1216,6 +1266,253 @@ private async Task CreateStats(SqlRootExpression expression, CancellationToken c cancel); } + private async Task SearchIncludeImpl(SqlSearchOptions sqlSearchOptions, CancellationToken cancellationToken) + { + var includesContinuationToken = IncludesContinuationToken.FromString(sqlSearchOptions.IncludesContinuationToken); + if (includesContinuationToken == null) + { + _logger.LogWarning("Bad Request (InvalidIncludesContinuationToken)"); + throw new BadRequestException(Resources.InvalidIncludesContinuationToken); + } + + var gteExpression = Expression.GreaterThanOrEqual( + SqlFieldName.ResourceSurrogateId, + null, + includesContinuationToken.MatchResourceSurrogateIdMin); + var lteExpression = Expression.LessThanOrEqual( + SqlFieldName.ResourceSurrogateId, + null, + includesContinuationToken.MatchResourceSurrogateIdMax); + var tokenExpression = Expression.And( + Expression.SearchParameter(SqlSearchParameters.ResourceSurrogateIdParameter, gteExpression), + Expression.SearchParameter(SqlSearchParameters.ResourceSurrogateIdParameter, lteExpression)); + Expression searchExpression = sqlSearchOptions.Expression == null ? tokenExpression : Expression.And(tokenExpression, sqlSearchOptions.Expression); + + var originalSort = new List<(SearchParameterInfo, SortOrder)>(sqlSearchOptions.Sort); + var clonedSearchOptions = UpdateSort(sqlSearchOptions, searchExpression); + + if (clonedSearchOptions.CountOnly) + { + // if we're only returning a count, discard any _include parameters since included resources are not counted. + searchExpression = searchExpression?.AcceptVisitor(RemoveIncludesRewriter.Instance); + } + + // ! - Trace + SqlRootExpression expression = (SqlRootExpression)searchExpression + ?.AcceptVisitor(LastUpdatedToResourceSurrogateIdRewriter.Instance) + .AcceptVisitor(_compartmentSearchRewriter) + .AcceptVisitor(_smartCompartmentSearchRewriter) + .AcceptVisitor(DateTimeEqualityRewriter.Instance) + .AcceptVisitor(FlatteningRewriter.Instance) + .AcceptVisitor(UntypedReferenceRewriter.Instance) + .AcceptVisitor(_sqlRootExpressionRewriter) + .AcceptVisitor(DateTimeTableExpressionCombiner.Instance) + .AcceptVisitor(_partitionEliminationRewriter) + .AcceptVisitor(_sortRewriter, clonedSearchOptions) + .AcceptVisitor(SearchParamTableExpressionReorderer.Instance) + .AcceptVisitor(MissingSearchParamVisitor.Instance) + .AcceptVisitor(NotExpressionRewriter.Instance) + .AcceptVisitor(_chainFlatteningRewriter) + .AcceptVisitor(ResourceColumnPredicatePushdownRewriter.Instance) + .AcceptVisitor(DateTimeBoundedRangeRewriter.Instance) + .AcceptVisitor( + (SqlExpressionRewriterWithInitialContext)(_schemaInformation.Current >= SchemaVersionConstants.PartitionedTables + ? StringOverflowRewriter.Instance + : LegacyStringOverflowRewriter.Instance)) + .AcceptVisitor(NumericRangeRewriter.Instance) + .AcceptVisitor(IncludeMatchSeedRewriter.Instance) + .AcceptVisitor(TopRewriter.Instance, clonedSearchOptions) + .AcceptVisitor(IncludesOperationRewriter.Instance) + ?? SqlRootExpression.WithResourceTableExpressions(); + + await CreateStats(expression, cancellationToken); + + SearchResult searchResult = null; + + await _sqlRetryService.ExecuteSql( + async (connection, cancellationToken, sqlException) => + { + using (SqlCommand sqlCommand = connection.CreateCommand()) // WARNING, this code will not set sqlCommand.Transaction. Sql transactions via C#/.NET are not supported in this method. + { + sqlCommand.CommandTimeout = (int)_sqlServerDataStoreConfiguration.CommandTimeout.TotalSeconds; + var isSortValueNeeded = false; + + var exportTimeTravel = clonedSearchOptions.QueryHints != null && ContainsGlobalEndSurrogateId(clonedSearchOptions); + if (exportTimeTravel) + { + PopulateSqlCommandFromQueryHints(clonedSearchOptions, sqlCommand); + sqlCommand.CommandTimeout = 1200; // set to 20 minutes, as dataset is usually large + } + else + { + var stringBuilder = new IndentedStringBuilder(new StringBuilder()); + + EnableTimeAndIoMessageLogging(stringBuilder, connection); + + var queryGenerator = new SqlQueryGenerator( + stringBuilder, + new HashingSqlQueryParameterManager(new SqlQueryParameterManager(sqlCommand.Parameters)), + _model, + _schemaInformation, + _reuseQueryPlans.IsEnabled(_sqlRetryService), + sqlException); + + expression.AcceptVisitor(queryGenerator, clonedSearchOptions); + isSortValueNeeded = queryGenerator.IsSortValueNeeded(clonedSearchOptions); + + SqlCommandSimplifier.RemoveRedundantParameters(stringBuilder, sqlCommand.Parameters, _logger); + + var queryText = stringBuilder.ToString(); + var queryHash = _queryHashCalculator.CalculateHash(queryText); + _logger.LogInformation("SQL Search Service query hash: {QueryHash}", queryHash); + var customQuery = CustomQueries.CheckQueryHash(connection, queryHash, _logger); + + if (!string.IsNullOrEmpty(customQuery)) + { + _logger.LogInformation("SQl Search Service, custom Query identified by hash {QueryHash}, {CustomQuery}", queryHash, customQuery); + queryText = customQuery; + sqlCommand.CommandType = CommandType.StoredProcedure; + } + + // Command text contains no direct user input. +#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities + sqlCommand.CommandText = queryText; +#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities + } + + LogSqlCommand(sqlCommand); + + using (var reader = await sqlCommand.ExecuteReaderAsync(CommandBehavior.SequentialAccess, cancellationToken)) + { + if (clonedSearchOptions.CountOnly) + { + await reader.ReadAsync(cancellationToken); + long count = reader.GetInt64(0); + if (count > int.MaxValue) + { + _requestContextAccessor.RequestContext.BundleIssues.Add( + new OperationOutcomeIssue( + OperationOutcomeConstants.IssueSeverity.Error, + OperationOutcomeConstants.IssueType.NotSupported, + string.Format(Core.Resources.SearchCountResultsExceedLimit, count, int.MaxValue))); + + _logger.LogWarning("Invalid Search Operation (SearchCountResultsExceedLimit)"); + throw new InvalidSearchOperationException(string.Format(Core.Resources.SearchCountResultsExceedLimit, count, int.MaxValue)); + } + + searchResult = new SearchResult((int)count, clonedSearchOptions.UnsupportedSearchParams); + + // call NextResultAsync to get the info messages + await reader.NextResultAsync(cancellationToken); + + return; + } + + var moreResults = false; + var resources = new List(sqlSearchOptions.IncludeCount); + + while (await reader.ReadAsync(cancellationToken)) + { + ReadWrapper( + reader, + out short resourceTypeId, + out string resourceId, + out int version, + out bool isDeleted, + out long resourceSurrogateId, + out string requestMethod, + out bool isMatch, + out bool isPartialEntry, + out bool isRawResourceMetaSet, + out string searchParameterHash, + out byte[] rawResourceBytes, + out bool isInvisible); + if (isInvisible) + { + continue; + } + + if (resources.Count < clonedSearchOptions.IncludeCount) + { + var rawResource = new Lazy(() => + { + using var rawResourceStream = new MemoryStream(rawResourceBytes); + var decompressedResource = _compressedRawResourceConverter.ReadCompressedRawResource(rawResourceStream); + + _logger.LogVerbose(_parameterStore, cancellationToken, "{NameOfResourceSurrogateId}: {ResourceSurrogateId}; {NameOfResourceTypeId}: {ResourceTypeId}; Decompressed length: {RawResourceLength}", nameof(resourceSurrogateId), resourceSurrogateId, nameof(resourceTypeId), resourceTypeId, decompressedResource.Length); + + if (string.IsNullOrEmpty(decompressedResource)) + { + decompressedResource = MissingResourceFactory.CreateJson(resourceId, _model.GetResourceTypeName(resourceTypeId), "warning", "incomplete"); + _requestContextAccessor.SetMissingResourceCode(System.Net.HttpStatusCode.PartialContent); + } + + return decompressedResource; + }); + + resources.Add(new SearchResultEntry( + new ResourceWrapper( + resourceId, + version.ToString(CultureInfo.InvariantCulture), + _model.GetResourceTypeName(resourceTypeId), + clonedSearchOptions.OnlyIds ? null : new RawResource(rawResource, FhirResourceFormat.Json, isMetaSet: isRawResourceMetaSet), + new ResourceRequest(requestMethod), + resourceSurrogateId.ToLastUpdated(), + isDeleted, + null, + null, + null, + searchParameterHash, + resourceSurrogateId), + SearchEntryMode.Include)); + } + else + { + moreResults = true; + } + } + + // call NextResultAsync to get the info messages + await reader.NextResultAsync(cancellationToken); + + IncludesContinuationToken nextIncludesContinuationToken = null; + if (moreResults) + { + _logger.LogWarning("Bundle Partial Result (TruncatedIncludeMessage)"); + _requestContextAccessor.RequestContext.BundleIssues.Add( + new OperationOutcomeIssue( + OperationOutcomeConstants.IssueSeverity.Warning, + OperationOutcomeConstants.IssueType.Incomplete, + Core.Resources.TruncatedIncludeMessage)); + + nextIncludesContinuationToken = new IncludesContinuationToken( + new object[] + { + includesContinuationToken.MatchResourceTypeId, + includesContinuationToken.MatchResourceSurrogateIdMin, + includesContinuationToken.MatchResourceSurrogateIdMax, + _model.GetResourceTypeId(resources[^1].Resource.ResourceTypeName), + resources[^1].Resource.ResourceSurrogateId, + }); + } + + searchResult = new SearchResult( + resources, + null, + originalSort, + clonedSearchOptions.UnsupportedSearchParams, + null, + nextIncludesContinuationToken?.ToJson()); + } + } + }, + _logger, + cancellationToken, + true); // this enables reads from replicas + + return searchResult; + } + private class ResourceSearchParamStats { private readonly ConcurrentDictionary<(string TableName, string ColumnName, short ResourceTypeId, short SearchParamId), bool> _stats; diff --git a/src/Microsoft.Health.Fhir.SqlServer/Resources.Designer.cs b/src/Microsoft.Health.Fhir.SqlServer/Resources.Designer.cs index e97cdb6a30..6005a03ca7 100644 --- a/src/Microsoft.Health.Fhir.SqlServer/Resources.Designer.cs +++ b/src/Microsoft.Health.Fhir.SqlServer/Resources.Designer.cs @@ -123,6 +123,15 @@ internal static string InvalidContinuationToken { } } + /// + /// Looks up a localized string similar to The provided continuation token for $includes operation is not valid.. + /// + internal static string InvalidIncludesContinuationToken { + get { + return ResourceManager.GetString("InvalidIncludesContinuationToken", resourceCulture); + } + } + /// /// Looks up a localized string similar to Invalid resource type '{0}'.. /// diff --git a/src/Microsoft.Health.Fhir.SqlServer/Resources.resx b/src/Microsoft.Health.Fhir.SqlServer/Resources.resx index c9477b0aea..5b1c19b0c8 100644 --- a/src/Microsoft.Health.Fhir.SqlServer/Resources.resx +++ b/src/Microsoft.Health.Fhir.SqlServer/Resources.resx @@ -170,4 +170,7 @@ Failed to import resource with version conflict, resource id: {0}, line: {1} - \ No newline at end of file + + The provided continuation token for $includes operation is not valid. + + From e1fe87706fd718abe1d06a1f033548fae0f4cfbb Mon Sep 17 00:00:00 2001 From: "Tarun Mathew (Centific Technologies Inc)" Date: Tue, 4 Feb 2025 09:31:46 -0800 Subject: [PATCH 2/2] Fixing some UT failures. --- .../Features/Routing/UrlResolverTests.cs | 26 +++++++++++++++++-- .../Controllers/IncludesController.cs | 5 +--- .../Behaviors/ListSearchBehaviorTests.cs | 8 +++--- .../Features/Search/SqlServerSearchService.cs | 2 -- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Routing/UrlResolverTests.cs b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Routing/UrlResolverTests.cs index 97a6d98757..5cdeb2e157 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Routing/UrlResolverTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api.UnitTests/Features/Routing/UrlResolverTests.cs @@ -36,6 +36,7 @@ public class UrlResolverTests private const string Scheme = "http"; private const string Host = "test"; private const string ContinuationTokenQueryParamName = "ct"; + private const string IncludesContinuationTokenQueryParamName = "includesCt"; private const string DefaultRouteName = "Route"; private readonly RequestContextAccessor _fhirRequestContextAccessor = Substitute.For>(); @@ -341,16 +342,37 @@ public void GivenABundleBeingProcessed_WhenUrlIsResolvedWithQuery_ThenTheCorrect TestAndValidateRouteWithQueryParameter(inputQueryString, null, unsupportedSearchParams, continuationToken, expectedRouteValues); } + [Fact] + public void GivenAnIncludesContinuationToken_WhenSearchUrlIsResolved_ThenCorrectUrlShouldBeReturned() + { + string inputQueryString = "?param1=value1¶m2=value2"; + Tuple[] unsupportedSearchParams = null; + string includesContinuationToken = "includescontinue"; + Dictionary expectedRouteValues = new Dictionary() + { + { "param1", new StringValues("value1") }, + { "param2", new StringValues("value2") }, + { IncludesContinuationTokenQueryParamName, includesContinuationToken }, + }; + + TestAndValidateRouteWithQueryParameter(inputQueryString, null, unsupportedSearchParams, null, expectedRouteValues, includesContinuationToken); + } + private void TestAndValidateRouteWithQueryParameter( string inputQueryString, IReadOnlyList<(SearchParameterInfo searchParameterInfo, SortOrder sortOrder)> resultSortOrder, Tuple[] unsupportedSearchParams, string continuationToken, - Dictionary expectedRouteValues) + Dictionary expectedRouteValues, + string includesContinuationToken = null) { _httpContext.Request.QueryString = new QueryString(inputQueryString); - _urlResolver.ResolveRouteUrl(unsupportedSearchParams, resultSortOrder, continuationToken); + _urlResolver.ResolveRouteUrl( + unsupportedSearchParams: unsupportedSearchParams, + resultSortOrder: resultSortOrder, + continuationToken: continuationToken, + includesContinuationToken: includesContinuationToken); ValidateUrlRouteContext( routeValuesValidator: routeValues => diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/IncludesController.cs b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/IncludesController.cs index f3d6227711..2121c5c19d 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/IncludesController.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/IncludesController.cs @@ -26,15 +26,12 @@ namespace Microsoft.Health.Fhir.Api.Controllers public class IncludesController : Controller { private readonly IMediator _mediator; - private readonly CoreFeatureConfiguration _coreFeaturesConfig; - public IncludesController(IMediator mediator, IOptions coreFeatures) + public IncludesController(IMediator mediator) { EnsureArg.IsNotNull(mediator, nameof(mediator)); - EnsureArg.IsNotNull(coreFeatures?.Value, nameof(coreFeatures)); _mediator = mediator; - _coreFeaturesConfig = coreFeatures.Value; } [HttpGet] diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Search/Behaviors/ListSearchBehaviorTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Search/Behaviors/ListSearchBehaviorTests.cs index 7d32e75e75..1a0e069043 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Search/Behaviors/ListSearchBehaviorTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Search/Behaviors/ListSearchBehaviorTests.cs @@ -120,7 +120,7 @@ public async Task GivenARequest_WhenNoListQuery_QueriesUnchanged() Tuple.Create("secondItem", guid2), }; - var getResourceRequest = Substitute.For("Patient", list); + var getResourceRequest = Substitute.For("Patient", list, false); SearchResourceResponse response = await behavior.Handle( getResourceRequest, @@ -149,7 +149,7 @@ public async Task GivenARequest_WhenListValueMissing_EmptyResultsReturned() Tuple.Create("_id", Guid.NewGuid().ToString()), }; - var getResourceRequest = Substitute.For("Patient", list); + var getResourceRequest = Substitute.For("Patient", list, false); SearchResourceResponse response = await behavior.Handle( getResourceRequest, () => @@ -175,7 +175,7 @@ public async Task GivenARequest_WhenListValueExistsButValueNotFound_EmptyRespons Tuple.Create("_id", Guid.NewGuid().ToString()), }; - var getResourceRequest = Substitute.For("Patient", list); + var getResourceRequest = Substitute.For("Patient", list, false); Assert.True(getResourceRequest.Queries.Count == 3); SearchResourceResponse response = await behavior.Handle( @@ -197,7 +197,7 @@ public async Task GivenARequest_WhenListValueFound_ExpectedIdQueriesAdded() new ListSearchPipeBehavior(_searchOptionsFactory, _bundleFactory, _scopedDataStore, Deserializers.ResourceDeserializer, new ReferenceSearchValueParser(new FhirRequestContextAccessor())); IReadOnlyList> list = new[] { Tuple.Create("_list", "existing-list") }; - var getResourceRequest = Substitute.For("Patient", list); + var getResourceRequest = Substitute.For("Patient", list, false); Assert.True(getResourceRequest.Queries.Count == 1); Assert.Equal("_list", getResourceRequest.Queries[0].Item1); diff --git a/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs index aba0722fb0..5212cfa321 100644 --- a/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs +++ b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs @@ -1335,7 +1335,6 @@ await _sqlRetryService.ExecuteSql( using (SqlCommand sqlCommand = connection.CreateCommand()) // WARNING, this code will not set sqlCommand.Transaction. Sql transactions via C#/.NET are not supported in this method. { sqlCommand.CommandTimeout = (int)_sqlServerDataStoreConfiguration.CommandTimeout.TotalSeconds; - var isSortValueNeeded = false; var exportTimeTravel = clonedSearchOptions.QueryHints != null && ContainsGlobalEndSurrogateId(clonedSearchOptions); if (exportTimeTravel) @@ -1358,7 +1357,6 @@ await _sqlRetryService.ExecuteSql( sqlException); expression.AcceptVisitor(queryGenerator, clonedSearchOptions); - isSortValueNeeded = queryGenerator.IsSortValueNeeded(clonedSearchOptions); SqlCommandSimplifier.RemoveRedundantParameters(stringBuilder, sqlCommand.Parameters, _logger);