Esempio n. 1
0
        public void GivenAChainedParameterPointingToASingleResourceType_WhenParsed_ThenCorrectExpressionShouldBeCreated()
        {
            ResourceType sourceResourceType = ResourceType.Patient;
            ResourceType targetResourceType = ResourceType.Organization;

            string param1 = "ref";
            string param2 = "param";

            string key   = $"{param1}.{param2}";
            string value = "Seattle";

            // Setup the search parameters.
            SetupReferenceSearchParameter(
                sourceResourceType,
                param1,
                targetResourceType);

            SearchParameterInfo searchParameter = SetupSearchParameter(targetResourceType, param2);

            Expression expectedExpression = SetupExpression(searchParameter, value);

            // Parse the expression.
            Expression expression = _expressionParser.Parse(sourceResourceType.ToString(), key, value);

            ValidateMultiaryExpression(
                expression,
                MultiaryOperator.Or,
                chainedExpression => ValidateChainedExpression(
                    chainedExpression,
                    sourceResourceType,
                    param1,
                    targetResourceType.ToString(),
                    actualSearchExpression => Assert.Equal(expectedExpression, actualSearchExpression)));
        }
Esempio n. 2
0
        private Expression SetupExpression(SearchParameterInfo searchParameter, string value)
        {
            Expression expectedExpression = Substitute.For <Expression>();

            _searchParameterExpressionParser.Parse(searchParameter, null, value).Returns(expectedExpression);

            return(expectedExpression);
        }
Esempio n. 3
0
        private Expression CreateSearchExpression(ResourceElement coverage, ResourceElement patient)
        {
            var coverageValues          = _searchIndexer.Extract(coverage);
            var patientValues           = _searchIndexer.Extract(patient);
            var expressions             = new List <Expression>();
            var reverseChainExpressions = new List <Expression>();

            expressions.Add(Expression.SearchParameter(_resourceTypeSearchParameter, Expression.StringEquals(FieldName.TokenCode, null, KnownResourceTypes.Patient, false)));
            foreach (var patientValue in patientValues)
            {
                if (IgnoreInSearch(patientValue))
                {
                    continue;
                }

                var modifier = string.Empty;
                if (patientValue.SearchParameter.Type == ValueSets.SearchParamType.String)
                {
                    modifier = ":exact";
                }

                expressions.Add(_expressionParser.Parse(new[] { KnownResourceTypes.Patient }, patientValue.SearchParameter.Code + modifier, patientValue.Value.ToString()));
            }

            foreach (var coverageValue in coverageValues)
            {
                if (IgnoreInSearch(coverageValue))
                {
                    continue;
                }

                var modifier = string.Empty;
                if (coverageValue.SearchParameter.Type == ValueSets.SearchParamType.String)
                {
                    modifier = ":exact";
                }

                reverseChainExpressions.Add(_expressionParser.Parse(new[] { KnownResourceTypes.Coverage }, coverageValue.SearchParameter.Code + modifier, coverageValue.Value.ToString()));
            }

            if (reverseChainExpressions.Count != 0)
            {
                Expression reverseChainedExpression;
                if (reverseChainExpressions.Count == 1)
                {
                    reverseChainedExpression = reverseChainExpressions[0];
                }
                else
                {
                    reverseChainedExpression = Expression.And(reverseChainExpressions);
                }

                var expression = Expression.Chained(new[] { KnownResourceTypes.Coverage }, _coverageBeneficiaryParameter, new[] { KnownResourceTypes.Patient }, true, reverseChainedExpression);
                expressions.Add(expression);
            }

            return(Expression.And(expressions));
        }
Esempio n. 4
0
        public void GivenMissingModifierIsSpecified_WhenBuilt_ThenMissingExpressionShouldBeCreated(string isMissingString, bool expectedIsMissing)
        {
            Expression expression = _parser.Parse(
                CreateSearchParameter(SearchParamType.String),
                new SearchModifier(SearchModifierCode.Missing),
                isMissingString);

            ValidateMissingParamExpression(expression, DefaultParamName, expectedIsMissing);
        }
Esempio n. 5
0
        private void Validate(
            SearchParameterInfo searchParameter,
            SearchModifier modifier,
            string value,
            Action <Expression> valueValidator)
        {
            Expression expression = _parser.Parse(searchParameter, modifier, value);

            Assert.NotNull(expression);
            ValidateSearchParameterExpression(expression, DefaultParamName, valueValidator);
        }
Esempio n. 6
0
        public void GivenAChainedParameterPointingToMultipleResourceTypes_WhenParsed_ThenCorrectExpressionShouldBeCreated()
        {
            ResourceType sourceResourceType = ResourceType.Patient;

            ResourceType[] targetResourceTypes = new[] { ResourceType.Organization, ResourceType.Practitioner };

            string param1 = "ref";
            string param2 = "param";

            string key   = $"{param1}.{param2}";
            string value = "Seattle";

            // Setup the search parameters.
            SetupReferenceSearchParameter(sourceResourceType, param1, targetResourceTypes);

            var expectedTargets = targetResourceTypes.Select(targetResourceType =>
            {
                SearchParameterInfo searchParameter = SetupSearchParameter(targetResourceType, param2);

                Expression expectedExpression = SetupExpression(searchParameter, value);

                return(new { TargetResourceType = targetResourceType, Expression = expectedExpression });
            })
                                  .ToArray();

            // Parse the expression.
            Expression expression = _expressionParser.Parse(sourceResourceType.ToString(), key, value);

            ValidateMultiaryExpression(
                expression,
                MultiaryOperator.Or,
                expectedTargets.Select(expected =>
            {
                return((Action <Expression>)(chainedExpression =>
                                             ValidateChainedExpression(
                                                 chainedExpression,
                                                 sourceResourceType,
                                                 param1,
                                                 expected.TargetResourceType.ToString(),
                                                 actualSearchExpression => Assert.Equal(expected.Expression, actualSearchExpression))));
            })
                .ToArray());
        }
Esempio n. 7
0
        public void GivenANestedChainedParameter_WhenParsed_ThenCorrectExpressionShouldBeCreated()
        {
            ResourceType sourceResourceType       = ResourceType.Patient;
            ResourceType firstTargetResourceType  = ResourceType.Organization;
            ResourceType secondTargetResourceType = ResourceType.Practitioner;

            string param1 = "ref1";
            string param2 = "ref2";
            string param3 = "param";

            string key   = $"{param1}.{param2}.{param3}";
            string value = "Microsoft";

            // Setup the search parameters.
            SetupReferenceSearchParameter(sourceResourceType, param1, firstTargetResourceType);
            SetupReferenceSearchParameter(firstTargetResourceType, param2, secondTargetResourceType);

            SearchParameterInfo searchParameter = SetupSearchParameter(secondTargetResourceType, param3);

            Expression expectedExpression = SetupExpression(searchParameter, value);

            // Parse the expression.
            Expression expression = _expressionParser.Parse(sourceResourceType.ToString(), key, value);

            ValidateMultiaryExpression(
                expression,
                MultiaryOperator.Or,
                chainedExpression => ValidateChainedExpression(
                    chainedExpression,
                    sourceResourceType,
                    param1,
                    firstTargetResourceType.ToString(),
                    nestedExpression => ValidateMultiaryExpression(
                        nestedExpression,
                        MultiaryOperator.Or,
                        nestedChainedExpression => ValidateChainedExpression(
                            nestedChainedExpression,
                            firstTargetResourceType,
                            param2,
                            secondTargetResourceType.ToString(),
                            actualSearchExpression => Assert.Equal(expectedExpression, actualSearchExpression)))));
        }
