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