diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index b17c58604..33e369550 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -1217,13 +1217,6 @@ out: the element type of the collection. True if the type is an enumeration; false otherwise. - - - Determines whether the specified represents an type. - - The to evaluate. - True if the type is an enumeration; false otherwise. - Check whether the given type is a primitive type or known type. @@ -1231,6 +1224,13 @@ The type to validate. True if type is primitive or known type, otherwise False. + + + Determines whether the specified represents an type. + + The to evaluate. + True if the type is an enumeration; false otherwise. + Returns type of T if the type implements IEnumerable of T, otherwise, return null. @@ -10561,6 +10561,32 @@ The query binder context. The LINQ created. + + + Binds a to create a LINQ that + represents the semantics of the . + + The query node to bind. + The query binder context. + The LINQ created. + + A represents an inline JSON object literal (e.g. {"Name":"John"}) + introduced in OData Library (ODL) 9.0. When the node has a valid (non-untyped) expected structured type, + it is materialized into a CLR instance of that type using a member-initialization expression, binding each + property value recursively. When the expected type is missing or untyped (e.g. a literal against an open + or untyped property), the node is materialized into a keyed by + property name. + + + + + Materializes an untyped into a + keyed by property name, binding each property value recursively. + + The query node to bind. + The query binder context. + The LINQ that builds the dictionary. + Creates an from the . @@ -12469,6 +12495,13 @@ The node to be translated. The original node. + + + Translate a ResourceConstantNode. + + The node to be translated. + The original node. + Translate a ConvertNode. @@ -13719,6 +13752,29 @@ The constant node to validate. The validation context. + + + Override this method to restrict the resource (inline JSON object) literal inside the filter query. + + + A represents an inline resource literal (e.g. {"Name":"John"}) + introduced in OData Library (ODL) 9.0. This method recursively validates each of its property value nodes. + + The resource constant node to validate. + The validator context. + + + + Override this method to restrict the collection (inline JSON array) literal inside the filter query. + + + This method recursively validates each item node of a . As of + OData Library (ODL) 9.0 those items may include and nested + instances. + + The collection constant node to validate. + The validator context. + Override this method to restrict the 'cast' inside the filter query. diff --git a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt index 433cc0744..80f725dac 100644 --- a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt +++ b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt @@ -35,3 +35,6 @@ Microsoft.AspNetCore.OData.Query.Validator.FilterValidatorContext.EnterFunctionC Microsoft.AspNetCore.OData.Query.Validator.FilterValidatorContext.ExitFunctionCall() -> void Microsoft.AspNetCore.OData.Query.Validator.ODataValidationSettings.MaxFunctionCallDepth.get -> int Microsoft.AspNetCore.OData.Query.Validator.ODataValidationSettings.MaxFunctionCallDepth.set -> void +virtual Microsoft.AspNetCore.OData.Query.Expressions.QueryBinder.BindResourceConstantNode(Microsoft.OData.UriParser.ResourceConstantNode node, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) -> System.Linq.Expressions.Expression +virtual Microsoft.AspNetCore.OData.Query.Validator.FilterQueryValidator.ValidateResourceConstantNode(Microsoft.OData.UriParser.ResourceConstantNode resourceConstantNode, Microsoft.AspNetCore.OData.Query.Validator.FilterValidatorContext validatorContext) -> void +virtual Microsoft.AspNetCore.OData.Query.Validator.FilterQueryValidator.ValidateCollectionConstantNode(Microsoft.OData.UriParser.CollectionConstantNode collectionConstantNode, Microsoft.AspNetCore.OData.Query.Validator.FilterValidatorContext validatorContext) -> void diff --git a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.cs b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.cs index 9883a7f8b..bc5828c37 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.cs @@ -137,6 +137,9 @@ public virtual Expression BindSingleValueNode(SingleValueNode node, QueryBinderC case QueryNodeKind.Constant: return BindConstantNode(node as ConstantNode, context); + case QueryNodeKind.ResourceConstant: + return BindResourceConstantNode(node as ResourceConstantNode, context); + case QueryNodeKind.Convert: return BindConvertNode(node as ConvertNode, context); @@ -937,8 +940,54 @@ public virtual Expression BindCollectionConstantNode(CollectionConstantNode node { CheckArgumentNull(node, context); + // ODL 9.0 replaced the (now [Obsolete]) 'Collection' property (IList) with the + // 'Items' property (IList). The 'Items' collection can be heterogeneous: besides + // primitive/enum ConstantNode items, it may contain ResourceConstantNode (a JSON object literal) + // and nested CollectionConstantNode items, in any combination. + // + // When every item is a plain ConstantNode we keep the original behavior and materialize a constant + // List (which is friendly to LINQ providers such as Entity Framework). Otherwise we build the + // list with a ListInit expression, binding each item recursively so that ConstantNode, + // ResourceConstantNode and nested CollectionConstantNode items are all handled. + if (!node.Items.All(item => item is ConstantNode)) + { + // Bind each item first so that ConstantNode, ResourceConstantNode and nested + // CollectionConstantNode items are all materialized into expressions. + List itemExpressions = node.Items.Select(item => Bind(item, context)).ToList(); + + // Prefer the CLR type mapped from the EDM item type. The model cannot map some item types + // (e.g. a nested Collection(...) type or an untyped item type), so fall back to the common CLR + // type of the bound items, using object when the items are heterogeneous. + Type elementClrType = context.Model.GetClrType(node.ItemType, context.AssembliesResolver); + if (elementClrType == null) + { + Type firstType = itemExpressions.FirstOrDefault()?.Type; + bool uniform = firstType != null && itemExpressions.All(e => firstType.IsAssignableFrom(e.Type)); + elementClrType = uniform ? firstType : typeof(object); + } + + Type listType = typeof(List<>).MakeGenericType(elementClrType); + NewExpression newList = Expression.New(listType); + List elementInits = new List(); + MethodInfo addMethod = listType.GetMethod("Add", new[] { elementClrType }); + + foreach (Expression itemExpression in itemExpressions) + { + Expression element = itemExpression; + if (element.Type != elementClrType && (!elementClrType.IsAssignableFrom(element.Type) || element.Type.IsValueType)) + { + element = Expression.Convert(element, elementClrType); + } + + elementInits.Add(Expression.ElementInit(addMethod, element)); + } + + return elementInits.Count == 0 ? (Expression)newList : Expression.ListInit(newList, elementInits); + } + // It's fine if the collection is empty; the returned value will be an empty list. - ConstantNode firstNode = node.Collection.FirstOrDefault(); + IList constantItems = node.Items.OfType().ToList(); + ConstantNode firstNode = constantItems.FirstOrDefault(); object value = null; if (firstNode != null) { @@ -949,12 +998,12 @@ public virtual Expression BindCollectionConstantNode(CollectionConstantNode node Type nullableConstantType = node.ItemType.IsNullable && constantType.IsValueType && Nullable.GetUnderlyingType(constantType) == null ? typeof(Nullable<>).MakeGenericType(constantType) : constantType; - Type listType = typeof(List<>).MakeGenericType(nullableConstantType); - IList castedList = Activator.CreateInstance(listType) as IList; + Type listConstantType = typeof(List<>).MakeGenericType(nullableConstantType); + IList castedList = Activator.CreateInstance(listConstantType) as IList; // Getting a LINQ expression to dynamically cast each item in the Collection during runtime is tricky, // so using a foreach loop and doing an implicit cast from object to the CLR type of ItemType. - foreach (ConstantNode item in node.Collection) + foreach (ConstantNode item in constantItems) { object member; if (item.Value == null) @@ -975,10 +1024,112 @@ public virtual Expression BindCollectionConstantNode(CollectionConstantNode node if (context.QuerySettings.EnableConstantParameterization) { - return LinqParameterContainer.Parameterize(listType, castedList); + return LinqParameterContainer.Parameterize(listConstantType, castedList); + } + + return Expression.Constant(castedList, listConstantType); + } + + /// + /// Binds a to create a LINQ that + /// represents the semantics of the . + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ created. + /// + /// A represents an inline JSON object literal (e.g. {"Name":"John"}) + /// introduced in OData Library (ODL) 9.0. When the node has a valid (non-untyped) expected structured type, + /// it is materialized into a CLR instance of that type using a member-initialization expression, binding each + /// property value recursively. When the expected type is missing or untyped (e.g. a literal against an open + /// or untyped property), the node is materialized into a keyed by + /// property name. + /// + public virtual Expression BindResourceConstantNode(ResourceConstantNode node, QueryBinderContext context) + { + CheckArgumentNull(node, context); + + IEdmStructuredTypeReference structuredTypeReference = node.ExpectedStructuredType; + + // No expected type (or an untyped one): materialize the literal into a Dictionary. + if (structuredTypeReference == null || structuredTypeReference.IsUntyped()) + { + return BindResourceConstantNodeAsDictionary(node, context); + } + + IEdmStructuredType structuredType = structuredTypeReference.StructuredDefinition(); + Type clrType = context.Model.GetClrType(structuredTypeReference, context.AssembliesResolver); + if (clrType == null) + { + throw new ODataException(Error.Format(SRResources.ClrTypeNotInModel, structuredTypeReference.FullName())); + } + + List memberBindings = new List(); + foreach (KeyValuePair property in node.Properties) + { + // Skip control information such as the "@odata.type" annotation. + if (property.Key == null || property.Key.StartsWith("@", StringComparison.Ordinal)) + { + continue; + } + + IEdmProperty edmProperty = structuredType.FindProperty(property.Key); + if (edmProperty == null) + { + continue; + } + + string clrPropertyName = context.Model.GetClrPropertyName(edmProperty); + PropertyInfo propertyInfo = clrType.GetProperty(clrPropertyName); + if (propertyInfo == null || !propertyInfo.CanWrite) + { + continue; + } + + Expression valueExpression = Bind(property.Value, context); + if (!propertyInfo.PropertyType.IsAssignableFrom(valueExpression.Type)) + { + valueExpression = Expression.Convert(valueExpression, propertyInfo.PropertyType); + } + + memberBindings.Add(Expression.Bind(propertyInfo, valueExpression)); + } + + return Expression.MemberInit(Expression.New(clrType), memberBindings); + } + + /// + /// Materializes an untyped into a + /// keyed by property name, binding each property value recursively. + /// + /// The query node to bind. + /// The query binder context. + /// The LINQ that builds the dictionary. + private Expression BindResourceConstantNodeAsDictionary(ResourceConstantNode node, QueryBinderContext context) + { + Type dictionaryType = typeof(Dictionary); + NewExpression newDictionary = Expression.New(dictionaryType); + MethodInfo addMethod = dictionaryType.GetMethod("Add", new[] { typeof(string), typeof(object) }); + + List elementInits = new List(); + foreach (KeyValuePair property in node.Properties) + { + // Skip control information such as the "@odata.type" annotation. + if (property.Key == null || property.Key.StartsWith("@", StringComparison.Ordinal)) + { + continue; + } + + Expression valueExpression = Bind(property.Value, context); + if (valueExpression.Type != typeof(object)) + { + valueExpression = Expression.Convert(valueExpression, typeof(object)); + } + + elementInits.Add(Expression.ElementInit(addMethod, Expression.Constant(property.Key, typeof(string)), valueExpression)); } - return Expression.Constant(castedList, listType); + return elementInits.Count == 0 ? (Expression)newDictionary : Expression.ListInit(newDictionary, elementInits); } /// diff --git a/src/Microsoft.AspNetCore.OData/Query/ParameterAliasNodeTranslator.cs b/src/Microsoft.AspNetCore.OData/Query/ParameterAliasNodeTranslator.cs index 6940418c9..b83303ed2 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ParameterAliasNodeTranslator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ParameterAliasNodeTranslator.cs @@ -208,6 +208,16 @@ public override QueryNode Visit(CollectionConstantNode nodeIn) return nodeIn; } + /// + /// Translate a ResourceConstantNode. + /// + /// The node to be translated. + /// The original node. + public override QueryNode Visit(ResourceConstantNode nodeIn) + { + return nodeIn; + } + /// /// Translate a ConvertNode. /// diff --git a/src/Microsoft.AspNetCore.OData/Query/Validator/FilterQueryValidator.cs b/src/Microsoft.AspNetCore.OData/Query/Validator/FilterQueryValidator.cs index 917ec207f..84db71215 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Validator/FilterQueryValidator.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Validator/FilterQueryValidator.cs @@ -5,6 +5,7 @@ // //------------------------------------------------------------------------------ +using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; using Microsoft.AspNetCore.OData.Edm; @@ -225,6 +226,51 @@ protected virtual void ValidateConstantNode(ConstantNode constantNode, FilterVal // No default validation logic here. } + /// + /// Override this method to restrict the resource (inline JSON object) literal inside the filter query. + /// + /// + /// A represents an inline resource literal (e.g. {"Name":"John"}) + /// introduced in OData Library (ODL) 9.0. This method recursively validates each of its property value nodes. + /// + /// The resource constant node to validate. + /// The validator context. + protected virtual void ValidateResourceConstantNode(ResourceConstantNode resourceConstantNode, FilterValidatorContext validatorContext) + { + if (resourceConstantNode == null) + { + return; + } + + foreach (KeyValuePair property in resourceConstantNode.Properties) + { + ValidateQueryNode(property.Value, validatorContext); + } + } + + /// + /// Override this method to restrict the collection (inline JSON array) literal inside the filter query. + /// + /// + /// This method recursively validates each item node of a . As of + /// OData Library (ODL) 9.0 those items may include and nested + /// instances. + /// + /// The collection constant node to validate. + /// The validator context. + protected virtual void ValidateCollectionConstantNode(CollectionConstantNode collectionConstantNode, FilterValidatorContext validatorContext) + { + if (collectionConstantNode == null) + { + return; + } + + foreach (QueryNode item in collectionConstantNode.Items) + { + ValidateQueryNode(item, validatorContext); + } + } + /// /// Override this method to restrict the 'cast' inside the filter query. /// @@ -653,6 +699,10 @@ protected virtual void ValidateCollectionNode(CollectionNode node, FilterValidat ValidateCollectionResourceCastNode(node as CollectionResourceCastNode, validatorContext); break; + case QueryNodeKind.CollectionConstant: + ValidateCollectionConstantNode(node as CollectionConstantNode, validatorContext); + break; + case QueryNodeKind.CollectionFunctionCall: case QueryNodeKind.CollectionResourceFunctionCall: // Unused or have unknown uses. @@ -678,6 +728,10 @@ protected virtual void ValidateSingleValueNode(SingleValueNode node, FilterValid ValidateConstantNode(node as ConstantNode, validatorContext); break; + case QueryNodeKind.ResourceConstant: + ValidateResourceConstantNode(node as ResourceConstantNode, validatorContext); + break; + case QueryNodeKind.Convert: ValidateConvertNode(node as ConvertNode, validatorContext); break; diff --git a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl index c4f9f8ca6..61450f10e 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl +++ b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.bsl @@ -3129,6 +3129,7 @@ public abstract class Microsoft.AspNetCore.OData.Query.Expressions.QueryBinder { protected virtual System.Linq.Expressions.Expression BindNow (Microsoft.OData.UriParser.SingleValueFunctionCallNode node, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) public virtual System.Linq.Expressions.Expression BindPropertyAccessQueryNode (Microsoft.OData.UriParser.SingleValuePropertyAccessNode propertyAccessNode, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) public virtual System.Linq.Expressions.Expression BindRangeVariable (Microsoft.OData.UriParser.RangeVariable rangeVariable, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) + public virtual System.Linq.Expressions.Expression BindResourceConstantNode (Microsoft.OData.UriParser.ResourceConstantNode node, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) protected virtual System.Linq.Expressions.Expression BindRound (Microsoft.OData.UriParser.SingleValueFunctionCallNode node, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) public virtual System.Linq.Expressions.Expression BindSingleComplexNode (Microsoft.OData.UriParser.SingleComplexNode singleComplexNode, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) public virtual System.Linq.Expressions.Expression BindSingleResourceCastFunctionCall (Microsoft.OData.UriParser.SingleResourceFunctionCallNode node, Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext context) @@ -3372,6 +3373,7 @@ public class Microsoft.AspNetCore.OData.Query.Validator.FilterQueryValidator : I protected virtual void ValidateArithmeticOperator (Microsoft.OData.UriParser.BinaryOperatorNode binaryNode, Microsoft.AspNetCore.OData.Query.Validator.FilterValidatorContext validatorContext) protected virtual void ValidateBinaryOperatorNode (Microsoft.OData.UriParser.BinaryOperatorNode binaryOperatorNode, Microsoft.AspNetCore.OData.Query.Validator.FilterValidatorContext validatorContext) protected virtual void ValidateCollectionComplexNode (Microsoft.OData.UriParser.CollectionComplexNode collectionComplexNode, Microsoft.AspNetCore.OData.Query.Validator.FilterValidatorContext validatorContext) + protected virtual void ValidateCollectionConstantNode (Microsoft.OData.UriParser.CollectionConstantNode collectionConstantNode, Microsoft.AspNetCore.OData.Query.Validator.FilterValidatorContext validatorContext) protected virtual void ValidateCollectionNode (Microsoft.OData.UriParser.CollectionNode node, Microsoft.AspNetCore.OData.Query.Validator.FilterValidatorContext validatorContext) protected virtual void ValidateCollectionPropertyAccessNode (Microsoft.OData.UriParser.CollectionPropertyAccessNode propertyAccessNode, Microsoft.AspNetCore.OData.Query.Validator.FilterValidatorContext validatorContext) protected virtual void ValidateCollectionResourceCastNode (Microsoft.OData.UriParser.CollectionResourceCastNode collectionResourceCastNode, Microsoft.AspNetCore.OData.Query.Validator.FilterValidatorContext validatorContext) @@ -3383,6 +3385,7 @@ public class Microsoft.AspNetCore.OData.Query.Validator.FilterQueryValidator : I protected virtual void ValidateNavigationPropertyNode (Microsoft.OData.UriParser.QueryNode sourceNode, Microsoft.OData.Edm.IEdmNavigationProperty navigationProperty, Microsoft.AspNetCore.OData.Query.Validator.FilterValidatorContext validatorContext) protected virtual void ValidateQueryNode (Microsoft.OData.UriParser.QueryNode node, Microsoft.AspNetCore.OData.Query.Validator.FilterValidatorContext validatorContext) protected virtual void ValidateRangeVariable (Microsoft.OData.UriParser.RangeVariable rangeVariable, Microsoft.AspNetCore.OData.Query.Validator.FilterValidatorContext validatorContext) + protected virtual void ValidateResourceConstantNode (Microsoft.OData.UriParser.ResourceConstantNode resourceConstantNode, Microsoft.AspNetCore.OData.Query.Validator.FilterValidatorContext validatorContext) protected virtual void ValidateSingleComplexNode (Microsoft.OData.UriParser.SingleComplexNode singleComplexNode, Microsoft.AspNetCore.OData.Query.Validator.FilterValidatorContext validatorContext) protected virtual void ValidateSingleResourceCastNode (Microsoft.OData.UriParser.SingleResourceCastNode singleResourceCastNode, Microsoft.AspNetCore.OData.Query.Validator.FilterValidatorContext validatorContext) protected virtual void ValidateSingleResourceFunctionCallNode (Microsoft.OData.UriParser.SingleResourceFunctionCallNode node, Microsoft.AspNetCore.OData.Query.Validator.FilterValidatorContext validatorContext) diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/FilterBinderTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/FilterBinderTests.cs index 7089ce98b..b0b4a5ed7 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/FilterBinderTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/FilterBinderTests.cs @@ -3480,6 +3480,26 @@ public void NestedDynamicSegment_DoesNotExposeNotMappedClrProperty_OnNonOpenRunt #endregion + #region ResourceConstantNode / CollectionConstantNode (ODL 9.0) end-to-end + + [Fact] + public void BindFilter_ParsesBindsAndInvokes_CollectionOfResourceLiterals_UsingInOperator() + { + // Arrange & Act - a collection of inline resource (JSON object) literals is parsed by ODL 9.0 into a + // CollectionConstantNode whose Items are ResourceConstantNode instances, then bound by QueryBinder. + (Expression, Expression) filters = BindFilterAndVerify( + "SupplierAddress in [{\"City\":\"Redmond\",\"Street\":\"NE 24th St.\"},{\"City\":\"Seattle\",\"Street\":\"Pine St.\"}]", + NotTesting, + NotTesting); + + // Assert - the filter binds and can be invoked. Membership uses the CLR type's default (reference) + // equality for the freshly constructed instances, so an equivalent-but-distinct instance is not matched. + Product product = new Product { SupplierAddress = new Address { City = "Redmond", Street = "NE 24th St." } }; + Assert.False(InvokeFilter(product, filters.Item1)); + } + + #endregion + #region Helpers internal static void InvokeFiltersAndThrows((Expression, Expression) filters, T instance, (Type, bool) expectedValue) { diff --git a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/QueryBinderTests.cs b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/QueryBinderTests.cs index e0524842e..6f061bfc2 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/QueryBinderTests.cs +++ b/test/Microsoft.AspNetCore.OData.Tests/Query/Expressions/QueryBinderTests.cs @@ -582,6 +582,246 @@ public void GetFullPropertyPath_WithSingleValueOpenPropertyAccessNodeInSingleCom // Assert Assert.Equal("Address\\ZipCode", fullPropertyPath); } + #region ResourceConstantNode / CollectionConstantNode (ODL 9.0) + + [Fact] + public void BindResourceConstantNode_ThrowsArgumentNull_ForInputs() + { + // Arrange + QueryBinder binder = new MyQueryBinder(); + ResourceConstantNode node = new ResourceConstantNode(GetAddressTypeReference()); + + // Act & Assert + ExceptionAssert.ThrowsArgumentNull(() => binder.BindResourceConstantNode(null, null), "node"); + ExceptionAssert.ThrowsArgumentNull(() => binder.BindResourceConstantNode(node, null), "context"); + } + + [Fact] + public void BindResourceConstantNode_ConstructsClrObject_FromProperties() + { + // Arrange + QueryBinder binder = new MyQueryBinder(); + IEdmComplexTypeReference addressTypeRef = GetAddressTypeReference(); + ResourceConstantNode node = new ResourceConstantNode(addressTypeRef); + node.Properties.Add(new KeyValuePair("Street", Const("NE 24th St.", EdmCoreModel.Instance.GetString(true)))); + node.Properties.Add(new KeyValuePair("City", Const("Redmond", EdmCoreModel.Instance.GetString(true)))); + node.Properties.Add(new KeyValuePair("HouseNumber", Const(42, EdmCoreModel.Instance.GetInt32(false)))); + + QueryBinderContext context = new QueryBinderContext(TestModel, new ODataQuerySettings(), typeof(Employee)); + + // Act + Expression expression = binder.BindResourceConstantNode(node, context); + + // Assert + Assert.Equal(ExpressionType.MemberInit, expression.NodeType); + Address address = (Address)Expression.Lambda(expression).Compile().DynamicInvoke(); + Assert.NotNull(address); + Assert.Equal("NE 24th St.", address.Street); + Assert.Equal("Redmond", address.City); + Assert.Equal(42, address.HouseNumber); + } + + [Fact] + public void BindResourceConstantNode_BuildsDictionary_ForUntypedResourceLiteral() + { + // Arrange - an untyped resource literal (null ExpectedStructuredType) is materialized into a dictionary. + QueryBinder binder = new MyQueryBinder(); + ResourceConstantNode node = new ResourceConstantNode(null); + node.Properties.Add(new KeyValuePair("Name", Const("John", EdmCoreModel.Instance.GetString(true)))); + node.Properties.Add(new KeyValuePair("Age", Const(30, EdmCoreModel.Instance.GetInt32(false)))); + node.Properties.Add(new KeyValuePair("@odata.type", Const("#Some.Type", EdmCoreModel.Instance.GetString(true)))); + + QueryBinderContext context = new QueryBinderContext(TestModel, new ODataQuerySettings(), typeof(Employee)); + + // Act + Expression expression = binder.BindResourceConstantNode(node, context); + + // Assert - annotation keys (starting with '@') are excluded. + Dictionary values = (Dictionary)Expression.Lambda(expression).Compile().DynamicInvoke(); + Assert.Equal(2, values.Count); + Assert.Equal("John", values["Name"]); + Assert.Equal(30, values["Age"]); + } + + [Fact] + public void BindCollectionConstantNode_BuildsListOfResources_FromItems() + { + // Arrange + QueryBinder binder = new MyQueryBinder(); + IEdmComplexTypeReference addressTypeRef = GetAddressTypeReference(); + + ResourceConstantNode first = new ResourceConstantNode(addressTypeRef); + first.Properties.Add(new KeyValuePair("City", Const("Redmond", EdmCoreModel.Instance.GetString(true)))); + + ResourceConstantNode second = new ResourceConstantNode(addressTypeRef); + second.Properties.Add(new KeyValuePair("City", Const("Seattle", EdmCoreModel.Instance.GetString(true)))); + + EdmCollectionTypeReference collectionTypeRef = new EdmCollectionTypeReference(new EdmCollectionType(addressTypeRef)); + CollectionConstantNode node = new CollectionConstantNode(collectionTypeRef); + node.Items.Add(first); + node.Items.Add(second); + + QueryBinderContext context = new QueryBinderContext(TestModel, new ODataQuerySettings(), typeof(Employee)); + + // Act + Expression expression = binder.BindCollectionConstantNode(node, context); + + // Assert + List
addresses = (List
)Expression.Lambda(expression).Compile().DynamicInvoke(); + Assert.Equal(2, addresses.Count); + Assert.Equal("Redmond", addresses[0].City); + Assert.Equal("Seattle", addresses[1].City); + } + + [Fact] + public void BindCollectionConstantNode_BuildsEmptyListOfResources_ForEmptyItems() + { + // Arrange + QueryBinder binder = new MyQueryBinder(); + IEdmComplexTypeReference addressTypeRef = GetAddressTypeReference(); + EdmCollectionTypeReference collectionTypeRef = new EdmCollectionTypeReference(new EdmCollectionType(addressTypeRef)); + CollectionConstantNode node = new CollectionConstantNode(collectionTypeRef); + + QueryBinderContext context = new QueryBinderContext(TestModel, new ODataQuerySettings(), typeof(Employee)); + + // Act + Expression expression = binder.BindCollectionConstantNode(node, context); + + // Assert + List
addresses = (List
)Expression.Lambda(expression).Compile().DynamicInvoke(); + Assert.Empty(addresses); + } + + [Fact] + public void BindCollectionConstantNode_BuildsListOfPrimitives_FromConstantItems() + { + // Arrange + QueryBinder binder = new MyQueryBinder(); + EdmCollectionTypeReference collectionTypeRef = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetInt32(false))); + CollectionConstantNode node = new CollectionConstantNode(collectionTypeRef); + node.Items.Add(Const(1, EdmCoreModel.Instance.GetInt32(false))); + node.Items.Add(Const(2, EdmCoreModel.Instance.GetInt32(false))); + node.Items.Add(Const(3, EdmCoreModel.Instance.GetInt32(false))); + + QueryBinderContext context = new QueryBinderContext(TestModel, new ODataQuerySettings(), typeof(Employee)); + + // Act + Expression expression = binder.BindCollectionConstantNode(node, context); + + // Assert + List values = (List)Expression.Lambda(expression).Compile().DynamicInvoke(); + Assert.Equal(new[] { 1, 2, 3 }, values); + } + + [Fact] + public void BindCollectionConstantNode_BuildsListWithMixedResourceAndNullItems() + { + // Arrange - Items contains a ResourceConstantNode and a null ConstantNode. + QueryBinder binder = new MyQueryBinder(); + IEdmComplexTypeReference addressTypeRef = GetAddressTypeReference(); + + ResourceConstantNode resource = new ResourceConstantNode(addressTypeRef); + resource.Properties.Add(new KeyValuePair("City", Const("Redmond", EdmCoreModel.Instance.GetString(true)))); + + EdmCollectionTypeReference collectionTypeRef = new EdmCollectionTypeReference(new EdmCollectionType(addressTypeRef)); + CollectionConstantNode node = new CollectionConstantNode(collectionTypeRef); + node.Items.Add(resource); + node.Items.Add(new ConstantNode(null, "null", addressTypeRef)); + + QueryBinderContext context = new QueryBinderContext(TestModel, new ODataQuerySettings(), typeof(Employee)); + + // Act + Expression expression = binder.BindCollectionConstantNode(node, context); + + // Assert + List
addresses = (List
)Expression.Lambda(expression).Compile().DynamicInvoke(); + Assert.Equal(2, addresses.Count); + Assert.Equal("Redmond", addresses[0].City); + Assert.Null(addresses[1]); + } + + [Fact] + public void BindCollectionConstantNode_BuildsNestedListOfCollections_FromNestedItems() + { + // Arrange - Items contains nested CollectionConstantNode instances (collection of collections). + QueryBinder binder = new MyQueryBinder(); + IEdmTypeReference int32 = EdmCoreModel.Instance.GetInt32(false); + EdmCollectionTypeReference innerCollectionTypeRef = new EdmCollectionTypeReference(new EdmCollectionType(int32)); + EdmCollectionTypeReference outerCollectionTypeRef = new EdmCollectionTypeReference(new EdmCollectionType(innerCollectionTypeRef)); + + CollectionConstantNode inner1 = new CollectionConstantNode(innerCollectionTypeRef); + inner1.Items.Add(Const(1, int32)); + inner1.Items.Add(Const(2, int32)); + + CollectionConstantNode inner2 = new CollectionConstantNode(innerCollectionTypeRef); + inner2.Items.Add(Const(3, int32)); + + CollectionConstantNode outer = new CollectionConstantNode(outerCollectionTypeRef); + outer.Items.Add(inner1); + outer.Items.Add(inner2); + + QueryBinderContext context = new QueryBinderContext(TestModel, new ODataQuerySettings(), typeof(Employee)); + + // Act + Expression expression = binder.BindCollectionConstantNode(outer, context); + + // Assert - the outer list contains two inner integer lists. + System.Collections.IList outerList = (System.Collections.IList)Expression.Lambda(expression).Compile().DynamicInvoke(); + Assert.Equal(2, outerList.Count); + Assert.Equal(new[] { 1, 2 }, ((IEnumerable)outerList[0]).ToArray()); + Assert.Equal(new[] { 3 }, ((IEnumerable)outerList[1]).ToArray()); + } + + [Fact] + public void BindCollectionConstantNode_BuildsHeterogeneousList_ForUntypedCollectionWithMixedItemKinds() + { + // Arrange - an untyped collection literal such as [1, {"Name":"John"}, [2, 3]] whose Items contain a + // ConstantNode, a ResourceConstantNode and a nested CollectionConstantNode. + QueryBinder binder = new MyQueryBinder(); + IEdmTypeReference int32 = EdmCoreModel.Instance.GetInt32(false); + + ResourceConstantNode resource = new ResourceConstantNode(null); + resource.Properties.Add(new KeyValuePair("Name", Const("John", EdmCoreModel.Instance.GetString(true)))); + + EdmCollectionTypeReference innerCollectionTypeRef = new EdmCollectionTypeReference(new EdmCollectionType(int32)); + CollectionConstantNode inner = new CollectionConstantNode(innerCollectionTypeRef); + inner.Items.Add(Const(2, int32)); + inner.Items.Add(Const(3, int32)); + + EdmCollectionTypeReference untypedCollectionTypeRef = new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetUntyped())); + CollectionConstantNode node = new CollectionConstantNode(untypedCollectionTypeRef); + node.Items.Add(Const(1, int32)); + node.Items.Add(resource); + node.Items.Add(inner); + + QueryBinderContext context = new QueryBinderContext(TestModel, new ODataQuerySettings(), typeof(Employee)); + + // Act + Expression expression = binder.BindCollectionConstantNode(node, context); + + // Assert + System.Collections.IList list = (System.Collections.IList)Expression.Lambda(expression).Compile().DynamicInvoke(); + Assert.Equal(3, list.Count); + Assert.Equal(1, list[0]); + Dictionary dictionary = Assert.IsType>(list[1]); + Assert.Equal("John", dictionary["Name"]); + Assert.Equal(new[] { 2, 3 }, ((IEnumerable)list[2]).ToArray()); + } + + private static IEdmComplexTypeReference GetAddressTypeReference() + { + IEdmComplexType addressType = GetEdmComplexTypeFor("Microsoft.AspNetCore.OData.Tests.Models.Address"); + Assert.NotNull(addressType); + return new EdmComplexTypeReference(addressType, true); + } + + private static ConstantNode Const(object value, IEdmTypeReference typeReference) + { + return new ConstantNode(value, value?.ToString(), typeReference); + } + + #endregion + private static SingleValueNode CreateEmployeeRangeVariableReferenceNode() { // Create the type reference and navigation source