Skip to content

Commit 55e1498

Browse files
author
fenzhao
committed
Add dollar levels support in webapi
1 parent d773ff0 commit 55e1498

File tree

12 files changed

+1242
-16
lines changed

12 files changed

+1242
-16
lines changed

OData/src/System.Web.OData/OData/EnableQueryAttribute.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,12 @@ private object ExecuteQuery(object response, HttpRequestMessage request, HttpAct
543543
elementClrType,
544544
request.ODataProperties().Path);
545545
ODataQueryOptions queryOptions = new ODataQueryOptions(queryContext, request);
546+
547+
if (queryOptions.SelectExpand != null)
548+
{
549+
queryOptions.SelectExpand.LevelsMaxLiteralExpansionDepth = _validationSettings.MaxExpansionDepth;
550+
}
551+
546552
ValidateQuery(request, queryOptions);
547553

548554
// apply the query

OData/src/System.Web.OData/OData/Query/ODataQueryOptions.cs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using System.Web.OData.Query.Validators;
1919
using Microsoft.OData.Core;
2020
using Microsoft.OData.Core.UriParser;
21+
using Microsoft.OData.Core.UriParser.Semantic;
2122
using Microsoft.OData.Edm;
2223

2324
namespace System.Web.OData.Query
@@ -284,8 +285,15 @@ public virtual IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySett
284285

285286
if (SelectExpand != null)
286287
{
287-
Request.ODataProperties().SelectExpandClause = SelectExpand.SelectExpandClause;
288-
result = SelectExpand.ApplyTo(result, querySettings);
288+
SelectExpandClause processedClause = SelectExpand.ProcessLevels();
289+
SelectExpandQueryOption newSelectExpand = new SelectExpandQueryOption(
290+
SelectExpand.RawSelect,
291+
SelectExpand.RawExpand,
292+
SelectExpand.Context,
293+
processedClause);
294+
295+
Request.ODataProperties().SelectExpandClause = processedClause;
296+
result = newSelectExpand.ApplyTo(result, querySettings);
289297
}
290298

291299
if (querySettings.PageSize.HasValue)
@@ -328,8 +336,15 @@ public virtual object ApplyTo(object entity, ODataQuerySettings querySettings)
328336

329337
if (SelectExpand != null)
330338
{
331-
Request.ODataProperties().SelectExpandClause = SelectExpand.SelectExpandClause;
332-
return SelectExpand.ApplyTo(entity, querySettings);
339+
SelectExpandClause processedClause = SelectExpand.ProcessLevels();
340+
SelectExpandQueryOption newSelectExpand = new SelectExpandQueryOption(
341+
SelectExpand.RawSelect,
342+
SelectExpand.RawExpand,
343+
SelectExpand.Context,
344+
processedClause);
345+
346+
Request.ODataProperties().SelectExpandClause = processedClause;
347+
return newSelectExpand.ApplyTo(entity, querySettings);
333348
}
334349

335350
return entity;

OData/src/System.Web.OData/OData/Query/ODataValidationSettings.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class ODataValidationSettings
1616
private const int MinMaxNodeCount = 1;
1717
private const int MinMaxAnyAllExpressionDepth = 1;
1818
private const int MinMaxOrderByNodeCount = 1;
19+
internal const int DefaultMaxExpansionDepth = 2;
1920

2021
private AllowedArithmeticOperators _allowedArithmeticOperators = AllowedArithmeticOperators.All;
2122
private AllowedFunctions _allowedFunctions = AllowedFunctions.AllFunctions;
@@ -26,7 +27,7 @@ public class ODataValidationSettings
2627
private int? _maxTop;
2728
private int _maxAnyAllExpressionDepth = 1;
2829
private int _maxNodeCount = 100;
29-
private int _maxExpansionDepth = 2;
30+
private int _maxExpansionDepth = DefaultMaxExpansionDepth;
3031
private int _maxOrderByNodeCount = 5;
3132

3233
/// <summary>