Esempio n. 8
0
        public void GivenAChainedParameterPointingToMultipleResourceTypesAndSearchParamIsNotSupportedByAllTargetResourceTypes_WhenParsed_ThenOnlyExpressionsForResourceTypeThatSupportsSearchParamShouldBeCreated()
        {
            ResourceType sourceResourceType = ResourceType.Patient;

            // The reference will support both Organization and Practitioner,
            // but the search value will only be supported by Practitioner.
            ResourceType[] targetResourceTypes = new[] { ResourceType.Organization, ResourceType.Practitioner };

            string param1 = "ref";
            string param2 = "param";

            string key   = $"{param1}.{param2}";
            string value = "Lewis";

            // Setup the search parameters.
            SetupReferenceSearchParameter(sourceResourceType, param1, targetResourceTypes);

            // Setup the Organization to not support this search param.
            _searchParameterDefinitionManager.GetSearchParameter(ResourceType.Organization.ToString(), param2)
            .Returns(x => throw new SearchParameterNotSupportedException(x.ArgAt <string>(0), x.ArgAt <string>(1)));

            // Setup the Practitioner to support this search param.
            SearchParameterInfo searchParameter = SetupSearchParameter(ResourceType.Practitioner, param2);

            Expression expectedExpression = SetupExpression(searchParameter, value);

            // Parse the expression.
            Expression expression = _expressionParser.Parse(sourceResourceType.ToString(), key, value);

            ValidateMultiaryExpression(
                expression,
                MultiaryOperator.Or,
                chainedExpression => ValidateChainedExpression(
                    chainedExpression,
                    sourceResourceType,
                    param1,
                    ResourceType.Practitioner.ToString(),
                    actualSearchExpression => Assert.Equal(expectedExpression, actualSearchExpression)));
        }
Esempio n. 9
0
        public void GivenAChainedParameterPointingToMultipleResourceTypesAndWithResourceTypeSpecified_WhenParsed_ThenOnlyExpressionForTheSpecifiedResourceTypeShouldBeCreated()
        {
            ResourceType sourceResourceType = ResourceType.Patient;

            // The reference will support both Organization and Practitioner,
            // but we will limit the search to Organization only in the key below.
            ResourceType[] targetResourceTypes = new[] { ResourceType.Organization, ResourceType.Practitioner };

            string param1 = "ref";
            string param2 = "param";

            string key   = $"{param1}:Organization.{param2}";
            string value = "Seattle";

            // Setup the search parameters.
            SetupReferenceSearchParameter(sourceResourceType, param1, targetResourceTypes);

            Expression[] expectedExpressions = targetResourceTypes.Select(targetResourceType =>
            {
                SearchParameterInfo searchParameter = SetupSearchParameter(targetResourceType, param2);

                return(SetupExpression(searchParameter, value));
            })
                                               .ToArray();

            // Parse the expression.
            Expression expression = _expressionParser.Parse(sourceResourceType.ToString(), key, value);

            ValidateMultiaryExpression(
                expression,
                MultiaryOperator.Or,
                chainedExpression => ValidateChainedExpression(
                    chainedExpression,
                    sourceResourceType,
                    param1,
                    ResourceType.Organization.ToString(),
                    actualSearchExpression => Assert.Equal(expectedExpressions[0], actualSearchExpression)));
        }
Esempio n. 10
0
        public void GivenAModifier_WhenParsed_ThenExceptionShouldBeThrown()
        {
            ResourceType resourceType = ResourceType.Patient;

            string param1   = "ref";
            string modifier = "missing";

            // Practitioner is a valid resource type but is not supported by the search parameter.
            string key   = $"{param1}:{modifier}";
            string value = "Seattle";

            SearchParameterInfo searchParameter = SetupSearchParameter(resourceType, param1);

            Expression expression = Substitute.For <Expression>();

            _searchParameterExpressionParser.Parse(searchParameter, SearchParameter.SearchModifierCode.Missing, value).Returns(expression);

            // Parse the expression.
            Expression actualExpression = _expressionParser.Parse(resourceType.ToString(), key, value);

            // The mock requires the modifier to match so if we get the same expression instance
            // then it means we got the modifier correctly.
            Assert.Equal(expression, actualExpression);
        }
Esempio n. 11
0
        public SearchOptions Create(string compartmentType, string compartmentId, string resourceType, IReadOnlyList <Tuple <string, string> > queryParameters)
        {
            var searchOptions = new SearchOptions();

            string continuationToken = null;

            var searchParams = new SearchParams();
            var unsupportedSearchParameters = new List <Tuple <string, string> >();

            // Extract the continuation token, filter out the other known query parameters that's not search related.
            foreach (Tuple <string, string> query in queryParameters ?? Enumerable.Empty <Tuple <string, string> >())
            {
                if (query.Item1 == KnownQueryParameterNames.ContinuationToken)
                {
                    if (continuationToken != null)
                    {
                        throw new InvalidSearchOperationException(
                                  string.Format(Core.Resources.MultipleQueryParametersNotAllowed, KnownQueryParameterNames.ContinuationToken));
                    }

                    continuationToken = query.Item2;
                }
                else if (query.Item1 == KnownQueryParameterNames.Format)
                {
                    // TODO: We need to handle format parameter.
                }
                else if (string.IsNullOrWhiteSpace(query.Item1) || string.IsNullOrWhiteSpace(query.Item2))
                {
                    // Query parameter with empty value is not supported.
                    unsupportedSearchParameters.Add(query);
                }
                else
                {
                    // Parse the search parameters.
                    try
                    {
                        searchParams.Add(query.Item1, query.Item2);
                    }
                    catch (Exception ex)
                    {
                        _logger.LogInformation(ex, "Failed to parse the query parameter. Skipping.");

                        // There was a problem parsing the parameter. Add it to list of unsupported parameters.
                        unsupportedSearchParameters.Add(query);
                    }
                }
            }

            searchOptions.ContinuationToken = continuationToken;

            // Check the item count.
            if (searchParams.Count != null)
            {
                searchOptions.MaxItemCount = searchParams.Count.Value;
            }

            // Check to see if only the count should be returned
            searchOptions.CountOnly = searchParams.Summary == SummaryType.Count;

            // If the resource type is not specified, then the common
            // search parameters should be used.
            ResourceType parsedResourceType = ResourceType.DomainResource;

            if (!string.IsNullOrWhiteSpace(resourceType) &&
                !Enum.TryParse(resourceType, out parsedResourceType))
            {
                throw new ResourceNotSupportedException(resourceType);
            }

            var searchExpressions = new List <Expression>();

            if (!string.IsNullOrWhiteSpace(resourceType))
            {
                searchExpressions.Add(Expression.SearchParameter(_resourceTypeSearchParameter, Expression.StringEquals(FieldName.TokenCode, null, resourceType, false)));
            }

            searchExpressions.AddRange(searchParams.Parameters.Select(
                                           q =>
            {
                try
                {
                    return(_expressionParser.Parse(parsedResourceType.ToString(), q.Item1, q.Item2));
                }
                catch (SearchParameterNotSupportedException)
                {
                    unsupportedSearchParameters.Add(q);

                    return(null);
                }
            })
                                       .Where(item => item != null));

            if (!string.IsNullOrWhiteSpace(compartmentType))
            {
                if (Enum.TryParse(compartmentType, out CompartmentType parsedCompartmentType))
                {
                    if (string.IsNullOrWhiteSpace(compartmentId))
                    {
                        throw new InvalidSearchOperationException(Core.Resources.CompartmentIdIsInvalid);
                    }

                    searchExpressions.Add(Expression.CompartmentSearch(compartmentType, compartmentId));
                }
                else
                {
                    throw new InvalidSearchOperationException(string.Format(Core.Resources.CompartmentTypeIsInvalid, compartmentType));
                }
            }

            if (searchExpressions.Count == 1)
            {
                searchOptions.Expression = searchExpressions[0];
            }
            else if (searchExpressions.Count > 1)
            {
                searchOptions.Expression = Expression.And(searchExpressions.ToArray());
            }

            if (unsupportedSearchParameters.Any())
            {
                // TODO: Client can specify whether exception should be raised or not when it encounters unknown search parameters.
                // For now, we will ignore any unknown search parameters.
            }

            searchOptions.UnsupportedSearchParams = unsupportedSearchParameters;

            return(searchOptions);
        }
