Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 63 additions & 7 deletions src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1217,20 +1217,20 @@
<param name="elementType">out: the element type of the collection.</param>
<returns>True if the type is an enumeration; false otherwise.</returns>
</member>
<member name="M:Microsoft.AspNetCore.OData.Common.TypeHelper.IsAsyncEnumerableType(System.Type)">
<summary>
Determines whether the specified <see cref="T:System.Type"/> represents an <see cref="T:System.Collections.Generic.IAsyncEnumerable`1"/> type.
</summary>
<param name="clrType">The <see cref="T:System.Type"/> to evaluate.</param>
<returns>True if the type is an enumeration; false otherwise.</returns>
</member>
<member name="M:Microsoft.AspNetCore.OData.Common.TypeHelper.IsPrimitiveOrKnownType(System.Type)">
<summary>
Check whether the given type is a primitive type or known type.
</summary>
<param name="type">The type to validate.</param>
<returns>True if type is primitive or known type, otherwise False.</returns>
</member>
<member name="M:Microsoft.AspNetCore.OData.Common.TypeHelper.IsAsyncEnumerableType(System.Type)">
<summary>
Determines whether the specified <see cref="T:System.Type"/> represents an <see cref="T:System.Collections.Generic.IAsyncEnumerable`1"/> type.
</summary>
<param name="clrType">The <see cref="T:System.Type"/> to evaluate.</param>
<returns>True if the type is an enumeration; false otherwise.</returns>
</member>
<member name="M:Microsoft.AspNetCore.OData.Common.TypeHelper.GetImplementedIEnumerableType(System.Type)">
<summary>
Returns type of T if the type implements IEnumerable of T, otherwise, return null.
Expand Down Expand Up @@ -10561,6 +10561,32 @@
<param name="context">The query binder context.</param>
<returns>The LINQ <see cref="T:System.Linq.Expressions.Expression"/> created.</returns>
</member>
<member name="M:Microsoft.AspNetCore.OData.Query.Expressions.QueryBinder.BindResourceConstantNode(Microsoft.OData.UriParser.ResourceConstantNode,Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext)">
<summary>
Binds a <see cref="T:Microsoft.OData.UriParser.ResourceConstantNode"/> to create a LINQ <see cref="T:System.Linq.Expressions.Expression"/> that
represents the semantics of the <see cref="T:Microsoft.OData.UriParser.ResourceConstantNode"/>.
</summary>
<param name="node">The query node to bind.</param>
<param name="context">The query binder context.</param>
<returns>The LINQ <see cref="T:System.Linq.Expressions.Expression"/> created.</returns>
<remarks>
A <see cref="T:Microsoft.OData.UriParser.ResourceConstantNode"/> represents an inline JSON object literal (e.g. <c>{"Name":"John"}</c>)
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 <see cref="T:System.Collections.Generic.Dictionary`2"/> keyed by
property name.
</remarks>
</member>
<member name="M:Microsoft.AspNetCore.OData.Query.Expressions.QueryBinder.BindResourceConstantNodeAsDictionary(Microsoft.OData.UriParser.ResourceConstantNode,Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext)">
<summary>
Materializes an untyped <see cref="T:Microsoft.OData.UriParser.ResourceConstantNode"/> into a
<see cref="T:System.Collections.Generic.Dictionary`2"/> keyed by property name, binding each property value recursively.
</summary>
<param name="node">The query node to bind.</param>
<param name="context">The query binder context.</param>
<returns>The LINQ <see cref="T:System.Linq.Expressions.Expression"/> that builds the dictionary.</returns>
</member>
<member name="M:Microsoft.AspNetCore.OData.Query.Expressions.QueryBinder.BindAccessExpression(Microsoft.OData.UriParser.QueryNode,Microsoft.AspNetCore.OData.Query.Expressions.QueryBinderContext,System.Linq.Expressions.Expression)">
<summary>
Creates an <see cref="T:System.Linq.Expressions.Expression"/> from the <see cref="T:Microsoft.OData.UriParser.QueryNode"/>.
Expand Down Expand Up @@ -12469,6 +12495,13 @@
<param name="nodeIn">The node to be translated.</param>
<returns>The original node.</returns>
</member>
<member name="M:Microsoft.AspNetCore.OData.Query.ParameterAliasNodeTranslator.Visit(Microsoft.OData.UriParser.ResourceConstantNode)">
<summary>
Translate a ResourceConstantNode.
</summary>
<param name="nodeIn">The node to be translated.</param>
<returns>The original node.</returns>
</member>
<member name="M:Microsoft.AspNetCore.OData.Query.ParameterAliasNodeTranslator.Visit(Microsoft.OData.UriParser.ConvertNode)">
<summary>
Translate a ConvertNode.
Expand Down Expand Up @@ -13719,6 +13752,29 @@
<param name="constantNode">The constant node to validate.</param>
<param name="validatorContext">The validation context.</param>
</member>
<member name="M:Microsoft.AspNetCore.OData.Query.Validator.FilterQueryValidator.ValidateResourceConstantNode(Microsoft.OData.UriParser.ResourceConstantNode,Microsoft.AspNetCore.OData.Query.Validator.FilterValidatorContext)">
<summary>
Override this method to restrict the resource (inline JSON object) literal inside the filter query.
</summary>
<remarks>
A <see cref="T:Microsoft.OData.UriParser.ResourceConstantNode"/> represents an inline resource literal (e.g. <c>{"Name":"John"}</c>)
introduced in OData Library (ODL) 9.0. This method recursively validates each of its property value nodes.
</remarks>
<param name="resourceConstantNode">The resource constant node to validate.</param>
<param name="validatorContext">The validator context.</param>
</member>
<member name="M:Microsoft.AspNetCore.OData.Query.Validator.FilterQueryValidator.ValidateCollectionConstantNode(Microsoft.OData.UriParser.CollectionConstantNode,Microsoft.AspNetCore.OData.Query.Validator.FilterValidatorContext)">
<summary>
Override this method to restrict the collection (inline JSON array) literal inside the filter query.
</summary>
<remarks>
This method recursively validates each item node of a <see cref="T:Microsoft.OData.UriParser.CollectionConstantNode"/>. As of
OData Library (ODL) 9.0 those items may include <see cref="T:Microsoft.OData.UriParser.ResourceConstantNode"/> and nested
<see cref="T:Microsoft.OData.UriParser.CollectionConstantNode"/> instances.
</remarks>
<param name="collectionConstantNode">The collection constant node to validate.</param>
<param name="validatorContext">The validator context.</param>
</member>
<member name="M:Microsoft.AspNetCore.OData.Query.Validator.FilterQueryValidator.ValidateConvertNode(Microsoft.OData.UriParser.ConvertNode,Microsoft.AspNetCore.OData.Query.Validator.FilterValidatorContext)">
<summary>
Override this method to restrict the 'cast' inside the filter query.
Expand Down
3 changes: 3 additions & 0 deletions src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
163 changes: 157 additions & 6 deletions src/Microsoft.AspNetCore.OData/Query/Expressions/QueryBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -937,8 +940,54 @@ public virtual Expression BindCollectionConstantNode(CollectionConstantNode node
{
CheckArgumentNull(node, context);

// ODL 9.0 replaced the (now [Obsolete]) 'Collection' property (IList<ConstantNode>) with the
// 'Items' property (IList<QueryNode>). 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<T> (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<Expression> 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<ElementInit> elementInits = new List<ElementInit>();
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<ConstantNode> constantItems = node.Items.OfType<ConstantNode>().ToList();
ConstantNode firstNode = constantItems.FirstOrDefault();
object value = null;
if (firstNode != null)
{
Expand All @@ -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)
Expand All @@ -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);
}

