diff --git a/src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs b/src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs
index 066bd41cff..72a09ec99f 100644
--- a/src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs
+++ b/src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs
@@ -720,12 +720,19 @@ private static IEdmTypeReference ValidateIsOfOrCast(BindingState state, bool isC
Error.Format(SRResources.MetadataBinder_CastOrIsOfExpressionWithWrongNumberOfOperands, args.Count));
}
- ConstantNode typeArgument = args.Last() as ConstantNode;
+ QueryNode queryNode = args[^1];
+ string typeArgumentFullName = null;
IEdmTypeReference returnType = null;
- if (typeArgument != null)
+ if (queryNode is SingleResourceCastNode singleResourceCastNode)
{
- returnType = TryGetTypeReference(state.Model, typeArgument.Value as string, state.Configuration.Resolver);
+ returnType = singleResourceCastNode.TypeReference;
+ typeArgumentFullName = returnType.FullName();
+ }
+ else if (queryNode is ConstantNode constantNode)
+ {
+ typeArgumentFullName = constantNode.Value as string;
+ returnType = TryGetTypeReference(state.Model, typeArgumentFullName, state.Configuration.Resolver);
}
if (returnType == null)
@@ -758,7 +765,7 @@ private static IEdmTypeReference ValidateIsOfOrCast(BindingState state, bool isC
{
// throw if cast enum to not-string :
if ((args[0].GetEdmTypeReference() is IEdmEnumTypeReference)
- && !string.Equals(typeArgument.Value as string, Microsoft.OData.Metadata.EdmConstants.EdmStringTypeName, StringComparison.Ordinal))
+ && !string.Equals(typeArgumentFullName, Microsoft.OData.Metadata.EdmConstants.EdmStringTypeName, state.Configuration.Resolver?.EnableCaseInsensitive == true ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
{
throw new ODataException(SRResources.CastBinder_EnumOnlyCastToOrFromString);
}
diff --git a/src/Microsoft.OData.Core/UriParser/Parsers/FunctionCallParser.cs b/src/Microsoft.OData.Core/UriParser/Parsers/FunctionCallParser.cs
index 1c5b24c4ac..96a6d23da7 100644
--- a/src/Microsoft.OData.Core/UriParser/Parsers/FunctionCallParser.cs
+++ b/src/Microsoft.OData.Core/UriParser/Parsers/FunctionCallParser.cs
@@ -11,6 +11,7 @@ namespace Microsoft.OData.UriParser
using System.Diagnostics;
using System.Linq;
using Microsoft.OData.Core;
+ using Microsoft.OData.Edm;
///
/// Implementation of IFunctionCallParser that allows functions calls and parses arguments with a provided method.
@@ -101,7 +102,7 @@ public bool TryParseIdentifierAsFunction(QueryToken parent, out QueryToken resul
this.Lexer.NextToken();
}
- FunctionParameterToken[] arguments = this.ParseArgumentListOrEntityKeyList(() => lexer.RestorePosition(position));
+ FunctionParameterToken[] arguments = this.ParseArgumentListOrEntityKeyList(() => lexer.RestorePosition(position), functionName);
if (arguments != null)
{
result = new FunctionCallToken(functionName.ToString(), arguments, parent);
@@ -114,8 +115,9 @@ public bool TryParseIdentifierAsFunction(QueryToken parent, out QueryToken resul
/// Parses argument lists or entity key value list.
///
/// Action invoked for restoring a state during failure.
+ /// The name of the function being called. Default is an empty span.
/// The lexical tokens representing the arguments.
- public FunctionParameterToken[] ParseArgumentListOrEntityKeyList(Action restoreAction = null)
+ public FunctionParameterToken[] ParseArgumentListOrEntityKeyList(Action restoreAction = null, ReadOnlySpan functionName = default)
{
if (this.Lexer.CurrentToken.Kind != ExpressionTokenKind.OpenParen)
{
@@ -136,7 +138,7 @@ public FunctionParameterToken[] ParseArgumentListOrEntityKeyList(Action restoreA
}
else
{
- arguments = this.ParseArguments();
+ arguments = this.ParseArguments(functionName);
}
if (this.Lexer.CurrentToken.Kind != ExpressionTokenKind.CloseParen)
@@ -161,8 +163,9 @@ public FunctionParameterToken[] ParseArgumentListOrEntityKeyList(Action restoreA
/// Arguments can either be of the form a=1,b=2,c=3 or 1,2,3.
/// They cannot be mixed between those two styles.
///
+ /// The name of the function being called. Default is an empty span.
/// The lexical tokens representing the arguments.
- public FunctionParameterToken[] ParseArguments()
+ public FunctionParameterToken[] ParseArguments(ReadOnlySpan functionName = default)
{
ICollection argList;
if (this.TryReadArgumentsAsNamedValues(out argList))
@@ -170,24 +173,43 @@ public FunctionParameterToken[] ParseArguments()
return argList.ToArray();
}
- return this.ReadArgumentsAsPositionalValues().ToArray();
+ return this.ReadArgumentsAsPositionalValues(functionName).ToArray();
}
///
/// Read the list of arguments as a set of positional values
///
+ /// The name of the function being called. Default is an empty span.
/// A list of FunctionParameterTokens representing each argument
- private List ReadArgumentsAsPositionalValues()
+ private List ReadArgumentsAsPositionalValues(ReadOnlySpan functionName = default)
{
+ // Store the parent expression of the current argument.
+ Stack expressionParents = new Stack();
+ bool isFunctionCallNameCastOrIsOf = functionName.Length > 0 &&
+ (functionName.SequenceEqual(ExpressionConstants.UnboundFunctionCast.AsSpan()) || functionName.SequenceEqual(ExpressionConstants.UnboundFunctionIsOf.AsSpan()));
List argList = new List();
while (true)
{
- argList.Add(new FunctionParameterToken(null, this.parser.ParseExpression()));
+ // If we have a parent expression, we need to set the parent of the next argument to the current argument.
+ QueryToken parentExpression = expressionParents.Count > 0 ? expressionParents.Pop() : null;
+ QueryToken parameterToken = this.parser.ParseExpression();
+
+ // If the function call is cast or isof, set the parent of the next argument to the current argument.
+ if (parentExpression != null && isFunctionCallNameCastOrIsOf)
+ {
+ parameterToken = SetParentForCurrentParameterToken(parentExpression, parameterToken);
+ }
+
+ argList.Add(new FunctionParameterToken(null, parameterToken));
if (this.Lexer.CurrentToken.Kind != ExpressionTokenKind.Comma)
{
break;
}
+ // In case of comma, we need to parse the next argument
+ // but first we need to set the parent of the next argument to the current argument.
+ expressionParents.Push(parameterToken);
+
this.Lexer.NextToken();
}
@@ -213,5 +235,35 @@ private bool TryReadArgumentsAsNamedValues(out ICollection
+ /// Set the parent of the next argument to the current argument.
+ /// For example, in the following query:
+ /// cast(Location, NS.HomeAddress) where Location is the parentExpression and NS.HomeAddress is the parameterToken.
+ /// isof(Location, NS.HomeAddress) where Location is the parentExpression and NS.HomeAddress is the parameterToken.
+ ///
+ /// The previous parameter token.
+ /// The current parameter token.
+ /// The updated parameter token.
+ private static QueryToken SetParentForCurrentParameterToken(QueryToken parentExpression, QueryToken parameterToken)
+ {
+ if (parameterToken is not DottedIdentifierToken dottedIdentifierToken || dottedIdentifierToken?.NextToken is not null)
+ {
+ return parameterToken;
+ }
+
+ // Check if the identifier is a primitive type
+ IEdmSchemaType schemaType = NormalizedModelElementsCache.EdmCoreModelInstance.FindSchemaTypes(dottedIdentifierToken.Identifier)?.FirstOrDefault();
+
+ // If the identifier is a primitive type
+ if (schemaType is IEdmPrimitiveType)
+ {
+ return parameterToken;
+ }
+
+ // Set the parent of the next argument to the current argument
+ dottedIdentifierToken.NextToken = parentExpression;
+ return dottedIdentifierToken;
+ }
}
}
diff --git a/src/Microsoft.OData.Core/UriParser/Resolver/NormalizedModelElementsCache.cs b/src/Microsoft.OData.Core/UriParser/Resolver/NormalizedModelElementsCache.cs
index 7d7b19f66b..41698ebc32 100644
--- a/src/Microsoft.OData.Core/UriParser/Resolver/NormalizedModelElementsCache.cs
+++ b/src/Microsoft.OData.Core/UriParser/Resolver/NormalizedModelElementsCache.cs
@@ -20,6 +20,11 @@ namespace Microsoft.OData.Edm
///
internal sealed class NormalizedModelElementsCache
{
+ ///
+ /// Provides a shared, normalized case-insensitive cache of model elements for the EDM core model.
+ ///
+ public static readonly NormalizedModelElementsCache EdmCoreModelInstance = new NormalizedModelElementsCache(EdmCoreModel.Instance);
+
// We create different caches for different types of schema elements because all current usage request schema elements
// of specific types. If we were to use a single dictionary we would need
// to do additional work (and allocations) during lookups to filter the results to the subset that matches the request type.
diff --git a/test/UnitTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/EnumFilterFunctionalTests.cs b/test/UnitTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/EnumFilterFunctionalTests.cs
index fd30a3a8a5..c979439e39 100644
--- a/test/UnitTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/EnumFilterFunctionalTests.cs
+++ b/test/UnitTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/EnumFilterFunctionalTests.cs
@@ -410,20 +410,26 @@ public void ParseFilterWithNullEnumValue()
convertNode.Source.ShouldBeConstantQueryNode((object)null);
}
- [Fact]
- public void ParseFilterCastMethod1()
+ [Theory]
+ [InlineData("cast(NS.Color'Green', 'Edm.String') eq 'blue'")]
+ [InlineData("cast(NS.Color'Green', Edm.String) eq 'blue'")]
+ [InlineData("cast(NS.Color'Green', edm.string) eq 'blue'")]
+ [InlineData("cast(NS.Color'Green', EDM.STRING) eq 'blue'")]
+ public void ParseFilterCastMethodWithEdmPrimitiveTypes(string filterQuery)
{
- var filter = ParseFilter("cast(NS.Color'Green', 'Edm.String') eq 'blue'", this.userModel, this.entityType, this.entitySet);
+ var filter = ParseFilter(filterQuery, true, this.userModel, this.entityType, this.entitySet);
var bon = filter.Expression.ShouldBeBinaryOperatorNode(BinaryOperatorKind.Equal);
var convertNode = Assert.IsType(bon.Left);
var functionCallNode = Assert.IsType(convertNode.Source);
Assert.Equal("cast", functionCallNode.Name); // ConvertNode is because cast() result's nullable=false.
}
- [Fact]
- public void ParseFilterCastMethod2()
+ [Theory]
+ [InlineData("cast('Green', 'NS.Color') eq NS.Color'Green'")]
+ [InlineData("cast('Green', NS.Color) eq NS.Color'Green'")]
+ public void ParseFilterCastMethodWithOrWithoutSingleQuotesOnType(string filterQuery)
{
- var filter = ParseFilter("cast('Green', 'NS.Color') eq NS.Color'Green'", this.userModel, this.entityType, this.entitySet);
+ var filter = ParseFilter(filterQuery, this.userModel, this.entityType, this.entitySet);
var bon = filter.Expression.ShouldBeBinaryOperatorNode(BinaryOperatorKind.Equal);
var functionCallNode = Assert.IsType(bon.Left);
Assert.Equal("cast", functionCallNode.Name);
@@ -500,17 +506,20 @@ public void ParseFilterEnumMemberUndefined4()
parse.Throws(Error.Format(SRResources.Binder_IsNotValidEnumConstant, "NS.ColorFlags'Red,2'"));
}
- [Fact]
- public void ParseFilterEnumTypesWrongCast1()
+ [Theory]
+ [InlineData("cast(NS.ColorFlags'Green', 'Edm.Int64') eq 2")]
+ [InlineData("cast(NS.ColorFlags'Green', Edm.Int64) eq 2")]
+ public void ParseFilterEnumTypesWrongCast1(string filter)
{
- Action parse = () => ParseFilter("cast(NS.ColorFlags'Green', 'Edm.Int64') eq 2", this.userModel, this.entityType, this.entitySet);
+ Action parse = () => ParseFilter(filter, this.userModel, this.entityType, this.entitySet);
parse.Throws(SRResources.CastBinder_EnumOnlyCastToOrFromString);
}
-
- [Fact]
- public void ParseFilterEnumTypesWrongCast2()
+ [Theory]
+ [InlineData("cast(321, 'NS.ColorFlags') eq 2")]
+ [InlineData("cast(321, NS.ColorFlags) eq 2")]
+ public void ParseFilterEnumTypesWrongCast2(string filter)
{
- Action parse = () => ParseFilter("cast(321, 'NS.ColorFlags') eq 2", this.userModel, this.entityType, this.entitySet);
+ Action parse = () => ParseFilter(filter, this.userModel, this.entityType, this.entitySet);
parse.Throws(SRResources.CastBinder_EnumOnlyCastToOrFromString);
}
@@ -536,7 +545,7 @@ public void ParseFilterWithInOperatorWithEnums(string filterOptionValue)
IEdmEnumType colorType = this.GetIEdmType(colorTypeName);
IEdmEnumMember enumMember = colorType.Members.First(m => m.Name == enumValue);
- string expectedLiteral = "'Green'";
+ string expectedLiteral = "'Green'";
if (filterOptionValue.StartsWith(colorTypeName)) // if the filterOptionValue is already fully qualified, then the expectedLiteral should be the same as filterOptionValue
{
expectedLiteral = "NS.Color'Green'";
@@ -651,5 +660,11 @@ private FilterClause ParseFilter(string text, IEdmModel edmModel, IEdmEntityType
{
return new ODataQueryOptionParser(edmModel, entityType, edmEntitySet, new Dictionary() { { "$filter", text } }).ParseFilter();
}
+
+ private FilterClause ParseFilter(string text, bool caseInsensitive, IEdmModel edmModel, IEdmEntityType edmEntityType, IEdmEntitySet edmEntitySet)
+ {
+ return new ODataQueryOptionParser(edmModel, entityType, edmEntitySet, new Dictionary() { { "$filter", text } })
+ { Resolver = new ODataUriResolver() { EnableCaseInsensitive = caseInsensitive } }.ParseFilter();
+ }
}
}
diff --git a/test/UnitTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/FilterAndOrderByFunctionalTests.cs b/test/UnitTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/FilterAndOrderByFunctionalTests.cs
index f0bdc5a270..1cc556e1f3 100644
--- a/test/UnitTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/FilterAndOrderByFunctionalTests.cs
+++ b/test/UnitTests/Microsoft.OData.Core.Tests/ScenarioTests/UriParser/FilterAndOrderByFunctionalTests.cs
@@ -15,6 +15,7 @@
using Microsoft.OData.UriParser;
using Microsoft.Spatial;
using Microsoft.Test.OData.Utils.Metadata;
+using Microsoft.VisualBasic;
using Xunit;
namespace Microsoft.OData.Tests.ScenarioTests.UriParser
@@ -760,6 +761,259 @@ public void IsOfFunctionWorksWithSingleQuotesOnType()
singleValueFunctionCallNode.Parameters.ElementAt(1).ShouldBeConstantQueryNode("Edm.String");
}
+ [Fact]
+ public void IsOfFunctionWorksWithOrWithoutSingleQuotesOnType()
+ {
+ FilterClause filter = ParseFilter("isof(Shoe, Edm.String)", HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet());
+ var singleValueFunctionCallNode = filter.Expression.ShouldBeSingleValueFunctionCallQueryNode("isof");
+ singleValueFunctionCallNode.Parameters.ElementAt(0).ShouldBeSingleValuePropertyAccessQueryNode(HardCodedTestModel.GetPersonShoeProp());
+ singleValueFunctionCallNode.Parameters.ElementAt(1).ShouldBeConstantQueryNode("Edm.String");
+ }
+
+ [Fact]
+ public void IsOfFunctionWithOneParameter_WithSingleQuotesOnTypeParameter_ShouldBeConstantQueryNode()
+ {
+ // Arrange
+ var filterQuery = "isof('Fully.Qualified.Namespace.Employee')";
+
+ // Act
+ FilterClause filter = ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet());
+
+ // Assert
+ SingleValueFunctionCallNode singleValueFunctionCallNode = filter.Expression.ShouldBeSingleValueFunctionCallQueryNode("isof");
+ ResourceRangeVariableReferenceNode rangeVariableReference = singleValueFunctionCallNode.Parameters.ElementAt(0).ShouldBeResourceRangeVariableReferenceNode("$it");
+ Assert.Equal("Fully.Qualified.Namespace.Person", rangeVariableReference.GetEdmTypeReference().FullName()); // $it is of type Person
+
+ var constantNode = singleValueFunctionCallNode.Parameters.ElementAt(1).ShouldBeConstantQueryNode("Fully.Qualified.Namespace.Employee");
+ Assert.Equal("Fully.Qualified.Namespace.Employee", constantNode.Value); // 'Fully.Qualified.Namespace.Employee' is the type parameter
+ }
+
+ [Fact]
+ public void IsOfFunctionWithOneParameter_WithoutSingleQuotesOnTypeParameter_ShouldBeSingleResourceCastNode()
+ {
+ // Arrange
+ var filterQuery = "isof(Fully.Qualified.Namespace.Employee)";
+
+ // Act
+ FilterClause filter = ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet());
+
+ // Assert
+ SingleValueFunctionCallNode singleValueFunctionCallNode = filter.Expression.ShouldBeSingleValueFunctionCallQueryNode("isof");
+ ResourceRangeVariableReferenceNode rangeVariableReference = singleValueFunctionCallNode.Parameters.ElementAt(0).ShouldBeResourceRangeVariableReferenceNode("$it");
+ Assert.Equal("Fully.Qualified.Namespace.Person", rangeVariableReference.GetEdmTypeReference().FullName()); // $it is of type Person
+
+ var singleResourceCastNode = singleValueFunctionCallNode.Parameters.ElementAt(1).ShouldBeSingleResourceCastNode(HardCodedTestModel.GetEmployeeTypeReference());
+ Assert.Equal("Fully.Qualified.Namespace.Employee", singleResourceCastNode.TypeReference.FullName()); // Fully.Qualified.Namespace.Employee is the type parameter
+ }
+
+ [Fact]
+ public void IsOfFunctionWithTwoParameters_WithSingleQuotesOnTypeParameter_ShouldBeConstantQueryNode()
+ {
+ // Arrange
+ var filterQuery = "isof(MyAddress, 'Fully.Qualified.Namespace.HomeAddress')";
+
+ // Act
+ FilterClause filter = ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet());
+
+ // Assert
+ SingleValueFunctionCallNode singleValueFunctionCallNode = filter.Expression.ShouldBeSingleValueFunctionCallQueryNode("isof");
+ SingleComplexNode singleComplexNode = singleValueFunctionCallNode.Parameters.ElementAt(0).ShouldBeSingleComplexNode(HardCodedTestModel.GetPersonAddressProp());
+ Assert.Equal("MyAddress", singleComplexNode.Property.Name); // MyAddress is the property name
+ Assert.Equal("Fully.Qualified.Namespace.Address", singleComplexNode.GetEdmTypeReference().FullName()); // MyAddress is of type Address
+
+ var constantNode = singleValueFunctionCallNode.Parameters.ElementAt(1).ShouldBeConstantQueryNode("Fully.Qualified.Namespace.HomeAddress");
+ Assert.Equal("Fully.Qualified.Namespace.HomeAddress", constantNode.Value); // 'Fully.Qualified.Namespace.Employee' is the type parameter
+ }
+
+ [Fact]
+ public void IsOfFunctionWithTwoParameters_WithoutSingleQuotesOnTypeParameter_ShouldBeSingleResourceCastNode()
+ {
+ // Arrange
+ var filterQuery = "isof(MyAddress, Fully.Qualified.Namespace.HomeAddress)";
+
+ // Act
+ FilterClause filter = ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet());
+
+ // Assert
+ SingleValueFunctionCallNode singleValueFunctionCallNode = filter.Expression.ShouldBeSingleValueFunctionCallQueryNode("isof");
+ SingleComplexNode singleComplexNode = singleValueFunctionCallNode.Parameters.ElementAt(0).ShouldBeSingleComplexNode(HardCodedTestModel.GetPersonAddressProp());
+ Assert.Equal("MyAddress", singleComplexNode.Property.Name); // MyAddress is the property name
+ Assert.Equal("Fully.Qualified.Namespace.Address", singleComplexNode.GetEdmTypeReference().FullName()); // MyAddress is of type Address
+
+ var singleResourceCastNode = singleValueFunctionCallNode.Parameters.ElementAt(1).ShouldBeSingleResourceCastNode(HardCodedTestModel.GetHomeAddressReference());
+ Assert.Equal("Fully.Qualified.Namespace.HomeAddress", singleResourceCastNode.TypeReference.FullName()); // Fully.Qualified.Namespace.HomeAddress is the type parameter
+ }
+
+ [Theory]
+ [InlineData("isof(ID, Edm.Int64)", "Edm.Int64")]
+ [InlineData("isof(ID, edm.int64)", "edm.int64")]
+ [InlineData("isof(ID, Edm.int64)", "Edm.int64")]
+ [InlineData("isof(ID, EDM.INT64)", "EDM.INT64")]
+ public void IsOfFunctionWorksWithoutSingleQuotesOnPrimitiveType_CaseInsentive(string queryFilter, string expectedConstantNodeValue)
+ {
+ FilterClause filter = ParseFilter(queryFilter, true, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet());
+ var singleValueFunctionCallNode = filter.Expression.ShouldBeSingleValueFunctionCallQueryNode("isof");
+ singleValueFunctionCallNode.Parameters.ElementAt(0).ShouldBeSingleValuePropertyAccessQueryNode(HardCodedTestModel.GetPersonIdProp());
+ singleValueFunctionCallNode.Parameters.ElementAt(1).ShouldBeConstantQueryNode(expectedConstantNodeValue);
+ }
+
+ [Theory]
+ [InlineData("isof(ID, Edm.Int32)", "Edm.Int32")]
+ [InlineData("isof(ID, edm.int32)", "edm.int32")]
+ [InlineData("isof(ID, Edm.int32)", "Edm.int32")]
+ [InlineData("isof(ID, EDM.INT32)", "EDM.INT32")]
+ public void IsOfFunctionWorksWithoutSingleQuotesOnPrimitiveType_ODataUriParserCaseInsentive(string queryFilter, string expectedConstantNodeValue)
+ {
+ FilterClause filter = ParseFilterODataUriParserCaseInsensitive($"/people?$filter={queryFilter}", HardCodedTestModel.TestModel);
+ var singleValueFunctionCallNode = filter.Expression.ShouldBeSingleValueFunctionCallQueryNode("isof");
+ singleValueFunctionCallNode.Parameters.ElementAt(0).ShouldBeSingleValuePropertyAccessQueryNode(HardCodedTestModel.GetPersonIdProp());
+ singleValueFunctionCallNode.Parameters.ElementAt(1).ShouldBeConstantQueryNode(expectedConstantNodeValue);
+ }
+
+ [Theory]
+ [InlineData("isof(Fully.Qualified.Namespace.Employee)")]
+ [InlineData("isof('Fully.Qualified.Namespace.Employee')")]
+ public void IsOfFunctionWithOneParameter_WithOrWithoutSingleQuotesOnTypeParameter_WorksAsExpected(string filterQuery)
+ {
+ // Arrange & Act
+ FilterClause filter = ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet());
+
+ // Assert
+ SingleValueFunctionCallNode singleValueFunctionCallNode = filter.Expression.ShouldBeSingleValueFunctionCallQueryNode("isof");
+ ResourceRangeVariableReferenceNode rangeVariableReference = singleValueFunctionCallNode.Parameters.ElementAt(0).ShouldBeResourceRangeVariableReferenceNode("$it");
+ Assert.Equal("Fully.Qualified.Namespace.Person", rangeVariableReference.GetEdmTypeReference().FullName()); // $it is of type Person
+
+ var secondParameter = singleValueFunctionCallNode.Parameters.ElementAt(1);
+ string fullyQualifiedTypeName = secondParameter switch
+ {
+ SingleResourceCastNode singleResourceCastNode => secondParameter.ShouldBeSingleResourceCastNode(HardCodedTestModel.GetEmployeeTypeReference()).TypeReference.FullName(),
+ ConstantNode constantNode => secondParameter.ShouldBeConstantQueryNode("Fully.Qualified.Namespace.Employee").Value as string,
+ _ => throw new InvalidOperationException("Unexpected node type")
+ };
+
+ Assert.Equal("Fully.Qualified.Namespace.Employee", fullyQualifiedTypeName);
+ }
+
+ [Theory]
+ [InlineData("isof(Fully.Qualified.Namespace.Employee)")]
+ [InlineData("isof(fully.Qualified.namespace.employee)")]
+ [InlineData("isof(FULLY.QUALIFIED.NAMESPACE.EMPLOYEE)")]
+ public void IsOfFunctionWithOneParameter_WithoutSingleQuotesOnTypeParameter_ShouldBeSingleResourceCastNode_CaseInsensitive(string filterQuery)
+ {
+ // Arrange
+ // Act
+ FilterClause filter = ParseFilterODataUriParserCaseInsensitive($"/people?$filter={filterQuery}", HardCodedTestModel.TestModel);
+
+ // Assert
+ SingleValueFunctionCallNode singleValueFunctionCallNode = filter.Expression.ShouldBeSingleValueFunctionCallQueryNode("isof");
+ ResourceRangeVariableReferenceNode rangeVariableReference = singleValueFunctionCallNode.Parameters.ElementAt(0).ShouldBeResourceRangeVariableReferenceNode("$it");
+ Assert.Equal("Fully.Qualified.Namespace.Person", rangeVariableReference.GetEdmTypeReference().FullName()); // $it is of type Person
+
+ var singleResourceCastNode = singleValueFunctionCallNode.Parameters.ElementAt(1).ShouldBeSingleResourceCastNode(HardCodedTestModel.GetEmployeeTypeReference());
+ Assert.Equal("Fully.Qualified.Namespace.Employee", singleResourceCastNode.TypeReference.FullName()); // Fully.Qualified.Namespace.Employee is the type parameter
+ }
+
+ [Theory]
+ [InlineData("isof(MyAddress, 'Fully.Qualified.Namespace.HomeAddress')")]
+ [InlineData("isof(MyAddress, Fully.Qualified.Namespace.HomeAddress)")]
+ public void IsOfFunctionWithTwoParameters_WithSingleQuotesOnTypeParameter_WorksAsExpected(string filterQuery)
+ {
+ // Arrange & Act
+ FilterClause filter = ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet());
+
+ // Assert
+ SingleValueFunctionCallNode singleValueFunctionCallNode = filter.Expression.ShouldBeSingleValueFunctionCallQueryNode("isof");
+ SingleComplexNode singleComplexNode = singleValueFunctionCallNode.Parameters.ElementAt(0).ShouldBeSingleComplexNode(HardCodedTestModel.GetPersonAddressProp());
+ Assert.Equal("MyAddress", singleComplexNode.Property.Name); // MyAddress is the property name
+ Assert.Equal("Fully.Qualified.Namespace.Address", singleComplexNode.GetEdmTypeReference().FullName()); // MyAddress is of type Address
+
+ var secondParameter = singleValueFunctionCallNode.Parameters.ElementAt(1);
+ string fullyQualifiedTypeName = secondParameter switch
+ {
+ SingleResourceCastNode singleResourceCastNode => secondParameter.ShouldBeSingleResourceCastNode(HardCodedTestModel.GetHomeAddressReference()).TypeReference.FullName(),
+ ConstantNode constantNode => secondParameter.ShouldBeConstantQueryNode("Fully.Qualified.Namespace.HomeAddress").Value as string,
+ _ => throw new InvalidOperationException("Unexpected node type")
+ };
+
+ Assert.Equal("Fully.Qualified.Namespace.HomeAddress", fullyQualifiedTypeName);
+ }
+
+ [Theory]
+ [InlineData("isof(MyAddress, Fully.Qualified.Namespace.HomeAddress)")]
+ [InlineData("isof(MyAddress, fully.Qualified.namespace.Homeaddress)")]
+ [InlineData("isof(MyAddress, FULLY.Qualified.Namespace.HOMEAddress)")]
+ public void IsOfFunctionWithTwoParameters_WithoutSingleQuotesOnTypeParameter_ShouldBeSingleResourceCastNode_CaseInsensitive(string filterQuery)
+ {
+ // Arrange
+ // Act
+ FilterClause filter = ParseFilterODataUriParserCaseInsensitive($"/people?$filter={filterQuery}", HardCodedTestModel.TestModel);
+
+ // Assert
+ SingleValueFunctionCallNode singleValueFunctionCallNode = filter.Expression.ShouldBeSingleValueFunctionCallQueryNode("isof");
+ SingleComplexNode singleComplexNode = singleValueFunctionCallNode.Parameters.ElementAt(0).ShouldBeSingleComplexNode(HardCodedTestModel.GetPersonAddressProp());
+ Assert.Equal("MyAddress", singleComplexNode.Property.Name); // MyAddress is the property name
+ Assert.Equal("Fully.Qualified.Namespace.Address", singleComplexNode.GetEdmTypeReference().FullName()); // MyAddress is of type Address
+
+ var singleResourceCastNode = singleValueFunctionCallNode.Parameters.ElementAt(1).ShouldBeSingleResourceCastNode(HardCodedTestModel.GetHomeAddressReference());
+ Assert.Equal("Fully.Qualified.Namespace.HomeAddress", singleResourceCastNode.TypeReference.FullName()); // Fully.Qualified.Namespace.HomeAddress is the type parameter
+ }
+
+ [Theory]
+ [InlineData("isof(Fully.Qualified.Namespace.Pet1)", "'Fully.Qualified.Namespace.Pet1' is not assignable from 'Fully.Qualified.Namespace.Person'.")]
+ [InlineData("isof(MyAddress,Fully.Qualified.Namespace.Pet1)", "'Fully.Qualified.Namespace.Pet1' is not assignable from 'Fully.Qualified.Namespace.Address'.")]
+ [InlineData("isof(null,Fully.Qualified.Namespace.Person)", "'Fully.Qualified.Namespace.Person' is not assignable from ''.")]
+ [InlineData("isof('',Fully.Qualified.Namespace.Person)", "'Fully.Qualified.Namespace.Person' is not assignable from 'Edm.String'.")]
+ public void IsOfFunctionsWithUnquotedTypeParameter_WithIncorrectType_ThrowException(string filterQuery, string errorMessage)
+ {
+ // Arrange & Act
+ var exception = Record.Exception(() => ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet()));
+
+ // Assert
+ Assert.NotNull(exception);
+ Assert.IsType(exception);
+ Assert.Equal($"Encountered invalid type cast. {errorMessage}", exception.Message);
+ }
+
+ [Theory]
+ [InlineData("cast(Fully.Qualified.Namespace.HomeAddress)/City eq 'City1'", "'Fully.Qualified.Namespace.HomeAddress' is not assignable from 'Fully.Qualified.Namespace.Person'.")]
+ [InlineData("cast(MyAddress,Fully.Qualified.Namespace.Employee)/WorkID eq 345", "'Fully.Qualified.Namespace.Employee' is not assignable from 'Fully.Qualified.Namespace.Address'.")]
+ [InlineData("cast(null,Fully.Qualified.Namespace.Employee)/WorkID eq 345", "'Fully.Qualified.Namespace.Employee' is not assignable from ''.")]
+ [InlineData("cast('',Fully.Qualified.Namespace.Employee)/WorkID eq 345", "'Fully.Qualified.Namespace.Employee' is not assignable from 'Edm.String'.")]
+ public void CastFunctionWithUnquotedTypeParameter_WithIncorrectType_ThrowException(string filterQuery, string errorMessage)
+ {
+ // Arrange & Act
+ var exception = Record.Exception(() => ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet()));
+
+ // Assert
+ Assert.NotNull(exception);
+ Assert.IsType(exception);
+ Assert.Equal($"Encountered invalid type cast. {errorMessage}", exception.Message);
+ }
+
+ [Theory]
+ [InlineData("isof(Fully.Qualified.Namespace.Pet1)", "Fully.Qualified.Namespace.Pet1")]
+ [InlineData("cast(Fully.Qualified.Namespace.HomeAddress)/City eq 'City1'", "Fully.Qualified.Namespace.HomeAddress")]
+ public void IsOfAndCastFunctionsWithSingleParameterWithoutSingleQuotes_WithIncorrectType_ThrowException(string filterQuery, string fullyQualifiedTypeName)
+ {
+ // Arrange & Act
+ Action test = () => ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet());
+
+ // Assert
+ test.Throws(Error.Format(SRResources.MetadataBinder_HierarchyNotFollowed, fullyQualifiedTypeName, "Fully.Qualified.Namespace.Person"));
+ }
+
+ [Theory]
+ [InlineData("isof(MyAddress,Fully.Qualified.Namespace.Pet1)", "Fully.Qualified.Namespace.Pet1")]
+ [InlineData("cast(MyAddress,Fully.Qualified.Namespace.Employee)/WorkID eq 345", "Fully.Qualified.Namespace.Employee")]
+ public void IsOfAndCastFunctionsWithTwoParameterWhereTypeParameterIsWithoutSingleQuotes_WithIncorrectType_ThrowException(string filterQuery, string fullyQualifiedTypeName)
+ {
+ // Arrange & Act
+ Action test = () => ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet());
+
+ // Assert
+ test.Throws(Error.Format(SRResources.MetadataBinder_HierarchyNotFollowed, fullyQualifiedTypeName, "Fully.Qualified.Namespace.Address"));
+ }
+
[Fact]
public void CastFunctionWorksWithNoSingleQuotesOnType()
{
@@ -772,6 +1026,39 @@ public void CastFunctionWorksWithNoSingleQuotesOnType()
bon.Right.ShouldBeConstantQueryNode("blue");
}
+ [Theory]
+ [InlineData("cast(Shoe, edm.string) eq 'blue'", "edm.string")]
+ [InlineData("cast(Shoe, Edm.string) eq 'blue'", "Edm.string")]
+ [InlineData("cast(Shoe, edm.String) eq 'blue'", "edm.String")]
+ [InlineData("cast(Shoe, Edm.String) eq 'blue'", "Edm.String")]
+ [InlineData("cast(Shoe, EDM.STRING) eq 'blue'", "EDM.STRING")]
+ public void CastFunctionWorksWithNoSingleQuotesOnTypeWithODataUriParserCaseInsensitive(string filterQuery, string expectedConstantQueryNode)
+ {
+ FilterClause filter = ParseFilterODataUriParserCaseInsensitive($"/people?$filter={filterQuery}", HardCodedTestModel.TestModel);
+ var bon = filter.Expression.ShouldBeBinaryOperatorNode(BinaryOperatorKind.Equal);
+ var convertQueryNode = bon.Left.ShouldBeConvertQueryNode(EdmPrimitiveTypeKind.String);
+ var singleFunctionCallNode = convertQueryNode.Source.ShouldBeSingleValueFunctionCallQueryNode("cast");
+ singleFunctionCallNode.Parameters.ElementAt(0).ShouldBeSingleValuePropertyAccessQueryNode(HardCodedTestModel.GetPersonShoeProp());
+ singleFunctionCallNode.Parameters.ElementAt(1).ShouldBeConstantQueryNode(expectedConstantQueryNode);
+ bon.Right.ShouldBeConstantQueryNode("blue");
+ }
+
+ [Theory]
+ [InlineData("cast(ID, edm.int32) lt 10", "edm.int32")]
+ [InlineData("cast(ID, Edm.int32) lt 10", "Edm.int32")]
+ [InlineData("cast(ID, edm.Int32) lt 10", "edm.Int32")]
+ [InlineData("cast(ID, Edm.Int32) lt 10", "Edm.Int32")]
+ [InlineData("cast(ID, EDM.INT32) lt 10", "EDM.INT32")]
+ public void CastFunctionWorksWithNoSingleQuotesOnTypeWithCaseInsensitive(string filterQuery, string expectedConstantQueryNode)
+ {
+ FilterClause filter = ParseFilter(filterQuery, true, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet());
+ var bon = filter.Expression.ShouldBeBinaryOperatorNode(BinaryOperatorKind.LessThan);
+ var singleFunctionCallNode = bon.Left.ShouldBeSingleValueFunctionCallQueryNode("cast");
+ singleFunctionCallNode.Parameters.ElementAt(0).ShouldBeSingleValuePropertyAccessQueryNode(HardCodedTestModel.GetPersonIdProp());
+ singleFunctionCallNode.Parameters.ElementAt(1).ShouldBeConstantQueryNode(expectedConstantQueryNode);
+ bon.Right.ShouldBeConstantQueryNode(10);
+ }
+
[Fact]
public void CastFunctionWorksForEnum()
{
@@ -783,14 +1070,26 @@ public void CastFunctionWorksForEnum()
bon.Right.ShouldBeEnumNode(HardCodedTestModel.TestModel.FindType("Fully.Qualified.Namespace.ColorPattern") as IEdmEnumType, 2L);
}
- [Fact]
- public void CastFunctionWorksForCastFromNullToEnum()
+ [Theory]
+ [InlineData("cast(null, Fully.Qualified.Namespace.ColorPattern) eq Fully.Qualified.Namespace.ColorPattern'blue'")]
+ [InlineData("cast(null, 'Fully.Qualified.Namespace.ColorPattern') eq Fully.Qualified.Namespace.ColorPattern'blue'")]
+ public void CastFunctionWorksForCastFromNullToEnum(string filterQuery)
{
- FilterClause filter = ParseFilter("cast(null, Fully.Qualified.Namespace.ColorPattern) eq Fully.Qualified.Namespace.ColorPattern'blue'", HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet());
+ FilterClause filter = ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet());
var bon = filter.Expression.ShouldBeBinaryOperatorNode(BinaryOperatorKind.Equal);
var singleFunctionCallNode = bon.Left.ShouldBeSingleValueFunctionCallQueryNode("cast");
Assert.Null(Assert.IsType(singleFunctionCallNode.Parameters.ElementAt(0)).Value);
- singleFunctionCallNode.Parameters.ElementAt(1).ShouldBeConstantQueryNode("Fully.Qualified.Namespace.ColorPattern");
+
+ QueryNode secondParameterNode = singleFunctionCallNode.Parameters.ElementAt(1);
+ string fullyQualifiedTypeName = secondParameterNode switch
+ {
+ SingleResourceCastNode singleResourceCastNode => singleResourceCastNode.TypeReference.FullName(),
+ ConstantNode constantNode => constantNode.Value as string,
+ _ => throw new InvalidOperationException("Unexpected node type")
+ };
+
+ Assert.Equal("Fully.Qualified.Namespace.ColorPattern", fullyQualifiedTypeName);
+ Assert.Equal("Fully.Qualified.Namespace.ColorPattern'blue'", Assert.IsType(bon.Right).LiteralText);
bon.Right.ShouldBeEnumNode(HardCodedTestModel.TestModel.FindType("Fully.Qualified.Namespace.ColorPattern") as IEdmEnumType, 2L);
}
@@ -805,19 +1104,153 @@ public void LiteralTextShouldNeverBeNullForConstantNodeOfDottedIdentifier()
Assert.Equal("Fully.Qualified.Namespace.ColorPattern'blue'", Assert.IsType(bon.Right).LiteralText);
}
- [Fact]
- public void CastFunctionProducesAnEntityType()
+ [Theory]
+ [InlineData("cast(MyDog, 'Fully.Qualified.Namespace.Dog')/Color eq 'blue'")]
+ [InlineData("cast(MyDog, Fully.Qualified.Namespace.Dog)/Color eq 'blue'")]
+ public void CastFunctionProducesAnEntityType(string filterQuery)
{
- FilterClause filter = ParseFilter("cast(MyDog, 'Fully.Qualified.Namespace.Dog')/Color eq 'blue'", HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet());
+ FilterClause filter = ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet());
+ SingleResourceFunctionCallNode function = filter.Expression.ShouldBeBinaryOperatorNode(BinaryOperatorKind.Equal)
+ .Left.ShouldBeSingleValuePropertyAccessQueryNode(HardCodedTestModel.GetDogColorProp())
+ .Source.ShouldBeSingleResourceFunctionCallNode("cast");
+ Assert.Equal(2, function.Parameters.Count());
+ function.Parameters.ElementAt(0).ShouldBeSingleNavigationNode(HardCodedTestModel.GetPersonMyDogNavProp());
+
+ var secondParameter = function.Parameters.ElementAt(1);
+ string fullyQualifiedTypeName = secondParameter switch
+ {
+ SingleResourceCastNode singleResourceCastNode => secondParameter.ShouldBeSingleResourceCastNode(HardCodedTestModel.GetDogTypeReference()).TypeReference.FullName(),
+ ConstantNode constantNode => secondParameter.ShouldBeConstantQueryNode("Fully.Qualified.Namespace.Dog").Value as string,
+ _ => throw new InvalidOperationException("Unexpected node type")
+ };
+
+ Assert.Equal("Fully.Qualified.Namespace.Dog", fullyQualifiedTypeName);
+ Assert.IsType(filter.Expression).Right.ShouldBeConstantQueryNode("blue");
+ }
+
+ [Theory]
+ [InlineData("cast(MyDog, fully.Qualified.Namespace.Dog)/Color eq 'blue'")]
+ [InlineData("cast(MyDog, fully.qualified.namespace.dog)/Color eq 'blue'")]
+ [InlineData("cast(MyDog, fully.Qualified.namespace.dog)/Color eq 'blue'")]
+ [InlineData("cast(MyDog, fully.qualified.Namespace.dog)/Color eq 'blue'")]
+ [InlineData("cast(MyDog, fully.qualified.namespace.Dog)/Color eq 'blue'")]
+ [InlineData("cast(MyDog, FULLY.QUALIFIED.NAMESPACE.DOG)/Color eq 'blue'")]
+ public void CastFunctionProducesAnEntityTypeWorksWithCaseInsensitiveODataUriParser(string filterQuery)
+ {
+ FilterClause filter = ParseFilter(filterQuery, true, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet());
SingleResourceFunctionCallNode function = filter.Expression.ShouldBeBinaryOperatorNode(BinaryOperatorKind.Equal)
.Left.ShouldBeSingleValuePropertyAccessQueryNode(HardCodedTestModel.GetDogColorProp())
.Source.ShouldBeSingleResourceFunctionCallNode("cast");
Assert.Equal(2, function.Parameters.Count());
function.Parameters.ElementAt(0).ShouldBeSingleNavigationNode(HardCodedTestModel.GetPersonMyDogNavProp());
- function.Parameters.ElementAt(1).ShouldBeConstantQueryNode("Fully.Qualified.Namespace.Dog");
+ var singleResourceCastNode = function.Parameters.ElementAt(1).ShouldBeSingleCastNode(HardCodedTestModel.GetDogTypeReference());
+ Assert.Equal("Fully.Qualified.Namespace.Dog", singleResourceCastNode.TypeReference.FullName());
Assert.IsType(filter.Expression).Right.ShouldBeConstantQueryNode("blue");
}
+ [Theory]
+ [InlineData("cast(MyDog, fully.Qualified.Namespace.Dog)/Color eq 'blue'")]
+ [InlineData("cast(MyDog, fully.qualified.namespace.dog)/Color eq 'blue'")]
+ [InlineData("cast(MyDog, fully.Qualified.namespace.dog)/Color eq 'blue'")]
+ [InlineData("cast(MyDog, fully.qualified.Namespace.dog)/Color eq 'blue'")]
+ [InlineData("cast(MyDog, fully.qualified.namespace.Dog)/Color eq 'blue'")]
+ public void CastFunctionProducesAnEntityTypeWorksWithCaseInsensitive(string filterQuery)
+ {
+ FilterClause filter = ParseFilterODataUriParserCaseInsensitive($"/people?$filter={filterQuery}", HardCodedTestModel.TestModel);
+ SingleResourceFunctionCallNode function = filter.Expression.ShouldBeBinaryOperatorNode(BinaryOperatorKind.Equal)
+ .Left.ShouldBeSingleValuePropertyAccessQueryNode(HardCodedTestModel.GetDogColorProp())
+ .Source.ShouldBeSingleResourceFunctionCallNode("cast");
+ Assert.Equal(2, function.Parameters.Count());
+ function.Parameters.ElementAt(0).ShouldBeSingleNavigationNode(HardCodedTestModel.GetPersonMyDogNavProp());
+ var singleResourceCastNode = function.Parameters.ElementAt(1).ShouldBeSingleCastNode(HardCodedTestModel.GetDogTypeReference());
+ Assert.Equal("Fully.Qualified.Namespace.Dog", singleResourceCastNode.TypeReference.FullName());
+ Assert.IsType(filter.Expression).Right.ShouldBeConstantQueryNode("blue");
+ }
+
+ [Theory]
+ [InlineData("cast(Fully.Qualified.Namespace.Employee)/WorkID eq 345")]
+ [InlineData("cast('Fully.Qualified.Namespace.Employee')/WorkID eq 345")]
+ public void CastFunctionWithOneParameter_WithOrWithoutSingleParameterProducesAnEntityType(string filterQuery)
+ {
+ // Arrange & Act
+ FilterClause filter = ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet());
+
+ // Assert
+ SingleResourceFunctionCallNode function = filter.Expression.ShouldBeBinaryOperatorNode(BinaryOperatorKind.Equal)
+ .Left.ShouldBeSingleValuePropertyAccessQueryNode(HardCodedTestModel.GetEmployeeWorkIDProperty())
+ .Source.ShouldBeSingleResourceFunctionCallNode("cast");
+
+ Assert.Equal(2, function.Parameters.Count());
+
+ ResourceRangeVariableReferenceNode resourceRangeVariable = function.Parameters.ElementAt(0).ShouldBeResourceRangeVariableReferenceNode("$it");
+ Assert.Equal("Fully.Qualified.Namespace.Person", resourceRangeVariable.GetEdmTypeReference().FullName()); // $it is of type Person
+
+ QueryNode node = function.Parameters.ElementAt(1);
+ string fullyQualifiedTypeName = node switch
+ {
+ SingleResourceCastNode singleResourceCastNode => node.ShouldBeSingleResourceCastNode(HardCodedTestModel.GetEmployeeTypeReference()).TypeReference.FullName(),
+ ConstantNode constantNode => node.ShouldBeConstantQueryNode("Fully.Qualified.Namespace.Employee").Value as string,
+ _ => throw new InvalidOperationException("Unexpected node type")
+ };
+
+ Assert.Equal("Fully.Qualified.Namespace.Employee", fullyQualifiedTypeName); // Fully.Qualified.Namespace.Employee is the type parameter
+ Assert.IsType(filter.Expression).Right.ShouldBeConstantQueryNode(345); // 345 is the right operand
+ }
+
+ [Theory]
+ [InlineData("cast(MyAddress, 'Fully.Qualified.Namespace.HomeAddress')/HomeNO eq 'H-1234'")]
+ [InlineData("cast(MyAddress, Fully.Qualified.Namespace.HomeAddress)/HomeNO eq 'H-1234'")]
+ public void CastFunctionWithTwoParameters_WithOrWithSingleParameterProducesAnEntityType(string filterQuery)
+ {
+ // Arrange & Act
+ FilterClause filter = ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet());
+
+ // Assert
+ SingleResourceFunctionCallNode function = filter.Expression.ShouldBeBinaryOperatorNode(BinaryOperatorKind.Equal)
+ .Left.ShouldBeSingleValuePropertyAccessQueryNode(HardCodedTestModel.GetAddressHomeNOProperty())
+ .Source.ShouldBeSingleResourceFunctionCallNode("cast");
+ Assert.Equal(2, function.Parameters.Count()); // There are two parameters
+
+ SingleComplexNode complexNode = function.Parameters.ElementAt(0).ShouldBeSingleComplexNode(HardCodedTestModel.GetPersonAddressProp());
+ Assert.Equal("Fully.Qualified.Namespace.Address", complexNode.GetEdmTypeReference().FullName()); // MyAddress is of type Address
+
+ QueryNode node = function.Parameters.ElementAt(1);
+ string fullyQualifiedTypeName = node switch
+ {
+ SingleResourceCastNode singleResourceCastNode => node.ShouldBeSingleResourceCastNode(HardCodedTestModel.GetHomeAddressReference()).TypeReference.FullName(),
+ ConstantNode constantNode => node.ShouldBeConstantQueryNode("Fully.Qualified.Namespace.HomeAddress").Value as string,
+ _ => throw new InvalidOperationException("Unexpected node type")
+ };
+
+ Assert.Equal("Fully.Qualified.Namespace.HomeAddress", fullyQualifiedTypeName); // Fully.Qualified.Namespace.HomeAddress is the type parameter
+ Assert.IsType(filter.Expression).Right.ShouldBeConstantQueryNode("H-1234");
+ }
+
+ [Theory]
+ [InlineData("cast(MyAddress/AddressType, Edm.String) eq '2'")] // AddressType is Enum
+ [InlineData("cast(MyAddress/AddressType, 'Edm.String') eq '2'")] // AddressType is Enum
+ public void CastFunctionWithTwoParametersWithBuiltInTypeProducesAnEntityType(string filterQuery)
+ {
+ // Arrange
+ string expectedAddressType = "Fully.Qualified.Namespace.AddressType";
+
+ // Act
+ FilterClause filter = ParseFilter(filterQuery, HardCodedTestModel.TestModel, HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet());
+
+ // Assert
+ SingleValueFunctionCallNode function = filter.Expression.ShouldBeBinaryOperatorNode(BinaryOperatorKind.Equal)
+ .Left.ShouldBeConvertQueryNode(EdmCoreModel.Instance.GetString(true))
+ .Source.ShouldBeSingleValueFunctionCallQueryNode("cast");
+
+ Assert.Equal(2, function.Parameters.Count());
+
+ SingleValuePropertyAccessNode singleValuePropertyAccessNode = function.Parameters.ElementAt(0).ShouldBeSingleValuePropertyAccessQueryNode(HardCodedTestModel.GetMyAddressAddressTypeProperty());
+ Assert.Equal(expectedAddressType, singleValuePropertyAccessNode.GetEdmTypeReference().FullName());
+
+ function.Parameters.ElementAt(1).ShouldBeConstantQueryNode("Edm.String");
+ Assert.IsType(filter.Expression).Right.ShouldBeConstantQueryNode("2");
+ }
+
[Fact]
public void OrderByWithExpression()
{
@@ -1299,6 +1732,23 @@ public void ComputedPropertyTreatedAsOpenPropertyInOrderBy()
orderByClause.Expression.ShouldBeSingleValueOpenPropertyAccessQueryNode("DoubleTotal");
}
+ [Fact]
+ public void ComputedPropertyTreatedAsOpenPropertyInCastAndOrderBy()
+ {
+ var odataQueryOptionParser = new ODataQueryOptionParser(HardCodedTestModel.TestModel,
+ HardCodedTestModel.GetPersonType(), HardCodedTestModel.GetPeopleSet(),
+ new Dictionary()
+ {
+ {"$orderby", "DoubleTotal asc"},
+ {"$apply", "aggregate(cast(FavoriteNumber, edm.int64) with sum as Total)/compute(Total mul 2 as DoubleTotal)"}
+ })
+ { Resolver = new ODataUriResolver() { EnableCaseInsensitive = true } };
+ var applyClause = odataQueryOptionParser.ParseApply();
+ var orderByClause = odataQueryOptionParser.ParseOrderBy();
+ Assert.Equal(OrderByDirection.Ascending, orderByClause.Direction);
+ orderByClause.Expression.ShouldBeSingleValueOpenPropertyAccessQueryNode("DoubleTotal");
+ }
+
[Fact]
public void DollarComputedPropertyTreatedAsOpenPropertyInOrderBy()
{
@@ -3192,5 +3642,30 @@ private static OrderByClause ParseOrderBy(string text, IEdmModel edmModel, IEdmT
{
return new ODataQueryOptionParser(edmModel, edmType, edmEntitySet, new Dictionary() { { "$orderby", text } }).ParseOrderBy();
}
+
+ private static FilterClause ParseFilter(string text, bool caseInsensitive, IEdmModel edmModel, IEdmType edmType, IEdmNavigationSource edmEntitySet = null)
+ {
+ return new ODataQueryOptionParser(edmModel,
+ edmType,
+ edmEntitySet,
+ new Dictionary() { { "$filter", text } })
+ { Resolver = new ODataUriResolver() { EnableCaseInsensitive = caseInsensitive } }
+ .ParseFilter();
+ }
+
+ private static FilterClause ParseFilterODataUriParserCaseInsensitive(string text, IEdmModel edmModel)
+ {
+ var parser = new ODataUriParser(edmModel, new Uri(text, UriKind.Relative))
+ {
+ Resolver = new UnqualifiedODataUriResolver()
+ {
+ EnableCaseInsensitive = true,
+ },
+ UrlKeyDelimiter = ODataUrlKeyDelimiter.Slash,
+ };
+ parser.Settings.MaximumExpansionDepth = 2;
+ parser.ParsePath();
+ return parser.ParseFilter();
+ }
}
}
diff --git a/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/HardCodedTestModel.cs b/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/HardCodedTestModel.cs
index fa021b4df8..d50df5b326 100644
--- a/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/HardCodedTestModel.cs
+++ b/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/HardCodedTestModel.cs
@@ -77,6 +77,13 @@ internal static IEdmModel GetEdmModel()
NonFlagShapeType.AddMember("Triangle", new EdmEnumMemberValue(2));
NonFlagShapeType.AddMember("foursquare", new EdmEnumMemberValue(3));
model.AddElement(NonFlagShapeType);
+
+ var addressTypeEnum = new EdmEnumType("Fully.Qualified.Namespace", "AddressType");
+ addressTypeEnum.AddMember(new EdmEnumMember(addressTypeEnum, "Home", new EdmEnumMemberValue(0)));
+ addressTypeEnum.AddMember(new EdmEnumMember(addressTypeEnum, "Work", new EdmEnumMemberValue(1)));
+ addressTypeEnum.AddMember(new EdmEnumMember(addressTypeEnum, "Other", new EdmEnumMemberValue(2)));
+ model.AddElement(addressTypeEnum);
+ var addressTypeEnumReference = new EdmEnumTypeReference(addressTypeEnum, false);
#endregion
#region Structured Types
@@ -350,6 +357,7 @@ internal static IEdmModel GetEdmModel()
FullyQualifiedNamespaceAddress.AddStructuralProperty("City", EdmCoreModel.Instance.GetString(true));
FullyQualifiedNamespaceAddress.AddStructuralProperty("NextHome", FullyQualifiedNamespaceAddressTypeReference);
FullyQualifiedNamespaceAddress.AddStructuralProperty("MyNeighbors", new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetString(true))));
+ FullyQualifiedNamespaceAddress.AddStructuralProperty("AddressType", addressTypeEnumReference);
FullyQualifiedNamespaceAddress.AddUnidirectionalNavigation(new EdmNavigationPropertyInfo { Name = "PostBoxPainting", TargetMultiplicity = EdmMultiplicity.ZeroOrOne, Target = FullyQualifiedNamespacePainting });
model.AddElement(FullyQualifiedNamespaceAddress);
@@ -978,6 +986,11 @@ internal class HardCodedTestModelXml
+
+
+
+
+
@@ -1180,6 +1193,7 @@ internal class HardCodedTestModelXml
+
@@ -1770,6 +1784,16 @@ public static IEdmEntityTypeReference GetDogTypeReference()
return new EdmEntityTypeReference(GetDogType(), false);
}
+ public static IEdmComplexType GetColorPatternType()
+ {
+ return TestModel.FindType("Fully.Qualified.Namespace.ColorPattern") as IEdmComplexType;
+ }
+
+ public static IEdmComplexTypeReference GetColorPatternTypeReference()
+ {
+ return new EdmComplexTypeReference(GetColorPatternType(), false);
+ }
+
public static IEdmEntityType GetPaintingType()
{
return TestModel.FindType("Fully.Qualified.Namespace.Painting") as IEdmEntityType;
@@ -2009,6 +2033,16 @@ public static IEdmStructuralProperty GetAddressCityProperty()
return (IEdmStructuralProperty)((IEdmStructuredType)TestModel.FindType("Fully.Qualified.Namespace.Address")).FindProperty("City");
}
+ public static IEdmStructuralProperty GetMyAddressAddressTypeProperty()
+ {
+ return (IEdmStructuralProperty)((IEdmStructuredType)TestModel.FindType("Fully.Qualified.Namespace.Address")).FindProperty("AddressType");
+ }
+
+ public static IEdmStructuralProperty GetAddressHomeNOProperty()
+ {
+ return (IEdmStructuralProperty)((IEdmStructuredType)TestModel.FindType("Fully.Qualified.Namespace.HomeAddress")).FindProperty("HomeNO");
+ }
+
public static IEdmStructuralProperty GetPet2PetColorPatternProperty()
{
return (IEdmStructuralProperty)((IEdmStructuredType)TestModel.FindType("Fully.Qualified.Namespace.Pet2")).FindProperty("PetColorPattern");
@@ -2026,6 +2060,11 @@ public static IEdmNavigationProperty GetAddressMyFavoriteNeighborNavProp()
return (IEdmNavigationProperty)((IEdmStructuredType)TestModel.FindType("Fully.Qualified.Namespace.Address")).FindProperty("MyFavoriteNeighbor");
}
+ public static IEdmStructuralProperty GetEmployeeWorkIDProperty()
+ {
+ return (IEdmStructuralProperty)((IEdmStructuredType)TestModel.FindType("Fully.Qualified.Namespace.Employee")).FindProperty("WorkID");
+ }
+
public static IEdmNavigationProperty GetDogFastestOwnerNavProp()
{
return (IEdmNavigationProperty)((IEdmStructuredType)TestModel.FindType("Fully.Qualified.Namespace.Dog")).FindProperty("FastestOwner");
diff --git a/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/Parsers/FunctionCallParserTests.cs b/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/Parsers/FunctionCallParserTests.cs
index cadd95b4db..107fb0f0a7 100644
--- a/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/Parsers/FunctionCallParserTests.cs
+++ b/test/UnitTests/Microsoft.OData.Core.Tests/UriParser/Parsers/FunctionCallParserTests.cs
@@ -136,6 +136,126 @@ public void FunctionCallWithOnlyOpeningParenthesis()
parse.Throws(Error.Format(SRResources.UriQueryExpressionParser_ExpressionExpected, 5, "func("));
}
+ [Theory]
+ [InlineData("cast(Location, NS.HomeAddress)", "cast")]
+ [InlineData("isof(Location, NS.HomeAddress)", "isof")]
+ public void TryParseIdentifierAsFunction_CastAndIsOfFunctions_Works(string expression, string functionName)
+ {
+ // Arrange
+ var lexer = new ExpressionLexer(expression, true, false);
+ var parser = new UriQueryExpressionParser(345, lexer);
+ var functionCallParser = new FunctionCallParser(lexer, parser);
+
+ // Act
+ bool success = functionCallParser.TryParseIdentifierAsFunction(null, out QueryToken result);
+
+ // Assert
+ Assert.True(success);
+ Assert.NotNull(result);
+ Assert.Equal(QueryTokenKind.FunctionCall, result.Kind);
+ var functionCall = Assert.IsType(result);
+ Assert.Equal(functionName, functionCall.Name);
+ Assert.Equal(2, functionCall.Arguments.Count());
+
+ var secondArgumentValueToken = functionCall.Arguments.ElementAt(1).ValueToken as DottedIdentifierToken;
+ Assert.NotNull(secondArgumentValueToken);
+ Assert.Equal("NS.HomeAddress", secondArgumentValueToken.Identifier);
+ Assert.Equal("Location", (secondArgumentValueToken.NextToken as EndPathToken).Identifier);
+ }
+
+ [Theory]
+ [InlineData("cast(Location, 'NS.HomeAddress')", "cast")]
+ [InlineData("isof(Location, 'NS.HomeAddress')", "isof")]
+ public void TryParseIdentifierAsFunction_CastAndIsOfFunctions_WithQuotesParameters(string expression, string functionName)
+ {
+ // Arrange
+ var lexer = new ExpressionLexer(expression, true, false);
+ var parser = new UriQueryExpressionParser(345, lexer);
+ var functionCallParser = new FunctionCallParser(lexer, parser);
+
+ // Act
+ bool success = functionCallParser.TryParseIdentifierAsFunction(null, out QueryToken result);
+
+ // Assert
+ Assert.True(success);
+ Assert.NotNull(result);
+ Assert.Equal(QueryTokenKind.FunctionCall, result.Kind);
+ var functionCall = Assert.IsType(result);
+ Assert.Equal(functionName, functionCall.Name);
+ Assert.Equal(2, functionCall.Arguments.Count());
+
+ var secondArgumentValueToken = functionCall.Arguments.ElementAt(1).ValueToken as LiteralToken;
+ Assert.NotNull(secondArgumentValueToken);
+ Assert.Equal("NS.HomeAddress", secondArgumentValueToken.Value);
+
+ var firstArgumentEndPathToken = functionCall.Arguments.ElementAt(0).ValueToken as EndPathToken;
+ Assert.NotNull(firstArgumentEndPathToken);
+ Assert.Equal("Location", firstArgumentEndPathToken.Identifier);
+ }
+
+ [Theory]
+ [InlineData("cast(Location, NS.HomeAddress)", "cast")]
+ [InlineData("isof(Location, NS.HomeAddress)", "isof")]
+ public void ParseArgumentListOrEntityKeyList_CastAndIsOfFunctions(string expression, string expectedFunctionName)
+ {
+ // Arrange
+ var lexer = new ExpressionLexer(expression, true, false);
+ var parser = new UriQueryExpressionParser(345, lexer);
+ var functionCallParser = new FunctionCallParser(lexer, parser);
+
+ var functionName = lexer.CurrentToken.Span;
+
+ // Move to the open paren
+ lexer.NextToken();
+
+ // Act
+ var arguments = functionCallParser.ParseArgumentListOrEntityKeyList(null, functionName);
+
+ // Assert
+ Assert.Equal(expectedFunctionName, functionName.ToString());
+ Assert.NotNull(arguments);
+ Assert.Equal(2, arguments.Length);
+ Assert.All(arguments, arg => Assert.IsType(arg));
+
+ var secondArgumentValueToken = arguments.ElementAt(1).ValueToken as DottedIdentifierToken;
+ Assert.NotNull(secondArgumentValueToken);
+ Assert.Equal("NS.HomeAddress", secondArgumentValueToken.Identifier);
+ Assert.Equal("Location", (secondArgumentValueToken.NextToken as EndPathToken).Identifier);
+ }
+
+ [Theory]
+ [InlineData("cast(Location, 'NS.HomeAddress')", "cast")]
+ [InlineData("isof(Location, 'NS.HomeAddress')", "isof")]
+ public void ParseArgumentListOrEntityKeyList_CastAndIsOfFunctions_WithQuotesParameters(string expression, string expectedFunctionName)
+ {
+ // Arrange
+ var lexer = new ExpressionLexer(expression, true, false);
+ var parser = new UriQueryExpressionParser(345, lexer);
+ var functionCallParser = new FunctionCallParser(lexer, parser);
+
+ var functionName = lexer.CurrentToken.Span;
+
+ // Move to the open paren
+ lexer.NextToken();
+
+ // Act
+ var arguments = functionCallParser.ParseArgumentListOrEntityKeyList(null, functionName);
+
+ // Assert
+ Assert.Equal(expectedFunctionName, functionName.ToString());
+ Assert.NotNull(arguments);
+ Assert.Equal(2, arguments.Length);
+ Assert.All(arguments, arg => Assert.IsType(arg));
+
+ var secondArgumentValueToken = arguments.ElementAt(1).ValueToken as LiteralToken;
+ Assert.NotNull(secondArgumentValueToken);
+ Assert.Equal("NS.HomeAddress", secondArgumentValueToken.Value);
+
+ var firstArgumentEndPathToken = arguments.ElementAt(0).ValueToken as EndPathToken;
+ Assert.NotNull(firstArgumentEndPathToken);
+ Assert.Equal("Location", firstArgumentEndPathToken.Identifier);
+ }
+
private static FunctionCallParser GetFunctionCallParser(string expression)
{
ExpressionLexer lexer = new ExpressionLexer(expression, true, false);