Skip to content

Commit a06f317

Browse files
committed
Add support for collection parameters in URI functions
1 parent 03c4975 commit a06f317

17 files changed

+1853
-211
lines changed

src/Microsoft.OData.Core/SRResources.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.OData.Core/SRResources.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2624,4 +2624,7 @@
26242624
<data name="TaskUtils_NullContinuationTask" xml:space="preserve">
26252625
<value>The continuation task returned by the operation cannot be null.</value>
26262626
</data>
2627+
<data name="MetadataBinder_FunctionArgumentNotSingleValueOrCollectionNode" xml:space="preserve">
2628+
<value>The argument for an invocation of a function with name '{0}' is not a single value or collection. Arguments for this function must be either a single value or collection.</value>
2629+
</data>
26272630
</root>

src/Microsoft.OData.Core/Uri/ODataUriConversionUtils.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -671,19 +671,19 @@ private static object ConvertFromResourceOrCollectionValue(string value, IEdmMod
671671
{
672672
ODataJsonPropertyAndValueDeserializer deserializer = new ODataJsonPropertyAndValueDeserializer(context);
673673

674-
// TODO: The way JSON array literals look in the URI is different that response payload with an array in it.
674+
// TODO: The way JSON array literals look in the URI is different than response payload with an array in it.
675675
// The fact that we have to manually setup the underlying reader shows this different in the protocol.
676676
// There is a discussion on if we should change this or not.
677677
deserializer.JsonReader.Read(); // Move to first thing
678678
object rawResult = deserializer.ReadNonEntityValue(
679-
null /*payloadTypeName*/,
680-
typeReference,
681-
null /*DuplicatePropertyNameChecker*/,
682-
null /*CollectionWithoutExpectedTypeValidator*/,
683-
true /*validateNullValue*/,
684-
false /*isTopLevelPropertyValue*/,
685-
false /*insideResourceValue*/,
686-
null /*propertyName*/);
679+
payloadTypeName: null,
680+
expectedValueTypeReference: typeReference,
681+
propertyAndAnnotationCollector: null,
682+
collectionValidator: null,
683+
validateNullValue: true,
684+
isTopLevelPropertyValue: false,
685+
insideResourceValue: false,
686+
propertyName: null);
687687
deserializer.ReadPayloadEnd(false);
688688

689689
return rawResult;

src/Microsoft.OData.Core/Uri/ODataUriUtils.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public static object ConvertFromUriLiteral(string value, ODataVersion version, I
5151

5252
if (model == null)
5353
{
54-
model = Microsoft.OData.Edm.EdmCoreModel.Instance;
54+
model = EdmCoreModel.Instance;
5555
}
5656

5757
// Let ExpressionLexer try to get a primitive
@@ -149,7 +149,7 @@ public static string ConvertToUriLiteral(object value, ODataVersion version, IEd
149149

150150
if (model == null)
151151
{
152-
model = Microsoft.OData.Edm.EdmCoreModel.Instance;
152+
model = EdmCoreModel.Instance;
153153
}
154154

155155
ODataNullValue nullValue = value as ODataNullValue;

src/Microsoft.OData.Core/UriParser/Binders/FunctionCallBinder.cs

Lines changed: 98 additions & 43 deletions
Large diffs are not rendered by default.

src/Microsoft.OData.Core/UriParser/Binders/LiteralBinder.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
namespace Microsoft.OData.UriParser
88
{
9-
using System.Diagnostics;
9+
using Microsoft.OData.Edm;
1010

1111
/// <summary>
1212
/// Class that knows how to bind literal values.
@@ -48,8 +48,8 @@ internal static QueryNode BindInLiteral(LiteralToken literalToken)
4848
{
4949
if (literalToken.ExpectedEdmTypeReference != null)
5050
{
51-
OData.Edm.IEdmCollectionTypeReference collectionReference =
52-
literalToken.ExpectedEdmTypeReference as OData.Edm.IEdmCollectionTypeReference;
51+
IEdmCollectionTypeReference collectionReference =
52+
literalToken.ExpectedEdmTypeReference as IEdmCollectionTypeReference;
5353
if (collectionReference != null)
5454
{
5555
ODataCollectionValue collectionValue = literalToken.Value as ODataCollectionValue;

src/Microsoft.OData.Core/UriParser/Binders/MetadataBindingUtils.cs

Lines changed: 139 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@
77
namespace Microsoft.OData.UriParser
88
{
99
using System;
10+
using System.Collections.Generic;
1011
using System.Diagnostics;
11-
using System.Linq;
12-
using Microsoft.OData.Edm;
1312
using Microsoft.OData;
14-
using Microsoft.OData.Metadata;
1513
using Microsoft.OData.Core;
14+
using Microsoft.OData.Edm;
15+
using Microsoft.OData.Metadata;
1616

1717
/// <summary>
1818
/// Helper methods for metadata binding.
@@ -66,14 +66,14 @@ internal static SingleValueNode ConvertToTypeIfNeeded(SingleValueNode source, IE
6666
IEdmEnumType enumType = targetTypeReference.Definition as IEdmEnumType;
6767
if (enumType.ContainsMember(memberName, StringComparison.Ordinal))
6868
{
69-
string literalText = ODataUriUtils.ConvertToUriLiteral(constantNode.Value, default(ODataVersion));
69+
string literalText = ODataUriUtils.ConvertToUriLiteral(constantNode.Value, default);
7070
return new ConstantNode(new ODataEnumValue(memberName, enumType.ToString()), literalText, targetTypeReference);
7171
}
7272

7373
// If the member name is an integral value, we should try to convert it to the enum member name and find the enum member with the matching integral value
7474
if (long.TryParse(memberName, out long memberIntegralValue) && enumType.TryParse(memberIntegralValue, out IEdmEnumMember enumMember))
7575
{
76-
string literalText = ODataUriUtils.ConvertToUriLiteral(enumMember.Name, default(ODataVersion));
76+
string literalText = ODataUriUtils.ConvertToUriLiteral(enumMember.Name, default);
7777
return new ConstantNode(new ODataEnumValue(enumMember.Name, enumType.ToString()), literalText, targetTypeReference);
7878
}
7979

@@ -111,8 +111,8 @@ internal static SingleValueNode ConvertToTypeIfNeeded(SingleValueNode source, IE
111111
var targetDecimalType = (IEdmDecimalTypeReference)targetTypeReference;
112112
return decimalType.Precision == targetDecimalType.Precision &&
113113
decimalType.Scale == targetDecimalType.Scale ?
114-
(SingleValueNode)candidate :
115-
(SingleValueNode)(new ConvertNode(candidate, targetTypeReference));
114+
candidate :
115+
new ConvertNode(candidate, targetTypeReference);
116116
}
117117
else
118118
{
@@ -134,6 +134,138 @@ internal static SingleValueNode ConvertToTypeIfNeeded(SingleValueNode source, IE
134134
}
135135
}
136136

137+
/// <summary>
138+
/// Converts a collection node's element type to <paramref name="targetTypeReference"/> when possible,
139+
/// materializing a new <see cref="CollectionConstantNode"/> for constant collections;
140+
/// leaves non-constant/open or non-convertible collections unchanged.
141+
/// </summary>
142+
/// <param name="source">Source collection node.</param>
143+
/// <param name="targetTypeReference">Desired collection type (must be a collection).</param>
144+
/// <returns>Converted collection node or original source.</returns>
145+
internal static CollectionNode ConvertToTypeIfNeeded(CollectionNode source, IEdmTypeReference targetTypeReference)
146+
{
147+
Debug.Assert(source != null, "source != null");
148+
149+
if (targetTypeReference == null)
150+
{
151+
return source;
152+
}
153+
154+
IEdmCollectionTypeReference sourceCollectionType = source.CollectionType;
155+
if (sourceCollectionType == null) // Open collection? Leave as is
156+
{
157+
return source;
158+
}
159+
160+
if (!targetTypeReference.IsCollection())
161+
{
162+
throw new ODataException(Error.Format(SRResources.MetadataBinder_CannotConvertToType, source.CollectionType.FullName(), targetTypeReference.FullName()));
163+
}
164+
165+
IEdmCollectionTypeReference targetCollectionType = targetTypeReference.AsCollection();
166+
167+
if (sourceCollectionType.IsEquivalentTo(targetCollectionType))
168+
{
169+
IEdmTypeReference sourceElemType = sourceCollectionType.ElementType();
170+
IEdmTypeReference targetElemType = targetCollectionType.ElementType();
171+
if (source is CollectionConstantNode colConstantNode
172+
&& sourceElemType.IsTypeDefinition()
173+
&& targetElemType.IsPrimitive()
174+
&& sourceElemType.AsPrimitive().PrimitiveKind() == targetElemType.AsPrimitive().PrimitiveKind())
175+
{
176+
List<ConstantNode> convertedNodes = ConvertNodes(colConstantNode.Collection, targetElemType);
177+
178+
return new CollectionConstantNode(convertedNodes, BuildCollectionLiteral(convertedNodes, targetElemType), targetCollectionType);
179+
}
180+
181+
return source;
182+
}
183+
184+
IEdmTypeReference sourceElementType = sourceCollectionType.ElementType();
185+
IEdmTypeReference targetElementType = targetCollectionType.ElementType();
186+
187+
if (!TypePromotionUtils.CanConvertTo(null, sourceElementType, targetElementType))
188+
{
189+
throw new ODataException(Error.Format(SRResources.MetadataBinder_CannotConvertToType, sourceElementType.FullName(), targetElementType.FullName()));
190+
}
191+
192+
if (source is CollectionConstantNode collectionConstantNode)
193+
{
194+
List<ConstantNode> convertedNodes = ConvertNodes(collectionConstantNode.Collection, targetElementType);
195+
196+
return new CollectionConstantNode(convertedNodes, BuildCollectionLiteral(convertedNodes, targetElementType), targetCollectionType);
197+
}
198+
199+
// Non-constant collections: leave as-is (conversion implicit)
200+
return source;
201+
}
202+
203+
/// <summary>
204+
/// Converts each constant value to <paramref name="targetElementType"/>, applying enum/numeric coercion; preserves null items.
205+
/// </summary>
206+
/// <param name="nodes">Original constant value nodes.</param>
207+
/// <param name="targetElementType">The target primitive type.</param>
208+
/// <returns>List of converted constant nodes.</returns>
209+
private static List<ConstantNode> ConvertNodes(IList<ConstantNode> nodes, IEdmTypeReference targetElementType)
210+
{
211+
List<ConstantNode> convertedNodes = new List<ConstantNode>(nodes.Count);
212+
213+
for (int i = 0; i < nodes.Count; i++)
214+
{
215+
ConstantNode item = nodes[i];
216+
if (item == null)
217+
{
218+
// Preserve null
219+
convertedNodes.Add(new ConstantNode(null, "null", targetElementType));
220+
continue;
221+
}
222+
223+
ConstantNode convertedNode = ConvertToTypeIfNeeded(item, targetElementType) as ConstantNode;
224+
225+
// If ConvertToTypeIfNeeded returned a ConvertNode, force materialization into a ConstantNode
226+
if (convertedNode == null)
227+
{
228+
// Try to keep original literal text if meaningful
229+
string literal = item.LiteralText ?? ODataUriUtils.ConvertToUriLiteral(item.Value, ODataVersion.V4);
230+
convertedNode = new ConstantNode(item.Value, literal, targetElementType);
231+
}
232+
233+
convertedNodes.Add(convertedNode);
234+
}
235+
236+
return convertedNodes;
237+
}
238+
239+
/// <summary>
240+
/// Builds a bracketed collection literal (e.g. [1,2,3]) from constant nodes, quoting/escaping strings and preserving nulls.
241+
/// </summary>
242+
/// <param name="nodes">Constant nodes representing items.</param>
243+
/// <param name="typeReference">Element type for string quoting rules.</param>
244+
/// <returns>OData collection literal text.</returns>
245+
private static string BuildCollectionLiteral(List<ConstantNode> nodes, IEdmTypeReference typeReference)
246+
{
247+
List<string> list = new List<string>();
248+
for (int i = 0; i < nodes.Count; i++)
249+
{
250+
ConstantNode node = nodes[i];
251+
if (node == null || node.Value == null)
252+
{
253+
list.Add("null");
254+
continue;
255+
}
256+
257+
string literal = node.LiteralText ?? ODataUriUtils.ConvertToUriLiteral(node.Value, ODataVersion.V4);
258+
if (typeReference.IsString() && !(literal.Length > 1 && literal[0] == '\'' && literal[^1] == '\''))
259+
{
260+
literal = $"'{literal.Replace("'", "''", StringComparison.Ordinal)}'";
261+
}
262+
263+
list.Add(literal);
264+
}
265+
266+
return "[" + string.Join(",", list) + "]";
267+
}
268+
137269
/// <summary>
138270
/// Retrieves type associated to a segment.
139271
/// </summary>

src/Microsoft.OData.Core/UriParser/Binders/ParameterAliasBinder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ private static QueryToken ParseComplexOrCollectionAlias(QueryToken queryToken, I
112112
string valueStr;
113113
if (valueToken != null && (valueStr = valueToken.Value as string) != null && !string.IsNullOrEmpty(valueToken.OriginalText))
114114
{
115-
var lexer = new ExpressionLexer(valueToken.OriginalText, true /*moveToFirstToken*/, false /*useSemicolonDelimiter*/, true /*parsingFunctionParameters*/);
115+
var lexer = new ExpressionLexer(valueToken.OriginalText, moveToFirstToken: true, useSemicolonDelimiter: false, parsingFunctionParameters: true);
116116
if (lexer.CurrentToken.Kind == ExpressionTokenKind.BracketedExpression || lexer.CurrentToken.Kind == ExpressionTokenKind.BracedExpression)
117117
{
118118
object result = valueStr;

src/Microsoft.OData.Core/UriParser/ODataUriParser.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -482,12 +482,13 @@ public ODataUri ParseUri()
482482
ExceptionUtils.CheckArgumentNotNull(this.uri, "uri");
483483

484484
ODataPath path = this.ParsePath();
485+
// NOTE: ParseCompute should be called before ParseSelectAndExpand because $compute may add computed properties to the select/expand clause.
486+
ComputeClause compute = this.ParseCompute();
485487
SelectExpandClause selectExpand = this.ParseSelectAndExpand();
486488
FilterClause filter = this.ParseFilter();
487489
OrderByClause orderBy = this.ParseOrderBy();
488490
SearchClause search = this.ParseSearch();
489491
ApplyClause apply = this.ParseApply();
490-
ComputeClause compute = this.ParseCompute();
491492
long? top = this.ParseTop();
492493
long? skip = this.ParseSkip();
493494
long? index = this.ParseIndex();

src/Microsoft.OData.Core/UriParser/Resolver/ODataUriResolver.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,8 @@ public virtual IEnumerable<IEdmOperationImport> ResolveOperationImports(IEdmMode
357357
.Where(source => string.Equals(identifier, source.Name, StringComparison.OrdinalIgnoreCase));
358358
}
359359

360+
// TODO: Why does this method only handle SingleValueNode?
361+
// What about other QueryNode types like CollectionNode?
360362
/// <summary>
361363
/// Resolve operation's parameters.
362364
/// </summary>

0 commit comments

Comments
 (0)