OData/src/System.Web.OData/OData/Query/SelectExpandQueryOption.cs

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
using System.Collections.Generic;
44
using System.Diagnostics.CodeAnalysis;
5+
using System.Diagnostics.Contracts;
56
using System.Linq;
67
using System.Web.Http;
78
using System.Web.OData.Properties;
@@ -20,6 +21,7 @@ public class SelectExpandQueryOption
2021
{
2122
private SelectExpandClause _selectExpandClause;
2223
private ODataQueryOptionParser _queryOptionParser;
24+
private int _levelsMaxLiteralExpansionDepth = ODataValidationSettings.DefaultMaxExpansionDepth;
2325

2426
/// <summary>
2527
/// Initializes a new instance of the <see cref="SelectExpandQueryOption"/> class.
@@ -59,6 +61,16 @@ public SelectExpandQueryOption(string select, string expand, ODataQueryContext c
5961
_queryOptionParser = queryOptionParser;
6062
}
6163

64+
internal SelectExpandQueryOption(
65+
string select,
66+
string expand,
67+
ODataQueryContext context,
68+
SelectExpandClause selectExpandClause)
69+
: this(select, expand, context)
70+
{
71+
_selectExpandClause = selectExpandClause;
72+
}
73+
6274
// This constructor is intended for unit testing only.
6375
internal SelectExpandQueryOption(string select, string expand, ODataQueryContext context)
6476
{
@@ -125,6 +137,32 @@ public SelectExpandClause SelectExpandClause
125137
}
126138
}
127139

