diff --git a/src/Microsoft.OData.Core/Microsoft.OData.Core.cs b/src/Microsoft.OData.Core/Microsoft.OData.Core.cs index 34673ae2f3..d5d2e0dd79 100644 --- a/src/Microsoft.OData.Core/Microsoft.OData.Core.cs +++ b/src/Microsoft.OData.Core/Microsoft.OData.Core.cs @@ -791,7 +791,9 @@ internal sealed class TextRes { internal const string ExceptionUtils_CheckLongPositive = "ExceptionUtils_CheckLongPositive"; internal const string ExceptionUtils_ArgumentStringNullOrEmpty = "ExceptionUtils_ArgumentStringNullOrEmpty"; internal const string ExpressionToken_OnlyRefAllowWithStarInExpand = "ExpressionToken_OnlyRefAllowWithStarInExpand"; + internal const string ExpressionToken_DollarCountNotAllowedInSelect = "ExpressionToken_DollarCountNotAllowedInSelect"; internal const string ExpressionToken_NoPropAllowedAfterRef = "ExpressionToken_NoPropAllowedAfterRef"; + internal const string ExpressionToken_NoPropAllowedAfterDollarCount = "ExpressionToken_NoPropAllowedAfterDollarCount"; internal const string ExpressionToken_NoSegmentAllowedBeforeStarInExpand = "ExpressionToken_NoSegmentAllowedBeforeStarInExpand"; internal const string ExpressionToken_IdentifierExpected = "ExpressionToken_IdentifierExpected"; internal const string ExpressionLexer_UnterminatedStringLiteral = "ExpressionLexer_UnterminatedStringLiteral"; diff --git a/src/Microsoft.OData.Core/Microsoft.OData.Core.txt b/src/Microsoft.OData.Core/Microsoft.OData.Core.txt index 25e43bac04..38fac3c22e 100644 --- a/src/Microsoft.OData.Core/Microsoft.OData.Core.txt +++ b/src/Microsoft.OData.Core/Microsoft.OData.Core.txt @@ -915,6 +915,8 @@ ExceptionUtils_CheckLongPositive=A positive long value was expected; however, th ExceptionUtils_ArgumentStringNullOrEmpty=Value cannot be null or empty. ExpressionToken_OnlyRefAllowWithStarInExpand=Only $ref is allowed with star in $expand option. +ExpressionToken_DollarCountNotAllowedInSelect=$count is not allowed in $select option. +ExpressionToken_NoPropAllowedAfterDollarCount=No property is allowed after $count segment. ExpressionToken_NoPropAllowedAfterRef=No property is allowed after $ref segment. ExpressionToken_NoSegmentAllowedBeforeStarInExpand=No segment is allowed before star in $expand. ExpressionToken_IdentifierExpected=An identifier was expected at position {0}. diff --git a/src/Microsoft.OData.Core/Parameterized.Microsoft.OData.Core.cs b/src/Microsoft.OData.Core/Parameterized.Microsoft.OData.Core.cs index eca1b36898..e6816eb148 100644 --- a/src/Microsoft.OData.Core/Parameterized.Microsoft.OData.Core.cs +++ b/src/Microsoft.OData.Core/Parameterized.Microsoft.OData.Core.cs @@ -6928,6 +6928,28 @@ internal static string ExpressionToken_NoPropAllowedAfterRef } } + /// + /// A string like "No property is allowed after $count segment." + /// + internal static string ExpressionToken_NoPropAllowedAfterDollarCount + { + get + { + return Microsoft.OData.TextRes.GetString(Microsoft.OData.TextRes.ExpressionToken_NoPropAllowedAfterDollarCount); + } + } + + /// + /// A string like "$count is not allowed in $select option." + /// + internal static string ExpressionToken_DollarCountNotAllowedInSelect + { + get + { + return Microsoft.OData.TextRes.GetString(Microsoft.OData.TextRes.ExpressionToken_DollarCountNotAllowedInSelect); + } + } + /// /// A string like "No segment is allowed before star in $expand." /// diff --git a/src/Microsoft.OData.Core/UriParser/Binders/SelectExpandBinder.cs b/src/Microsoft.OData.Core/UriParser/Binders/SelectExpandBinder.cs index c4ca958634..795a5f6ce5 100644 --- a/src/Microsoft.OData.Core/UriParser/Binders/SelectExpandBinder.cs +++ b/src/Microsoft.OData.Core/UriParser/Binders/SelectExpandBinder.cs @@ -376,6 +376,8 @@ private SelectItem GenerateExpandItem(ExpandTermToken tokenIn) } bool isRef = false; + bool isCount = false; + if (firstNonTypeToken.NextToken != null) { // lastly... make sure that, since we're on a NavProp, that the next token isn't null. @@ -383,6 +385,10 @@ private SelectItem GenerateExpandItem(ExpandTermToken tokenIn) { isRef = true; } + else if (firstNonTypeToken.NextToken.Identifier == UriQueryConstants.CountSegment) + { + isCount = true; + } else { throw new ODataException(ODataErrorStrings.ExpandItemBinder_TraversingMultipleNavPropsInTheSamePath); @@ -428,6 +434,11 @@ private SelectItem GenerateExpandItem(ExpandTermToken tokenIn) return new ExpandedReferenceSelectItem(pathToNavProp, targetNavigationSource, filterOption, orderbyOption, tokenIn.TopOption, tokenIn.SkipOption, tokenIn.CountQueryOption, searchOption, computeOption, applyOption); } + if (isCount) + { + return new ExpandedCountSelectItem(pathToNavProp, targetNavigationSource, filterOption, searchOption); + } + // $select & $expand SelectExpandClause subSelectExpand = BindSelectExpand(tokenIn.ExpandOption, tokenIn.SelectOption, parsedPath, this.ResourcePathNavigationSource, targetNavigationSource, null, generatedProperties, collapsed); diff --git a/src/Microsoft.OData.Core/UriParser/Parsers/SelectExpandTermParser.cs b/src/Microsoft.OData.Core/UriParser/Parsers/SelectExpandTermParser.cs index 79bb1345d1..d98d44cd1d 100644 --- a/src/Microsoft.OData.Core/UriParser/Parsers/SelectExpandTermParser.cs +++ b/src/Microsoft.OData.Core/UriParser/Parsers/SelectExpandTermParser.cs @@ -109,18 +109,19 @@ private void CheckPathLength(int pathLength) /// A parsed PathSegmentToken representing the next segment in this path. private PathSegmentToken ParseSegment(PathSegmentToken previousSegment, bool allowRef) { - // TODO $count is defined in specification for expand, it is not supported now. Also note $count is not supported with star as expand option. - if (this.lexer.CurrentToken.Text.StartsWith("$", StringComparison.Ordinal) && (!allowRef || this.lexer.CurrentToken.Text != UriQueryConstants.RefSegment)) + if (this.lexer.CurrentToken.Text.StartsWith("$", StringComparison.Ordinal) + && (!allowRef || this.lexer.CurrentToken.Text != UriQueryConstants.RefSegment) + && this.lexer.CurrentToken.Text != UriQueryConstants.CountSegment) { throw new ODataException(ODataErrorStrings.UriSelectParser_SystemTokenInSelectExpand(this.lexer.CurrentToken.Text, this.lexer.ExpressionText)); } - // Some check here to throw exception, both prop1/*/prop2 and */$ref/prop will throw exception, both are for $expand cases + // Some check here to throw exception, prop1/*/prop2 and */$ref/prop and prop1/$count/prop2 will throw exception, all are $expand cases. if (!isSelect) { if (previousSegment != null && previousSegment.Identifier == UriQueryConstants.Star && this.lexer.CurrentToken.GetIdentifier() != UriQueryConstants.RefSegment) { - // Star can only be followed with $ref + // Star can only be followed with $ref. $count is not supported with star as expand option throw new ODataException(ODataErrorStrings.ExpressionToken_OnlyRefAllowWithStarInExpand); } else if (previousSegment != null && previousSegment.Identifier == UriQueryConstants.RefSegment) @@ -128,6 +129,17 @@ private PathSegmentToken ParseSegment(PathSegmentToken previousSegment, bool all // $ref should not have more property followed. throw new ODataException(ODataErrorStrings.ExpressionToken_NoPropAllowedAfterRef); } + else if (previousSegment != null && previousSegment.Identifier == UriQueryConstants.CountSegment) + { + // $count should not have more property followed. e.g $expand=NavProperty/$count/MyProperty + throw new ODataException(ODataErrorStrings.ExpressionToken_NoPropAllowedAfterDollarCount); + } + } + + if (this.lexer.CurrentToken.Text == UriQueryConstants.CountSegment && isSelect) + { + // $count is not allowed in $select e.g $select=NavProperty/$count + throw new ODataException(ODataErrorStrings.ExpressionToken_DollarCountNotAllowedInSelect); } string propertyName; diff --git a/src/Microsoft.OData.Core/UriParser/SemanticAst/ExpandedCountSelectItem.cs b/src/Microsoft.OData.Core/UriParser/SemanticAst/ExpandedCountSelectItem.cs new file mode 100644 index 0000000000..0aae952136 --- /dev/null +++ b/src/Microsoft.OData.Core/UriParser/SemanticAst/ExpandedCountSelectItem.cs @@ -0,0 +1,53 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +namespace Microsoft.OData.UriParser +{ + using Aggregation; + using Microsoft.OData.Edm; + + /// + /// This represents one level of expansion for a particular expansion tree. + /// + public sealed class ExpandedCountSelectItem : ExpandedReferenceSelectItem + { + /// + /// Create an Expand item using a nav prop, its entity set and a SelectExpandClause + /// + /// the path to the navigation property for this expand item, including any type segments + /// the navigation source for this ExpandItem + /// A filter clause for this expand (can be null) + /// A search clause for this expand (can be null) + /// Throws if input pathToNavigationProperty is null. + public ExpandedCountSelectItem(ODataExpandPath pathToNavigationProperty, IEdmNavigationSource navigationSource, FilterClause filterOption, SearchClause searchOption) + : base(pathToNavigationProperty, navigationSource, filterOption, null, null, null, null, searchOption) + { + ExceptionUtils.CheckArgumentNotNull(pathToNavigationProperty, "pathToNavigationProperty"); + } + + /// + /// Translate using a . + /// + /// Type that the translator will return after visiting this item. + /// An implementation of the translator interface. + /// An object whose type is determined by the type parameter of the translator. + /// Throws if the input translator is null. + public override T TranslateWith(SelectItemTranslator translator) + { + return translator.Translate(this); + } + + /// + /// Handle using a . + /// + /// An implementation of the handler interface. + /// Throws if the input handler is null. + public override void HandleWith(SelectItemHandler handler) + { + handler.Handle(this); + } + } +} diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/SelectExpandFunctionalTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/SelectExpandFunctionalTests.cs index 27772fd686..408fc8eb99 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/SelectExpandFunctionalTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/SelectExpandFunctionalTests.cs @@ -60,6 +60,13 @@ public void SelectPropertiesWithRefOperationThrows() readResult.Throws(ODataErrorStrings.UriSelectParser_SystemTokenInSelectExpand("$ref", "MyLions/$ref")); } + [Fact] + public void SelectPropertiesWithDollarCountOperationThrows() + { + Action readResult = () => RunParseSelectExpand("MyLions/$count", null, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet()); + readResult.Throws(ODataErrorStrings.ExpressionToken_DollarCountNotAllowedInSelect); + } + [Fact] public void SelectWithAsteriskMeansWildcard() { @@ -572,6 +579,14 @@ public void ExpandNavigationWithNavigationAfterRefOperationThrows() readResult.Throws(ODataErrorStrings.ExpressionToken_NoPropAllowedAfterRef); } + [Fact] + public void ExpandNavigationWithNavigationAfterDollarCountOperationThrows() + { + const string expandClauseText = "MyDog/$count/MyPeople"; + Action readResult = () => RunParseSelectExpand(null, expandClauseText, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet()); + readResult.Throws(ODataErrorStrings.ExpressionToken_NoPropAllowedAfterDollarCount); + } + [Fact] public void ExpandNavigationWithNestedQueryOptionOnRef() { @@ -1582,10 +1597,76 @@ public void SelectWithNestedTopSkipAndCountWorks() Assert.NotNull(pathSelectItem.TopOption); Assert.Equal(4, pathSelectItem.TopOption); - Assert.NotNull(pathSelectItem.TopOption); + Assert.NotNull(pathSelectItem.SkipOption); Assert.Equal(2, pathSelectItem.SkipOption); } + // $expand=navProp/$count + [Fact] + public void ExpandWithNavigationPropCountWorks() + { + // Arrange + var odataQueryOptionParser = new ODataQueryOptionParser(HardCodedTestModel.TestModel, + HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet(), + new Dictionary() + { + {"$expand", "MyPaintings/$count"} + }); + + // Act + var selectExpandClause = odataQueryOptionParser.ParseSelectAndExpand(); + + // Assert + Assert.NotNull(selectExpandClause); + ExpandedCountSelectItem expandedCountSelectItem = Assert.IsType(Assert.Single(selectExpandClause.SelectedItems)); + Assert.Null(expandedCountSelectItem.FilterOption); + Assert.Null(expandedCountSelectItem.SearchOption); + } + + // $expand=navProp/$count($filter=prop) + [Fact] + public void ExpandWithNavigationPropCountWithFilterOptionWorks() + { + // Arrange + var odataQueryOptionParser = new ODataQueryOptionParser(HardCodedTestModel.TestModel, + HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet(), + new Dictionary() + { + {"$expand", "MyPaintings/$count($filter=Artist eq 'Artist One')"} + }); + + // Act + var selectExpandClause = odataQueryOptionParser.ParseSelectAndExpand(); + + // Assert + Assert.NotNull(selectExpandClause); + ExpandedCountSelectItem expandedCountSelectItem = Assert.IsType(Assert.Single(selectExpandClause.SelectedItems)); + Assert.NotNull(expandedCountSelectItem.FilterOption); + Assert.Null(expandedCountSelectItem.SearchOption); + } + + // $expand=navProp/$count($search=prop) + [Fact] + public void ExpandWithNavigationPropCountWithSearchOptionWorks() + { + // Arrange + var odataQueryOptionParser = new ODataQueryOptionParser(HardCodedTestModel.TestModel, + HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet(), + new Dictionary() + { + {"$expand", "MyPaintings/$count($search=Blue)"} + }); + + // Act + var selectExpandClause = odataQueryOptionParser.ParseSelectAndExpand(); + + // Assert + Assert.NotNull(selectExpandClause); + ExpandedCountSelectItem expandedCountSelectItem = Assert.IsType(Assert.Single(selectExpandClause.SelectedItems)); + Assert.Null(expandedCountSelectItem.FilterOption); + Assert.NotNull(expandedCountSelectItem.SearchOption); + } + [Fact] public void SelectWithNestedSelectWorks() {