From 393be2f56161ece77aecbc588deba421b8554a85 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 15 Jul 2016 12:36:43 -0700 Subject: [PATCH] Support no-$ for system query parameters in OData library (Issue #637) --- .../UriParser/ODataQueryOptionParser.cs | 39 +++- .../UriParser/ODataUriParser.cs | 11 ++ .../UriParser/ODataUriParserConfiguration.cs | 7 + .../UriParser/Parsers/ExpandOptionParser.cs | 49 ++++- .../UriParser/Parsers/SelectExpandParser.cs | 34 +++- .../Parsers/SelectExpandSyntacticParser.cs | 8 +- .../UriParser/UriQueryConstants.cs | 3 + .../UriParser/ODataUriParserTests.cs | 171 +++++++++++++++--- .../PublicApi/PublicApi.bsl | 1 + 9 files changed, 269 insertions(+), 54 deletions(-) diff --git a/src/Microsoft.OData.Core/UriParser/ODataQueryOptionParser.cs b/src/Microsoft.OData.Core/UriParser/ODataQueryOptionParser.cs index 00c31328c1..dbabb40bd9 100644 --- a/src/Microsoft.OData.Core/UriParser/ODataQueryOptionParser.cs +++ b/src/Microsoft.OData.Core/UriParser/ODataQueryOptionParser.cs @@ -9,6 +9,7 @@ namespace Microsoft.OData.Core.UriParser #region namespaces using System; using System.Collections.Generic; + using System.Globalization; using System.Linq; using Microsoft.OData.Core.Metadata; using Microsoft.OData.Core.UriParser.Aggregation; @@ -506,21 +507,40 @@ private static SearchClause ParseSearchImplementation(string search, ODataUriPar } /// - /// Get query options according to case insensitive. + /// Gets query options according to case sensitivity and + /// whether no dollar query options is enabled. /// - /// The query option's name. - /// The value for that query option. + /// The query option name. + /// The value of the query option. /// Whether value successfully retrived. private bool TryGetQueryOption(string queryOptionName, out string value) { - if (!this.Resolver.EnableCaseInsensitive) + value = null; + if (queryOptionName == null) { - return this.queryOptions.TryGetValue(queryOptionName, out value); + return false; } - value = null; + // Trim queryOptionName to prevent caller from passing in untrimmed name for comparison with + // already trimmed keys in queryOptions dictionary. + string trimmedQueryOptionName = queryOptionName.Trim(); + + bool isCaseInsensitiveEnabled = this.Resolver.EnableCaseInsensitive; + bool isNoDollarQueryOptionsEnabled = this.Configuration.EnableNoDollarQueryOptions; + + if (!isCaseInsensitiveEnabled && !isNoDollarQueryOptionsEnabled) + { + return this.queryOptions.TryGetValue(trimmedQueryOptionName, out value); + } + + StringComparison stringComparison = isCaseInsensitiveEnabled ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + + string noDollarQueryOptionName = (isNoDollarQueryOptionsEnabled && trimmedQueryOptionName.StartsWith(UriQueryConstants.DollarSign)) ? + trimmedQueryOptionName.Substring(1) : null; + var list = this.queryOptions - .Where(pair => string.Equals(queryOptionName, pair.Key, StringComparison.OrdinalIgnoreCase)) + .Where(pair => string.Equals(trimmedQueryOptionName, pair.Key, stringComparison) + || (noDollarQueryOptionName != null && string.Equals(noDollarQueryOptionName, pair.Key, stringComparison))) .ToList(); if (list.Count == 0) @@ -533,8 +553,9 @@ private bool TryGetQueryOption(string queryOptionName, out string value) return true; } - throw new ODataException(Strings.QueryOptionUtils_QueryParameterMustBeSpecifiedOnce(queryOptionName)); + throw new ODataException(Strings.QueryOptionUtils_QueryParameterMustBeSpecifiedOnce( + isNoDollarQueryOptionsEnabled ? string.Format(CultureInfo.InvariantCulture, "${0}/{0}", noDollarQueryOptionName ?? string.Empty) : trimmedQueryOptionName)); } #endregion private methods } -} +} \ No newline at end of file diff --git a/src/Microsoft.OData.Core/UriParser/ODataUriParser.cs b/src/Microsoft.OData.Core/UriParser/ODataUriParser.cs index e58515502f..616085f696 100644 --- a/src/Microsoft.OData.Core/UriParser/ODataUriParser.cs +++ b/src/Microsoft.OData.Core/UriParser/ODataUriParser.cs @@ -144,6 +144,17 @@ public Func BatchReferenceCallback set { this.configuration.BatchReferenceCallback = value; } } + /// + /// Whether no dollar query options is enabled. + /// If it is enabled, the '$' prefix of system query options becomes optional. + /// For example, "select" and "$select" are equivalent in this case. + /// + public bool EnableNoDollarQueryOptions + { + get { return this.configuration.EnableNoDollarQueryOptions; } + set { this.configuration.EnableNoDollarQueryOptions = value; } + } + /// /// Whether Uri template parsing is enabled. Uri template for keys and function parameters are supported. /// See class for detail. diff --git a/src/Microsoft.OData.Core/UriParser/ODataUriParserConfiguration.cs b/src/Microsoft.OData.Core/UriParser/ODataUriParserConfiguration.cs index ad9236c800..3e632d3693 100644 --- a/src/Microsoft.OData.Core/UriParser/ODataUriParserConfiguration.cs +++ b/src/Microsoft.OData.Core/UriParser/ODataUriParserConfiguration.cs @@ -96,6 +96,13 @@ internal bool EnableCaseInsensitiveUriFunctionIdentifier } } + /// + /// Whether no dollar query options is enabled. + /// If it is enabled, the '$' prefix of system query options becomes optional. + /// For example, "select" and "$select" are equivalent in this case. + /// + internal bool EnableNoDollarQueryOptions { get; set; } + /// /// Whether Uri template parsing is enabled. See class for detail. /// diff --git a/src/Microsoft.OData.Core/UriParser/Parsers/ExpandOptionParser.cs b/src/Microsoft.OData.Core/UriParser/Parsers/ExpandOptionParser.cs index c64431b5c1..ba743f9f8b 100644 --- a/src/Microsoft.OData.Core/UriParser/Parsers/ExpandOptionParser.cs +++ b/src/Microsoft.OData.Core/UriParser/Parsers/ExpandOptionParser.cs @@ -32,7 +32,7 @@ internal sealed class ExpandOptionParser /// The parent entity type for expand option in case expand option is star, get all parent navigation properties /// private readonly IEdmStructuredType parentEntityType; - + /// /// Max recursion depth. As we recurse, each new instance of this class will have this lowered by 1. /// @@ -49,15 +49,25 @@ internal sealed class ExpandOptionParser /// private bool enableCaseInsensitiveBuiltinIdentifier; + /// + /// Whether to enable no dollar query options. + /// + private bool enableNoDollarQueryOptions; + /// /// Creates an instance of this class to parse options. /// /// Max recursion depth left. /// Whether to allow case insensitive for builtin identifier. - internal ExpandOptionParser(int maxRecursionDepth, bool enableCaseInsensitiveBuiltinIdentifier = false) + /// Whether to enable no dollar query options. + internal ExpandOptionParser( + int maxRecursionDepth, + bool enableCaseInsensitiveBuiltinIdentifier = false, + bool enableNoDollarQueryOptions = false) { this.maxRecursionDepth = maxRecursionDepth; this.enableCaseInsensitiveBuiltinIdentifier = enableCaseInsensitiveBuiltinIdentifier; + this.enableNoDollarQueryOptions = enableNoDollarQueryOptions; } /// @@ -67,8 +77,14 @@ internal ExpandOptionParser(int maxRecursionDepth, bool enableCaseInsensitiveBui /// The parent entity type for expand option /// Max recursion depth left. /// Whether to allow case insensitive for builtin identifier. - internal ExpandOptionParser(ODataUriResolver resolver, IEdmStructuredType parentEntityType, int maxRecursionDepth, bool enableCaseInsensitiveBuiltinIdentifier = false) - : this(maxRecursionDepth, enableCaseInsensitiveBuiltinIdentifier) + /// Whether to enable no dollar query options. + internal ExpandOptionParser( + ODataUriResolver resolver, + IEdmStructuredType parentEntityType, + int maxRecursionDepth, + bool enableCaseInsensitiveBuiltinIdentifier = false, + bool enableNoDollarQueryOptions = false) + : this(maxRecursionDepth, enableCaseInsensitiveBuiltinIdentifier, enableNoDollarQueryOptions) { this.resolver = resolver; this.parentEntityType = parentEntityType; @@ -134,6 +150,13 @@ internal List BuildExpandTermToken(PathSegmentToken pathToken, string text = this.enableCaseInsensitiveBuiltinIdentifier ? this.lexer.CurrentToken.Text.ToLowerInvariant() : this.lexer.CurrentToken.Text; + + // Prepend '$' prefix if needed. + if (this.enableNoDollarQueryOptions && !text.StartsWith(UriQueryConstants.DollarSign)) + { + text = string.Format(CultureInfo.InvariantCulture, "{0}{1}", UriQueryConstants.DollarSign, text); + } + switch (text) { case ExpressionConstants.QueryOptionFilter: @@ -261,14 +284,22 @@ internal List BuildExpandTermToken(PathSegmentToken pathToken, { var parentProperty = this.resolver.ResolveProperty(parentEntityType, pathToken.Identifier) as IEdmNavigationProperty; - // it is a navigation property, need to find the type. Like $expand=Friends($expand=Trips($expand=*)), when expandText becomes "Trips($expand=*)", find navigation property Trips of Friends, then get Entity type of Trips. + // it is a navigation property, need to find the type. + // Like $expand=Friends($expand=Trips($expand=*)), when expandText becomes "Trips($expand=*)", + // find navigation property Trips of Friends, then get Entity type of Trips. if (parentProperty != null) - { + { targetEntityType = parentProperty.ToEntityType(); } } - SelectExpandParser innerExpandParser = new SelectExpandParser(resolver, expandText, targetEntityType, this.maxRecursionDepth - 1, enableCaseInsensitiveBuiltinIdentifier); + SelectExpandParser innerExpandParser = new SelectExpandParser( + resolver, + expandText, + targetEntityType, + this.maxRecursionDepth - 1, + this.enableCaseInsensitiveBuiltinIdentifier, + this.enableNoDollarQueryOptions); expandOption = innerExpandParser.ParseExpand(); break; } @@ -347,8 +378,8 @@ private List BuildStarExpandTermToken(PathSegmentToken pathToke { case ExpressionConstants.QueryOptionLevels: { - if (!isRefExpand) - { + if (!isRefExpand) + { levelsOption = ResolveLevelOption(); } else diff --git a/src/Microsoft.OData.Core/UriParser/Parsers/SelectExpandParser.cs b/src/Microsoft.OData.Core/UriParser/Parsers/SelectExpandParser.cs index 9919dedc15..c1e524344d 100644 --- a/src/Microsoft.OData.Core/UriParser/Parsers/SelectExpandParser.cs +++ b/src/Microsoft.OData.Core/UriParser/Parsers/SelectExpandParser.cs @@ -55,6 +55,11 @@ internal sealed class SelectExpandParser /// private bool enableCaseInsensitiveBuiltinIdentifier; + /// + /// Whether to enable no dollar query options. + /// + private bool enableNoDollarQueryOptions; + /// /// Build the SelectOption strategy. /// TODO: Really should not take the clauseToParse here. Instead it should be provided with a call to ParseSelect() or ParseExpand(). @@ -62,7 +67,12 @@ internal sealed class SelectExpandParser /// the clause to parse /// max recursive depth /// Whether to allow case insensitive for builtin identifier. - public SelectExpandParser(string clauseToParse, int maxRecursiveDepth, bool enableCaseInsensitiveBuiltinIdentifier = false) + /// Whether to enable no dollar query options. + public SelectExpandParser( + string clauseToParse, + int maxRecursiveDepth, + bool enableCaseInsensitiveBuiltinIdentifier = false, + bool enableNoDollarQueryOptions = false) { this.maxRecursiveDepth = maxRecursiveDepth; @@ -77,6 +87,8 @@ public SelectExpandParser(string clauseToParse, int maxRecursiveDepth, bool enab this.lexer = clauseToParse != null ? new ExpressionLexer(clauseToParse, false /*moveToFirstToken*/, false /*useSemicolonDelimiter*/) : null; this.enableCaseInsensitiveBuiltinIdentifier = enableCaseInsensitiveBuiltinIdentifier; + + this.enableNoDollarQueryOptions = enableNoDollarQueryOptions; } /// @@ -87,13 +99,20 @@ public SelectExpandParser(string clauseToParse, int maxRecursiveDepth, bool enab /// the parent entity type for expand option /// max recursive depth /// Whether to allow case insensitive for builtin identifier. - public SelectExpandParser(ODataUriResolver resolver, string clauseToParse, IEdmStructuredType parentEntityType, int maxRecursiveDepth, bool enableCaseInsensitiveBuiltinIdentifier = false) - : this(clauseToParse, maxRecursiveDepth, enableCaseInsensitiveBuiltinIdentifier) + /// Whether to enable no dollar query options. + public SelectExpandParser( + ODataUriResolver resolver, + string clauseToParse, + IEdmStructuredType parentEntityType, + int maxRecursiveDepth, + bool enableCaseInsensitiveBuiltinIdentifier = false, + bool enableNoDollarQueryOptions = false) + : this(clauseToParse, maxRecursiveDepth, enableCaseInsensitiveBuiltinIdentifier, enableNoDollarQueryOptions) { this.resolver = resolver; this.parentEntityType = parentEntityType; } - + /// /// The maximum depth for path nested in $expand. /// @@ -168,7 +187,12 @@ private List ParseSingleExpandTerm() if (expandOptionParser == null) { - expandOptionParser = new ExpandOptionParser(this.resolver, this.parentEntityType, this.maxRecursiveDepth, enableCaseInsensitiveBuiltinIdentifier) + expandOptionParser = new ExpandOptionParser( + this.resolver, + this.parentEntityType, + this.maxRecursiveDepth, + this.enableCaseInsensitiveBuiltinIdentifier, + this.enableNoDollarQueryOptions) { MaxFilterDepth = MaxFilterDepth, MaxOrderByDepth = MaxOrderByDepth, diff --git a/src/Microsoft.OData.Core/UriParser/Parsers/SelectExpandSyntacticParser.cs b/src/Microsoft.OData.Core/UriParser/Parsers/SelectExpandSyntacticParser.cs index 620e449bc3..c32a2cbd74 100644 --- a/src/Microsoft.OData.Core/UriParser/Parsers/SelectExpandSyntacticParser.cs +++ b/src/Microsoft.OData.Core/UriParser/Parsers/SelectExpandSyntacticParser.cs @@ -37,7 +37,13 @@ public static void Parse( }; selectTree = selectParser.ParseSelect(); - SelectExpandParser expandParser = new SelectExpandParser(configuration.Resolver, expandClause, parentEntityType, configuration.Settings.SelectExpandLimit, configuration.EnableCaseInsensitiveUriFunctionIdentifier) + SelectExpandParser expandParser = new SelectExpandParser( + configuration.Resolver, + expandClause, + parentEntityType, + configuration.Settings.SelectExpandLimit, + configuration.EnableCaseInsensitiveUriFunctionIdentifier, + configuration.EnableNoDollarQueryOptions) { MaxPathDepth = configuration.Settings.PathLimit, MaxFilterDepth = configuration.Settings.FilterLimit, diff --git a/src/Microsoft.OData.Core/UriParser/UriQueryConstants.cs b/src/Microsoft.OData.Core/UriParser/UriQueryConstants.cs index e2bfe02691..aaf46cb434 100644 --- a/src/Microsoft.OData.Core/UriParser/UriQueryConstants.cs +++ b/src/Microsoft.OData.Core/UriParser/UriQueryConstants.cs @@ -76,5 +76,8 @@ internal static class UriQueryConstants /// A search query option name. internal const string SearchQueryOption = "$search"; + + /// Dollar sign. + internal const string DollarSign = "$"; } } \ No newline at end of file diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/ODataUriParserTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/ODataUriParserTests.cs index c96cea9d4e..89eba7394c 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/ODataUriParserTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/UriParser/ODataUriParserTests.cs @@ -1,11 +1,12 @@ //--------------------------------------------------------------------- -// +// // Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. // //--------------------------------------------------------------------- using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using FluentAssertions; using Microsoft.OData.Core.UriParser; @@ -26,10 +27,13 @@ public class ODataUriParserTests private readonly Uri ServiceRoot = new Uri("http://host"); private readonly Uri FullUri = new Uri("http://host/People"); - [Fact] - public void NoneQueryOptionShouldWork() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void NonQueryOptionShouldWork(bool enableNoDollarQueryOptions) { var uriParser = new ODataUriParser(HardCodedTestModel.TestModel, ServiceRoot, FullUri); + uriParser.EnableNoDollarQueryOptions = enableNoDollarQueryOptions; var path = uriParser.ParsePath(); path.Should().HaveCount(1); path.LastSegment.ShouldBeEntitySetSegment(HardCodedTestModel.GetPeopleSet()); @@ -44,10 +48,14 @@ public void NoneQueryOptionShouldWork() uriParser.ParseDeltaToken().Should().BeNull(); } - [Fact] - public void EmptyValueQueryOptionShouldWork() + [Theory] + [InlineData("?filter=&select=&expand=&orderby=&top=&skip=&count=&search=&unknown=&$unknownvalue&skiptoken=&deltatoken=", true)] + [InlineData("?$filter=&$select=&$expand=&$orderby=&$top=&$skip=&$count=&$search=&$unknown=&$unknownvalue&$skiptoken=&$deltatoken=", true)] + [InlineData("?$filter=&$select=&$expand=&$orderby=&$top=&$skip=&$count=&$search=&$unknown=&$unknownvalue&$skiptoken=&$deltatoken=", false)] + public void EmptyValueQueryOptionShouldWork(string relativeUriString, bool enableNoDollarQueryOptions) { - var uriParser = new ODataUriParser(HardCodedTestModel.TestModel, ServiceRoot, new Uri(FullUri, "?$filter=&$select=&$expand=&$orderby=&$top=&$skip=&$count=&$search=&$unknow=&$unknowvalue&$skiptoken=&$deltatoken=")); + var uriParser = new ODataUriParser(HardCodedTestModel.TestModel, ServiceRoot, new Uri(FullUri, relativeUriString)); + uriParser.EnableNoDollarQueryOptions = enableNoDollarQueryOptions; var path = uriParser.ParsePath(); path.Should().HaveCount(1); path.LastSegment.ShouldBeEntitySetSegment(HardCodedTestModel.GetPeopleSet()); @@ -126,18 +134,26 @@ public void FilterLimitIsSettable() parser.Settings.FilterLimit.Should().Be(3); } - [Fact] - public void FilterLimitIsRespectedForFilter() + [Theory] + [InlineData("http://host/People?filter=1 eq 1", true)] + [InlineData("http://host/People?$filter=1 eq 1", true)] + [InlineData("http://host/People?$filter=1 eq 1", false)] + public void FilterLimitIsRespectedForFilter(string fullUriString, bool enableNoDollarQueryOptions) { - ODataUriParser parser = new ODataUriParser(HardCodedTestModel.TestModel, ServiceRoot, new Uri("http://host/People?$filter=1 eq 1")) { Settings = { FilterLimit = 0 } }; + ODataUriParser parser = new ODataUriParser(HardCodedTestModel.TestModel, ServiceRoot, new Uri(fullUriString)) { Settings = { FilterLimit = 0 } }; + parser.EnableNoDollarQueryOptions = enableNoDollarQueryOptions; Action parseWithLimit = () => parser.ParseFilter(); parseWithLimit.ShouldThrow().WithMessage(ODataErrorStrings.UriQueryExpressionParser_TooDeep); } - [Fact] - public void FilterLimitWithInterestingTreeStructures() + [Theory] + [InlineData("http://host/People?filter=MyDog/Color eq 'Brown' or MyDog/Color eq 'White'", true)] + [InlineData("http://host/People?$filter=MyDog/Color eq 'Brown' or MyDog/Color eq 'White'", true)] + [InlineData("http://host/People?$filter=MyDog/Color eq 'Brown' or MyDog/Color eq 'White'", false)] + public void FilterLimitWithInterestingTreeStructures(string fullUriString, bool enableNoDollarQueryOptions) { - ODataUriParser parser = new ODataUriParser(HardCodedTestModel.TestModel, ServiceRoot, new Uri("http://host/People?$filter=MyDog/Color eq 'Brown' or MyDog/Color eq 'White'")) { Settings = { FilterLimit = 5 } }; + ODataUriParser parser = new ODataUriParser(HardCodedTestModel.TestModel, ServiceRoot, new Uri(fullUriString)) { Settings = { FilterLimit = 5 } }; + parser.EnableNoDollarQueryOptions = enableNoDollarQueryOptions; Action parseWithLimit = () => parser.ParseFilter(); parseWithLimit.ShouldThrow().WithMessage(ODataErrorStrings.UriQueryExpressionParser_TooDeep); } @@ -156,18 +172,26 @@ public void OrderbyLimitIsSettable() parser.Settings.OrderByLimit.Should().Be(3); } - [Fact] - public void OrderByLimitIsRespectedForOrderby() + [Theory] + [InlineData("http://host/People?orderby= 1 eq 1", true)] + [InlineData("http://host/People?$orderby= 1 eq 1", true)] + [InlineData("http://host/People?$orderby= 1 eq 1", false)] + public void OrderByLimitIsRespectedForOrderby(string fullUriString, bool enableNoDollarQueryOptions) { - ODataUriParser parser = new ODataUriParser(HardCodedTestModel.TestModel, ServiceRoot, new Uri("http://host/People?$orderby= 1 eq 1")) { Settings = { OrderByLimit = 0 } }; + ODataUriParser parser = new ODataUriParser(HardCodedTestModel.TestModel, ServiceRoot, new Uri(fullUriString)) { Settings = { OrderByLimit = 0 } }; + parser.EnableNoDollarQueryOptions = enableNoDollarQueryOptions; Action parseWithLimit = () => parser.ParseOrderBy(); parseWithLimit.ShouldThrow().WithMessage(ODataErrorStrings.UriQueryExpressionParser_TooDeep); } - [Fact] - public void OrderByLimitWithInterestingTreeStructures() + [Theory] + [InlineData("http://host/People?orderby=MyDog/MyPeople/MyDog/MyPeople/MyPaintings asc", true)] + [InlineData("http://host/People?$orderby=MyDog/MyPeople/MyDog/MyPeople/MyPaintings asc", true)] + [InlineData("http://host/People?$orderby=MyDog/MyPeople/MyDog/MyPeople/MyPaintings asc", false)] + public void OrderByLimitWithInterestingTreeStructures(string fullUriString, bool enableNoDollarQueryOptions) { - ODataUriParser parser = new ODataUriParser(HardCodedTestModel.TestModel, ServiceRoot, new Uri("http://host/People?$orderby=MyDog/MyPeople/MyDog/MyPeople/MyPaintings asc")) { Settings = { OrderByLimit = 5 } }; + ODataUriParser parser = new ODataUriParser(HardCodedTestModel.TestModel, ServiceRoot, new Uri(fullUriString)) { Settings = { OrderByLimit = 5 } }; + parser.EnableNoDollarQueryOptions = enableNoDollarQueryOptions; Action parseWithLimit = () => parser.ParseOrderBy(); parseWithLimit.ShouldThrow().WithMessage(ODataErrorStrings.UriQueryExpressionParser_TooDeep); } @@ -208,10 +232,14 @@ public void SelectExpandLimitIsSettable() parser.Settings.SelectExpandLimit.Should().Be(3); } - [Fact] - public void SelectExpandLimitIsRespectedForSelectExpand() + [Theory] + [InlineData("http://host/People?select=MyDog&expand=MyDog(select=color)", true)] + [InlineData("http://host/People?$select=MyDog&$expand=MyDog($select=color)", true)] + [InlineData("http://host/People?$select=MyDog&$expand=MyDog($select=color)", false)] + public void SelectExpandLimitIsRespectedForSelectExpand(string fullUriString, bool enableNoDollarQueryOptions) { - ODataUriParser parser = new ODataUriParser(HardCodedTestModel.TestModel, ServiceRoot, new Uri("http://host/People?$select=MyDog&$expand=MyDog($select=color)")) { Settings = { SelectExpandLimit = 0 } }; + ODataUriParser parser = new ODataUriParser(HardCodedTestModel.TestModel, ServiceRoot, new Uri(fullUriString)) { Settings = { SelectExpandLimit = 0 } }; + parser.EnableNoDollarQueryOptions = enableNoDollarQueryOptions; Action parseWithLimit = () => parser.ParseSelectAndExpand(); parseWithLimit.ShouldThrow().WithMessage(ODataErrorStrings.UriQueryExpressionParser_TooDeep); } @@ -258,10 +286,14 @@ public void DefaultParameterAliasNodesShouldBeEmtpy() } #endregion - [Fact] - public void ParseSelectExpandForContainment() + [Theory] + [InlineData("http://host/People?select=MyContainedDog&expand=MyContainedDog", true)] + [InlineData("http://host/People?$select=MyContainedDog&$expand=MyContainedDog", true)] + [InlineData("http://host/People?$select=MyContainedDog&$expand=MyContainedDog", false)] + public void ParseSelectExpandForContainment(string fullUriString, bool enableNoDollarQueryOptions) { - ODataUriParser parser = new ODataUriParser(HardCodedTestModel.TestModel, ServiceRoot, new Uri("http://host/People?$select=MyContainedDog&$expand=MyContainedDog")) { Settings = { SelectExpandLimit = 5 } }; + ODataUriParser parser = new ODataUriParser(HardCodedTestModel.TestModel, ServiceRoot, new Uri(fullUriString)) { Settings = { SelectExpandLimit = 5 } }; + parser.EnableNoDollarQueryOptions = enableNoDollarQueryOptions; SelectExpandClause containedSelectExpandClause = parser.ParseSelectAndExpand(); IEnumerator enumerator = containedSelectExpandClause.SelectedItems.GetEnumerator(); enumerator.MoveNext(); @@ -337,10 +369,14 @@ public void ParsePathShouldWork() path.LastSegment.ShouldBeEntitySetSegment(HardCodedTestModel.GetPeopleSet()); } - [Fact] - public void ParseQueryOptionsShouldWork() + [Theory] + [InlineData("People?filter=MyDog/Color eq 'Brown'&select=ID&expand=MyDog&orderby=ID&top=1&skip=2&count=true&search=FA&$unknown=&$unknownvalue&skiptoken=abc&deltatoken=def", true)] + [InlineData("People?$filter=MyDog/Color eq 'Brown'&$select=ID&$expand=MyDog&$orderby=ID&$top=1&$skip=2&$count=true&$search=FA&$unknown=&$unknownvalue&$skiptoken=abc&$deltatoken=def", true)] + [InlineData("People?$filter=MyDog/Color eq 'Brown'&$select=ID&$expand=MyDog&$orderby=ID&$top=1&$skip=2&$count=true&$search=FA&$unknown=&$unknownvalue&$skiptoken=abc&$deltatoken=def", false)] + public void ParseQueryOptionsShouldWork(string relativeUriString, bool enableNoDollarQueryOptions) { - var parser = new ODataUriParser(HardCodedTestModel.TestModel, new Uri("People?$filter=MyDog/Color eq 'Brown'&$select=ID&$expand=MyDog&$orderby=ID&$top=1&$skip=2&$count=true&$search=FA&$unknow=&$unknowvalue&$skiptoken=abc&$deltatoken=def", UriKind.Relative)); + var parser = new ODataUriParser(HardCodedTestModel.TestModel, new Uri(relativeUriString, UriKind.Relative)); + parser.EnableNoDollarQueryOptions = enableNoDollarQueryOptions; parser.ParseSelectAndExpand().Should().NotBeNull(); parser.ParseFilter().Should().NotBeNull(); parser.ParseOrderBy().Should().NotBeNull(); @@ -353,9 +389,84 @@ public void ParseQueryOptionsShouldWork() } [Fact] - public void ParseDeltaTokenWithKindsofCharactorsShouldWork() + public void ParseNoDollarQueryOptionsShouldReturnNullIfNoDollarQueryOptionsIsNotEnabled() + { + var parser = new ODataUriParser(HardCodedTestModel.TestModel, new Uri("People?filter=MyDog/Color eq 'Brown'&select=ID&expand=MyDog&orderby=ID&top=1&skip=2&count=true&search=FA&$unknown=&$unknownvalue&skiptoken=abc&deltatoken=def", UriKind.Relative)); + parser.ParseFilter().Should().BeNull(); + parser.ParseSelectAndExpand().Should().BeNull(); + parser.ParseOrderBy().Should().BeNull(); + parser.ParseTop().Should().Be(null); + parser.ParseSkip().Should().Be(null); + parser.ParseCount().Should().Be(null); + parser.ParseSearch().Should().BeNull(); + parser.ParseSkipToken().Should().BeNull(); + parser.ParseDeltaToken().Should().BeNull(); + } + + [Theory] + // Should not throw duplicate query options exception. + // 1. Case sensitive, No dollar enabled. + [InlineData("People?select=ID&$SELECT=Name", false, true, "select", false)] + [InlineData("People?SELECT=ID&$select=Name", false, true, "select", false)] + [InlineData("People?SELECT=ID&$SELECT=Name", false, true, "select", false)] + // 2. Case insensitive, No dollar not enabled. + [InlineData("People?$select=ID&select=Name", true, false, "$select", false)] + [InlineData("People?$select=ID&SELECT=Name", true, false, "$select", false)] + + // Should throw duplicate query options exception. + [InlineData("People?select=ID&select=Name", false, false, "select", true)] + [InlineData("People?$select=ID&$select=Name", false, false, "$select", true)] + [InlineData("People?select=ID&$select=Name", false, true, "select", true)] + [InlineData("People?$select=ID&$SELECT=Name", true, false, "$select", true)] + [InlineData("People?$select=ID&$SELECT=Name", true, true, "select", true)] + [InlineData("People?select=ID&$SELECT=Name", true, true, "select", true)] + public void ParseShouldFailWithDuplicateQueryOptions(string relativeUriString, bool enableCaseInsensitive, bool enableNoDollarQueryOptions, string queryOptionName, bool shouldThrow) + { + Uri relativeUri = new Uri(relativeUriString, UriKind.Relative); + Action action = () => new ODataUriParser(HardCodedTestModel.TestModel, relativeUri) + { + Resolver = new ODataUriResolver() + { + EnableCaseInsensitive = enableCaseInsensitive + }, + + EnableNoDollarQueryOptions = enableNoDollarQueryOptions + + }.ParseSelectAndExpand(); + + if (shouldThrow) + { + action.ShouldThrow().WithMessage(Strings.QueryOptionUtils_QueryParameterMustBeSpecifiedOnce( + enableNoDollarQueryOptions ? string.Format(CultureInfo.InvariantCulture, "${0}/{0}", queryOptionName ?? string.Empty) : queryOptionName)); + } + else + { + action.ShouldNotThrow(); + } + } + + [Theory] + [InlineData("People?expand=MyDog(select=ID,Color)")] + [InlineData("People?$expand=MyDog(select=ID,Color)")] + [InlineData("People?expand=MyDog(expand=MyPeople(select=Name))")] + [InlineData("People?expand=MyDog($expand=MyPeople($select=Name))")] + [InlineData("People?expand=MyDog(select=Color;expand=MyPeople(select=Name;count=true))")] + [InlineData("People?$expand=MyDog($select=Color;expand=MyPeople(select=Name;$count=true))")] + public void ParseNestedNoDollarQueryOptionsShouldWorkWhenNoDollarQueryOptionsIsEnabled(string relativeUriString) + { + var parser = new ODataUriParser(HardCodedTestModel.TestModel, new Uri(relativeUriString, UriKind.Relative)); + parser.EnableNoDollarQueryOptions = true; + parser.ParseSelectAndExpand().Should().NotBeNull(); + } + + [Theory] + [InlineData("People?deltatoken=Start@Next_Chunk:From%26$To=Here!?()*+%2B,1-._~;", true)] + [InlineData("People?$deltatoken=Start@Next_Chunk:From%26$To=Here!?()*+%2B,1-._~;", true)] + [InlineData("People?$deltatoken=Start@Next_Chunk:From%26$To=Here!?()*+%2B,1-._~;", false)] + public void ParseDeltaTokenWithKindsofCharactorsShouldWork(string relativeUriString, bool enableNoDollarQueryOptions) { - var parser = new ODataUriParser(HardCodedTestModel.TestModel, new Uri("People?$deltatoken=Start@Next_Chunk:From%26$To=Here!?()*+%2B,1-._~;", UriKind.Relative)); + var parser = new ODataUriParser(HardCodedTestModel.TestModel, new Uri(relativeUriString, UriKind.Relative)); + parser.EnableNoDollarQueryOptions = enableNoDollarQueryOptions; parser.ParseDeltaToken().Should().Be("Start@Next_Chunk:From&$To=Here!?()* +,1-._~;"); } @@ -564,4 +675,4 @@ public void ParsePathFunctionWithNullEntitySetPath() #endregion } -} +} \ No newline at end of file diff --git a/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/PublicApi/PublicApi.bsl b/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/PublicApi/PublicApi.bsl index 355bc3305e..c76fd4e53f 100644 --- a/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/PublicApi/PublicApi.bsl +++ b/test/FunctionalTests/Tests/DataOData/Tests/OData.Common.Tests/PublicApi/PublicApi.bsl @@ -5172,6 +5172,7 @@ public sealed class Microsoft.OData.Core.UriParser.ODataUriParser { public ODataUriParser (Microsoft.OData.Edm.IEdmModel model, System.Uri serviceRoot, System.Uri fullUri) System.Func`2[[System.String],[Microsoft.OData.Core.UriParser.Semantic.BatchReferenceSegment]] BatchReferenceCallback { public get; public set; } + bool EnableNoDollarQueryOptions { public get; public set; } bool EnableUriTemplateParsing { public get; public set; } Microsoft.OData.Edm.IEdmModel Model { public get; } System.Collections.Generic.IDictionary`2[[System.String],[Microsoft.OData.Core.UriParser.Semantic.SingleValueNode]] ParameterAliasNodes { public get; }