Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support reusing Expressions between filters on the expand level and any/all conditions on the root level. #124

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace OData.QueryBuilder.Conventions.Operators
{
public interface IODataOperator
{
bool In<T>(T columnName, IEnumerable<T> values);

bool All<T>(IEnumerable<T> columnName, Func<T, bool> func);
bool All<T>(IEnumerable<T> columnName, Expression<Func<T, bool>> func);

bool Any<T>(IEnumerable<T> columnName);

bool Any<T>(IEnumerable<T> columnName, Func<T, bool> func);
bool Any<T>(IEnumerable<T> columnName, Expression<Func<T, bool>> func);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,21 @@ protected override string VisitBinaryExpression(LambdaExpression topExpression,
:
$"{left} {binaryExpression.NodeType.ToODataOperator()} {right}";
}

protected override string VisitMemberExpression(LambdaExpression topExpression, MemberExpression memberExpression) =>
IsMemberExpressionBelongsResource(memberExpression)
? base.VisitMemberExpression(topExpression, memberExpression)
: _valueExpression.GetValue(memberExpression).ToQuery(_odataQueryBuilderOptions);

protected override string VisitMemberExpression(LambdaExpression topExpression, MemberExpression memberExpression)
{
if (IsMemberExpressionBelongsResource(memberExpression))
{
return base.VisitMemberExpression(topExpression, memberExpression);
}

var value = _valueExpression.GetValue(memberExpression);
if (value is Expression expression)
{
return VisitExpression(topExpression, expression);
}
return value.ToQuery(_odataQueryBuilderOptions);
}

protected override string VisitConstantExpression(LambdaExpression topExpression, ConstantExpression constantExpression) =>
constantExpression.Value.ToQuery(_odataQueryBuilderOptions);
Expand All @@ -65,8 +75,8 @@ protected override string VisitMethodCallExpression(LambdaExpression topExpressi
{
case nameof(IODataOperator.In):
var in0 = VisitExpression(topExpression, methodCallExpression.Arguments[0]);
var in1 = _valueExpression
.GetValue(methodCallExpression.Arguments[1])
var in1 = _valueExpression
.GetValue(methodCallExpression.Arguments[1])
.ToQuery(_odataQueryBuilderOptions);

if (in1.IsNullOrQuotes())
Expand Down Expand Up @@ -106,8 +116,8 @@ protected override string VisitMethodCallExpression(LambdaExpression topExpressi

return $"{any0}/{nameof(IODataOperator.Any).ToLowerInvariant()}({any1})";
}
}
}

if (declaringType.IsAssignableFrom(typeof(IODataFunction)))
{
switch (methodCallExpression.Method.Name)
Expand All @@ -117,8 +127,8 @@ protected override string VisitMethodCallExpression(LambdaExpression topExpressi

return $"{nameof(IODataFunction.Date).ToLowerInvariant()}({date0})";
case nameof(IODataFunction.SubstringOf):
var substringOf0 = _valueExpression
.GetValue(methodCallExpression.Arguments[0])
var substringOf0 = _valueExpression
.GetValue(methodCallExpression.Arguments[0])
.ToQuery(_odataQueryBuilderOptions);
var substringOf1 = VisitExpression(topExpression, methodCallExpression.Arguments[1]);

Expand All @@ -136,8 +146,8 @@ protected override string VisitMethodCallExpression(LambdaExpression topExpressi
$"{nameof(IODataFunction.SubstringOf).ToLowerInvariant()}({substringOf0},{substringOf1})";
case nameof(IODataFunction.Contains):
var contains0 = VisitExpression(topExpression, methodCallExpression.Arguments[0]);
var contains1 = _valueExpression
.GetValue(methodCallExpression.Arguments[1])
var contains1 = _valueExpression
.GetValue(methodCallExpression.Arguments[1])
.ToQuery(_odataQueryBuilderOptions);

if (contains1.IsNullOrQuotes())
Expand All @@ -153,8 +163,8 @@ protected override string VisitMethodCallExpression(LambdaExpression topExpressi
return $"{nameof(IODataFunction.Contains).ToLowerInvariant()}({contains0},{contains1})";
case nameof(IODataFunction.StartsWith):
var startsWith0 = VisitExpression(topExpression, methodCallExpression.Arguments[0]);
var startsWith1 = _valueExpression
.GetValue(methodCallExpression.Arguments[1])
var startsWith1 = _valueExpression
.GetValue(methodCallExpression.Arguments[1])
.ToQuery(_odataQueryBuilderOptions);

if (startsWith1.IsNullOrQuotes())
Expand Down Expand Up @@ -213,8 +223,8 @@ protected override string VisitMethodCallExpression(LambdaExpression topExpressi
return dateTimeOffset.ToString(
(string)_valueExpression.GetValue(methodCallExpression.Arguments[1]));
case nameof(ICustomFunction.ReplaceCharacters):
var @symbol0 = _valueExpression
.GetValue(methodCallExpression.Arguments[0])
var @symbol0 = _valueExpression
.GetValue(methodCallExpression.Arguments[0])
.ToQuery(_odataQueryBuilderOptions);
var @symbol1 = _valueExpression.GetValue(methodCallExpression.Arguments[1]);

Expand Down Expand Up @@ -247,9 +257,9 @@ protected override string VisitMethodCallExpression(LambdaExpression topExpressi
switch (methodCallExpression.Method.Name)
{
case nameof(object.ToString):
return _valueExpression
.GetValue(methodCallExpression.Object)
.ToString()
return _valueExpression
.GetValue(methodCallExpression.Object)
.ToString()
.ToQuery(_odataQueryBuilderOptions);
}
}
Expand All @@ -268,8 +278,8 @@ protected override string VisitNewExpression(LambdaExpression topExpression, New
arguments[i] = _valueExpression.GetValue(newExpression.Arguments[i]);
}

return (arguments.Length == 0
? Activator.CreateInstance(newExpression.Type)
return (arguments.Length == 0
? Activator.CreateInstance(newExpression.Type)
: newExpression.Constructor.Invoke(arguments)).ToQuery(_odataQueryBuilderOptions);
}

Expand Down
126 changes: 86 additions & 40 deletions test/OData.QueryBuilder.Test/ODataQueryCollectionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using OData.QueryBuilder.Conventions.Functions;
using Xunit;

namespace OData.QueryBuilder.Test
Expand Down Expand Up @@ -45,8 +47,8 @@ public void ODataQueryBuilderList_Expand_DynamicProperty_Success()
.ToUri();

uri.Should().Be("http://mock/odata/ODataType?$expand=ODataKind");
}
}

[Fact(DisplayName = "Select simple => Success")]
public void ODataQueryBuilderList_Select_Simple_Success()
{
Expand All @@ -69,8 +71,8 @@ public void ODataQueryBuilderList_Select_DynamicProperty_Success()
.ToUri();

uri.Should().Be("http://mock/odata/ODataType?$select=IdType");
}
}

[Fact(DisplayName = "OrderBy simple => Success")]
public void ODataQueryBuilderList_OrderBy_Simple_Success()
{
Expand All @@ -93,8 +95,8 @@ public void ODataQueryBuilderList_OrderBy_DynamicProperty_Success()
.ToUri();

uri.Should().Be("http://mock/odata/ODataType?$orderby=IdType asc");
}
}

[Fact(DisplayName = "Filter orderBy multiple sort => Success")]
public void ODataQueryBuilderList_Filter_OrderBy_Multiple_Sort_Success()
{
Expand Down Expand Up @@ -354,8 +356,8 @@ public void ODataQueryBuilderList_Filter_With_ReplaceCharacters_KeyValuePairs_Ar
[Fact(DisplayName = "Filter variable dynamic property int=> Success")]
public void ODataQueryBuilderList_Filter_Simple_Variable_DynamicProperty_Success()
{
string propertyName = "ODataKind.ODataCode.IdCode";
string propertyName = "ODataKind.ODataCode.IdCode";

var uri = _odataQueryBuilderDefault
.For<ODataTypeEntity>(s => s.ODataType)
.ByList()
Expand All @@ -376,8 +378,8 @@ public void ODataQueryBuilderList_Filter_Simple_Variable_DynamicProperty_WrongTy
.ByList()
.Filter((s, f, _) => ODataProperty.FromPath<string>(propertyName) == "test")
.ToUri()).Should().Throw<ArgumentException>();
}
}

[Fact(DisplayName = "Filter const dynamic property int=> Success")]
public void ODataQueryBuilderList_Filter_Simple_Const_DynamicProperty_Success()
{
Expand All @@ -388,8 +390,8 @@ public void ODataQueryBuilderList_Filter_Simple_Const_DynamicProperty_Success()
.ToUri();

uri.Should().Be("http://mock/odata/ODataType?$filter=ODataKind/ODataCode/IdCode ge 3");
}
}

[Fact(DisplayName = "Filter simple const int=> Success")]
public void ODataQueryBuilderList_Filter_Simple_Const_Int_Success()
{
Expand Down Expand Up @@ -441,6 +443,35 @@ public void ODataQueryBuilderList_Filter_Any_Success1()

uri.Should().Be("http://mock/odata/ODataType?$filter=Tags/any(t:t eq 'testTag')");
}

[Fact(DisplayName = "(ODataQueryBuilderList) FilterExpressionReuseInExpand => Success")]
public void ODataQueryBuilderList_Filter_AnyReuse_Success()
{
Expression<Func<string, bool>> isTestTag = t => t == "testTag";

var uri = _odataQueryBuilderDefault
.For<ODataTypeEntity>(s => s.ODataType)
.ByList()
.Expand(x => x.For<string>(t => t.Tags).Filter(isTestTag))
.Filter((s, f, o) => o.Any(s.Tags, isTestTag))
.ToUri();

uri.Should().Be("http://mock/odata/ODataType?$expand=Tags($filter='testTag')&$filter=Tags/any(t:t eq 'testTag')");
}

[Fact(DisplayName = "(ODataQueryBuilderList) ExpandAnyExpressionVariable => Success")]
public void ExpandAnyExpressionVariable()
{
Expression<Func<string, bool>> isTestTagFunc = t => t == "testTag";

var uri = _odataQueryBuilderDefault
.For<ODataTypeEntity>(s => s.ODataType)
.ByList()
.Filter((s, f, o) => o.Any(s.Tags, isTestTagFunc))
.ToUri();

uri.Should().Be("http://mock/odata/ODataType?$filter=Tags/any(t:t eq 'testTag')");
}

[Fact(DisplayName = "(ODataQueryBuilderList) Filter Any Dynamic property => Success")]
public void ODataQueryBuilderList_Filter_Any_DynamicProperty_Success()
Expand Down Expand Up @@ -477,6 +508,21 @@ public void ODataQueryBuilderList_Filter_Any_With_Func_Success()

uri.Should().Be("http://mock/odata/ODataType?$filter=ODataKind/ODataCodes/any(v:date(v/Created) eq 2019-02-09T00:00:00Z)");
}

