Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<IReadOnlyList<Tuple<string, string>>> ExpandAsync(
string resourceType,
IReadOnlyList<Tuple<string, string>> queryParameters,
CancellationToken cancellationToken);
}
}
33 changes: 33 additions & 0 deletions src/Microsoft.Health.Fhir.Core/Features/Search/SearchService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public abstract class SearchService : ISearchService
private readonly ISearchOptionsFactory _searchOptionsFactory;
private readonly IFhirDataStore _fhirDataStore;
private readonly ILogger _logger;
private readonly IReadOnlyCollection<ISearchParameterQueryParameterExpander> _queryParameterExpanders;

/// <summary>
/// Initializes a new instance of the <see cref="SearchService"/> class.
Expand All @@ -36,6 +37,22 @@ public abstract class SearchService : ISearchService
/// <param name="fhirDataStore">The data store</param>
/// <param name="logger">Logger</param>
protected SearchService(ISearchOptionsFactory searchOptionsFactory, IFhirDataStore fhirDataStore, ILogger logger)
: this(searchOptionsFactory, fhirDataStore, logger, Array.Empty<ISearchParameterQueryParameterExpander>())
{
}

/// <summary>
/// Initializes a new instance of the <see cref="SearchService"/> class.
/// </summary>
/// <param name="searchOptionsFactory">The search options factory.</param>
/// <param name="fhirDataStore">The data store</param>
/// <param name="logger">Logger</param>
/// <param name="queryParameterExpanders">Search query parameter expanders.</param>
protected SearchService(
ISearchOptionsFactory searchOptionsFactory,
IFhirDataStore fhirDataStore,
ILogger logger,
IEnumerable<ISearchParameterQueryParameterExpander> queryParameterExpanders)
{
EnsureArg.IsNotNull(searchOptionsFactory, nameof(searchOptionsFactory));
EnsureArg.IsNotNull(fhirDataStore, nameof(fhirDataStore));
Expand All @@ -44,6 +61,7 @@ protected SearchService(ISearchOptionsFactory searchOptionsFactory, IFhirDataSto
_searchOptionsFactory = searchOptionsFactory;
_fhirDataStore = fhirDataStore;
_logger = logger;
_queryParameterExpanders = queryParameterExpanders?.ToArray() ?? Array.Empty<ISearchParameterQueryParameterExpander>();
}

public async Task TryLogEvent(string process, string status, string text, DateTime? startDate, CancellationToken cancellationToken)
Expand All @@ -61,6 +79,7 @@ public virtual async Task<SearchResult> 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.
Expand All @@ -77,6 +96,7 @@ public async Task<SearchResult> 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.
Expand Down Expand Up @@ -286,6 +306,19 @@ public abstract Task<SearchResult> SearchAsync(
SearchOptions searchOptions,
CancellationToken cancellationToken);

private async Task<IReadOnlyList<Tuple<string, string>>> ExpandQueryParametersAsync(
string resourceType,
IReadOnlyList<Tuple<string, string>> queryParameters,
CancellationToken cancellationToken)
{
foreach (ISearchParameterQueryParameterExpander expander in _queryParameterExpanders)
{
queryParameters = await expander.ExpandAsync(resourceType, queryParameters, cancellationToken);
}

return queryParameters;
}

protected abstract Task<SearchResult> SearchForReindexInternalAsync(
SearchOptions searchOptions,
string searchParameterHash,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ public FhirCosmosSearchService(
ICosmosDbCollectionPhysicalPartitionInfo physicalPartitionInfo,
CompartmentSearchRewriter compartmentSearchRewriter,
SmartCompartmentSearchRewriter smartCompartmentSearchRewriter,
IEnumerable<ISearchParameterQueryParameterExpander> queryParameterExpanders,
ILogger<FhirCosmosSearchService> logger)
: base(searchOptionsFactory, fhirDataStore, logger)
: base(searchOptionsFactory, fhirDataStore, logger, queryParameterExpanders)
{
EnsureArg.IsNotNull(fhirDataStore, nameof(fhirDataStore));
EnsureArg.IsNotNull(queryBuilder, nameof(queryBuilder));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ public void Load(IServiceCollection services)

services.AddSingleton<ISearchParameterExpressionParser, SearchParameterExpressionParser>();
services.AddSingleton<IExpressionParser, ExpressionParser>();
services.AddSingleton<ISearchParameterQueryParameterExpander, SearchParameterValueSetExpander>();
services.AddSingleton<ISearchOptionsFactory, SearchOptionsFactory>();
services.AddSingleton<IReferenceToElementResolver, LightweightReferenceToElementResolver>();
services.AddTransient<ExpressionAccessControl>();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ITerminologyServiceProxy>();
private readonly ISearchParameterDefinitionManager _searchParameterDefinitionManager = Substitute.For<ISearchParameterDefinitionManager>();
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<IReadOnlyList<Tuple<string, string>>>(),
null,
Arg.Any<CancellationToken>())
.Returns(Task.FromResult(new ValueSet
{
Expansion = new ValueSet.ExpansionComponent
{
Contains = new List<ValueSet.ContainsComponent>
{
new ValueSet.ContainsComponent { System = "http://rx.example", Code = "a" },
new ValueSet.ContainsComponent { System = "http://rx.example", Code = "b" },
},
},
}.ToResourceElement()));

IReadOnlyList<Tuple<string, string>> result = await _expander.ExpandAsync(
"Medication",
new[] { Tuple.Create("code:in", valueSetUrl) },
CancellationToken.None);

Tuple<string, string> 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<IReadOnlyList<Tuple<string, string>>>(x => x.Single().Item1 == TerminologyOperationParameterNames.Expand.Url && x.Single().Item2 == valueSetUrl),
null,
Arg.Any<CancellationToken>());
}

