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()
{