[Fact(DisplayName = "Filter operators predefined expression Any with func => Success")]
public void ODataQueryBuilderList_Filter_Predefined_Expression_Any_With_Func_Success()
{
IODataFunction f = default;
Expression<Func<ODataCodeEntity, bool>> expression = v => f.Date(v.Created) == new DateTime(2019, 2, 9);

var uri = _odataQueryBuilderDefault
.For<ODataTypeEntity>(s => s.ODataType)
.ByList()
.Filter((s, _, o) => o.Any(s.ODataKind.ODataCodes, expression))
.ToUri();

uri.Should().Be("http://mock/odata/ODataType?$filter=ODataKind/ODataCodes/any(v:date(v/Created) eq 2019-02-09T00:00:00Z)");
}

[Fact(DisplayName = "(ODataQueryBuilderList) Filter Any without func => Success")]
public void ODataQueryBuilderList_Filter_Any_Without_Func()
Expand All @@ -496,23 +542,23 @@ public void ODataQueryBuilderList_Filter_Any_With_Func_null_Supressed()
var odataQueryBuilderOptions = new ODataQueryBuilderOptions { SuppressExceptionOfNullOrEmptyOperatorArgs = true };
var odataQueryBuilder = new ODataQueryBuilder<ODataInfoContainer>(
_commonFixture.BaseUri, odataQueryBuilderOptions);