[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<Tuple<string, string>> 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<SearchParameterInfo>())
.Returns(x =>
{
x[2] = searchParameter;
return true;
});
}

private static SearchParameterInfo CreateSearchParameter(SearchParamType searchParamType)
{
return new SearchParameter
{
Name = "code",
Code = "code",
Type = Enum.Parse<Hl7.Fhir.Model.SearchParamType>(searchParamType.ToString()),
}.ToInfo();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ISearchParameterQueryParameterExpander>();
queryParameterExpander.ExpandAsync(resourceType, queryParameters, Arg.Any<CancellationToken>()).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()
{
Expand Down Expand Up @@ -202,6 +227,15 @@ public TestSearchService(ISearchOptionsFactory searchOptionsFactory, IFhirDataSt
SearchImplementation = options => null;
}

public TestSearchService(
ISearchOptionsFactory searchOptionsFactory,
IFhirDataStore fhirDataStore,
IEnumerable<ISearchParameterQueryParameterExpander> queryParameterExpanders)
: base(searchOptionsFactory, fhirDataStore, NullLogger.Instance, queryParameterExpanders)
{
SearchImplementation = options => null;
}

public Func<SearchOptions, SearchResult> SearchImplementation { get; set; }

public override Task<IReadOnlyList<string>> GetUsedResourceTypes(CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Features\Search\Expressions\Parsers\SearchValueExpressionBuilderTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Features\Search\SearchExpressionTestHelper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Features\Search\SearchOptionsFactoryTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Features\Search\SearchParameterValueSetExpanderTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Features\Search\SearchResourceHandlerTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Features\Search\SearchResourceHistoryHandlerTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Features\Search\SearchServiceTests.cs" />
Expand Down Expand Up @@ -158,4 +159,4 @@
<ItemGroup>
<Folder Include="$(MSBuildThisFileDirectory)Features\Operations\MemberMatch\" />
</ItemGroup>
</Project>
</Project>
Loading
Loading