Esempio n. 12
0
        public SearchOptions Create(string compartmentType, string compartmentId, string resourceType, IReadOnlyList <Tuple <string, string> > queryParameters)
        {
            var searchOptions = new SearchOptions();

            string continuationToken = null;

            var  searchParams = new SearchParams();
            var  unsupportedSearchParameters = new List <Tuple <string, string> >();
            bool setDefaultBundleTotal       = true;

            // Extract the continuation token, filter out the other known query parameters that's not search related.
            foreach (Tuple <string, string> query in queryParameters ?? Enumerable.Empty <Tuple <string, string> >())
            {
                if (query.Item1 == KnownQueryParameterNames.ContinuationToken)
                {
                    // This is an unreachable case. The mapping of the query parameters makes it so only one continuation token can exist.
                    if (continuationToken != null)
                    {
                        throw new InvalidSearchOperationException(
                                  string.Format(Core.Resources.MultipleQueryParametersNotAllowed, KnownQueryParameterNames.ContinuationToken));
                    }

                    try
                    {
                        continuationToken = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(query.Item2));
                    }
                    catch (FormatException)
                    {
                        throw new BadRequestException(Core.Resources.InvalidContinuationToken);
                    }

                    setDefaultBundleTotal = false;
                }
                else if (query.Item1 == KnownQueryParameterNames.Format)
                {
                    // TODO: We need to handle format parameter.
                }
                else if (string.IsNullOrWhiteSpace(query.Item1) || string.IsNullOrWhiteSpace(query.Item2))
                {
                    // Query parameter with empty value is not supported.
                    unsupportedSearchParameters.Add(query);
                }
                else if (string.Compare(query.Item1, KnownQueryParameterNames.Total, StringComparison.OrdinalIgnoreCase) == 0)
                {
                    if (Enum.TryParse <TotalType>(query.Item2, true, out var totalType))
                    {
                        ValidateTotalType(totalType);

                        searchOptions.IncludeTotal = totalType;
                        setDefaultBundleTotal      = false;
                    }
                    else
                    {
                        throw new BadRequestException(string.Format(Core.Resources.InvalidTotalParameter, query.Item2, SupportedTotalTypes));
                    }
                }
                else
                {
                    // Parse the search parameters.
                    try
                    {
                        // Basic format checking (e.g. integer value for _count key etc.).
                        searchParams.Add(query.Item1, query.Item2);
                    }
                    catch (Exception ex)
                    {
                        throw new BadRequestException(ex.Message);
                    }
                }
            }

            searchOptions.ContinuationToken = continuationToken;

            if (setDefaultBundleTotal)
            {
                ValidateTotalType(_featureConfiguration.IncludeTotalInBundle);
                searchOptions.IncludeTotal = _featureConfiguration.IncludeTotalInBundle;
            }

            // Check the item count.
            if (searchParams.Count != null)
            {
                if (searchParams.Count > _featureConfiguration.MaxItemCountPerSearch)
                {
                    searchOptions.MaxItemCount = _featureConfiguration.MaxItemCountPerSearch;

                    _contextAccessor.FhirRequestContext.BundleIssues.Add(
                        new OperationOutcomeIssue(
                            OperationOutcomeConstants.IssueSeverity.Information,
                            OperationOutcomeConstants.IssueType.Informational,
                            string.Format(Core.Resources.SearchParamaterCountExceedLimit, _featureConfiguration.MaxItemCountPerSearch, searchParams.Count)));
                }
                else
                {
                    searchOptions.MaxItemCount = searchParams.Count.Value;
                }
            }
            else
            {
                searchOptions.MaxItemCount = _featureConfiguration.DefaultItemCountPerSearch;
            }

            searchOptions.IncludeCount = _featureConfiguration.DefaultIncludeCountPerSearch;

            // Check to see if only the count should be returned
            searchOptions.CountOnly = searchParams.Summary == SummaryType.Count;

            // If the resource type is not specified, then the common
            // search parameters should be used.
            ResourceType parsedResourceType = ResourceType.DomainResource;

            if (!string.IsNullOrWhiteSpace(resourceType) &&
                !Enum.TryParse(resourceType, out parsedResourceType))
            {
                throw new ResourceNotSupportedException(resourceType);
            }

            var searchExpressions = new List <Expression>();

            if (!string.IsNullOrWhiteSpace(resourceType))
            {
                searchExpressions.Add(Expression.SearchParameter(_resourceTypeSearchParameter, Expression.StringEquals(FieldName.TokenCode, null, resourceType, false)));
            }

            searchExpressions.AddRange(searchParams.Parameters.Select(
                                           q =>
            {
                try
                {
                    return(_expressionParser.Parse(parsedResourceType.ToString(), q.Item1, q.Item2));
                }
                catch (SearchParameterNotSupportedException)
                {
                    unsupportedSearchParameters.Add(q);

                    return(null);
                }
            })
                                       .Where(item => item != null));

            if (searchParams.Include?.Count > 0)
            {
                searchExpressions.AddRange(searchParams.Include.Select(
                                               q => _expressionParser.ParseInclude(parsedResourceType.ToString(), q, false /* not reversed */, false /* no iterate */))
                                           .Where(item => item != null));
            }

            if (searchParams.RevInclude?.Count > 0)
            {
                searchExpressions.AddRange(searchParams.RevInclude.Select(
                                               q => _expressionParser.ParseInclude(parsedResourceType.ToString(), q, true /* reversed */, false /* no iterate */))
                                           .Where(item => item != null));
            }

            // Parse _include:iterate (_include:recurse) parameters.
            // :iterate (:recurse) modifiers are not supported by Hl7.Fhir.Rest, hence not added to the Include collection and exist in the Parameters list.
            // See https://github.com/FirelyTeam/fhir-net-api/issues/222
            // _include:iterate (_include:recurse) expression may appear without a preceding _include parameter
            // when applied on a circular reference
            searchExpressions.AddRange(ParseIncludeIterateExpressions(searchParams));

            // remove _include:iterate and _revinclude:iterate parameters from unsupportedSearchParameters
            unsupportedSearchParameters.RemoveAll(p => AllIterateModifiers.Contains(p.Item1));

            if (!string.IsNullOrWhiteSpace(compartmentType))
            {
                if (Enum.TryParse(compartmentType, out CompartmentType parsedCompartmentType))
                {
                    if (string.IsNullOrWhiteSpace(compartmentId))
                    {
                        throw new InvalidSearchOperationException(Core.Resources.CompartmentIdIsInvalid);
                    }

                    searchExpressions.Add(Expression.CompartmentSearch(compartmentType, compartmentId));
                }
                else
                {
                    throw new InvalidSearchOperationException(string.Format(Core.Resources.CompartmentTypeIsInvalid, compartmentType));
                }
            }

            if (searchExpressions.Count == 1)
            {
                searchOptions.Expression = searchExpressions[0];
            }
            else if (searchExpressions.Count > 1)
            {
                searchOptions.Expression = Expression.And(searchExpressions.ToArray());
            }

            if (unsupportedSearchParameters.Any())
            {
                // TODO: Client can specify whether exception should be raised or not when it encounters unknown search parameters.
                // For now, we will ignore any unknown search parameters.
            }

            searchOptions.UnsupportedSearchParams = unsupportedSearchParameters;

            if (searchParams.Sort?.Count > 0)
            {
                var sortings = new List <(SearchParameterInfo, SortOrder)>();
                List <(string parameterName, string reason)> unsupportedSortings = null;

                foreach (Tuple <string, Hl7.Fhir.Rest.SortOrder> sorting in searchParams.Sort)
                {
                    try
                    {
                        SearchParameterInfo searchParameterInfo = _searchParameterDefinitionManager.GetSearchParameter(parsedResourceType.ToString(), sorting.Item1);

                        if (searchParameterInfo.IsSortSupported())
                        {
                            sortings.Add((searchParameterInfo, sorting.Item2.ToCoreSortOrder()));
                        }
                        else
                        {
                            throw new SearchParameterNotSupportedException(string.Format(Core.Resources.SearchSortParameterNotSupported, searchParameterInfo.Name));
                        }
                    }
                    catch (SearchParameterNotSupportedException)
                    {
                        (unsupportedSortings ??= new List <(string parameterName, string reason)>()).Add((sorting.Item1, string.Format(Core.Resources.SearchSortParameterNotSupported, sorting.Item1)));
                    }
                }

                searchOptions.Sort = sortings;
                searchOptions.UnsupportedSortingParams = (IReadOnlyList <(string parameterName, string reason)>)unsupportedSortings ?? Array.Empty <(string parameterName, string reason)>();
            }
            else
            {
                searchOptions.Sort = Array.Empty <(SearchParameterInfo searchParameterInfo, SortOrder sortOrder)>();
                searchOptions.UnsupportedSortingParams = Array.Empty <(string parameterName, string reason)>();
            }

            return(searchOptions);

            IEnumerable <IncludeExpression> ParseIncludeIterateExpressions(SearchParams searchParams)
            {
                return(searchParams.Parameters
                       .Where(p => p != null && AllIterateModifiers.Where(m => string.Equals(p.Item1, m, StringComparison.OrdinalIgnoreCase)).Any())
                       .Select(p =>
                {
                    var includeResourceType = p.Item2?.Split(':')[0];
                    if (!ModelInfoProvider.IsKnownResource(includeResourceType))
                    {
                        throw new ResourceNotSupportedException(includeResourceType);
                    }

                    var reversed = RevIncludeIterateModifiers.Contains(p.Item1);
                    var expression = _expressionParser.ParseInclude(includeResourceType, p.Item2, reversed, true);

                    // Reversed Iterate expressions (not wildcard) must specify target type if there is more than one possible target type
                    if (expression.Reversed && expression.Iterate && expression.TargetResourceType == null && expression.ReferenceSearchParameter?.TargetResourceTypes?.Count > 1)
                    {
                        throw new BadRequestException(string.Format(Core.Resources.RevIncludeIterateTargetTypeNotSpecified, p.Item2));
                    }

                    // For circular include iterate expressions, add an informational issue indicating that a single iteration is supported.
                    // See https://www.hl7.org/fhir/search.html#revinclude.
                    if (expression.Iterate && expression.CircularReference)
                    {
                        _contextAccessor.FhirRequestContext.BundleIssues.Add(
                            new OperationOutcomeIssue(
                                OperationOutcomeConstants.IssueSeverity.Information,
                                OperationOutcomeConstants.IssueType.Informational,
                                string.Format(Core.Resources.IncludeIterateCircularReferenceExecutedOnce, p.Item1, p.Item2)));
                    }

                    return expression;
                }));
            }

            void ValidateTotalType(TotalType totalType)
            {
                // Estimate is not yet supported.
                if (totalType == TotalType.Estimate)
                {
                    throw new SearchOperationNotSupportedException(string.Format(Core.Resources.UnsupportedTotalParameter, totalType, SupportedTotalTypes));
                }
            }
        }