var func = default(Func<string, bool>);

var func = default(Expression<Func<string, bool>>);
var uri = odataQueryBuilder
.For<ODataTypeEntity>(s => s.ODataType)
.ByList()
.Filter((s, _, o) => o.Any(s.Labels, func))
.ToUri();

uri.Should().Be("http://mock/odata/ODataType?$filter=");
}

[Fact(DisplayName = "(ODataQueryBuilderList) Filter Any with func null => ArgumentException")]
public void ODataQueryBuilderList_Filter_Any_With_Func_null()
{
var func = default(Func<string, bool>);

var func = default(Expression<Func<string, bool>>);
_odataQueryBuilderDefault.Invoking(
(r) => r
.For<ODataTypeEntity>(s => s.ODataType)
Expand Down Expand Up @@ -1560,39 +1606,39 @@ public void ODataQueryBuilder_Function_Cast_Skip_Exception(string value)
.ToUri();

uri.Should().Be("http://mock/odata/ODataType?$filter=contains(,'55')");
}
}

[Fact(DisplayName = "UseCorrectDateTimeFormat Convert => Success")]
public void ODataQueryBuilderList_UseCorrectDatetimeFormat_Convert_Success()
{
var builder = new ODataQueryBuilder<ODataInfoContainer>(
_commonFixture.BaseUri,
new ODataQueryBuilderOptions { UseCorrectDateTimeFormat = true });
var dateTimeLocal = new DateTime(
year: 2023, month: 04, day: 07, hour: 12, minute: 30, second: 20, kind: DateTimeKind.Local);
var dateTimeUtc = new DateTime(
year: 2023, month: 04, day: 07, hour: 12, minute: 30, second: 20, kind: DateTimeKind.Utc);
var dateTimeOffset = new DateTimeOffset(
year: 2023, month: 04, day: 07, hour: 12, minute: 30, second: 20, offset: TimeSpan.FromHours(+7));
var dateTimeOffset2 = new DateTimeOffset(
year: 2023, month: 04, day: 07, hour: 12, minute: 30, second: 20, offset: TimeSpan.FromHours(-7));
{
var builder = new ODataQueryBuilder<ODataInfoContainer>(
_commonFixture.BaseUri,
new ODataQueryBuilderOptions { UseCorrectDateTimeFormat = true });

var dateTimeLocal = new DateTime(
year: 2023, month: 04, day: 07, hour: 12, minute: 30, second: 20, kind: DateTimeKind.Local);
var dateTimeUtc = new DateTime(
year: 2023, month: 04, day: 07, hour: 12, minute: 30, second: 20, kind: DateTimeKind.Utc);
var dateTimeOffset = new DateTimeOffset(
year: 2023, month: 04, day: 07, hour: 12, minute: 30, second: 20, offset: TimeSpan.FromHours(+7));
var dateTimeOffset2 = new DateTimeOffset(
year: 2023, month: 04, day: 07, hour: 12, minute: 30, second: 20, offset: TimeSpan.FromHours(-7));
var nowOffset = $"{DateTimeOffset.Now:zzz}".Replace("+", "%2B");

var uri = builder
.For<ODataTypeEntity>(s => s.ODataType)
.ByList()
.Filter((o) =>
o.DateTime == dateTimeLocal
&& o.DateTime == dateTimeUtc
&& o.DateTime == dateTimeOffset
.Filter((o) =>
o.DateTime == dateTimeLocal
&& o.DateTime == dateTimeUtc
&& o.DateTime == dateTimeOffset
&& o.DateTime == dateTimeOffset2)
.ToUri();

uri.Should().Be($"http://mock/odata/ODataType?$filter=" +
$"DateTime eq 2023-04-07T12:30:20{nowOffset} and " +
$"DateTime eq 2023-04-07T12:30:20%2B00:00 and " +
$"DateTime eq 2023-04-07T12:30:20%2B07:00 and " +
$"DateTime eq 2023-04-07T12:30:20{nowOffset} and " +
$"DateTime eq 2023-04-07T12:30:20%2B00:00 and " +
$"DateTime eq 2023-04-07T12:30:20%2B07:00 and " +
$"DateTime eq 2023-04-07T12:30:20-07:00");
}
}
Expand Down
Loading