140+
/// <summary>
141+
/// Gets or sets the number of levels that a top level $expand=NavigationProperty($levels=max)
142+
/// will be expanded.
143+
/// This value will decrease by one with each nesting level in the $expand clause.
144+
/// For example, with a property value 5, the following query $expand=A($expand=B($expand=C($levels=max)))
145+
/// will be interpreted as $expand=A($expand=B($expand=C($levels=3))).
146+
/// If the query gets validated, the <see cref="ODataValidationSettings.MaxExpansionDepth"/> value
147+
/// must be greater than or equal to this value.
148+
/// </summary>
149+
public int LevelsMaxLiteralExpansionDepth
150+
{
151+
get
152+
{
153+
return _levelsMaxLiteralExpansionDepth;
154+
}
155+
set
156+
{
157+
if (value < 0)
158+
{
159+
throw Error.ArgumentMustBeGreaterThanOrEqualTo("LevelsMaxLiteralExpansionDepth", value, 0);
160+
}
161+
162+
_levelsMaxLiteralExpansionDepth = value;
163+
}
164+
}
165+
128166
/// <summary>
129167
/// Applies the $select and $expand query options to the given <see cref="IQueryable"/> using the given
130168
/// <see cref="ODataQuerySettings"/>.
@@ -207,5 +245,155 @@ public void Validate(ODataValidationSettings validationSettings)
207245
Validator.Validate(this, validationSettings);
208246
}
209247
}
248+
249+
internal SelectExpandClause ProcessLevels()
250+
{
251+
bool levelsEncountered;
252+
return ProcessLevels(SelectExpandClause, LevelsMaxLiteralExpansionDepth, out levelsEncountered);
253+
}
254+
255+
// Process $levels in SelectExpandClause.
256+
private static SelectExpandClause ProcessLevels(
257+
SelectExpandClause selectExpandClause,
258+
int levelsMaxLiteralExpansionDepth,
259+
out bool levelsEncountered)
260+
{
261+
levelsEncountered = false;
262+
263+
if (selectExpandClause == null)
264+
{
265+
return null;
266+
}
267+
268+
// Process $levels in SelectItems of SelectExpandClause.
269+
IEnumerable<SelectItem> selectItems = ProcessLevels(
270+
selectExpandClause.SelectedItems,
271+
levelsMaxLiteralExpansionDepth,
272+
out levelsEncountered);
273+
274+
if (levelsEncountered)
275+
{
276+
return new SelectExpandClause(selectItems, selectExpandClause.AllSelected);
277+
}
278+
else
279+
{
280+
// Return the original SelectExpandClause if no $levels is found.
281+
return selectExpandClause;
282+
}
283+
}
284+
285+
// Process $levels in SelectedItems.
286+
private static IEnumerable<SelectItem> ProcessLevels(
287+
IEnumerable<SelectItem> selectItems,
288+
int levelsMaxLiteralExpansionDepth,
289+
out bool levelsEncountered)
290+
{
291+
levelsEncountered = false;
292+
IList<SelectItem> items = new List<SelectItem>();
293+
294+
foreach (SelectItem selectItem in selectItems)
295+
{
296+
ExpandedNavigationSelectItem item = selectItem as ExpandedNavigationSelectItem;
297+
298+
if (item == null)
299+
{
300+
// There is no $levels in non-ExpandedNavigationSelectItem.
301+
items.Add(selectItem);
302+
}
303+
else
304+
{
305+
bool levelsEncouteredInExpand;
306+
// Process $levels in ExpandedNavigationSelectItem.
307+
ExpandedNavigationSelectItem expandItem = ProcessLevels(
308+
item,
309+
levelsMaxLiteralExpansionDepth,
310+
out levelsEncouteredInExpand);
311+
levelsEncountered = levelsEncountered || levelsEncouteredInExpand;
312+
313+
if (expandItem != null)
314+
{
315+
items.Add(expandItem);
316+
}
317+
}
318+
}
319+
320+
return items;
321+
}
322+
323+
// Process $levels in ExpandedNavigationSelectItem.
324+
private static ExpandedNavigationSelectItem ProcessLevels(
325+
ExpandedNavigationSelectItem expandItem,
326+
int levelsMaxLiteralExpansionDepth,
327+
out bool levelsEncounteredInExpand)
328+
{
329+
// Call ProcessLevels on SelectExpandClause recursively.
330+
SelectExpandClause selectExpandClause = ProcessLevels(
331+
expandItem.SelectAndExpand,
332+
levelsMaxLiteralExpansionDepth - 1,
333+
out levelsEncounteredInExpand);
334+
335+
if (expandItem.LevelsOption == null)
336+
{
337+
if (levelsEncounteredInExpand)
338+
{
339+
return new ExpandedNavigationSelectItem(
340+
expandItem.PathToNavigationProperty,
341+
expandItem.NavigationSource,
342+
selectExpandClause);
343+
}
344+
else
345+
{
346+
// Return the original ExpandedNavigationSelectItem if no $levels is found.
347+
return expandItem;
348+
}
349+
}
350+
351+
// There is $levels in current ExpandedNavigationSelectItem.
352+
levelsEncounteredInExpand = true;
353+
int level = expandItem.LevelsOption.IsMaxLevel ?
354+
levelsMaxLiteralExpansionDepth :
355+
(int)expandItem.LevelsOption.Level;
356+
357+
if (level <= 0)
358+
{
359+
// Do not expand if $levels is equal to 0.
360+
return null;
361+
}
362+
363+
// Initialize current SelectExpandClause with processed SelectExpandClause.
364+
SelectExpandClause currentSelectExpandClause = selectExpandClause;
365+
ExpandedNavigationSelectItem item = null;
366+
367+
// Construct new ExpandedNavigationSelectItem with recursive expansion.
368+
while (level > 0)
369+
{
370+
// Construct a new ExpandedNavigationSelectItem with current SelectExpandClause.
371+
item = new ExpandedNavigationSelectItem(
372+
expandItem.PathToNavigationProperty,
373+
expandItem.NavigationSource,
374+
currentSelectExpandClause);
375+
376+
// Update current SelectExpandClause with the new ExpandedNavigationSelectItem.
377+
if (selectExpandClause.AllSelected)
378+
{
379+
currentSelectExpandClause = new SelectExpandClause(
380+
new[] { item }.Concat(selectExpandClause.SelectedItems),
381+
selectExpandClause.AllSelected);
382+
}
383+
else
384+
{
385+
// PathSelectItem is needed for the expanded item if AllSelected is false.
386+
PathSelectItem pathSelectItem = new PathSelectItem(
387+
new ODataSelectPath(expandItem.PathToNavigationProperty));
388+
currentSelectExpandClause = new SelectExpandClause(
389+
new SelectItem[] { item, pathSelectItem }.Concat(selectExpandClause.SelectedItems),
390+
selectExpandClause.AllSelected);
391+
}
392+
393+
level--;
394+
}
395+
396+
return item;
397+
}
210398
}
211399
}