Esempio n. 13
0
        public SearchOptions Create(string compartmentType, string compartmentId, string resourceType, IReadOnlyList <Tuple <string, string> > queryParameters)
        {
            var searchOptions = new SearchOptions();

            string continuationToken = null;

            var  searchParams = new SearchParams();
            var  unsupportedSearchParameters = new List <Tuple <string, string> >();
            bool setDefaultBundleTotal       = true;

            // Extract the continuation token, filter out the other known query parameters that's not search related.
            foreach (Tuple <string, string> query in queryParameters ?? Enumerable.Empty <Tuple <string, string> >())
            {
                if (query.Item1 == KnownQueryParameterNames.ContinuationToken)
                {
                    // This is an unreachable case. The mapping of the query parameters makes it so only one continuation token can exist.
                    if (continuationToken != null)
                    {
                        throw new InvalidSearchOperationException(
                                  string.Format(Core.Resources.MultipleQueryParametersNotAllowed, KnownQueryParameterNames.ContinuationToken));
                    }

                    try
                    {
                        continuationToken = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(query.Item2));
                    }
                    catch (FormatException)
                    {
                        throw new BadRequestException(Core.Resources.InvalidContinuationToken);
                    }

                    setDefaultBundleTotal = false;
                }
                else if (query.Item1 == KnownQueryParameterNames.Format || query.Item1 == KnownQueryParameterNames.Pretty)
                {
                    // _format and _pretty are not search parameters, so we can ignore them.
                }
                else if (string.Equals(query.Item1, KnownQueryParameterNames.Type, StringComparison.OrdinalIgnoreCase))
                {
                    if (string.IsNullOrWhiteSpace(query.Item2))
                    {
                        throw new BadRequestException(string.Format(Core.Resources.InvalidTypeParameter, query.Item2));
                    }

                    var types    = query.Item2.SplitByOrSeparator();
                    var badTypes = types.Where(type => !ModelInfoProvider.IsKnownResource(type)).ToHashSet();

                    if (badTypes.Count != 0)
                    {
                        _contextAccessor.RequestContext?.BundleIssues.Add(
                            new OperationOutcomeIssue(
                                OperationOutcomeConstants.IssueSeverity.Warning,
                                OperationOutcomeConstants.IssueType.NotSupported,
                                string.Format(Core.Resources.InvalidTypeParameter, badTypes.OrderBy(x => x).Select(type => $"'{type}'").JoinByOrSeparator())));
                        if (badTypes.Count != types.Count)
                        {
                            // In case of we have acceptable types, we filter invalid types from search.
                            searchParams.Add(KnownQueryParameterNames.Type, types.Except(badTypes).JoinByOrSeparator());
                        }
                        else
                        {
                            // If all types are invalid, we add them to search params. If we remove them, we wouldn't filter by type, and return all types,
                            // which is incorrect behaviour. Optimally we should indicate in search options what it would yield nothing, and skip search,
                            // but there is no option for that right now.
                            searchParams.Add(KnownQueryParameterNames.Type, query.Item2);
                        }
                    }
                    else
                    {
                        searchParams.Add(KnownQueryParameterNames.Type, query.Item2);
                    }
                }
                else if (string.IsNullOrWhiteSpace(query.Item1) || string.IsNullOrWhiteSpace(query.Item2))
                {
                    // Query parameter with empty value is not supported.
                    unsupportedSearchParameters.Add(query);
                }
                else if (string.Compare(query.Item1, KnownQueryParameterNames.Total, StringComparison.OrdinalIgnoreCase) == 0)
                {
                    if (Enum.TryParse <TotalType>(query.Item2, true, out var totalType))
                    {
                        ValidateTotalType(totalType);

                        searchOptions.IncludeTotal = totalType;
                        setDefaultBundleTotal      = false;
                    }
                    else
                    {
                        throw new BadRequestException(string.Format(Core.Resources.InvalidTotalParameter, query.Item2, SupportedTotalTypes));
                    }
                }
                else
                {
                    // Parse the search parameters.
                    try
                    {
                        // Basic format checking (e.g. integer value for _count key etc.).
                        searchParams.Add(query.Item1, query.Item2);
                    }
                    catch (Exception ex)
                    {
                        throw new BadRequestException(ex.Message);
                    }
                }
            }

            searchOptions.ContinuationToken = continuationToken;

            if (setDefaultBundleTotal)
            {
                ValidateTotalType(_featureConfiguration.IncludeTotalInBundle);
                searchOptions.IncludeTotal = _featureConfiguration.IncludeTotalInBundle;
            }

            // Check the item count.
            if (searchParams.Count != null)
            {
                searchOptions.MaxItemCountSpecifiedByClient = true;

                if (searchParams.Count > _featureConfiguration.MaxItemCountPerSearch)
                {
                    searchOptions.MaxItemCount = _featureConfiguration.MaxItemCountPerSearch;

                    _contextAccessor.RequestContext?.BundleIssues.Add(
                        new OperationOutcomeIssue(
                            OperationOutcomeConstants.IssueSeverity.Information,
                            OperationOutcomeConstants.IssueType.Informational,
                            string.Format(Core.Resources.SearchParamaterCountExceedLimit, _featureConfiguration.MaxItemCountPerSearch, searchParams.Count)));
                }
                else
                {
                    searchOptions.MaxItemCount = searchParams.Count.Value;
                }
            }
            else
            {
                searchOptions.MaxItemCount = _featureConfiguration.DefaultItemCountPerSearch;
            }

            searchOptions.IncludeCount = _featureConfiguration.DefaultIncludeCountPerSearch;

            if (searchParams.Elements?.Any() == true && searchParams.Summary != null && searchParams.Summary != SummaryType.False)
            {
                // The search parameters _elements and _summarize cannot be specified for the same request.
                throw new BadRequestException(string.Format(Core.Resources.ElementsAndSummaryParametersAreIncompatible, KnownQueryParameterNames.Summary, KnownQueryParameterNames.Elements));
            }

            // Check to see if only the count should be returned
            searchOptions.CountOnly = searchParams.Summary == SummaryType.Count;

            // If the resource type is not specified, then the common
            // search parameters should be used.
            ResourceType[] parsedResourceTypes = new[] { ResourceType.DomainResource };

            var searchExpressions = new List <Expression>();

            if (string.IsNullOrWhiteSpace(resourceType))
            {
                // Try to parse resource types from _type Search Parameter
                // This will result in empty array if _type has any modifiers
                // Which is good, since :not modifier changes the meaning of the
                // search parameter and we can no longer use it to deduce types
                // (and should proceed with ResourceType.DomainResource in that case)
                var resourceTypes = searchParams.Parameters
                                    .Where(q => q.Item1 == KnownQueryParameterNames.Type) // <-- Equality comparison to avoid modifiers
                                    .SelectMany(q => q.Item2.SplitByOrSeparator())
                                    .Where(type => ModelInfoProvider.IsKnownResource(type))
                                    .Select(x =>
                {
                    if (!Enum.TryParse(x, out ResourceType parsedType))
                    {
                        // Should never get here
                        throw new ResourceNotSupportedException(x);
                    }

                    return(parsedType);
                })
                                    .Distinct();

                if (resourceTypes.Any())
                {
                    parsedResourceTypes = resourceTypes.ToArray();
                }
            }
            else
            {
                if (!Enum.TryParse(resourceType, out parsedResourceTypes[0]))
                {
                    throw new ResourceNotSupportedException(resourceType);
                }

                searchExpressions.Add(Expression.SearchParameter(_resourceTypeSearchParameter, Expression.StringEquals(FieldName.TokenCode, null, resourceType, false)));
            }

            var resourceTypesString = parsedResourceTypes.Select(x => x.ToString()).ToArray();

            searchExpressions.AddRange(searchParams.Parameters.Select(
                                           q =>
            {
                try
                {
                    return(_expressionParser.Parse(resourceTypesString, q.Item1, q.Item2));
                }
                catch (SearchParameterNotSupportedException)
                {
                    unsupportedSearchParameters.Add(q);

                    return(null);
                }
            })
                                       .Where(item => item != null));

            if (searchParams.Include?.Count > 0)
            {
                searchExpressions.AddRange(searchParams.Include.Select(
                                               q => _expressionParser.ParseInclude(resourceTypesString, q, false /* not reversed */, false /* no iterate */))
                                           .Where(item => item != null));
            }

            if (searchParams.RevInclude?.Count > 0)
            {
                searchExpressions.AddRange(searchParams.RevInclude.Select(
                                               q => _expressionParser.ParseInclude(resourceTypesString, q, true /* reversed */, false /* no iterate */))
                                           .Where(item => item != null));
            }

            // Parse _include:iterate (_include:recurse) parameters.
            // :iterate (:recurse) modifiers are not supported by Hl7.Fhir.Rest, hence not added to the Include collection and exist in the Parameters list.
            // See https://github.com/FirelyTeam/fhir-net-api/issues/222
            // _include:iterate (_include:recurse) expression may appear without a preceding _include parameter
            // when applied on a circular reference
            searchExpressions.AddRange(ParseIncludeIterateExpressions(searchParams));

            // remove _include:iterate and _revinclude:iterate parameters from unsupportedSearchParameters
            unsupportedSearchParameters.RemoveAll(p => AllIterateModifiers.Contains(p.Item1));

            if (!string.IsNullOrWhiteSpace(compartmentType))
            {
                if (Enum.TryParse(compartmentType, out CompartmentType parsedCompartmentType))
                {
                    if (string.IsNullOrWhiteSpace(compartmentId))
                    {
                        throw new InvalidSearchOperationException(Core.Resources.CompartmentIdIsInvalid);
                    }

                    searchExpressions.Add(Expression.CompartmentSearch(compartmentType, compartmentId));
                }
                else
                {
                    throw new InvalidSearchOperationException(string.Format(Core.Resources.CompartmentTypeIsInvalid, compartmentType));
                }
            }

            if (searchExpressions.Count == 1)
            {
                searchOptions.Expression = searchExpressions[0];
            }
            else if (searchExpressions.Count > 1)
            {
                searchOptions.Expression = Expression.And(searchExpressions.ToArray());
            }

            if (unsupportedSearchParameters.Any())
            {
                bool throwForUnsupported = false;
                if (_contextAccessor.RequestContext?.RequestHeaders != null &&
                    _contextAccessor.RequestContext.RequestHeaders.TryGetValue(KnownHeaders.Prefer, out var values))
                {
                    var handlingValue = values.FirstOrDefault(x => x.StartsWith("handling=", StringComparison.OrdinalIgnoreCase));
                    if (handlingValue != default)
                    {
                        handlingValue = handlingValue.Substring("handling=".Length);

                        if (string.IsNullOrWhiteSpace(handlingValue) || !Enum.TryParse <SearchParameterHandling>(handlingValue, true, out var handling))
                        {
                            throw new BadRequestException(string.Format(
                                                              Core.Resources.InvalidHandlingValue,
                                                              handlingValue,
                                                              string.Join(",", Enum.GetNames <SearchParameterHandling>())));
                        }

                        if (handling == SearchParameterHandling.Strict)
                        {
                            throwForUnsupported = true;
                        }
                    }
                }

                if (throwForUnsupported)
                {
                    throw new BadRequestException(string.Format(
                                                      Core.Resources.SearchParameterNotSupported,
                                                      string.Join(",", unsupportedSearchParameters.Select(x => x.Item1)),
                                                      string.Join(",", resourceTypesString)));
                }
                else
                {
                    foreach (var unsupported in unsupportedSearchParameters)
                    {
                        _contextAccessor.RequestContext?.BundleIssues.Add(new OperationOutcomeIssue(
                                                                              OperationOutcomeConstants.IssueSeverity.Warning,
                                                                              OperationOutcomeConstants.IssueType.NotSupported,
                                                                              string.Format(CultureInfo.InvariantCulture, Core.Resources.SearchParameterNotSupported, unsupported.Item1, string.Join(",", resourceTypesString))));
                    }
                }
            }

            searchOptions.UnsupportedSearchParams = unsupportedSearchParameters;

            if (searchParams.Sort?.Count > 0)
            {
                var  sortings      = new List <(SearchParameterInfo, SortOrder)>(searchParams.Sort.Count);
                bool sortingsValid = true;

                foreach (Tuple <string, Hl7.Fhir.Rest.SortOrder> sorting in searchParams.Sort)
                {
                    try
                    {
                        SearchParameterInfo searchParameterInfo = resourceTypesString.Select(t => _searchParameterDefinitionManager.GetSearchParameter(t, sorting.Item1)).Distinct().First();
                        sortings.Add((searchParameterInfo, sorting.Item2.ToCoreSortOrder()));
                    }
                    catch (SearchParameterNotSupportedException)
                    {
                        sortingsValid = false;
                        _contextAccessor.RequestContext?.BundleIssues.Add(new OperationOutcomeIssue(
                                                                              OperationOutcomeConstants.IssueSeverity.Warning,
                                                                              OperationOutcomeConstants.IssueType.NotSupported,
                                                                              string.Format(CultureInfo.InvariantCulture, Core.Resources.SearchParameterNotSupported, sorting.Item1, string.Join(", ", resourceTypesString))));
                    }
                }

                if (sortingsValid)
                {
                    if (!_sortingValidator.ValidateSorting(sortings, out IReadOnlyList <string> errorMessages))
                    {
                        if (errorMessages == null || errorMessages.Count == 0)
                        {
                            throw new InvalidOperationException($"Expected {_sortingValidator.GetType().Name} to return error messages when {nameof(_sortingValidator.ValidateSorting)} returns false");
                        }

                        sortingsValid = false;

                        foreach (var errorMessage in errorMessages)
                        {
                            _contextAccessor.RequestContext?.BundleIssues.Add(new OperationOutcomeIssue(
                                                                                  OperationOutcomeConstants.IssueSeverity.Warning,
                                                                                  OperationOutcomeConstants.IssueType.NotSupported,
                                                                                  errorMessage));
                        }
                    }
                }

                if (sortingsValid)
                {
                    searchOptions.Sort = sortings;
                }
            }

            if (searchOptions.Sort == null)
            {
                searchOptions.Sort = Array.Empty <(SearchParameterInfo searchParameterInfo, SortOrder sortOrder)>();
            }

            return(searchOptions);

            IEnumerable <IncludeExpression> ParseIncludeIterateExpressions(SearchParams searchParams)
            {
                return(searchParams.Parameters
                       .Where(p => p != null && AllIterateModifiers.Where(m => string.Equals(p.Item1, m, StringComparison.OrdinalIgnoreCase)).Any())
                       .Select(p =>
                {
                    var includeResourceType = p.Item2?.Split(':')[0];
                    if (!ModelInfoProvider.IsKnownResource(includeResourceType))
                    {
                        throw new ResourceNotSupportedException(includeResourceType);
                    }

                    var reversed = RevIncludeIterateModifiers.Contains(p.Item1);
                    var expression = _expressionParser.ParseInclude(new[] { includeResourceType }, p.Item2, reversed, true);

                    // Reversed Iterate expressions (not wildcard) must specify target type if there is more than one possible target type
                    if (expression.Reversed && expression.Iterate && expression.TargetResourceType == null && expression.ReferenceSearchParameter?.TargetResourceTypes?.Count > 1)
                    {
                        throw new BadRequestException(string.Format(Core.Resources.RevIncludeIterateTargetTypeNotSpecified, p.Item2));
                    }

                    // For circular include iterate expressions, add an informational issue indicating that a single iteration is supported.
                    // See https://www.hl7.org/fhir/search.html#revinclude.
                    if (expression.Iterate && expression.CircularReference)
                    {
                        _contextAccessor.RequestContext?.BundleIssues.Add(
                            new OperationOutcomeIssue(
                                OperationOutcomeConstants.IssueSeverity.Information,
                                OperationOutcomeConstants.IssueType.Informational,
                                string.Format(Core.Resources.IncludeIterateCircularReferenceExecutedOnce, p.Item1, p.Item2)));
                    }

                    return expression;
                }));
            }

            void ValidateTotalType(TotalType totalType)
            {
                // Estimate is not yet supported.
                if (totalType == TotalType.Estimate)
                {
                    throw new SearchOperationNotSupportedException(string.Format(Core.Resources.UnsupportedTotalParameter, totalType, SupportedTotalTypes));
                }
            }
        }
