Skip to content

Commit

Permalink
Add dollar levels support in webapi
Browse files Browse the repository at this point in the history
  • Loading branch information
fenzhao committed Aug 12, 2014
1 parent d773ff0 commit 55e1498
Show file tree
Hide file tree
Showing 12 changed files with 1,242 additions and 16 deletions.
6 changes: 6 additions & 0 deletions OData/src/System.Web.OData/OData/EnableQueryAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,12 @@ private object ExecuteQuery(object response, HttpRequestMessage request, HttpAct
elementClrType,
request.ODataProperties().Path);
ODataQueryOptions queryOptions = new ODataQueryOptions(queryContext, request);

if (queryOptions.SelectExpand != null)
{
queryOptions.SelectExpand.LevelsMaxLiteralExpansionDepth = _validationSettings.MaxExpansionDepth;
}

ValidateQuery(request, queryOptions);

// apply the query
Expand Down
23 changes: 19 additions & 4 deletions OData/src/System.Web.OData/OData/Query/ODataQueryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using System.Web.OData.Query.Validators;
using Microsoft.OData.Core;
using Microsoft.OData.Core.UriParser;
using Microsoft.OData.Core.UriParser.Semantic;
using Microsoft.OData.Edm;

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

if (SelectExpand != null)
{
Request.ODataProperties().SelectExpandClause = SelectExpand.SelectExpandClause;
result = SelectExpand.ApplyTo(result, querySettings);
SelectExpandClause processedClause = SelectExpand.ProcessLevels();
SelectExpandQueryOption newSelectExpand = new SelectExpandQueryOption(
SelectExpand.RawSelect,
SelectExpand.RawExpand,
SelectExpand.Context,
processedClause);

Request.ODataProperties().SelectExpandClause = processedClause;
result = newSelectExpand.ApplyTo(result, querySettings);
}

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

if (SelectExpand != null)
{
Request.ODataProperties().SelectExpandClause = SelectExpand.SelectExpandClause;
return SelectExpand.ApplyTo(entity, querySettings);
SelectExpandClause processedClause = SelectExpand.ProcessLevels();
SelectExpandQueryOption newSelectExpand = new SelectExpandQueryOption(
SelectExpand.RawSelect,
SelectExpand.RawExpand,
SelectExpand.Context,
processedClause);

Request.ODataProperties().SelectExpandClause = processedClause;
return newSelectExpand.ApplyTo(entity, querySettings);
}

return entity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class ODataValidationSettings
private const int MinMaxNodeCount = 1;
private const int MinMaxAnyAllExpressionDepth = 1;
private const int MinMaxOrderByNodeCount = 1;
internal const int DefaultMaxExpansionDepth = 2;

private AllowedArithmeticOperators _allowedArithmeticOperators = AllowedArithmeticOperators.All;
private AllowedFunctions _allowedFunctions = AllowedFunctions.AllFunctions;
Expand All @@ -26,7 +27,7 @@ public class ODataValidationSettings
private int? _maxTop;
private int _maxAnyAllExpressionDepth = 1;
private int _maxNodeCount = 100;
private int _maxExpansionDepth = 2;
private int _maxExpansionDepth = DefaultMaxExpansionDepth;
private int _maxOrderByNodeCount = 5;

/// <summary>
Expand Down
188 changes: 188 additions & 0 deletions OData/src/System.Web.OData/OData/Query/SelectExpandQueryOption.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Web.Http;
using System.Web.OData.Properties;
Expand All @@ -20,6 +21,7 @@ public class SelectExpandQueryOption
{
private SelectExpandClause _selectExpandClause;
private ODataQueryOptionParser _queryOptionParser;
private int _levelsMaxLiteralExpansionDepth = ODataValidationSettings.DefaultMaxExpansionDepth;

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

internal SelectExpandQueryOption(
string select,
string expand,
ODataQueryContext context,
SelectExpandClause selectExpandClause)
: this(select, expand, context)
{
_selectExpandClause = selectExpandClause;
}

// This constructor is intended for unit testing only.
internal SelectExpandQueryOption(string select, string expand, ODataQueryContext context)
{
Expand Down Expand Up @@ -125,6 +137,32 @@ public SelectExpandClause SelectExpandClause
}
}