/// <summary>
/// Binds a <see cref="ResourceConstantNode"/> to create a LINQ <see cref="Expression"/> that
/// represents the semantics of the <see cref="ResourceConstantNode"/>.
/// </summary>
/// <param name="node">The query node to bind.</param>
/// <param name="context">The query binder context.</param>
/// <returns>The LINQ <see cref="Expression"/> created.</returns>
/// <remarks>
/// A <see cref="ResourceConstantNode"/> represents an inline JSON object literal (e.g. <c>{"Name":"John"}</c>)
/// 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 <see cref="Dictionary{TKey, TValue}"/> keyed by
/// property name.
/// </remarks>
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<string, object>.
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<MemberBinding> memberBindings = new List<MemberBinding>();
foreach (KeyValuePair<string, QueryNode> 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);
}

/// <summary>
/// Materializes an untyped <see cref="ResourceConstantNode"/> into a
/// <see cref="Dictionary{TKey, TValue}"/> keyed by property name, binding each property value recursively.
/// </summary>
/// <param name="node">The query node to bind.</param>
/// <param name="context">The query binder context.</param>
/// <returns>The LINQ <see cref="Expression"/> that builds the dictionary.</returns>
private Expression BindResourceConstantNodeAsDictionary(ResourceConstantNode node, QueryBinderContext context)
{
Type dictionaryType = typeof(Dictionary<string, object>);
NewExpression newDictionary = Expression.New(dictionaryType);
MethodInfo addMethod = dictionaryType.GetMethod("Add", new[] { typeof(string), typeof(object) });

List<ElementInit> elementInits = new List<ElementInit>();
foreach (KeyValuePair<string, QueryNode> 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);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,16 @@ public override QueryNode Visit(CollectionConstantNode nodeIn)
return nodeIn;
}

/// <summary>
/// Translate a ResourceConstantNode.
/// </summary>
/// <param name="nodeIn">The node to be translated.</param>
/// <returns>The original node.</returns>
public override QueryNode Visit(ResourceConstantNode nodeIn)
{
return nodeIn;
}
Comment on lines +216 to +219

/// <summary>
/// Translate a ConvertNode.
/// </summary>
Expand Down
Loading