Skip to content

Commit

Permalink
Add support for dynamic properties to queries (#102)
Browse files Browse the repository at this point in the history
* Added support for dynamic properties to query filters.

* Refactored dynamic property resolving to static method ODataProperty.FromPath()

* Added validation of model type when using ODataProperty.FromPath<T>()

* Added dynamic property resolving to README.
  • Loading branch information
prochnowc authored Sep 1, 2022
1 parent 0168279 commit 4c2eb8c
Show file tree
Hide file tree
Showing 13 changed files with 381 additions and 201 deletions.
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ Library for creating complex OData queries (OData version 4.01) based on data mo
* type
* [`cast`](#cast)
* sorting by several fields with indication of direction

* dynamic properties

## Installation
To install `OData.QueryBuilder` from `Visual Studio`, find `OData.QueryBuilder` in the `NuGet` package manager user interface or enter the following command in the package manager console:
```
Expand Down Expand Up @@ -383,6 +384,20 @@ var strings = new string[] {
```
> $filter=ODataKind/ODataCode/Code in ('test%5C%5CYUYYUT','test1%5C%5CYUYY123')

## Using dynamic properties

For use-cases where a property which is used for queries needs to be dynamically resolved (eg. the user chooses the property name to filter on)
a special method `ODataProperty.FromPath<TProperty>()` can be invoked in the query expressions:

```csharp
...
.Filter(s => ODataProperty.FromPath<int>("IdRule") == 3))
```

Note that the generic type argument `TProperty` needs to match the type of the dynamically
resolved property in the OData model.


## Suppress exceptions

:warning: __May result in loss of control over the expected result.__
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public ODataQueryExpand(ODataQueryBuilderOptions odataQueryBuilderOptions)
}
public IODataQueryExpand<TEntity> Expand(Expression<Func<TEntity, object>> expandNested)
{
var query = new ODataOptionExpandExpressionVisitor().ToQuery(expandNested.Body);
var query = new ODataOptionExpandExpressionVisitor().ToQuery(expandNested);

_stringBuilder.Append($"{ODataOptionNames.Expand}{QuerySeparators.EqualSign}{query}{QuerySeparators.Nested}");

Expand All @@ -41,28 +41,28 @@ public IODataQueryExpand<TEntity> Expand(Action<IODataExpandResource<TEntity>> e

public IODataQueryExpand<TEntity> Filter(Expression<Func<TEntity, bool>> filterNested, bool useParenthesis = false)
{
var query = new ODataOptionFilterExpressionVisitor(_odataQueryBuilderOptions).ToQuery(filterNested.Body, useParenthesis);
var query = new ODataOptionFilterExpressionVisitor(_odataQueryBuilderOptions).ToQuery(filterNested, useParenthesis);

return Filter(query);
}

public IODataQueryExpand<TEntity> Filter(Expression<Func<TEntity, IODataFunction, bool>> filterNested, bool useParenthesis = false)
{
var query = new ODataOptionFilterExpressionVisitor(_odataQueryBuilderOptions).ToQuery(filterNested.Body, useParenthesis);
var query = new ODataOptionFilterExpressionVisitor(_odataQueryBuilderOptions).ToQuery(filterNested, useParenthesis);

return Filter(query);
}

public IODataQueryExpand<TEntity> Filter(Expression<Func<TEntity, IODataFunction, IODataOperator, bool>> filterNested, bool useParenthesis = false)
{
var query = new ODataOptionFilterExpressionVisitor(_odataQueryBuilderOptions).ToQuery(filterNested.Body, useParenthesis);
var query = new ODataOptionFilterExpressionVisitor(_odataQueryBuilderOptions).ToQuery(filterNested, useParenthesis);

return Filter(query);
}

public IODataQueryExpand<TEntity> OrderBy(Expression<Func<TEntity, object>> orderByNested)
{
var query = new ODataOptionOrderByExpressionVisitor().ToQuery(orderByNested.Body);
var query = new ODataOptionOrderByExpressionVisitor().ToQuery(orderByNested);

_stringBuilder.Append($"{ODataOptionNames.OrderBy}{QuerySeparators.EqualSign}{query} {QuerySorts.Asc}{QuerySeparators.Nested}");

Expand All @@ -71,7 +71,7 @@ public IODataQueryExpand<TEntity> OrderBy(Expression<Func<TEntity, object>> orde

public IODataQueryExpand<TEntity> OrderBy(Expression<Func<TEntity, ISortFunction, object>> orderByNested)
{
var query = new ODataOptionOrderByExpressionVisitor().ToQuery(orderByNested.Body);
var query = new ODataOptionOrderByExpressionVisitor().ToQuery(orderByNested);

_stringBuilder.Append($"{ODataOptionNames.OrderBy}{QuerySeparators.EqualSign}{query}{QuerySeparators.Nested}");

Expand All @@ -80,7 +80,7 @@ public IODataQueryExpand<TEntity> OrderBy(Expression<Func<TEntity, ISortFunction

public IODataQueryExpand<TEntity> OrderByDescending(Expression<Func<TEntity, object>> orderByDescendingNested)
{
var query = new ODataOptionOrderByExpressionVisitor().ToQuery(orderByDescendingNested.Body);
var query = new ODataOptionOrderByExpressionVisitor().ToQuery(orderByDescendingNested);

_stringBuilder.Append($"{ODataOptionNames.OrderBy}{QuerySeparators.EqualSign}{query} {QuerySorts.Desc}{QuerySeparators.Nested}");

Expand All @@ -89,7 +89,7 @@ public IODataQueryExpand<TEntity> OrderByDescending(Expression<Func<TEntity, obj

public IODataQueryExpand<TEntity> Select(Expression<Func<TEntity, object>> selectNested)
{
var query = new ODataOptionSelectExpressionVisitor().ToQuery(selectNested.Body);
var query = new ODataOptionSelectExpressionVisitor().ToQuery(selectNested);

_stringBuilder.Append($"{ODataOptionNames.Select}{QuerySeparators.EqualSign}{query}{QuerySeparators.Nested}");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,28 @@ public ODataQueryCollection(StringBuilder stringBuilder, ODataQueryBuilderOption

public IODataQueryCollection<TEntity> Filter(Expression<Func<TEntity, bool>> filter, bool useParenthesis = false)
{
var query = new ODataOptionFilterExpressionVisitor(_odataQueryBuilderOptions).ToQuery(filter.Body, useParenthesis);
var query = new ODataOptionFilterExpressionVisitor(_odataQueryBuilderOptions).ToQuery(filter, useParenthesis);

return Filter(query);
}

public IODataQueryCollection<TEntity> Filter(Expression<Func<TEntity, IODataFunction, bool>> filter, bool useParenthesis = false)
{
var query = new ODataOptionFilterExpressionVisitor(_odataQueryBuilderOptions).ToQuery(filter.Body, useParenthesis);
var query = new ODataOptionFilterExpressionVisitor(_odataQueryBuilderOptions).ToQuery(filter, useParenthesis);

return Filter(query);
}

public IODataQueryCollection<TEntity> Filter(Expression<Func<TEntity, IODataFunction, IODataOperator, bool>> filter, bool useParenthesis = false)
{
var query = new ODataOptionFilterExpressionVisitor(_odataQueryBuilderOptions).ToQuery(filter.Body, useParenthesis);
var query = new ODataOptionFilterExpressionVisitor(_odataQueryBuilderOptions).ToQuery(filter, useParenthesis);

return Filter(query);
}

public IODataQueryCollection<TEntity> Expand(Expression<Func<TEntity, object>> expand)
{
var query = new ODataOptionExpandExpressionVisitor().ToQuery(expand.Body);
var query = new ODataOptionExpandExpressionVisitor().ToQuery(expand);

return Expand(query);
}
Expand All @@ -62,7 +62,7 @@ public IODataQueryCollection<TEntity> Expand(Action<IODataExpandResource<TEntity

public IODataQueryCollection<TEntity> Select(Expression<Func<TEntity, object>> select)
{
var query = new ODataOptionSelectExpressionVisitor().ToQuery(select.Body);
var query = new ODataOptionSelectExpressionVisitor().ToQuery(select);

_stringBuilder.Append($"{ODataOptionNames.Select}{QuerySeparators.EqualSign}{query}{QuerySeparators.Main}");

Expand All @@ -71,7 +71,7 @@ public IODataQueryCollection<TEntity> Select(Expression<Func<TEntity, object>> s

public IODataQueryCollection<TEntity> OrderBy(Expression<Func<TEntity, object>> orderBy)
{
var query = new ODataOptionOrderByExpressionVisitor().ToQuery(orderBy.Body);
var query = new ODataOptionOrderByExpressionVisitor().ToQuery(orderBy);

_stringBuilder.Append($"{ODataOptionNames.OrderBy}{QuerySeparators.EqualSign}{query} {QuerySorts.Asc}{QuerySeparators.Main}");

Expand All @@ -80,7 +80,7 @@ public IODataQueryCollection<TEntity> OrderBy(Expression<Func<TEntity, object>>

public IODataQueryCollection<TEntity> OrderBy(Expression<Func<TEntity, ISortFunction, object>> orderBy)
{
var query = new ODataOptionOrderByExpressionVisitor().ToQuery(orderBy.Body);
var query = new ODataOptionOrderByExpressionVisitor().ToQuery(orderBy);

_stringBuilder.Append($"{ODataOptionNames.OrderBy}{QuerySeparators.EqualSign}{query}{QuerySeparators.Main}");

Expand All @@ -89,7 +89,7 @@ public IODataQueryCollection<TEntity> OrderBy(Expression<Func<TEntity, ISortFunc

public IODataQueryCollection<TEntity> OrderByDescending(Expression<Func<TEntity, object>> orderByDescending)
{
var query = new ODataOptionOrderByExpressionVisitor().ToQuery(orderByDescending.Body);
var query = new ODataOptionOrderByExpressionVisitor().ToQuery(orderByDescending);

_stringBuilder.Append($"{ODataOptionNames.OrderBy}{QuerySeparators.EqualSign}{query} {QuerySorts.Desc}{QuerySeparators.Main}");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public IAddressingEntries<TResource> For<TResource>(Expression<Func<TEntity, obj

public IODataQueryKey<TEntity> Expand(Expression<Func<TEntity, object>> expand)
{
var query = new ODataOptionExpandExpressionVisitor().ToQuery(expand.Body);
var query = new ODataOptionExpandExpressionVisitor().ToQuery(expand);

return Expand(query);
}
Expand All @@ -45,7 +45,7 @@ public IODataQueryKey<TEntity> Expand(Action<IODataExpandResource<TEntity>> expa

public IODataQueryKey<TEntity> Select(Expression<Func<TEntity, object>> select)
{
var query = new ODataOptionSelectExpressionVisitor().ToQuery(select.Body);
var query = new ODataOptionSelectExpressionVisitor().ToQuery(select);

_stringBuilder.Append($"{ODataOptionNames.Select}{QuerySeparators.EqualSign}{query}{QuerySeparators.Main}");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public ODataExpandResource(ODataQueryBuilderOptions odataQueryBuilderOptions)

public IODataQueryExpand<TNestedEntity> For<TNestedEntity>(Expression<Func<TEntity, object>> nestedExpand)
{
var query = new ODataResourceExpressionVisitor().ToQuery(nestedExpand.Body);
var query = new ODataResourceExpressionVisitor().ToQuery(nestedExpand);

if (_odataQueryExpand?.Query?.Length > 0)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public IAddressingEntries<TEntity> For<TEntity>(Expression<Func<TResource, objec
throw new ArgumentNullException(nameof(resource), "Resource name is null");
}

var query = new ODataResourceExpressionVisitor().ToQuery(resource.Body);
var query = new ODataResourceExpressionVisitor().ToQuery(resource);

_stringBuilder.Append(query);

Expand Down
17 changes: 17 additions & 0 deletions src/OData.QueryBuilder/Conventions/Functions/ODataProperty.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;

namespace OData.QueryBuilder
{
public static class ODataProperty
{
/// <summary>
/// Dynamically resolved a property from the OData entity.
/// </summary>
/// <param name="propertyPath">The path to the property or field using dot separation for each path component.</param>
/// <typeparam name="TProperty">The type of the property.</typeparam>
public static TProperty FromPath<TProperty>(string propertyPath)
{
throw new NotSupportedException("This method is only valid in OData query expressions.");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using OData.QueryBuilder.Extensions;
using System;
using OData.QueryBuilder.Extensions;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;

Expand All @@ -10,44 +11,72 @@ public ODataExpressionVisitor()
{
}

protected string VisitExpression(Expression expression) => expression switch
protected string VisitExpression(LambdaExpression topExpression, Expression expression) => expression switch
{
BinaryExpression binaryExpression => VisitBinaryExpression(binaryExpression),
MemberExpression memberExpression => VisitMemberExpression(memberExpression),
ConstantExpression constantExpression => VisitConstantExpression(constantExpression),
MethodCallExpression methodCallExpression => VisitMethodCallExpression(methodCallExpression),
NewExpression newExpression => VisitNewExpression(newExpression),
UnaryExpression unaryExpression => VisitUnaryExpression(unaryExpression),
LambdaExpression lambdaExpression => VisitLambdaExpression(lambdaExpression),
ParameterExpression parameterExpression => VisitParameterExpression(parameterExpression),
BinaryExpression binaryExpression => VisitBinaryExpression(topExpression, binaryExpression),
MemberExpression memberExpression => VisitMemberExpression(topExpression, memberExpression),
ConstantExpression constantExpression => VisitConstantExpression(topExpression, constantExpression),
MethodCallExpression methodCallExpression => VisitMethodCallExpression(topExpression, methodCallExpression),
NewExpression newExpression => VisitNewExpression(topExpression, newExpression),
UnaryExpression unaryExpression => VisitUnaryExpression(topExpression, unaryExpression),
LambdaExpression lambdaExpression => VisitLambdaExpression(topExpression, lambdaExpression),
ParameterExpression parameterExpression => VisitParameterExpression(topExpression, parameterExpression),
_ => default,
};

[ExcludeFromCodeCoverage]
protected virtual string VisitBinaryExpression(BinaryExpression binaryExpression) => default;
protected virtual string VisitBinaryExpression(LambdaExpression topExpression, BinaryExpression binaryExpression) => default;

[ExcludeFromCodeCoverage]
protected virtual string VisitConstantExpression(ConstantExpression constantExpression) => default;
protected virtual string VisitConstantExpression(LambdaExpression topExpression, ConstantExpression constantExpression) => default;

[ExcludeFromCodeCoverage]
protected virtual string VisitMethodCallExpression(MethodCallExpression methodCallExpression) => default;
protected virtual string VisitMethodCallExpression(LambdaExpression topExpression, MethodCallExpression methodCallExpression)
{
if (methodCallExpression.Method.DeclaringType == typeof(ODataProperty))
{
switch (methodCallExpression.Method.Name)
{
case nameof(ODataProperty.FromPath):
string propertyPath = (string)new ValueExpression().GetValue(methodCallExpression.Arguments[0]);
var propertyNames = propertyPath.Split('.');

MemberExpression memberExpression = Expression.PropertyOrField(
Expression.Parameter(topExpression.Parameters[0].Type, "m"),
propertyNames[0]);

for (var index = 1; index < propertyNames.Length; index++)
{
memberExpression = Expression.PropertyOrField(memberExpression, propertyNames[index]);
}

var genericMethodType = methodCallExpression.Method.GetGenericArguments()[0];
if (genericMethodType != memberExpression.Type)
throw new ArgumentException(
$"The type '{genericMethodType.FullName}' specified when calling 'ODataProperty.FromPath<T>(\"{propertyPath}\")' does not match the expected type '{memberExpression.Type.FullName}' defined by the model.");

return VisitMemberExpression(topExpression, memberExpression);
}
}

throw new NotSupportedException($"Method {methodCallExpression.Method.Name} not supported");
}

[ExcludeFromCodeCoverage]
protected virtual string VisitNewExpression(NewExpression newExpression) => default;
protected virtual string VisitNewExpression(LambdaExpression topExpression, NewExpression newExpression) => default;

[ExcludeFromCodeCoverage]
protected virtual string VisitUnaryExpression(UnaryExpression unaryExpression) => default;
protected virtual string VisitUnaryExpression(LambdaExpression topExpression, UnaryExpression unaryExpression) => default;

[ExcludeFromCodeCoverage]
protected virtual string VisitLambdaExpression(LambdaExpression lambdaExpression) => default;
protected virtual string VisitLambdaExpression(LambdaExpression topExpression, LambdaExpression lambdaExpression) => default;

[ExcludeFromCodeCoverage]
protected virtual string VisitParameterExpression(ParameterExpression parameterExpression) => default;
protected virtual string VisitParameterExpression(LambdaExpression topExpression, ParameterExpression parameterExpression) => default;

[ExcludeFromCodeCoverage]
protected virtual string VisitMemberExpression(MemberExpression memberExpression)
protected virtual string VisitMemberExpression(LambdaExpression topExpression, MemberExpression memberExpression)
{
var memberName = VisitExpression(memberExpression.Expression);
var memberName = VisitExpression(topExpression, memberExpression.Expression);

if (string.IsNullOrEmpty(memberName))
{
Expand All @@ -60,7 +89,7 @@ protected virtual string VisitMemberExpression(MemberExpression memberExpression
$"{memberName}/{memberExpression.Member.Name}";
}

public virtual string ToQuery(Expression expression) =>
VisitExpression(expression);
public virtual string ToQuery(LambdaExpression expression) =>
VisitExpression(expression, expression.Body);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ public ODataOptionExpressionVisitor()
{
}

protected override string VisitUnaryExpression(UnaryExpression unaryExpression)
protected override string VisitUnaryExpression(LambdaExpression topExpression, UnaryExpression unaryExpression)
{
var odataOperator = unaryExpression.NodeType.ToODataOperator();
var whitespace = odataOperator != default ? " " : default;

return $"{odataOperator}{whitespace}{VisitExpression(unaryExpression.Operand)}";
return $"{odataOperator}{whitespace}{VisitExpression(topExpression, unaryExpression.Operand)}";
}

protected override string VisitNewExpression(NewExpression newExpression)
protected override string VisitNewExpression(LambdaExpression topExpression, NewExpression newExpression)
{
var names = new string[newExpression.Members.Count];

Expand Down
Loading

0 comments on commit 4c2eb8c

Please sign in to comment.