Esempio n. 14
0
        public SearchOptions Create(string compartmentType, string compartmentId, string resourceType, IReadOnlyList <Tuple <string, string> > queryParameters)
        {
            var searchOptions = new SearchOptions();

            string continuationToken = null;

            var searchParams = new SearchParams();
            var unsupportedSearchParameters = new List <Tuple <string, string> >();

            // Extract the continuation token, filter out the other known query parameters that's not search related.
            foreach (Tuple <string, string> query in queryParameters ?? Enumerable.Empty <Tuple <string, string> >())
            {
                if (query.Item1 == KnownQueryParameterNames.ContinuationToken)
                {
                    // This is an unreachable case. The mapping of the query parameters makes it so only one continuation token can exist.
                    if (continuationToken != null)
                    {
                        throw new InvalidSearchOperationException(
                                  string.Format(Core.Resources.MultipleQueryParametersNotAllowed, KnownQueryParameterNames.ContinuationToken));
                    }

                    // Checks if the continuation token is base 64 bit encoded. Needed for systems that have cached continuation tokens from before they were encoded.
                    if (Base64FormatRegex.IsMatch(query.Item2))
                    {
                        continuationToken = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(query.Item2));
                    }
                    else
                    {
                        continuationToken = query.Item2;
                    }
                }
                else if (query.Item1 == KnownQueryParameterNames.Format)
                {
                    // TODO: We need to handle format parameter.
                }
                else if (string.IsNullOrWhiteSpace(query.Item1) || string.IsNullOrWhiteSpace(query.Item2))
                {
                    // Query parameter with empty value is not supported.
                    unsupportedSearchParameters.Add(query);
                }
                else
                {
                    // Parse the search parameters.
                    try
                    {
                        searchParams.Add(query.Item1, query.Item2);
                    }
                    catch (Exception ex)
                    {
                        _logger.LogInformation(ex, "Failed to parse the query parameter. Skipping.");

                        // There was a problem parsing the parameter. Add it to list of unsupported parameters.
                        unsupportedSearchParameters.Add(query);
                    }
                }
            }

            searchOptions.ContinuationToken = continuationToken;

            // Check the item count.
            if (searchParams.Count != null)
            {
                searchOptions.MaxItemCount = searchParams.Count.Value;
            }

            // Check to see if only the count should be returned
            searchOptions.CountOnly = searchParams.Summary == SummaryType.Count;

            // If the resource type is not specified, then the common
            // search parameters should be used.
            ResourceType parsedResourceType = ResourceType.DomainResource;

            if (!string.IsNullOrWhiteSpace(resourceType) &&
                !Enum.TryParse(resourceType, out parsedResourceType))
            {
                throw new ResourceNotSupportedException(resourceType);
            }

            var searchExpressions = new List <Expression>();

            if (!string.IsNullOrWhiteSpace(resourceType))
            {
                searchExpressions.Add(Expression.SearchParameter(_resourceTypeSearchParameter, Expression.StringEquals(FieldName.TokenCode, null, resourceType, false)));
            }

            searchExpressions.AddRange(searchParams.Parameters.Select(
                                           q =>
            {
                try
                {
                    return(_expressionParser.Parse(parsedResourceType.ToString(), q.Item1, q.Item2));
                }
                catch (SearchParameterNotSupportedException)
                {
                    unsupportedSearchParameters.Add(q);

                    return(null);
                }
            })
                                       .Where(item => item != null));

            if (!string.IsNullOrWhiteSpace(compartmentType))
            {
                if (Enum.TryParse(compartmentType, out CompartmentType parsedCompartmentType))
                {
                    if (string.IsNullOrWhiteSpace(compartmentId))
                    {
                        throw new InvalidSearchOperationException(Core.Resources.CompartmentIdIsInvalid);
                    }

                    searchExpressions.Add(Expression.CompartmentSearch(compartmentType, compartmentId));
                }
                else
                {
                    throw new InvalidSearchOperationException(string.Format(Core.Resources.CompartmentTypeIsInvalid, compartmentType));
                }
            }

            if (searchExpressions.Count == 1)
            {
                searchOptions.Expression = searchExpressions[0];
            }
            else if (searchExpressions.Count > 1)
            {
                searchOptions.Expression = Expression.And(searchExpressions.ToArray());
            }

            if (unsupportedSearchParameters.Any())
            {
                // TODO: Client can specify whether exception should be raised or not when it encounters unknown search parameters.
                // For now, we will ignore any unknown search parameters.
            }

            searchOptions.UnsupportedSearchParams = unsupportedSearchParameters;

            if (searchParams.Sort?.Count > 0)
            {
                var sortings = new List <(SearchParameterInfo, SortOrder)>();
                List <(string parameterName, string reason)> unsupportedSortings = null;

                foreach (Tuple <string, Hl7.Fhir.Rest.SortOrder> sorting in searchParams.Sort)
                {
                    try
                    {
                        SearchParameterInfo searchParameterInfo = _searchParameterDefinitionManager.GetSearchParameter(parsedResourceType.ToString(), sorting.Item1);
                        sortings.Add((searchParameterInfo, sorting.Item2.ToCoreSortOrder()));
                    }
                    catch (SearchParameterNotSupportedException)
                    {
                        (unsupportedSortings ?? (unsupportedSortings = new List <(string parameterName, string reason)>())).Add((sorting.Item1, string.Format(Core.Resources.SearchParameterNotSupported, sorting.Item1, resourceType)));
                    }
                }

                searchOptions.Sort = sortings;
                searchOptions.UnsupportedSortingParams = (IReadOnlyList <(string parameterName, string reason)>)unsupportedSortings ?? Array.Empty <(string parameterName, string reason)>();
            }
            else
            {
                searchOptions.Sort = Array.Empty <(SearchParameterInfo searchParameterInfo, SortOrder sortOrder)>();
                searchOptions.UnsupportedSortingParams = Array.Empty <(string parameterName, string reason)>();
            }

            return(searchOptions);
        }
        public SearchOptions Create(string compartmentType, string compartmentId, string resourceType, IReadOnlyList <Tuple <string, string> > queryParameters)
        {
            var searchOptions = new SearchOptions();

            string continuationToken = null;

            var  searchParams = new SearchParams();
            var  unsupportedSearchParameters = new List <Tuple <string, string> >();
            bool setDefaultBundleTotal       = true;

            // Extract the continuation token, filter out the other known query parameters that's not search related.
            foreach (Tuple <string, string> query in queryParameters ?? Enumerable.Empty <Tuple <string, string> >())
            {
                if (query.Item1 == KnownQueryParameterNames.ContinuationToken)
                {
                    // This is an unreachable case. The mapping of the query parameters makes it so only one continuation token can exist.
                    if (continuationToken != null)
                    {
                        throw new InvalidSearchOperationException(
                                  string.Format(Core.Resources.MultipleQueryParametersNotAllowed, KnownQueryParameterNames.ContinuationToken));
                    }

                    // Checks if the continuation token is base 64 bit encoded. Needed for systems that have cached continuation tokens from before they were encoded.
                    if (Base64FormatRegex.IsMatch(query.Item2))
                    {
                        continuationToken = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(query.Item2));
                    }
                    else
                    {
                        continuationToken = query.Item2;
                    }

                    setDefaultBundleTotal = false;
                }
                else if (query.Item1 == KnownQueryParameterNames.Format)
                {
                    // TODO: We need to handle format parameter.
                }
                else if (string.IsNullOrWhiteSpace(query.Item1) || string.IsNullOrWhiteSpace(query.Item2))
                {
                    // Query parameter with empty value is not supported.
                    unsupportedSearchParameters.Add(query);
                }
                else if (string.Compare(query.Item1, KnownQueryParameterNames.Total, StringComparison.OrdinalIgnoreCase) == 0)
                {
                    if (Enum.TryParse <TotalType>(query.Item2, true, out var totalType))
                    {
                        ValidateTotalType(totalType);

                        searchOptions.IncludeTotal = totalType;
                        setDefaultBundleTotal      = false;
                    }
                    else
                    {
                        throw new BadRequestException(string.Format(Core.Resources.InvalidTotalParameter, query.Item2, SupportedTotalTypes));
                    }
                }
                else
                {
                    // Parse the search parameters.
                    try
                    {
                        // Basic format checking (e.g. integer value for _count key etc.).
                        searchParams.Add(query.Item1, query.Item2);
                    }
                    catch (Exception ex)
                    {
                        throw new BadRequestException(ex.Message);
                    }
                }
            }

            searchOptions.ContinuationToken = continuationToken;

            if (setDefaultBundleTotal)
            {
                ValidateTotalType(_featureConfiguration.IncludeTotalInBundle);
                searchOptions.IncludeTotal = _featureConfiguration.IncludeTotalInBundle;
            }

            // Check the item count.
            if (searchParams.Count != null)
            {
                searchOptions.MaxItemCount = searchParams.Count.Value;
            }

            // Check to see if only the count should be returned
            searchOptions.CountOnly = searchParams.Summary == SummaryType.Count;

            // If the resource type is not specified, then the common
            // search parameters should be used.
            ResourceType parsedResourceType = ResourceType.DomainResource;

            if (!string.IsNullOrWhiteSpace(resourceType) &&
                !Enum.TryParse(resourceType, out parsedResourceType))
            {
                throw new ResourceNotSupportedException(resourceType);
            }

            var searchExpressions = new List <Expression>();

            if (!string.IsNullOrWhiteSpace(resourceType))
            {
                searchExpressions.Add(Expression.SearchParameter(_resourceTypeSearchParameter, Expression.StringEquals(FieldName.TokenCode, null, resourceType, false)));
            }

            searchExpressions.AddRange(searchParams.Parameters.Select(
                                           q =>
            {
                try
                {
                    return(_expressionParser.Parse(parsedResourceType.ToString(), q.Item1, q.Item2));
                }
                catch (SearchParameterNotSupportedException)
                {
                    unsupportedSearchParameters.Add(q);

                    return(null);
                }
            })
                                       .Where(item => item != null));

            if (searchParams.Include?.Count > 0)
            {
                searchExpressions.AddRange(searchParams.Include.Select(
                                               q => _expressionParser.ParseInclude(parsedResourceType.ToString(), q, false /* not reversed */))
                                           .Where(item => item != null));
            }

            if (searchParams.RevInclude?.Count > 0)
            {
                searchExpressions.AddRange(searchParams.RevInclude.Select(
                                               q => _expressionParser.ParseInclude(parsedResourceType.ToString(), q, true /* reversed */))
                                           .Where(item => item != null));
            }

            if (!string.IsNullOrWhiteSpace(compartmentType))
            {
                if (Enum.TryParse(compartmentType, out CompartmentType parsedCompartmentType))
                {
                    if (string.IsNullOrWhiteSpace(compartmentId))
                    {
                        throw new InvalidSearchOperationException(Core.Resources.CompartmentIdIsInvalid);
                    }

                    searchExpressions.Add(Expression.CompartmentSearch(compartmentType, compartmentId));
                }
                else
                {
                    throw new InvalidSearchOperationException(string.Format(Core.Resources.CompartmentTypeIsInvalid, compartmentType));
                }
            }

            if (searchExpressions.Count == 1)
            {
                searchOptions.Expression = searchExpressions[0];
            }
            else if (searchExpressions.Count > 1)
            {
                searchOptions.Expression = Expression.And(searchExpressions.ToArray());
            }

            if (unsupportedSearchParameters.Any())
            {
                // TODO: Client can specify whether exception should be raised or not when it encounters unknown search parameters.
                // For now, we will ignore any unknown search parameters.
            }

            searchOptions.UnsupportedSearchParams = unsupportedSearchParameters;

            if (searchParams.Sort?.Count > 0)
            {
                var sortings = new List <(SearchParameterInfo, SortOrder)>();
                List <(string parameterName, string reason)> unsupportedSortings = null;

                foreach (Tuple <string, Hl7.Fhir.Rest.SortOrder> sorting in searchParams.Sort)
                {
                    try
                    {
                        SearchParameterInfo searchParameterInfo = _searchParameterDefinitionManager.GetSearchParameter(parsedResourceType.ToString(), sorting.Item1);

                        if (searchParameterInfo.IsSortSupported())
                        {
                            sortings.Add((searchParameterInfo, sorting.Item2.ToCoreSortOrder()));
                        }
                        else
                        {
                            throw new SearchParameterNotSupportedException(string.Format(Core.Resources.SearchSortParameterNotSupported, searchParameterInfo.Name));
                        }
                    }
                    catch (SearchParameterNotSupportedException)
                    {
                        (unsupportedSortings ??= new List <(string parameterName, string reason)>()).Add((sorting.Item1, string.Format(Core.Resources.SearchSortParameterNotSupported, sorting.Item1)));
                    }
                }

                searchOptions.Sort = sortings;
                searchOptions.UnsupportedSortingParams = (IReadOnlyList <(string parameterName, string reason)>)unsupportedSortings ?? Array.Empty <(string parameterName, string reason)>();
            }
            else
            {
                searchOptions.Sort = Array.Empty <(SearchParameterInfo searchParameterInfo, SortOrder sortOrder)>();
                searchOptions.UnsupportedSortingParams = Array.Empty <(string parameterName, string reason)>();
            }

            return(searchOptions);

            void ValidateTotalType(TotalType totalType)
            {
                // Estimate is not yet supported.
                if (totalType == TotalType.Estimate)
                {
                    throw new SearchOperationNotSupportedException(string.Format(Core.Resources.UnsupportedTotalParameter, totalType, SupportedTotalTypes));
                }
            }
        }