Skip to content

Commit

Permalink
Support reusing Expressions between filters on the expand level and a…
Browse files Browse the repository at this point in the history
…ny/all conditions on the root level.

Possible breaking:
Changed the All and Any methods to take Expression<Func<T, bool>> instead of Func<T, bool>.
As far as I can tell this does not impact anything since explicitly passing a non-null Func<T, bool> previously was unsupported.

fixes #123
  • Loading branch information
LinusCenterstrom committed Jan 31, 2024
1 parent 62efe8b commit 10e248f
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 65 deletions.
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

0 comments on commit 10e248f

Please sign in to comment.