diff --git a/src/Microsoft.Health.Fhir.Core/Features/Search/ISearchParameterQueryParameterExpander.cs b/src/Microsoft.Health.Fhir.Core/Features/Search/ISearchParameterQueryParameterExpander.cs new file mode 100644 index 0000000000..b3672954d8 --- /dev/null +++ b/src/Microsoft.Health.Fhir.Core/Features/Search/ISearchParameterQueryParameterExpander.cs @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------------- +// 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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Health.Fhir.Core.Features.Search +{ + public interface ISearchParameterQueryParameterExpander + { + Task>> ExpandAsync( + string resourceType, + IReadOnlyList> queryParameters, + CancellationToken cancellationToken); + } +} diff --git a/src/Microsoft.Health.Fhir.Core/Features/Search/SearchService.cs b/src/Microsoft.Health.Fhir.Core/Features/Search/SearchService.cs index 4104d8f5b7..1904cc4532 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/Search/SearchService.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Search/SearchService.cs @@ -28,6 +28,7 @@ public abstract class SearchService : ISearchService private readonly ISearchOptionsFactory _searchOptionsFactory; private readonly IFhirDataStore _fhirDataStore; private readonly ILogger _logger; + private readonly IReadOnlyCollection _queryParameterExpanders; /// /// Initializes a new instance of the class. @@ -36,6 +37,22 @@ public abstract class SearchService : ISearchService /// The data store /// Logger protected SearchService(ISearchOptionsFactory searchOptionsFactory, IFhirDataStore fhirDataStore, ILogger logger) + : this(searchOptionsFactory, fhirDataStore, logger, Array.Empty()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The search options factory. + /// The data store + /// Logger + /// Search query parameter expanders. + protected SearchService( + ISearchOptionsFactory searchOptionsFactory, + IFhirDataStore fhirDataStore, + ILogger logger, + IEnumerable queryParameterExpanders) { EnsureArg.IsNotNull(searchOptionsFactory, nameof(searchOptionsFactory)); EnsureArg.IsNotNull(fhirDataStore, nameof(fhirDataStore)); @@ -44,6 +61,7 @@ protected SearchService(ISearchOptionsFactory searchOptionsFactory, IFhirDataSto _searchOptionsFactory = searchOptionsFactory; _fhirDataStore = fhirDataStore; _logger = logger; + _queryParameterExpanders = queryParameterExpanders?.ToArray() ?? Array.Empty(); } public async Task TryLogEvent(string process, string status, string text, DateTime? startDate, CancellationToken cancellationToken) @@ -61,6 +79,7 @@ public virtual async Task SearchAsync( bool onlyIds = false, bool isIncludesOperation = false) { + queryParameters = await ExpandQueryParametersAsync(resourceType, queryParameters, cancellationToken); SearchOptions searchOptions = _searchOptionsFactory.Create(resourceType, queryParameters, isAsyncOperation, resourceVersionTypes, onlyIds, isIncludesOperation); // Execute the actual search. @@ -77,6 +96,7 @@ public async Task SearchCompartmentAsync( bool isAsyncOperation = false, bool useSmartCompartmentDefinition = false) { + queryParameters = await ExpandQueryParametersAsync(resourceType, queryParameters, cancellationToken); SearchOptions searchOptions = _searchOptionsFactory.Create(compartmentType, compartmentId, resourceType, queryParameters, isAsyncOperation, useSmartCompartmentDefinition); // Execute the actual search. @@ -286,6 +306,19 @@ public abstract Task SearchAsync( SearchOptions searchOptions, CancellationToken cancellationToken); + private async Task>> ExpandQueryParametersAsync( + string resourceType, + IReadOnlyList> queryParameters, + CancellationToken cancellationToken) + { + foreach (ISearchParameterQueryParameterExpander expander in _queryParameterExpanders) + { + queryParameters = await expander.ExpandAsync(resourceType, queryParameters, cancellationToken); + } + + return queryParameters; + } + protected abstract Task SearchForReindexInternalAsync( SearchOptions searchOptions, string searchParameterHash, diff --git a/src/Microsoft.Health.Fhir.CosmosDb/Features/Search/FhirCosmosSearchService.cs b/src/Microsoft.Health.Fhir.CosmosDb/Features/Search/FhirCosmosSearchService.cs index 8061d60b86..130ae581f9 100644 --- a/src/Microsoft.Health.Fhir.CosmosDb/Features/Search/FhirCosmosSearchService.cs +++ b/src/Microsoft.Health.Fhir.CosmosDb/Features/Search/FhirCosmosSearchService.cs @@ -54,8 +54,9 @@ public FhirCosmosSearchService( ICosmosDbCollectionPhysicalPartitionInfo physicalPartitionInfo, CompartmentSearchRewriter compartmentSearchRewriter, SmartCompartmentSearchRewriter smartCompartmentSearchRewriter, + IEnumerable queryParameterExpanders, ILogger logger) - : base(searchOptionsFactory, fhirDataStore, logger) + : base(searchOptionsFactory, fhirDataStore, logger, queryParameterExpanders) { EnsureArg.IsNotNull(fhirDataStore, nameof(fhirDataStore)); EnsureArg.IsNotNull(queryBuilder, nameof(queryBuilder)); diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Modules/SearchModule.cs b/src/Microsoft.Health.Fhir.Shared.Api/Modules/SearchModule.cs index 1b0819e65b..0eabeb4398 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Modules/SearchModule.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Modules/SearchModule.cs @@ -143,6 +143,7 @@ public void Load(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddTransient(); diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Search/SearchParameterValueSetExpanderTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Search/SearchParameterValueSetExpanderTests.cs new file mode 100644 index 0000000000..858f609526 --- /dev/null +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Search/SearchParameterValueSetExpanderTests.cs @@ -0,0 +1,115 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Hl7.Fhir.Model; +using Microsoft.Health.Fhir.Core.Extensions; +using Microsoft.Health.Fhir.Core.Features.Conformance; +using Microsoft.Health.Fhir.Core.Features.Definition; +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 SearchParamType = Microsoft.Health.Fhir.ValueSets.SearchParamType; +using Task = System.Threading.Tasks.Task; + +namespace Microsoft.Health.Fhir.Core.UnitTests.Features.Search +{ + [Trait(Traits.OwningTeam, OwningTeam.Fhir)] + [Trait(Traits.Category, Categories.Search)] + public class SearchParameterValueSetExpanderTests + { + private readonly ITerminologyServiceProxy _terminologyServiceProxy = Substitute.For(); + private readonly ISearchParameterDefinitionManager _searchParameterDefinitionManager = Substitute.For(); + private readonly SearchParameterValueSetExpander _expander; + + public SearchParameterValueSetExpanderTests() + { + _expander = new SearchParameterValueSetExpander( + _terminologyServiceProxy, + () => _searchParameterDefinitionManager); + } + + [Fact] + public async Task GivenTokenInModifier_WhenExpanding_ThenValueSetExpansionIsConvertedToTokenSearch() + { + const string valueSetUrl = "http://example.org/fhir/ValueSet/medications"; + var searchParameter = CreateSearchParameter(SearchParamType.Token); + ConfigureSearchParameter("Medication", "code", searchParameter); + + _terminologyServiceProxy.ExpandAsync( + Arg.Any>>(), + null, + Arg.Any()) + .Returns(Task.FromResult(new ValueSet + { + Expansion = new ValueSet.ExpansionComponent + { + Contains = new List + { + new ValueSet.ContainsComponent { System = "http://rx.example", Code = "a" }, + new ValueSet.ContainsComponent { System = "http://rx.example", Code = "b" }, + }, + }, + }.ToResourceElement())); + + IReadOnlyList> result = await _expander.ExpandAsync( + "Medication", + new[] { Tuple.Create("code:in", valueSetUrl) }, + CancellationToken.None); + + Tuple expanded = Assert.Single(result); + Assert.Equal("code", expanded.Item1); + Assert.Equal("http://rx.example|a,http://rx.example|b", expanded.Item2); + + await _terminologyServiceProxy.Received(1).ExpandAsync( + Arg.Is>>(x => x.Single().Item1 == TerminologyOperationParameterNames.Expand.Url && x.Single().Item2 == valueSetUrl), + null, + Arg.Any()); + } + + [Fact] + public async Task GivenNonTokenInModifier_WhenExpanding_ThenQueryParameterIsNotChanged() + { + var queryParameter = Tuple.Create("name:in", "http://example.org/fhir/ValueSet/names"); + ConfigureSearchParameter("Patient", "name", CreateSearchParameter(SearchParamType.String)); + + IReadOnlyList> result = await _expander.ExpandAsync( + "Patient", + new[] { queryParameter }, + CancellationToken.None); + + Assert.Same(queryParameter, Assert.Single(result)); + await _terminologyServiceProxy.DidNotReceiveWithAnyArgs().ExpandAsync(default, default, default); + } + + private void ConfigureSearchParameter(string resourceType, string code, SearchParameterInfo searchParameter) + { + _searchParameterDefinitionManager + .TryGetSearchParameter(resourceType, code, out Arg.Any()) + .Returns(x => + { + x[2] = searchParameter; + return true; + }); + } + + private static SearchParameterInfo CreateSearchParameter(SearchParamType searchParamType) + { + return new SearchParameter + { + Name = "code", + Code = "code", + Type = Enum.Parse(searchParamType.ToString()), + }.ToInfo(); + } + } +} diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Search/SearchServiceTests.cs b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Search/SearchServiceTests.cs index 861d49385e..b6daeaf3da 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Search/SearchServiceTests.cs +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Features/Search/SearchServiceTests.cs @@ -75,6 +75,31 @@ public async Task GivenSearching_WhenSearched_ThenCorrectOptionIsUsedAndCorrectS Assert.Same(expectedSearchResult, actual); } + [Fact] + public async Task GivenSearchingWithQueryParameterExpander_WhenSearched_ThenExpandedQueryParametersAreUsed() + { + const string resourceType = "Medication"; + var queryParameters = new[] { Tuple.Create("code:in", "http://example.org/fhir/ValueSet/medications") }; + var expandedQueryParameters = new[] { Tuple.Create("code", "http://example.org/system|code") }; + var queryParameterExpander = Substitute.For(); + queryParameterExpander.ExpandAsync(resourceType, queryParameters, Arg.Any()).Returns(expandedQueryParameters); + + var searchService = new TestSearchService(_searchOptionsFactory, _fhirDataStore, new[] { queryParameterExpander }); + var expectedSearchOptions = new SearchOptions(); + _searchOptionsFactory.Create(resourceType, expandedQueryParameters).Returns(expectedSearchOptions); + + var expectedSearchResult = SearchResult.Empty(_unsupportedQueryParameters); + searchService.SearchImplementation = options => + { + Assert.Same(expectedSearchOptions, options); + return expectedSearchResult; + }; + + SearchResult actual = await searchService.SearchAsync(resourceType, queryParameters, CancellationToken.None); + + Assert.Same(expectedSearchResult, actual); + } + [Fact] public async Task GivenCompartmentSearching_WhenSearched_ThenCorrectOptionIsUsedAndCorrectSearchResultsReturned() { @@ -202,6 +227,15 @@ public TestSearchService(ISearchOptionsFactory searchOptionsFactory, IFhirDataSt SearchImplementation = options => null; } + public TestSearchService( + ISearchOptionsFactory searchOptionsFactory, + IFhirDataStore fhirDataStore, + IEnumerable queryParameterExpanders) + : base(searchOptionsFactory, fhirDataStore, NullLogger.Instance, queryParameterExpanders) + { + SearchImplementation = options => null; + } + public Func SearchImplementation { get; set; } public override Task> GetUsedResourceTypes(CancellationToken cancellationToken) diff --git a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Microsoft.Health.Fhir.Shared.Core.UnitTests.projitems b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Microsoft.Health.Fhir.Shared.Core.UnitTests.projitems index a3518aa589..4064779f4d 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Microsoft.Health.Fhir.Shared.Core.UnitTests.projitems +++ b/src/Microsoft.Health.Fhir.Shared.Core.UnitTests/Microsoft.Health.Fhir.Shared.Core.UnitTests.projitems @@ -127,6 +127,7 @@ + @@ -158,4 +159,4 @@ - \ No newline at end of file + diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Features/Search/SearchParameterValueSetExpander.cs b/src/Microsoft.Health.Fhir.Shared.Core/Features/Search/SearchParameterValueSetExpander.cs new file mode 100644 index 0000000000..2ae209d5bb --- /dev/null +++ b/src/Microsoft.Health.Fhir.Shared.Core/Features/Search/SearchParameterValueSetExpander.cs @@ -0,0 +1,151 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EnsureThat; +using Hl7.Fhir.Model; +using Hl7.Fhir.Utility; +using Microsoft.Health.Fhir.Core.Exceptions; +using Microsoft.Health.Fhir.Core.Extensions; +using Microsoft.Health.Fhir.Core.Features.Conformance; +using Microsoft.Health.Fhir.Core.Features.Definition; +using Microsoft.Health.Fhir.Core.Features.Search; +using Microsoft.Health.Fhir.Core.Features.Search.Expressions.Parsers; +using Microsoft.Health.Fhir.Core.Features.Search.SearchValues; +using Microsoft.Health.Fhir.Core.Models; +using SearchModifierCode = Microsoft.Health.Fhir.ValueSets.SearchModifierCode; +using SearchParamType = Microsoft.Health.Fhir.ValueSets.SearchParamType; + +namespace Microsoft.Health.Fhir.Core.Features.Search +{ + public class SearchParameterValueSetExpander : ISearchParameterQueryParameterExpander + { + private const string InModifierSuffix = ":in"; + private const string EmptyValueSetTokenSystem = "urn:microsoft:fhir:search:empty-valueset"; + private const string EmptyValueSetTokenCode = "__empty_valueset__"; + + private readonly ITerminologyServiceProxy _terminologyServiceProxy; + private readonly ISearchParameterDefinitionManager _searchParameterDefinitionManager; + + public SearchParameterValueSetExpander( + ITerminologyServiceProxy terminologyServiceProxy, + ISearchParameterDefinitionManager.SearchableSearchParameterDefinitionManagerResolver searchParameterDefinitionManagerResolver) + { + EnsureArg.IsNotNull(terminologyServiceProxy, nameof(terminologyServiceProxy)); + EnsureArg.IsNotNull(searchParameterDefinitionManagerResolver, nameof(searchParameterDefinitionManagerResolver)); + + _terminologyServiceProxy = terminologyServiceProxy; + _searchParameterDefinitionManager = searchParameterDefinitionManagerResolver(); + } + + public async Task>> ExpandAsync( + string resourceType, + IReadOnlyList> queryParameters, + CancellationToken cancellationToken) + { + if (queryParameters == null || queryParameters.Count == 0) + { + return queryParameters; + } + + List> expandedParameters = null; + + for (int i = 0; i < queryParameters.Count; i++) + { + Tuple queryParameter = queryParameters[i]; + Tuple expandedParameter = await ExpandParameterAsync(resourceType, queryParameter, cancellationToken); + + if (expandedParameter != queryParameter && expandedParameters == null) + { + expandedParameters = queryParameters.Take(i).ToList(); + } + + expandedParameters?.Add(expandedParameter); + } + + return expandedParameters ?? queryParameters; + } + + private async Task> ExpandParameterAsync( + string resourceType, + Tuple queryParameter, + CancellationToken cancellationToken) + { + if (queryParameter == null || + string.IsNullOrWhiteSpace(queryParameter.Item1) || + string.IsNullOrWhiteSpace(queryParameter.Item2) || + !queryParameter.Item1.EndsWith(InModifierSuffix, StringComparison.Ordinal)) + { + return queryParameter; + } + + string searchParameterCode = queryParameter.Item1.Substring(0, queryParameter.Item1.Length - InModifierSuffix.Length); + if (ExpressionParser.ContainsChainOrReverseParameter(searchParameterCode) || + !_searchParameterDefinitionManager.TryGetSearchParameter(resourceType, searchParameterCode, out SearchParameterInfo searchParameter) || + searchParameter.Type != SearchParamType.Token) + { + return queryParameter; + } + + IReadOnlyList valueSetUrls = queryParameter.Item2.SplitByOrSeparator(); + var tokens = new List(); + + foreach (string valueSetUrl in valueSetUrls) + { + ValueSet expandedValueSet = await ExpandValueSetAsync(valueSetUrl, cancellationToken); + tokens.AddRange(GetTokenSearchValues(expandedValueSet.Expansion?.Contains)); + } + + string expandedValue = tokens.Count == 0 + ? new TokenSearchValue(EmptyValueSetTokenSystem, EmptyValueSetTokenCode, null).ToString() + : string.Join(",", tokens.Select(token => token.ToString()).Distinct(StringComparer.Ordinal)); + + return Tuple.Create(searchParameterCode, expandedValue); + } + + private async Task ExpandValueSetAsync(string valueSetUrl, CancellationToken cancellationToken) + { + var parameters = new[] + { + Tuple.Create(TerminologyOperationParameterNames.Expand.Url, valueSetUrl), + }; + + var resource = await _terminologyServiceProxy.ExpandAsync(parameters, null, cancellationToken); + + if (resource?.ToPoco() is ValueSet valueSet) + { + return valueSet; + } + + throw new InvalidSearchOperationException( + string.Format(Core.Resources.ModifierNotSupported, SearchModifierCode.In.GetLiteral(), valueSetUrl)); + } + + private static IEnumerable GetTokenSearchValues(IEnumerable contains) + { + if (contains == null) + { + yield break; + } + + foreach (ValueSet.ContainsComponent component in contains) + { + if (!string.IsNullOrWhiteSpace(component.Code)) + { + yield return new TokenSearchValue(component.System, component.Code, null); + } + + foreach (TokenSearchValue nestedToken in GetTokenSearchValues(component.Contains)) + { + yield return nestedToken; + } + } + } + } +} diff --git a/src/Microsoft.Health.Fhir.Shared.Core/Microsoft.Health.Fhir.Shared.Core.projitems b/src/Microsoft.Health.Fhir.Shared.Core/Microsoft.Health.Fhir.Shared.Core.projitems index 4d87238f6e..15718f3054 100644 --- a/src/Microsoft.Health.Fhir.Shared.Core/Microsoft.Health.Fhir.Shared.Core.projitems +++ b/src/Microsoft.Health.Fhir.Shared.Core/Microsoft.Health.Fhir.Shared.Core.projitems @@ -90,6 +90,7 @@ + @@ -97,4 +98,4 @@ - \ No newline at end of file + diff --git a/src/Microsoft.Health.Fhir.SqlServer.UnitTests/Features/Search/SqlServerSearchServiceTests.cs b/src/Microsoft.Health.Fhir.SqlServer.UnitTests/Features/Search/SqlServerSearchServiceTests.cs index be7224d56a..a44646e457 100644 --- a/src/Microsoft.Health.Fhir.SqlServer.UnitTests/Features/Search/SqlServerSearchServiceTests.cs +++ b/src/Microsoft.Health.Fhir.SqlServer.UnitTests/Features/Search/SqlServerSearchServiceTests.cs @@ -113,6 +113,7 @@ public SqlServerSearchServiceTests() _compressedRawResourceConverter, _queryHashCalculator, _queryPlanReuseChecker, + Array.Empty(), NullLogger.Instance); } @@ -158,6 +159,7 @@ public void Constructor_WithNullSearchOptionsFactory_ThrowsArgumentNullException _compressedRawResourceConverter, _queryHashCalculator, _queryPlanReuseChecker, + Array.Empty(), NullLogger.Instance); }); @@ -206,6 +208,7 @@ public void Constructor_WithNullSqlRetryService_ThrowsArgumentNullException() _compressedRawResourceConverter, _queryHashCalculator, _queryPlanReuseChecker, + Array.Empty(), NullLogger.Instance); }); @@ -254,6 +257,7 @@ public void Constructor_WithNullSchemaInformation_ThrowsArgumentNullException() _compressedRawResourceConverter, _queryHashCalculator, _queryPlanReuseChecker, + Array.Empty(), NullLogger.Instance); }); diff --git a/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs index cd6ee8be33..d8de1c8e22 100644 --- a/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs +++ b/src/Microsoft.Health.Fhir.SqlServer/Features/Search/SqlServerSearchService.cs @@ -108,8 +108,9 @@ public SqlServerSearchService( ICompressedRawResourceConverter compressedRawResourceConverter, ISqlQueryHashCalculator queryHashCalculator, IQueryPlanReuseChecker queryPlanReuseChecker, + IEnumerable queryParameterExpanders, ILogger logger) - : base(searchOptionsFactory, fhirDataStore, logger) + : base(searchOptionsFactory, fhirDataStore, logger, queryParameterExpanders) { EnsureArg.IsNotNull(sqlRootExpressionRewriter, nameof(sqlRootExpressionRewriter)); EnsureArg.IsNotNull(chainFlatteningRewriter, nameof(chainFlatteningRewriter)); diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.Integration/Persistence/CosmosDbFhirStorageTestsFixture.cs b/test/Microsoft.Health.Fhir.Shared.Tests.Integration/Persistence/CosmosDbFhirStorageTestsFixture.cs index 28b191b156..9a88dde2ab 100644 --- a/test/Microsoft.Health.Fhir.Shared.Tests.Integration/Persistence/CosmosDbFhirStorageTestsFixture.cs +++ b/test/Microsoft.Health.Fhir.Shared.Tests.Integration/Persistence/CosmosDbFhirStorageTestsFixture.cs @@ -294,6 +294,7 @@ public virtual async Task InitializeAsync() cosmosDbPhysicalPartitionInfo, compartmentSearchRewriter, smartCompartmentSearchRewriter, + Array.Empty(), NullLogger.Instance); await _searchParameterDefinitionManager.EnsureInitializedAsync(CancellationToken.None); diff --git a/test/Microsoft.Health.Fhir.Shared.Tests.Integration/Persistence/SqlServerFhirStorageTestsFixture.cs b/test/Microsoft.Health.Fhir.Shared.Tests.Integration/Persistence/SqlServerFhirStorageTestsFixture.cs index b94e8ff261..bef3ec2889 100644 --- a/test/Microsoft.Health.Fhir.Shared.Tests.Integration/Persistence/SqlServerFhirStorageTestsFixture.cs +++ b/test/Microsoft.Health.Fhir.Shared.Tests.Integration/Persistence/SqlServerFhirStorageTestsFixture.cs @@ -315,6 +315,7 @@ public async Task InitializeAsync() new CompressedRawResourceConverter(), SqlQueryHashCalculator, queryPlanReuseChecker, + Array.Empty(), NullLogger.Instance); ISearchParameterSupportResolver searchParameterSupportResolver = Substitute.For();