diff --git a/src/OData.QueryBuilder/Conventions/Operators/IODataOperator.cs b/src/OData.QueryBuilder/Conventions/Operators/IODataOperator.cs index 3246876d..9bc32adc 100644 --- a/src/OData.QueryBuilder/Conventions/Operators/IODataOperator.cs +++ b/src/OData.QueryBuilder/Conventions/Operators/IODataOperator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq.Expressions; namespace OData.QueryBuilder.Conventions.Operators { @@ -7,10 +8,10 @@ public interface IODataOperator { bool In(T columnName, IEnumerable values); - bool All(IEnumerable columnName, Func func); + bool All(IEnumerable columnName, Expression> func); bool Any(IEnumerable columnName); - - bool Any(IEnumerable columnName, Func func); + + bool Any(IEnumerable columnName, Expression> func); } } diff --git a/src/OData.QueryBuilder/Expressions/Visitors/ODataOptionFilterExpressionVisitor.cs b/src/OData.QueryBuilder/Expressions/Visitors/ODataOptionFilterExpressionVisitor.cs index eb636bae..2a0839b7 100644 --- a/src/OData.QueryBuilder/Expressions/Visitors/ODataOptionFilterExpressionVisitor.cs +++ b/src/OData.QueryBuilder/Expressions/Visitors/ODataOptionFilterExpressionVisitor.cs @@ -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); @@ -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()) @@ -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) @@ -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]); @@ -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()) @@ -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()) @@ -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]); @@ -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); } } @@ -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); } diff --git a/test/OData.QueryBuilder.Test/ODataQueryCollectionTest.cs b/test/OData.QueryBuilder.Test/ODataQueryCollectionTest.cs index 3f32f5e4..e2784828 100644 --- a/test/OData.QueryBuilder.Test/ODataQueryCollectionTest.cs +++ b/test/OData.QueryBuilder.Test/ODataQueryCollectionTest.cs @@ -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 @@ -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() { @@ -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() { @@ -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() { @@ -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(s => s.ODataType) .ByList() @@ -376,8 +378,8 @@ public void ODataQueryBuilderList_Filter_Simple_Variable_DynamicProperty_WrongTy .ByList() .Filter((s, f, _) => ODataProperty.FromPath(propertyName) == "test") .ToUri()).Should().Throw(); - } - + } + [Fact(DisplayName = "Filter const dynamic property int=> Success")] public void ODataQueryBuilderList_Filter_Simple_Const_DynamicProperty_Success() { @@ -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() { @@ -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> isTestTag = t => t == "testTag"; + + var uri = _odataQueryBuilderDefault + .For(s => s.ODataType) + .ByList() + .Expand(x => x.For(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> isTestTagFunc = t => t == "testTag"; + + var uri = _odataQueryBuilderDefault + .For(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() @@ -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> expression = v => f.Date(v.Created) == new DateTime(2019, 2, 9); + + var uri = _odataQueryBuilderDefault + .For(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() @@ -496,23 +542,23 @@ public void ODataQueryBuilderList_Filter_Any_With_Func_null_Supressed() var odataQueryBuilderOptions = new ODataQueryBuilderOptions { SuppressExceptionOfNullOrEmptyOperatorArgs = true }; var odataQueryBuilder = new ODataQueryBuilder( _commonFixture.BaseUri, odataQueryBuilderOptions); - - var func = default(Func); - + + var func = default(Expression>); + var uri = odataQueryBuilder .For(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); - + var func = default(Expression>); + _odataQueryBuilderDefault.Invoking( (r) => r .For(s => s.ODataType) @@ -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( - _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( + _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(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"); } }