/// <summary>
/// Gets or sets the number of levels that a top level $expand=NavigationProperty($levels=max)
/// will be expanded.
/// This value will decrease by one with each nesting level in the $expand clause.
/// For example, with a property value 5, the following query $expand=A($expand=B($expand=C($levels=max)))
/// will be interpreted as $expand=A($expand=B($expand=C($levels=3))).
/// If the query gets validated, the <see cref="ODataValidationSettings.MaxExpansionDepth"/> value
/// must be greater than or equal to this value.
/// </summary>
public int LevelsMaxLiteralExpansionDepth
{
get
{
return _levelsMaxLiteralExpansionDepth;
}
set
{
if (value < 0)
{
throw Error.ArgumentMustBeGreaterThanOrEqualTo("LevelsMaxLiteralExpansionDepth", value, 0);
}

_levelsMaxLiteralExpansionDepth = value;
}
}

/// <summary>
/// Applies the $select and $expand query options to the given <see cref="IQueryable"/> using the given
/// <see cref="ODataQuerySettings"/>.
Expand Down Expand Up @@ -207,5 +245,155 @@ public void Validate(ODataValidationSettings validationSettings)
Validator.Validate(this, validationSettings);
}
}

internal SelectExpandClause ProcessLevels()
{
bool levelsEncountered;
return ProcessLevels(SelectExpandClause, LevelsMaxLiteralExpansionDepth, out levelsEncountered);
}

// Process $levels in SelectExpandClause.
private static SelectExpandClause ProcessLevels(
SelectExpandClause selectExpandClause,
int levelsMaxLiteralExpansionDepth,
out bool levelsEncountered)
{
levelsEncountered = false;

if (selectExpandClause == null)
{
return null;
}

// Process $levels in SelectItems of SelectExpandClause.
IEnumerable<SelectItem> selectItems = ProcessLevels(
selectExpandClause.SelectedItems,
levelsMaxLiteralExpansionDepth,
out levelsEncountered);

if (levelsEncountered)
{
return new SelectExpandClause(selectItems, selectExpandClause.AllSelected);
}
else
{
// Return the original SelectExpandClause if no $levels is found.
return selectExpandClause;
}
}

// Process $levels in SelectedItems.
private static IEnumerable<SelectItem> ProcessLevels(
IEnumerable<SelectItem> selectItems,
int levelsMaxLiteralExpansionDepth,
out bool levelsEncountered)
{
levelsEncountered = false;
IList<SelectItem> items = new List<SelectItem>();

foreach (SelectItem selectItem in selectItems)
{
ExpandedNavigationSelectItem item = selectItem as ExpandedNavigationSelectItem;

if (item == null)
{
// There is no $levels in non-ExpandedNavigationSelectItem.
items.Add(selectItem);
}
else
{
bool levelsEncouteredInExpand;
// Process $levels in ExpandedNavigationSelectItem.
ExpandedNavigationSelectItem expandItem = ProcessLevels(
item,
levelsMaxLiteralExpansionDepth,
out levelsEncouteredInExpand);
levelsEncountered = levelsEncountered || levelsEncouteredInExpand;

if (expandItem != null)
{
items.Add(expandItem);
}
}
}

return items;
}

