Skip to content

Commit 4be3f41

Browse files
CustomFilterCompilers (#356)
1 parent 7b31dc0 commit 4be3f41

4 files changed

Lines changed: 175 additions & 3 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using DevExtreme.AspNet.Data.Helpers;
2+
using Newtonsoft.Json;
3+
using System;
4+
using System.Collections;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Linq.Expressions;
8+
using Xunit;
9+
10+
namespace DevExtreme.AspNet.Data.Tests {
11+
12+
public class CustomFilterCompilersTests {
13+
14+
[Fact]
15+
public void OneToManyContains() {
16+
// https://github.com/DevExpress/DevExtreme.AspNet.Data/issues/277#issuecomment-497249871
17+
18+
try {
19+
CustomFilterCompilers.RegisterBinaryExpressionCompiler(info => {
20+
if(info.DataItemExpression.Type == typeof(Category) && info.AccessorText == "Products" && info.Operation == "contains") {
21+
var text = Convert.ToString(info.Value);
22+
23+
Expression<Func<Product, bool>> innerLambda = (p) => p.Name.ToLower().Contains(text);
24+
25+
return Expression.Call(
26+
typeof(Enumerable), nameof(Enumerable.Any), new[] { typeof(Product) },
27+
Expression.Property(info.DataItemExpression, "Products"), innerLambda
28+
);
29+
}
30+
31+
return null;
32+
});
33+
34+
var source = new[] { new Category(), new Category() };
35+
source[0].Products.Add(new Product { Name = "Chai" });
36+
37+
var filter = JsonConvert.DeserializeObject<IList>(@"[ ""Products"", ""Contains"", ""ch"" ]");
38+
39+
var loadOptions = new SampleLoadOptions {
40+
Filter = filter,
41+
RequireTotalCount = true
42+
};
43+
44+
var loadResult = DataSourceLoader.Load(source, loadOptions);
45+
Assert.Equal(1, loadResult.totalCount);
46+
Assert.Contains(loadOptions.ExpressionLog, line => line.Contains(".Products.Any(p => p.Name.ToLower().Contains("));
47+
} finally {
48+
CustomFilterCompilers.Binary.CompilerFuncs.Clear();
49+
}
50+
}
51+
52+
[Fact]
53+
public void ArrayContainsWithNullGuard() {
54+
try {
55+
CustomFilterCompilers.RegisterBinaryExpressionCompiler(info => {
56+
if(info.DataItemExpression.Type == typeof(Post) && info.AccessorText == "Tags" && info.Operation == "contains") {
57+
var text = Convert.ToString(info.Value);
58+
59+
var tagsAccessor = Expression.Property(info.DataItemExpression, "Tags");
60+
61+
var containsCall = Expression.Call(
62+
typeof(Enumerable), nameof(Enumerable.Contains), new[] { typeof(string) },
63+
tagsAccessor, Expression.Constant(text) /*, Expression.Constant(StringComparer.InvariantCultureIgnoreCase)*/
64+
);
65+
66+
return Expression.Condition(
67+
Expression.MakeBinary(ExpressionType.NotEqual, tagsAccessor, Expression.Constant(null, typeof(string[]))),
68+
containsCall,
69+
Expression.Constant(false)
70+
);
71+
}
72+
73+
return null;
74+
});
75+
76+
var source = new[] {
77+
new Post { Tags = new[] { "news", "article" } },
78+
new Post { Tags = new[] { "announcement" } }
79+
};
80+
81+
var loadOptions = new SampleLoadOptions {
82+
Filter = new[] { "Tags", "contains", "news" },
83+
RequireTotalCount = true
84+
};
85+
86+
var loadResult = DataSourceLoader.Load(source, loadOptions);
87+
Assert.Equal(1, loadResult.totalCount);
88+
Assert.Contains(loadOptions.ExpressionLog, line => line.Contains(@".Where(obj => IIF((obj.Tags != null), obj.Tags.Contains(""news""), False))"));
89+
} finally {
90+
CustomFilterCompilers.Binary.CompilerFuncs.Clear();
91+
}
92+
}
93+
94+
95+
class Product {
96+
public string Name { get; set; }
97+
}
98+
99+
class Category {
100+
public ICollection<Product> Products { get; } = new List<Product>();
101+
}
102+
103+
class Post {
104+
public string[] Tags { get; set; }
105+
}
106+
107+
}
108+
109+
}

net/DevExtreme.AspNet.Data/FilterExpressionCompiler.cs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System;
1+
using DevExtreme.AspNet.Data.Helpers;
2+
using System;
23
using System.Collections;
34
using System.Collections.Generic;
45
using System.Linq;
@@ -44,11 +45,23 @@ Expression CompileBinary(ParameterExpression dataItemExpr, IList criteriaJson) {
4445

4546
var clientAccessor = Convert.ToString(criteriaJson[0]);
4647
var clientOperation = hasExplicitOperation ? Convert.ToString(criteriaJson[1]).ToLower() : "=";
47-
var clientValue = criteriaJson[hasExplicitOperation ? 2 : 1];
48+
var clientValue = Utils.UnwrapNewtonsoftValue(criteriaJson[hasExplicitOperation ? 2 : 1]);
4849
var isStringOperation = clientOperation == CONTAINS || clientOperation == NOT_CONTAINS || clientOperation == STARTS_WITH || clientOperation == ENDS_WITH;
4950

51+
if(CustomFilterCompilers.Binary.CompilerFuncs.Count > 0) {
52+
var customResult = CustomFilterCompilers.Binary.TryCompile(new BinaryExpressionInfo {
53+
DataItemExpression = dataItemExpr,
54+
AccessorText = clientAccessor,
55+
Operation = clientOperation,
56+
Value = clientValue
57+
});
58+
59+
if(customResult != null)
60+
return customResult;
61+
}
62+
5063
var accessorExpr = CompileAccessorExpression(dataItemExpr, clientAccessor, progression => {
51-
if(isStringOperation || progression.Last().Type == typeof(Object) && Utils.UnwrapNewtonsoftValue(clientValue) is String)
64+
if(isStringOperation || progression.Last().Type == typeof(Object) && clientValue is String)
5265
ForceToString(progression);
5366

5467
if(_stringToLower)
@@ -271,6 +284,13 @@ static void AddToLower(List<Expression> progression) {
271284

272285
progression.Add(toLowerCall);
273286
}
287+
288+
class BinaryExpressionInfo : IBinaryExpressionInfo {
289+
public Expression DataItemExpression { get; set; }
290+
public string AccessorText { get; set; }
291+
public string Operation { get; set; }
292+
public object Value { get; set; }
293+
}
274294
}
275295

276296
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Linq.Expressions;
5+
6+
namespace DevExtreme.AspNet.Data.Helpers {
7+
using BinaryExpressionCompilerFunc = Func<IBinaryExpressionInfo, Expression>;
8+
9+
public static class CustomFilterCompilers {
10+
11+
internal static class Binary {
12+
internal readonly static ICollection<BinaryExpressionCompilerFunc> CompilerFuncs = new List<BinaryExpressionCompilerFunc>();
13+
14+
internal static Expression TryCompile(IBinaryExpressionInfo info) {
15+
foreach(var func in CompilerFuncs) {
16+
var result = func(info);
17+
if(result != null)
18+
return result;
19+
}
20+
return null;
21+
}
22+
}
23+
24+
public static void RegisterBinaryExpressionCompiler(BinaryExpressionCompilerFunc compilerFunc) {
25+
Binary.CompilerFuncs.Add(compilerFunc);
26+
}
27+
28+
}
29+
30+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
using System.Linq.Expressions;
3+
4+
namespace DevExtreme.AspNet.Data.Helpers {
5+
6+
public interface IBinaryExpressionInfo {
7+
Expression DataItemExpression { get; }
8+
string AccessorText { get; }
9+
string Operation { get; }
10+
object Value { get; }
11+
}
12+
13+
}

0 commit comments

Comments
 (0)