OData/src/System.Web.OData/OData/Query/Validators/SelectExpandQueryValidator.cs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ public virtual void Validate(SelectExpandQueryOption selectExpandQueryOption, OD
3838

3939
if (validationSettings.MaxExpansionDepth > 0)
4040
{
41+
if (selectExpandQueryOption.LevelsMaxLiteralExpansionDepth > validationSettings.MaxExpansionDepth)
42+
{
43+
throw new ODataException(Error.Format(
44+
SRResources.InvalidExpansionDepthValue,
45+
"LevelsMaxLiteralExpansionDepth",
46+
"MaxExpansionDepth"));
47+
}
48+
4149
ValidateDepth(selectExpandQueryOption.SelectExpandClause, validationSettings.MaxExpansionDepth);
4250
}
4351
}
@@ -54,15 +62,34 @@ private static void ValidateDepth(SelectExpandClause selectExpand, int maxDepth)
5462
SelectExpandClause currentNode = tuple.Item2;
5563

5664
ExpandedNavigationSelectItem[] expandItems = currentNode.SelectedItems.OfType<ExpandedNavigationSelectItem>().ToArray();
57-
if (expandItems.Length > 0 && currentDepth == maxDepth)
65+
66+
if (expandItems.Length > 0 &&
67+
((currentDepth == maxDepth &&
68+
expandItems.Any(expandItem =>
69+
expandItem.LevelsOption == null ||
70+
expandItem.LevelsOption.IsMaxLevel ||
71+
expandItem.LevelsOption.Level != 0)) ||
72+
expandItems.Any(expandItem =>
73+
expandItem.LevelsOption != null &&
74+
!expandItem.LevelsOption.IsMaxLevel &&
75+
(expandItem.LevelsOption.Level > Int32.MaxValue ||
76+
expandItem.LevelsOption.Level + currentDepth > maxDepth))))
5877
{
5978
throw new ODataException(
6079
Error.Format(SRResources.MaxExpandDepthExceeded, maxDepth, "MaxExpansionDepth"));
6180
}
6281

6382
foreach (ExpandedNavigationSelectItem expandItem in expandItems)
6483
{
65-
nodesToVisit.Push(Tuple.Create(currentDepth + 1, expandItem.SelectAndExpand));
84+
int depth = currentDepth + 1;
85+
86+
if (expandItem.LevelsOption != null && !expandItem.LevelsOption.IsMaxLevel)
87+
{
88+
// Add the value of $levels for next depth.
89+
depth = depth + (int)expandItem.LevelsOption.Level - 1;
90+
}
91+
92+
nodesToVisit.Push(Tuple.Create(depth, expandItem.SelectAndExpand));
6693
}
6794
}
6895
}

OData/src/System.Web.OData/Properties/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.

OData/src/System.Web.OData/Properties/SRResources.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,9 @@
535535
<data name="BatchRequestMissingContent" xml:space="preserve">
536536
<value>The 'Content' property on the batch request cannot be null.</value>
537537
</data>
538+
<data name="InvalidExpansionDepthValue" xml:space="preserve">
539+
<value>'{0}' should be less than or equal to '{1}'.</value>
540+
</data>
538541
<data name="MaxExpandDepthExceeded" xml:space="preserve">
539542
<value>The request includes a $expand path which is too deep. The maximum depth allowed is {0}. To increase the limit, set the '{1}' property on EnableQueryAttribute or ODataValidationSettings.</value>
540543
</data>

0 commit comments

Comments
 (0)