// Process $levels in ExpandedNavigationSelectItem.
private static ExpandedNavigationSelectItem ProcessLevels(
ExpandedNavigationSelectItem expandItem,
int levelsMaxLiteralExpansionDepth,
out bool levelsEncounteredInExpand)
{
// Call ProcessLevels on SelectExpandClause recursively.
SelectExpandClause selectExpandClause = ProcessLevels(
expandItem.SelectAndExpand,
levelsMaxLiteralExpansionDepth - 1,
out levelsEncounteredInExpand);

if (expandItem.LevelsOption == null)
{
if (levelsEncounteredInExpand)
{
return new ExpandedNavigationSelectItem(
expandItem.PathToNavigationProperty,
expandItem.NavigationSource,
selectExpandClause);
}
else
{
// Return the original ExpandedNavigationSelectItem if no $levels is found.
return expandItem;
}
}

// There is $levels in current ExpandedNavigationSelectItem.
levelsEncounteredInExpand = true;
int level = expandItem.LevelsOption.IsMaxLevel ?
levelsMaxLiteralExpansionDepth :
(int)expandItem.LevelsOption.Level;

if (level <= 0)
{
// Do not expand if $levels is equal to 0.
return null;
}

// Initialize current SelectExpandClause with processed SelectExpandClause.
SelectExpandClause currentSelectExpandClause = selectExpandClause;
ExpandedNavigationSelectItem item = null;

// Construct new ExpandedNavigationSelectItem with recursive expansion.
while (level > 0)
{
// Construct a new ExpandedNavigationSelectItem with current SelectExpandClause.
item = new ExpandedNavigationSelectItem(
expandItem.PathToNavigationProperty,
expandItem.NavigationSource,
currentSelectExpandClause);

// Update current SelectExpandClause with the new ExpandedNavigationSelectItem.
if (selectExpandClause.AllSelected)
{
currentSelectExpandClause = new SelectExpandClause(
new[] { item }.Concat(selectExpandClause.SelectedItems),
selectExpandClause.AllSelected);
}
else
{
// PathSelectItem is needed for the expanded item if AllSelected is false.
PathSelectItem pathSelectItem = new PathSelectItem(
new ODataSelectPath(expandItem.PathToNavigationProperty));
currentSelectExpandClause = new SelectExpandClause(
new SelectItem[] { item, pathSelectItem }.Concat(selectExpandClause.SelectedItems),
selectExpandClause.AllSelected);
}

level--;
}

return item;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ public virtual void Validate(SelectExpandQueryOption selectExpandQueryOption, OD

if (validationSettings.MaxExpansionDepth > 0)
{
if (selectExpandQueryOption.LevelsMaxLiteralExpansionDepth > validationSettings.MaxExpansionDepth)
{
throw new ODataException(Error.Format(
SRResources.InvalidExpansionDepthValue,
"LevelsMaxLiteralExpansionDepth",
"MaxExpansionDepth"));
}

ValidateDepth(selectExpandQueryOption.SelectExpandClause, validationSettings.MaxExpansionDepth);
}
}
Expand All @@ -54,15 +62,34 @@ private static void ValidateDepth(SelectExpandClause selectExpand, int maxDepth)
SelectExpandClause currentNode = tuple.Item2;

ExpandedNavigationSelectItem[] expandItems = currentNode.SelectedItems.OfType<ExpandedNavigationSelectItem>().ToArray();
if (expandItems.Length > 0 && currentDepth == maxDepth)

if (expandItems.Length > 0 &&
((currentDepth == maxDepth &&
expandItems.Any(expandItem =>
expandItem.LevelsOption == null ||
expandItem.LevelsOption.IsMaxLevel ||
expandItem.LevelsOption.Level != 0)) ||
expandItems.Any(expandItem =>
expandItem.LevelsOption != null &&
!expandItem.LevelsOption.IsMaxLevel &&
(expandItem.LevelsOption.Level > Int32.MaxValue ||
expandItem.LevelsOption.Level + currentDepth > maxDepth))))
{
throw new ODataException(
Error.Format(SRResources.MaxExpandDepthExceeded, maxDepth, "MaxExpansionDepth"));
}

foreach (ExpandedNavigationSelectItem expandItem in expandItems)
{
nodesToVisit.Push(Tuple.Create(currentDepth + 1, expandItem.SelectAndExpand));
int depth = currentDepth + 1;

if (expandItem.LevelsOption != null && !expandItem.LevelsOption.IsMaxLevel)
{
// Add the value of $levels for next depth.
depth = depth + (int)expandItem.LevelsOption.Level - 1;
}

nodesToVisit.Push(Tuple.Create(depth, expandItem.SelectAndExpand));
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions OData/src/System.Web.OData/Properties/SRResources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions OData/src/System.Web.OData/Properties/SRResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,9 @@
<data name="BatchRequestMissingContent" xml:space="preserve">
<value>The 'Content' property on the batch request cannot be null.</value>
</data>
<data name="InvalidExpansionDepthValue" xml:space="preserve">
<value>'{0}' should be less than or equal to '{1}'.</value>
</data>
<data name="MaxExpandDepthExceeded" xml:space="preserve">
<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>
</data>
Expand Down
Loading

0 comments on commit 55e1498

Please sign in to comment.